(最终总结)Acwing算法课

目录

  • 基础算法
    • 快速排序
      • AcWing 785. 快速排序
      • AcWing 786. 第k个数
    • 归并排序
      • AcWing 787. 归并排序
      • AcWing 788. 逆序对的数量
    • 二分
      • AcWing 789. 数的范围
      • AcWing 790. 浮点数的三次方根
      • 衍生题:浮点数的平方根
    • 高精度
      • AcWing 791. 高精度加法
      • AcWing 792. 高精度减法
      • AcWing 793. 高精度乘法
      • AcWing 794. 高精度除法
    • 前缀和与差分
      • AcWing 795. 前缀和
      • AcWing 796. 子矩阵的和
      • AcWing 797. 差分
      • AcWing 798. 差分矩阵
    • 双指针算法
      • AcWing 799. 最长连续不重复子序列
      • AcWing 800. 数组元素的目标和
      • AcWing 2816. 判断子序列
    • 位运算
      • AcWing 801. 二进制中1的个数
    • 离散化
      • AcWing 802. 区间和
    • 区间合并
      • AcWing 803. 区间合并
  • 数据结构
    • 单链表
      • AcWing 826. 单链表
    • 双链表
      • AcWing 827. 双链表
      • AcWing 828. 模拟栈
    • 队列
      • AcWing 829. 模拟队列
      • 扩展:循环队列
    • 单调栈
      • AcWing 830. 单调栈
    • 单调队列
      • AcWing 154. 滑动窗口
    • KMP
      • AcWing 831. KMP字符串
    • Trie
      • AcWing 835. Trie字符串统计
      • AcWing 143. 最大异或对
    • 并查集
      • AcWing 836. 合并集合
      • AcWing 837. 连通块中点的数量
      • AcWing 240. 食物链
      • AcWing 838. 堆排序
      • AcWing 839. 模拟堆
    • 哈希表
      • AcWing 840. 模拟散列表
      • AcWing 841. 字符串哈希
      • AcWing 139. 回文子串的最大长度
    • C++ STL简介
  • 搜索与图论
    • DFS
      • AcWing 842. 排列数字
      • AcWing 843. n-皇后问题
    • BFS
      • AcWing 844. 走迷宫
      • AcWing 845. 八数码
    • 树与图的存储
    • 树与图的深度优先遍历
      • AcWing 846. 树的重心
    • 树与图的广度优先遍历
      • AcWing 847. 图中点的层次
    • 拓扑排序
      • AcWing 848. 有向图的拓扑序列
    • Dijkstra
      • AcWing 849. Dijkstra求最短路 I
      • AcWing 850. Dijkstra求最短路 II
    • bellman-ford
      • AcWing 853. 有边数限制的最短路
    • spfa
      • AcWing 851. spfa求最短路
      • AcWing 852. spfa判断负环
    • Floyd
      • AcWing 854. Floyd求最短路
    • Prim
      • AcWing 858. Prim算法求最小生成树
    • Kruskal
      • AcWing 859. Kruskal算法求最小生成树
    • 染色法判定二分图
      • AcWing 860. 染色法判定二分图
    • 匈牙利算法
      • AcWing 861. 二分图的最大匹配
  • 数学知识
    • 质数
      • AcWing 866. 试除法判定质数
      • AcWing 867. 分解质因数
      • AcWing 868. 筛质数
    • 约数
      • AcWing 869. 试除法求约数
      • AcWing 870. 约数个数
      • AcWing 871. 约数之和
      • AcWing 872. 最大公约数
    • 欧拉函数
      • AcWing 873. 欧拉函数
      • AcWing 874. 筛法求欧拉函数
    • 快速幂
      • AcWing 875. 快速幂
      • AcWing 876. 快速幂求逆元
    • 扩展欧几里得算法
      • AcWing 877. 扩展欧几里得算法
      • AcWing 878. 线性同余方程
    • 中国剩余定理
      • AcWing 204. 表达整数的奇怪方式
    • 高斯消元
      • AcWing 883. 高斯消元解线性方程组
      • AcWing 884. 高斯消元解异或线性方程组
    • 求组合数
      • AcWing 885. 求组合数 I
      • AcWing 886. 求组合数 II
      • AcWing 887. 求组合数 III
      • AcWing 888. 求组合数 IV
      • AcWing 889. 满足条件的01序列
    • 容斥原理
      • AcWing 890. 能被整除的数
    • 博弈论
      • AcWing 891. Nim游戏
      • AcWing 892. 台阶-Nim游戏
      • AcWing 893. 集合-Nim游戏
      • AcWing 894. 拆分-Nim游戏
  • 动态规划
    • 背包问题
      • AcWing 2. 01背包问题
      • AcWing 3. 完全背包问题
      • AcWing 4. 多重背包问题
      • AcWing 5. 多重背包问题 II(二进制优化)
      • AcWing 9. 分组背包问题
    • 线性DP
      • AcWing 898. 数字三角形
      • AcWing 895. 最长上升子序列
      • AcWing 896. 最长上升子序列 II
      • AcWing 897. 最长公共子序列
      • AcWing 902. 最短编辑距离
      • AcWing 899. 编辑距离
    • 区间DP
      • AcWing 282. 石子合并
    • 计数类DP
      • AcWing 900. 整数划分
    • 数位统计DP
      • AcWing 338. 计数问题
    • 状态压缩DP
      • AcWing 291. 蒙德里安的梦想
      • AcWing 91. 最短Hamilton路径
    • 树形DP
      • AcWing 285. 没有上司的舞会
    • 记忆化搜索
      • AcWing 901. 滑雪
  • 贪心
    • 区间问题
      • AcWing 905. 区间选点
      • AcWing 908. 最大不相交区间数量
      • AcWing 906. 区间分组
      • AcWing 907. 区间覆盖
    • Huffman树
      • AcWing 148. 合并果子
    • 排序不等式
      • AcWing 913. 排队打水
    • 绝对值不等式
      • AcWing 104. 货仓选址
    • 推公式
      • AcWing 125. 耍杂技的牛

基础算法

快速排序

AcWing 785. 快速排序

/*
快速排序基于分治法
1.在数组中找一个元素作为分界点x
2.根据分界点x重新划分区间,使得所有小于x的在左边,大于x的在右边(使得x在它该在的位置)
    利用双指针
3.递归处理左右两边(这样让每一个元素都在该在的位置)
*/
#include
using namespace std;
const int N=100010;
int n;
int q[N];

void quick_sort(int q[],int l, int r){//不用纠结为什么没有传引用,因为数组传递的是头指针
    if(l>=r) return;//递归时,一开始注意终止条件
    
    int i=l-1,j=r+1,x=q[(l+r)/2];//下取整
    while(ix);
        if(i>n;
    for(int i=0;i>q[i];
    
    quick_sort(q,0,n-1);
    
    for(int i=0;i

AcWing 786. 第k个数

/*
n+n/2+n/4+...<=2n 所以快速选择算法时间复杂度是O(n)
注意快速排序的时间复杂度是n+n+...(logn个n相加,所以总共是nlogn)
*/
#include
using namespace std;
const int N=100010;
int n,k;
int q[N];

int quick_sort(int l,int r,int k){//这里求的是第k小的数
    if(l>=r) return q[l];
    
    int i=l-1,j=r+1,x=q[(l+r)>>1];
    while(ix);
        if(i=k) return quick_sort(l,j,k);
    else return quick_sort(j+1,r,k-(j-l+1));
}
int main(){
    cin>>n>>k;
    for(int i=0;i>q[i];
    
    cout<

归并排序

AcWing 787. 归并排序

/*
也是基于分治
1.选数组中中间元素的下标作为确定分界点 mid=(l+r)>>1
(归并排序确定的是中间的位置,是下标的中间值;而快速排序是选择数组中的一个元素)
2.递归排序左右两边
3.归并—合二为一(双指针) 合并两个有序数组
快速排序是先分完之后,然后再递归两边
归并排序是先递归两边,然后再操作

时间复杂度:O(nlogn)
*/
#include
using namespace std;
const int N=100010;
int n;
int q[N],tmp[N];

void merge_sort(int q[], int l,int r){//不用纠结为什么没有传引用,因为数组传递的是头指针
    if(l>=r) return;
    int mid=(l+r)/2;
    
    merge_sort(q,l,mid);
    merge_sort(q,mid+1,r);
    
    int k=0,i=l,j=mid+1;
    while(i<=mid&&j<=r){
        if(q[i]<=q[j]) tmp[k++]=q[i++];
        else tmp[k++]=q[j++];
    }
    while(i<=mid) tmp[k++]=q[i++];
    while(j<=r) tmp[k++]=q[j++];
    
    for(int i=l,j=0;i<=r;i++,j++) q[i]=tmp[j];
}

int main(){
    cin>>n;
    for(int i=0;i>q[i];
    
    merge_sort(q,0,n-1);
    
    for(int i=0;i

AcWing 788. 逆序对的数量

#include
using namespace std;
const int N=100010;
typedef long long ll;
//这道题数据范围10万,当整个序列倒序时,逆序对数量最多(n-1)+(n-2)+...1==n(n-1)/2==5*1e9 溢出 
int n;
int q[N],tmp[N];

ll merge_sort(int l,int r){
    if(l>=r) return 0;
    int mid=(l+r)>>1;
    ll res=merge_sort(l,mid)+merge_sort(mid+1,r);
    int i=l,j=mid+1,k=0;
    while(i<=mid&&j<=r){
        if(q[i]<=q[j]) tmp[k++]=q[i++];
        else{
            res+=mid-i+1;
            tmp[k++]=q[j++];
        }
    }
    while(i<=mid) tmp[k++]=q[i++];
    while(j<=r) tmp[k++]=q[j++];
    
    for(int i=l,j=0;i<=r;i++,j++) q[i]=tmp[j];
    return res;
}

int main(){
    cin>>n;
    for(int i=0;i>q[i];
    
    cout<

二分

AcWing 789. 数的范围

/*
二分本质是用来查找满足某种性质的边界点
在给定区间内,能找到某种性质使得区间分为两部分,一部分满足,另外一部分不满足这种性质,
二分就可以用来寻找这个性质的边界:满足某种性质的第一个元素;或者是满足某种性质的最后一个元素。
从而有两个不同模板。
注意二分法一定能找到满足某种性质的边界点,但它不一定是你要找的目标,目标不一定在区间内
先写check函数,如何根据check函数更新区间,再判断选用哪个模板
*/
#include
using namespace std;
const int N =100010;
int n,m;
int q[N];//读入整个数组,N稍大于最大取值,以防边界情况

int main(){
    cin>>n>>m;;
    for(int i=0;i>q[i];
    }
    
    while(m--){
        int x;
        cin>>x;
        
        //二分找到满足值>=x这个性质的第一个元素
        int l=0,r=n-1;
        while(l>1;
            if(q[mid]>=x) r=mid;
            else l=mid+1;
        }
        //注意可能找不到>=x的第一个元素,全部都比它小
        if(q[l]!=x) cout<<"-1 -1"<>1;
                if(q[mid]<=x) l=mid;
                else r=mid-1;
            }  
            cout<

AcWing 790. 浮点数的三次方根

#include
using namespace std;
double x;
int main(){
    cin>>x;
    
    double l=-100,r=100;//开三次方根,估计答案大概范围
    while(r-l>1e-8){//经验值,保留6位小数,1e-8  4位小数,1e-6 多2
        double mid=(l+r)/2;
        if(mid*mid*mid>=x) r=mid;
        //不能写mid^3这叫异或,根据题中所给的数据范围,mid三次方之后不会溢出
        else l=mid;
    }
    printf("%.6lf",l);//结果保留6位小数
    return 0;
}

衍生题:浮点数的平方根

#include
using namespace std;
double x;
int main(){
    cin>>x;
    
    double l=0, r;
    //一个浮点数开平方,答案绝对值一定大于等于0,但是右边界根据输入浮点数是否大于1确定
    if(x>1) r=x;
    else r=1;
    while(r-l>1e-8){//经验值,保留6位小数,1e-8  4位小数,1e-6 多2
        double mid=(l+r)/2;
        if(mid*mid>=x) r=mid;
        // //不能写mid^2这叫异或,根据题中所给的数据范围,mid平方之后不会溢出
        else l=mid;
    }
    printf("%.6lf -%.6lf",l,l);
    return 0;
}

高精度

AcWing 791. 高精度加法

/*
两个特别大的正整数相加
把大整数存到容器里面去,每一位存一位数字,低位在前便于进位,这样就在数组最后去操作
*/
#include
#include//习惯用vector表示大整数,自带.size()函数,不需要开一个额外的变量来存

using namespace std;

vector add(vector &A, vector &B)//加引用提高效率
{
    if (A.size() < B.size()) return add(B, A);

    vector C;//定义答案
    int t=0;//定义进位
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }

    if (t) C.push_back(t);
    return C;
}

int main(){
    string a,b;//两个大整数太长,用字符串读入
    vector A,B;//存到vector里面去
    
    cin>>a>>b;//a="123456"
    //每一位拿出来放入vector中,注意低位在前
    for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');//把a中的字符变成数字
    for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0');
    
    auto C=add(A,B);
    
    for(int i=C.size()-1;i>=0;i--) cout<
//压9位的意思是:将高精度整数转化成数组时,数组中的每个数存储9位。
//这样数组的长度会缩小到原来的1/9,数组里每一个数存0~9
//压9位就是每个数存0~999999999。这样数组长度会缩小到九分之一。
#include 
#include 

using namespace std;

const int base = 1e9;

vector add(vector &A, vector &B)
{
    if (A.size() < B.size()) return add(B, A);

    vector C;
    int t = 0;
    for (int i = 0; i < A.size(); i ++ )
    {
        t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % base);//%10改成%1e9
        t /= base;
    }

    if (t) C.push_back(t);
    return C;
}

int main()
{
    string a, b;
    vector A, B;
    cin >> a >> b;

    for (int i = a.size() - 1, s = 0, j = 0, t = 1; i >= 0; i -- )
    {//数组中的每个数存储9位
        s += (a[i] - '0') * t;
        j ++, t *= 10;
        if (j == 9 || i == 0)
        {
            A.push_back(s);
            s = j = 0;
            t = 1;
        }
    }
    for (int i = b.size() - 1, s = 0, j = 0, t = 1; i >= 0; i -- )
    {
        s += (b[i] - '0') * t;
        j ++, t *= 10;
        if (j == 9 || i == 0)
        {
            B.push_back(s);
            s = j = 0;
            t = 1;
        }
    }

    auto C = add(A, B);

    cout << C.back();//先输出第一个数
    //每次输出不足九位的要在最高位补0
    for (int i = C.size() - 2; i >= 0; i -- ) printf("%09d", C[i]);
    cout << endl;

    return 0;
}

AcWing 792. 高精度减法

#include
#include

using namespace std;

//判断A是否大于等于B
bool cmp(vector& A, vector& B){
    //先判断位数
    if(A.size()!=B.size()) return A.size()>B.size();
    //位数相同,从最高位开始比较,注意从最后开始比较
    for(int i=A.size()-1;i>=0;i--){
        if(A[i]!=B[i]) return A[i]>B[i];
    }
    return true;
}
// C = A - B, 满足A >= B, A >= 0, B >= 0
vector sub(vector &A, vector &B){
    //这里模板只考虑两个正数相加减的情况,如果有负数,一定可以转化为绝对值相加减的情况
    //但是要注意输入输出
    vector C;//定义答案数组
    int t=0;//借位
    for(int i=0;i1&&C.back()==0) C.pop_back();
    
    return C;
}

int main(){
    string a,b;//两个大整数太长,用字符串读入
    vector A,B;//存到vector里面去
    
    cin>>a>>b;//a="123456"
    //每一位拿出来放入vector中,注意低位在前
    for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');//把a中的字符变成数字
    for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0');
    
    vector C;
    //判断谁大,要保证是大的减去小的
    if(cmp(A,B)) C=sub(A,B);
    else{
        C=sub(B,A);
        cout<<'-';
    } 
    
    for(int i=C.size()-1;i>=0;i--) cout<

AcWing 793. 高精度乘法

#include
#include
using namespace std;

// C = A * b, A >= 0, b > 0
vector mul(vector&A,int b){
    vector C;
    int t=0;//进位
    for(int i=0;i1&&C.back()==0) C.pop_back();
    return C;
}

int main(){
    string a;
    int b;
    cin>>a>>b;
    vector A;
    for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
    
    auto C=mul(A,b);
    
    for(int i=C.size()-1;i>=0;i--) cout<

AcWing 794. 高精度除法

#include
#include
#include
using namespace std;

// A / b = C ... r, A >= 0, b > 0
vector div(vector&A,int &b,int&r){//余数通过引用传递
    vector C;
    for(int i=A.size()-1;i>=0;i--){//从高位到低位
        r=r*10+A[i];
        C.push_back(r/b);//高精度除以低精度,把较小的b当做一个整体
        r%=b;
    }
    reverse(C.begin(),C.end());
    while(C.size()>1&&C.back()==0) C.pop_back();
    return C;
}

int main(){
    string a;
    int b;
    vector A;
    
    cin>>a>>b;
    for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
    
    int r=0;//余数
    auto C=div(A,b,r);
    
    for(int i=C.size()-1;i>=0;i--) cout<

前缀和与差分

AcWing 795. 前缀和

/*
S[i] = a[1] + a[2] + ... a[i]
a[l] + ... + a[r] = S[r] - S[l - 1]
*/
#include
using namespace std;

const int N=100010;

int a[N];
int n,m;

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>a[i];//前缀和下标从1开始,是为了求前缀和数组时,s[0]合法
    
    for(int i=1;i<=n;i++) a[i]+=a[i-1];//求前缀和数组
    
    while(m--){
        int l,r;
        cin>>l>>r;
        cout<

AcWing 796. 子矩阵的和

/*
S[i, j] = 第i行j列格子左上部分所有元素的和
以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为:
S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]
*/
#include
using namespace std;
const int N=1010;
int n,m,q;
int a[N][N];

int main(){
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>a[i][j];
        }
    }
    
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            a[i][j]+=a[i-1][j]+a[i][j-1]-a[i-1][j-1];//求出前缀和数组
        }
    }
    
    while(q--){
        int x1,y1,x2,y2;
        cin>>x1>>y1>>x2>>y2;
        cout<

AcWing 797. 差分

/*
差分是前缀和的逆运算,类似于求导和求积分,所给数组是a[i],构造数组b[]使得
a[i]=b[1]+b[2]+b[3]+...+b[i]
b[1]=a[1]
b[2]=a[2]-a[1]
...
b[n]=a[n]-a[n-1]
假想一个数组b使得数组a是其前缀和,数组b就称为数组a的差分
原数组a在区间[l,r]上都加上c等价于其差分数组b b[l]+=c  b[r+1]-=c
*/
#include
using namespace std;
const int N=100010;
int n,m;
int a[N],b[N];

void insert(int l, int r, int c)
{
    b[l] += c;
    b[r + 1] -= c;
}

int main(){
    cin>>n>>m;

    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);

    for (int i = 1; i <= n; i ++ ) insert(i, i, a[i]);
    
    // for(int i=1;i<=n;i++){
    //     cin>>a[i];
    //     b[i]+=a[i];//构造差分数组,可看成原数组均为0,在[i,i]这个区间插入a[i]
    //     b[i+1]-=a[i];//所以b[i]+=a[i] b[i+1]-=a[i]
    // }
    
    while(m--){
        int l,r,c;
        cin>>l>>r>>c;
        b[l]+=c;
        b[r+1]-=c;
    }
    
    for(int i=1;i<=n;i++){
        b[i]+=b[i-1];
        cout<

AcWing 798. 差分矩阵

/*
给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
*/
#include
using namespace std;
const int N=1010;
int n,m,q;
int a[N][N],b[N][N];

void insert(int x1,int y1,int x2,int y2,int c){//构造差分矩阵
    b[x1][y1]+=c;
    b[x2+1][y1]-=c;
    b[x1][y2+1]-=c;
    b[x2+1][y2+1]+=c;
}

int main(){
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            cin>>a[i][j];
            insert(i,j,i,j,a[i][j]);
        }
    }
    
    while(q--){
        int x1,y1,x2,y2,c;
        cin>>x1>>y1>>x2>>y2>>c;
        insert(x1,y1,x2,y2,c);
    }
    
    for(int i=1;i<=n;i++){//求前缀和
        for(int j=1;j<=m;j++){
            b[i][j]+=b[i-1][j]+b[i][j-1]-b[i-1][j-1];
            cout<

双指针算法

AcWing 799. 最长连续不重复子序列

/*
for (int i = 0, j = 0; i < n; i ++ )
{
    while (j < i && check(i, j)) j ++ ;

    // 具体问题的逻辑
}
常见问题分类:
    (1) 对于一个序列,用两个指针维护一段区间
    (2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
    
双指针算法核心:优化
两根指针本来暴力求解是n^2级别,双指针运用某些单调性质优化后,时间复杂度变为n,
因为总体看来两根指针都是遍历序列一次
*/
#include
using namespace std;
const int N=100010;
int n;
int a[N],b[N];
//a[N]存储整数序列,b[N]动态存储当前探索区间每个元素出现的次数
//注意a的数组范围是元素有多少个,b的数组范围是数组a中元素的取值范围

int main(){
    cin>>n;
    for(int i=0;i>a[i];
    
    int res=0;
    for(int i=0,j=0;i1) b[a[j++]]--;
        res=max(res,i-j+1);
    }
    cout<

AcWing 800. 数组元素的目标和

#include
using namespace std;
const int N=100010;
int n,m,x;
int A[N],B[N];

int main(){
    cin>>n>>m>>x;
    for(int i=0;i>A[i];
    for(int i=0;i>B[i];
    
    for(int i=0,j=m-1;i=0&&A[i]+B[j]>x) j--;
        if(j>=0&&A[i]+B[j]==x) cout<

AcWing 2816. 判断子序列

/*
序列a中每个元素能否顺次映射到b
从前往后扫描数组b,每次扫描时,查看数组b中当前数与数组a中当前数是否一样
如果一样,此时a[i]与b[j]匹配,i++
总之就是在数组b中一个一个找与a[i]相匹配的元素,匹配成功就i++
*/
#include 

using namespace std;

const int N = 100010;

int n, m;
int a[N], b[N];

int main()
{
    cin>>n>>m;
    for (int i = 0; i < n; i ++ ) cin>>a[i];
    for (int i = 0; i < m; i ++ ) cin>>b[i];

    int i = 0, j = 0;
    while (i < n && j < m)
    {
        if (a[i] == b[j]) i ++ ;
        j ++ ;
    }

    if (i == n) puts("Yes");
    else puts("No");

    return 0;
}

位运算

AcWing 801. 二进制中1的个数

/*
数字n的二进制表示中倒数第k位数是多少(k从0开始)
n>>k&1
先把第k位移到最后一位n>>k
再看个位是多少:&1


返回x最后一个1 
lowbit(x)=x&-x
补码-x是对x取反+1
反码是对x取反
x=10100   lowbit(x)=100 树状数组的基本操作
*/
#include
using namespace std;

int main(){
    int n;
    cin>>n;
    while(n--){
        int x;
        cin>>x;
        int res=0;
        while(x){
            x-=x&(-x);//每次把x中最后一个1减掉
            res++;
        }
        cout<

离散化

AcWing 802. 区间和

/*
模板:
vector alls; // 存储所有待离散化的值
sort(alls.begin(), alls.end()); // 将所有值排序
alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 去掉重复元素

// 二分求出x对应的离散化的值
int find(int x) // 找到第一个大于等于x的位置
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1; // 映射到1, 2, ...n
}

离散化(整数离散化,保序)
一些数值域很大(0~1e9),个数很少(0~1e5)
一些题目需要以这些值为下标来做,如果直接开这么大的数组,根本不现实
而且以这道题为例,这些数是数轴下标的话,可能存在负值
因此需要把这些数映射到从0开始的连续的自然数(这道题因为要用到前缀和,所以映射到从1开始的自然数)
比如数组a三个数:1, 2, 1e9 映射为0,1,2 这个过程叫做离散化

离散化中的两个问题:
1.数组a中可能有重复元素,所以需要去重
2.如何算出a[i]离散化之后的值,也就是找a[i]在数组中的下标,因为数组有序,所以二分查找

这道题数组下标较小时,直接前缀和就行,因为题中数轴下标值域最多1e9,
但是总共最多用到n+2m也就是3*1e5个下标,绝大多数下标没有用到,典型离散化
*/
#include
#include
#include
using namespace std;
const int N=300010;//离散化之后最多三十万个数
int n,m;
int s[N];//离散化之后的数组及前缀和数组
vector alls;// 存储所有待离散化的值,这道题中就是指数轴下标
typedef pair pii;
vector add,query;


// vector::iterator unique(vector&alls){
//     int j=0;
//     for(int i=0;i>1;
        if(alls[mid]>=x) r=mid;
        else l=mid+1;
    }
    return l+1; // 返回下标+1,是否+1与题目有关,题目要求前缀和,所以需要从1开始映射
}

int main(){
    cin>>n>>m;
    while(n--){
        int x,c;
        cin>>x>>c;
        add.push_back({x,c});
        alls.push_back(x);
    }
    
    while(m--){
        int l,r;
        cin>>l>>r;
        query.push_back({l,r});
        alls.push_back(l);
        alls.push_back(r);
    }
    
    sort(alls.begin(),alls.end());// 将所有值排序
    alls.erase(unique(alls.begin(), alls.end()),alls.end()); // 去掉重复元素

    
    for(auto item:add){
        int x=find(item.first),c=item.second;
        s[x]+=c;
    }
    
    for(int i=1;i<=alls.size();i++){
        s[i]+=s[i-1];
    }
    
    for(auto item:query){
        int l=find(item.first),r=find(item.second);
        cout<

区间合并

AcWing 803. 区间合并

/*
合并所有有交集的区间,输出合并之后的区间个数
1.按照区间左端点排序
2.扫描整个区间,扫描过程中把所有可能有交集的区间合并,每次维护一个当前的区间[st,ed]
假设当前已经扫描到第i个区间,它与此时维护的区间要么合并,要么不合并

*/
#include
#include
#include
using namespace std;
typedef pair PII;
int n;
vector segs;

vector merge(vector&segs){
    vector res;
    sort(segs.begin(),segs.end());
    int st=-2e9,ed=-2e9;//[st,ed]代表当前探索区间之前最后一个可能待合并的区间
    for(auto seg:segs){
        if(ed>n;
    while(n--){
        int l,r;
        cin>>l>>r;
        segs.push_back({l,r});
    }
    
    auto res=merge(segs);
    
    cout<

数据结构

单链表

AcWing 826. 单链表

/*
数组模拟单(双)链表,用struct+指针方式,每次需要调用new函数,动态分配新节点,非常慢,可能超时
    - 单链表:用的最多的是邻接表(n个单链表,用来存储树或图)
    - 双链表:优化某些问题
两个数组:e[N],ne[N]表示每个节点的值和下一个节点,用下标关联,下标相同表示节点相同
空节点下标用-1表示
*/
#include
using namespace std;
const int N=1e5+10;

int head, e[N], ne[N], idx;
/*
head表示当前链表此时头结点的下标(所有节点都可以用下标来索引)
    比如某个节点的下标为i(从0开始),表示这个节点是第i+1个插入的点(从1开始)
e[]存储每个节点的值,下标表示当前节点的下标,数组中存储的是当前节点的值
ne[]存储每个节点的next指针,e和ne数组用下标关联
    下标表示当前节点的下标,数组中存储的是当前节点的所指向下一个节点的下标
idx是下标,表示当前可以从哪儿开始分配新节点
    也就是说两个数组中idx这个下标可以用来表示新的节点的值和next指针
*/

// 初始化
void init()
{
    head = -1;//空节点用-1表示
    idx = 0;//当前可以从下标0开始分配新节点
}

// 在链表头插入一个值为a的节点
void add_to_head(int a)
{   /*
    e[idx] = a; 创建新节点
    ne[idx] = head; 令新节点指向当前链表此时的头结点
    head = idx ++ ; 更新当前链表头结点的下标和idx
    */
    e[idx] = a, ne[idx] = head, head = idx ++ ;
}

/*
将值为x的节点插到下标是k的点后面
注意下标为k,则对应节点的下标就是k,所以如果题目中的描述是第k个输入,第k个插入的
节点的话,这个节点对应的下标是k-1
*/
void add(int k, int x)
{
    e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ;
}

// 将下标是k的点后面的点删掉,一定注意题目中所描述的节点的下标
void remove(int k)
{
    ne[k] = ne[ne[k]];
}

int main(){
    int m;
    cin>>m;
    
    init();
    // head=-1;
    // idx=0;
    
    while(m--){
        char op;
        int x,k;
        
        cin>>op;
        if(op=='H'){
            cin>>x;
            add_to_head(x);
            // e[idx]=x;
            // ne[idx]=head;
            // head=idx++;
        }
        else if(op=='D'){
            cin>>k;
            if(!k) head=ne[head];
            else{
                remove(k-1);
                // ne[k-1]=ne[ne[k-1]];
            }
        }
        else{
            cin>>k>>x;
            add(k-1,x);
            // e[idx]=x;
            // ne[idx]=ne[k-1];
            // ne[k-1]=idx++;
        }
    }
    
    for(int i=head;i!=-1;i=ne[i]) cout<

双链表

AcWing 827. 双链表

#include
using namespace std;
const int N=1e5+10;
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针
//idx是下标,表示当前可以从哪儿开始分配新节点
//不定义头节点的下标head,下标0,1表示左右端点
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
    //0是左端点,1是右端点,用来处理边界条件
    r[0] = 1, l[1] = 0;
    idx = 2;
    //这里注意第一个输入的节点所分配的下标是2,所以第i个输入的点,下标为i+1
}

// 在下标为a的节点的右边插入一个值为x的节点
//这里是在右边插入一个节点,如果题目要求在左边插入一个值为x的节点
//则等同于在下标的l[a]的节点右边插入一个值为x的节点
void insert(int a, int x)
{
    e[idx] = x;//创建新节点
    l[idx] = a, r[idx] = r[a];
    //新节点左右指针指向创建的先后顺序没有任何影响,lr是为了下面先l后r对齐
    l[r[a]] = idx, r[a] = idx ++ ;
    //原链表中节点指向要特别注意,它可以被很多节点指向
    //但他自己一边只能指向一个节点,所以这里先l再r(先不确定的点,再确定的点)
    //最后注意更新idx
}

// 删除下标为a的节点
void remove(int a)
{
    l[r[a]] = l[a];
    r[l[a]] = r[a];
}

int main(){
    int m;
    cin>>m;
    
    // init();//注意链表记得初始化
    l[1]=0,r[0]=1;
    idx=2;

    while(m--){
        string op;
        int k,x;
        
        cin>>op;
        if(op=="L"){
            cin>>x;
            insert(0,x);
//在链表最左端插入一个节点,代表在左端点也就是下标为0的右边插入一个值为x的节点
        }
        else if(op=="R"){
            cin>>x;
            insert(l[1],x);
//在链表最右端插入一个节点,代表在右端点也就是下标为1的左边插入一个值为x的节点
//也就是在下标为l[1]的节点的右边插入一个值为x的节点
        }
        else if(op=="D"){
            cin>>k;
            remove(k+1);
        }
        else if(op=="IL"){
            cin>>k>>x;
            insert(l[k+1],x);
        }
        else{
            cin>>k>>x;
            insert(k+1,x);
        }
    }
    
    for(int i=r[0];i!=1;i=r[i]) cout<

AcWing 828. 模拟栈

/*
栈:先进后出
队列:先进先出
*/
#include
using namespace std;
const int N=100010;

int m,stk[N],tt;//tt始终指向栈中最后一个进去,最先出来的元素
//插入:stk[++tt]=x
//删除:tt--
//判断是否为空:tt>0
//栈顶:stk[tt]

int main()
{
    cin >> m;
    while (m -- )
    {
        string op;
        int x;

        cin >> op;
        if (op == "push")
        {
            cin >> x;
            stk[ ++ tt] = x;
        }
        else if (op == "pop") tt -- ;
        else if (op == "empty") cout << (tt ? "NO" : "YES") << endl;
        else cout << stk[tt] << endl;
    }

    return 0;
}

队列

AcWing 829. 模拟队列

#include
using namespace std;
const int N=100010;
//在队尾插入元素,在队头删除元素
int q[N],hh,tt=-1;
//tt为0或者-1取决于在队尾插入元素,q[++tt]=x时,队列中是否已经有元素
//插入:q[++tt]=x  和栈一样
//弹出:hh++ 栈是tt--
//判断是否为空:hh<=tt 栈是tt>0

int main()
{
    int m;
    cin >> m;

    while (m -- )
    {
        string op;
        int x;

        cin >> op;
        if (op == "push")
        {
            cin >> x;
            q[ ++ tt] = x;
        }
        else if (op == "pop") hh ++ ;
        else if (op == "empty") cout <<(hh <= tt ? "NO" : "YES") << endl;
        else cout << q[hh] << endl;
    }

    return 0;
}

扩展:循环队列

// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;

// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;

// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;

// 队头的值
q[hh];

// 判断队列是否为空
if (hh != tt)

单调栈

AcWing 830. 单调栈

/*
给定一个长度为N的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出-1
暴力做法 时间复杂度O(n^2)
int main(){
    cin>>n;
    for(int i=0;i>a[i];
    cout<<-1<<' ';//数组第一个元素对应的输出一定是-1
    for(int i=1;i=0;j--)//内层循环遍历外层遍历的数之前的所有数
            if(a[j]=a[j]则a[i]一定不会作为j后面的元素的答案输出
单调栈:遍历每个元素,入栈并保证栈的单调递减/增的性质
常见模型:找出每个数左边第一个比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )//可变
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}
时间复杂度 O(n),因为每个元素一定会入栈一次,可能会出栈一次,总体来看是n级别
*/

#include 
using namespace std;

const int N = 100010;
int stk[N], tt;

int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        cin>>x;
/*
找每个数左边第一个比它小的元素,所以是单调递增栈
如果求左边第一个比它大的元素,stk[tt]>=x改成<,仅此
每个元素入栈时必须保证它是栈中最大的元素,栈中不能有元素比它更大
所以删除之前单调栈栈顶元素的循环条件是栈非空并且栈顶元素大于等于它
*/
        while (tt && stk[tt] >= x) tt --;
        if (!tt) cout<<-1<<' ';
        else cout<

单调队列

AcWing 154. 滑动窗口

/*
常见模型:找出滑动窗口中的最大值/最小值
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
    while (hh <= tt && check_out(q[hh])) hh ++ ;  // 判断队头是否滑出窗口
    while (hh <= tt && check(q[tt], i)) tt -- ;
    q[ ++ tt] = i;
}
*/
#include 
using namespace std;

const int N = 1000010;
int a[N], q[N];

int main()
{
    int n, k;
    cin>>n>>k;
    for (int i = 0; i < n; i ++ ) cin>>a[i];

    int hh = 0, tt = -1;
/*
队列中存储的是下标索引,这样才能时刻保证队列长度小于等于窗口
每次遍历是准备把一个元素的下标入队
但必须保证这个下标入队后,队列仍然单调递增(因为此时是求滑动窗口最小值)
*/
    for (int i = 0; i < n; i ++ )
    {
/*
首先判断当这个元素的下标入队后,当前的队头元素是否会滑出窗口
i - k + 1 代表当前元素的下标i入队后,队列大小为k的队头元素的下标
而且又因为每次遍历最多一个下标入队,一个下标出队,所以if就行,不需要模板的while
*/
        if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;
//注意这里的q[tt]和i都是数组a的下标
        while (hh <= tt && a[q[tt]] >= a[i]) tt -- ;
        q[ ++ tt] = i;
/*
另外这里是先把下标入队之后再去判断是否要输出答案,所以不需要像单调栈一样
先考虑跳出while循环是否因为队列为空,是否要输出-1
*/
        if (i >= k - 1) cout< q[hh]) hh ++ ;

        while (hh <= tt && a[q[tt]] <= a[i]) tt -- ;
        q[ ++ tt] = i;

        if (i >= k - 1) cout<

KMP

AcWing 831. KMP字符串

/*
//朴素做法 时间复杂度O(mn)
for(int i = 1; i <= n; i++) {//遍历字符串s中每一个字母,探索从当前所遍历的字母开始时
//能否与p完全匹配
    bool flag = true;
    for(int j = 1; j <= m;j++) {
        if(s[i+j-1] != p[j]) { //视频中是if(s[i]!=p[j]
            flag = false;
            break;
        }        
    }
}
每次匹配过程中,当前一部分完全匹配,s中i所指向的字母与p中j+1所指向的字母不相等时
朴素做法是只往后移动一位,比较浪费前部分已经匹配的信息
KMP思路是能否利用前一部分完全匹配这个信息,多移动几位,并且保证
移动之后仍然去探索s中下标i所指向的字母与p中更新后的下标j+1所指向的字母是否相等
而s中下标i所指向的字母前面那部分与p中更新后的下标j+1所指向的字母前面那部分确定匹配
因此引出最长前缀后缀这个概念,而最长是因为这样移动较少,确定相匹配的字母越多

// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
求模式串的Next数组:
for (int i = 2, j = 0; i <= m; i ++ )
{
    while (j && p[i] != p[j + 1]) j = ne[j];
    if (p[i] == p[j + 1]) j ++ ;
    ne[i] = j;
}

// 匹配
for (int i = 1, j = 0; i <= n; i ++ )
{
    while (j && s[i] != p[j + 1]) j = ne[j];
    if (s[i] == p[j + 1]) j ++ ;
    if (j == m)
    {
        j = ne[j];
        // 匹配成功后的逻辑
    }
}
*/
#include 
using namespace std;

const int N = 100010, M = 1000010;
int n, m;
int ne[N];
char s[M], p[N];

int main()
{//KMP下标习惯从1开始
    cin >> n >> p + 1 >> m >> s + 1;
//求ne数组过程与下面KMP匹配过程非常相似,p可以看成下面KMP匹配的s
//p中以每个字母结尾的p中的那部分字符串可以看成下面的p
    for (int i = 2, j = 0; i <= n; i ++ )
    {//本来也是从1开始,但是对于ne数组而言,ne[1]==0,ne[2]开始可能为1,所以从2开始
        while (j && p[i] != p[j + 1]) j = ne[j];
        if (p[i] == p[j + 1]) j ++ ;
//此时的i与原来的j+1,现在++之后的j匹配,因此得到此时ne[i]=j
        ne[i] = j;
    }
/*
遍历s中每一个字母,每一个字母都可能是刚好从它开始不相等的字母
并且是s中i所指向的字母与p中j+1所指向的字母去比较,所以i既然从1开始,
与之对应的j+1也是从1开始,所以j从0开始
而之所以是j+1与i对应,是因为当s中的某个下标对应的字母和
p中某个下标对应的字母不相等时,我们要去使用的最长前缀后缀长度是
p中不相等的那个下标对应的字母的前面那个字母,所以令前面那个下标为j
s中不相等的那个下标为i,因此得到i与j+1对应
*/
    for (int i = 1, j = 0; i <= m; i ++ )//KMP匹配过程
    {
/*
当s中i所指向的字母与p中j+1所指向的字母不相等时,让j退一点去看j+1与i能否对应
所以while循环的思路是指当j还可以退,也就是不为0时,并且此时的i与j+1还是不相等的话
那j就再退一点
*/
        while (j && s[i] != p[j + 1]) j = ne[j];
//如果跳出循环的原因是因为s中i所指向的字母与p中j+1所指向的字母相等的话,j向后移动
//而i每次遍历都会往后移动一位
        if (s[i] == p[j + 1]) j ++ ;
        if (j == n)
        {
//这里注意此时的j是已经+1之后的j,也就是此时j与i各自所指向的字母是匹配的
//也就是此时输出的s中的起点下标按理说应该是i-n+1,但注意题中所要求的的下标从0开始
//只是我们编程时假定下标从1开始,所以输出的起点下标应该是i-n
//最后在更新一下j
            printf("%d ", i - n);
            j = ne[j];
        }
    }

    return 0;
}

Trie

AcWing 835. Trie字符串统计

/*
用来高效存储和查找字符串集合的数据结构
*/
#include
using namespace std;
const int N=100010;
/*
son[][]存储树中每个节点的子节点,题目告诉字符串仅包含小写英文字母
所以每一个节点最多有26个子节点
*/
int son[N][26];
//表示以当前节点结尾的单词有多少个
int cnt[N];
/*
idx与单链表中idx不一样,这里的idx表示最后一个插入字母的下标,新字母创建时
对应的下标是++idx,而单链表中的idx直接就可以用来分配给新的节点
下标是0的点,既是根节点,也是空节点
注意这里先++idx别死记硬背,这里是因为要用idx=0表示空节点,因此当你初始化idx为0时
给新节点的赋值必须先++之后再赋值,所以你也完全可以先初始化idx=1,之后赋值是idx++
*/
int idx;

void insert(char str[]){
    int p=0;//从根节点开始向下探索
    for(int i=0;str[i];i++){
//遍历待插入字符串的每一个字符,因为cpp中字符串结尾是\0
//所以可以作为循环条件去判断
        int u=str[i]-'a';//当前遍历字母对应的子节点编号
        if(!son[p][u]) son[p][u]=++idx;
//如果当前探索节点不存在这个字符子节点的话,创建出来
        p=son[p][u];//向下更新当前探索节点
    }
    cnt[p]++;
//遍历结束时,此时的p表示待插入字符串的最后一个节点,此时以这个节点结尾的单词数加1
}

int query(char str[]){
    int p=0;
    for(int i=0;str[i];i++){
        int u=str[i]-'a';
        if(!son[p][u]) return 0;
        p=son[p][u];
    }
    
    return cnt[p];
}

int main(){
    int n;
    cin>>n;
    char str[N];
    while(n--){
        string op;
        cin>>op>>str;
        if(op=="I") insert(str);
        else cout<

AcWing 143. 最大异或对

/*
二进制XOR运算是两者相同为0,不同为1
暴力
for(int i=0;i
#include
using namespace std;
const int N=100010,M=31*N;

int n;
int a[N];
int son[M][2],idx;
//trie树中每一条分支代表一个数,每条分支有31个节点,所以最多有310万个节点

void insert(int x){
    int p=0;//从根节点开始探索
    for(int i=30;i>=0;i--){//从最高位开始探索
        int u=x>>i&1;//得到x的第i位二进制
        if(!son[p][u]) son[p][u]=++idx;
        p=son[p][u];
    }
//不用像字符串一样记录每个节点结尾的单词有多少个
//是因为每一个数都有31位,不会存在覆盖的情况
}

int query(int x){
    int p=0,res=0;
    for(int i=30;i>=0;i--){
        int u=x>>i&1;
        if(son[p][!u]) p=son[p][!u],res=res*2+!u;//尽量往与当前这一位不同的方向走
        else p=son[p][u],res=res*2+u;
    }
    return res;
}

int main(){
    cin>>n;
    for(int i=0;i>a[i];
    
    int res=0;
    for(int i=0;i

并查集

(1)朴素并查集:

    int p[N]; //存储每个点的祖宗节点

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) p[i] = i;

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);


(2)维护size的并查集:

    int p[N], size[N];
    //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        size[i] = 1;
    }

    // 合并a和b所在的两个集合:
    size[find(b)] += size[find(a)];
    p[find(a)] = find(b);


(3)维护到祖宗节点距离的并查集:

    int p[N], d[N];
    //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离

    // 返回x的祖宗节点
    int find(int x)
    {
        if (p[x] != x)
        {
            int u = find(p[x]);
            d[x] += d[p[x]];
            p[x] = u;
        }
        return p[x];
    }

    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
    {
        p[i] = i;
        d[i] = 0;
    }

    // 合并a和b所在的两个集合:
    p[find(a)] = find(b);
    d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量

AcWing 836. 合并集合

/*
1.将两个集合合并
2.查询两个元素是否在一个集合当中
时间复杂度可以优化成近乎O(1),快速支持两个操作

基本原理:每个集合用树维护,根节点编号就是当前集合编号,对于每一个节点存储其父节点p[i]

问题1 如何判断树根:if(p[x]==x)
问题2 如何求节点x的集合编号:while(p[x]!=x) x=p[x];
注意此处最为耗时,时间复杂度为树的深度,在此处路径优化,将路径上所有点直接指向根节点
也就是每探索一个节点,将这个节点到根节点探索路径上的所有节点指向根节点,
之后对于这一些节点来说,判断其所属集合时间复杂度为O(1) 但因为是部分节点,所以是近乎
也注意此时的while->if 
问题3 如何合并两个集合:p[x]=y 根节点相连
*/
#include
using namespace std;
const int N=100010;

int p[N];//表示每个节点对应的父节点

int find(int x){//返回x的根节点(所在集合的编号)+路径压缩
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}

int main(){
    int n,m;
    cin>>n>>m;
    for(int i=0;i>op>>a>>b;
        
        if(op[0]=='M'){
            if(find(a)==find(b)) continue;
            else p[find(a)]=find(b);
        }
        else{
            if(find(a)==find(b)) cout<<"Yes"<

AcWing 837. 连通块中点的数量

/*
并查集扩展:
维护size的并查集(要动态知道每个集合当前有多少元素)
int p[N], size[N];
p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}
初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
    p[i] = i;
    size[i] = 1;
}
合并a和b所在的两个集合:
size[find(b)] += size[find(a)];
p[find(a)] = find(b);
*/
#include
using namespace std;
const int N=100010;
int p[N],cnt[N];

int find(int x){
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}

int main(){
    int n,m;
    cin>>n>>m;
    for(int i=0;i>op;
        if(op=="C"){
            cin>>a>>b;
            a = find(a), b = find(b);
            if(a!=b)
            {
                p[a]=b;
                cnt[b]+=cnt[a];
            }
        }
        else if(op=="Q1") {
            cin>>a>>b;
            if(find(a)==find(b)) cout<<"Yes"<>a;
            cout<

AcWing 240. 食物链

/*
并查集维护额外信息,并查集中每个集合是一棵树的形式,不管题目中告诉的两个元素是同类还是异类,只要知道两个动物的关系,就把他们放到一个集合里面中,
就可以推断出来同一个集合中所有动物的关系,那么如何确定他们之间的关系呢?
记录每个点与根节点之间的距离,表示两者的关系,从而推断出任意两点之间的关系,如果某个点到根节点距离为1的话,表示他可以吃根节点,
距离为2表示他可以被根节点吃,距离为3的话表示与根节点同类。
比如在一个组织中,我不需要知道任意两个人之间的关系,这样的话是n^2级别,只需要知道每个人与领袖之间的关系,然后可以推断出来任意两个人之间的关系
*/
#include 
using namespace std;

const int N = 50010;

int n, m;
int p[N], d[N];

int find(int x)
{
    if (p[x] != x)
    {
        int t = find(p[x]);
        d[x] += d[p[x]];
        p[x] = t;
    }
    return p[x];
}

int main()
{
    cin>>n>>m;

    for (int i = 1; i <= n; i ++ ) p[i] = i;

    int res = 0;
    while (m -- )
    {
        int t, x, y;
        cin>>t>>x>>y;

        if (x > n || y > n) res ++ ;
        else
        {
            int px = find(x), py = find(y);
            if (t == 1)
            {
                if (px == py && (d[x] - d[y]) % 3) res ++ ;
                else if (px != py)
                {
                    p[px] = py;
                    d[px] = d[y] - d[x];//因为xy同类,所以(d[px]+d[x]-d[y])%3==0
                }
            }
            else
            {
                if (px == py && (d[x] - d[y] - 1) % 3) res ++ ;
                else if (px != py)
                {
                    p[px] = py;
                    d[px] = d[y] + 1 - d[x];
                }
            }
        }
    }

    cout<

AcWing 838. 堆排序

/*
手写堆:一棵完全二叉树
1.插入一个数 
    heap[++size]=x; up(size);
2.求集合中的最小值 
    heap[1]
3.删除最小值 一维数组删除头结点很困难,但是删除尾节点很方便
    heap[1]=heap[size];size--;down(1);
(前三个STL中优先队列支持)
4.删除任意一个元素
    heap[k]=heap[size];size--;down(k);up(K);
    前两步操作之后可能变大也可能变小,所以直接down(k);up(K);这两步只会执行一步
5.修改任意一个元素
    heap[k]=x;down(k);up(k);


以小根堆为例,每一个点都小于等于左右儿子,根节点是整棵树的最小值
堆的存储:一维数组来存储
1号点是根节点,节点x的左儿子是2x,右儿子是2x+1
两个基本操作:时间复杂度与树的高度成正比,logn
up(x):往上调整 堆中某个元素变小之后需要往上调整
down(x):往下调整 堆中某个元素变大之后需要往下调整
下标从1开始,如果从0开始,0号节点的左儿子还是0,冲突

这道题是堆排序,就是先把整个待排序的数组建成堆,每次输出堆顶元素
首先要建堆,然后输出堆顶,然后再把堆顶删掉 这些操作只需要down操作
求最小值O(1) 插入和删除都是O(logn)
*/
#include 
#include 
using namespace std;

const int N = 100010;
int n, m;
int h[N], cnt;//cnt表示当前有多少元素

void down(int u)
{
    int t = u;//用t表示三个点的最小值的节点编号
    //前者是判断有无左右儿子
    if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t)
    {
        swap(h[u], h[t]);
        down(t);//
    }
}

void up(int u)
{
    while (u / 2 && h[u] < h[u / 2])
    {
        swap(h[u], h[u / 2]);
        u >>= 1;
    }
}


int main()
{
    cin>>n>>m;
    for (int i = 1; i <= n; i ++ ) cin>>h[i];//下标从1开始
    cnt = n;
    /*
    如何建堆:一个一个元素往堆里面插的时间复杂度是O(nlogn)
    O(n)的建堆方式 从n/2 down 到1就行
    n/2也就是从倒数第二层开始往下down,倒数第二层n/4个元素down 1次
    倒数第三层n/8个元素down 2次...依次递推
    n/4+n/8*2+n/16*3+...

AcWing 839. 模拟堆

/*
注意最后两个操作是删除或修改第k个插入的元素(这里与单链表双链表中类似)
所以还需要快速找到第k个插入的元素
开额外两个数组
*/
#include 
#include 
using namespace std;

const int N = 100010;
int h[N], ph[N], hp[N], cnt;
/*
ph[k]存的是第k个插入的元素在堆中的下标
hp[k]存的是堆中下标是k的节点是第几个插入的点
之前操作的时候,不涉及到第几个插入,所以仅交换值就行,而这里不行,
交换时候需要知道两个节点各自是第几个插入的节点,然后再把把第几个插入的元素在堆中的下标改变
*/

void heap_swap(int a, int b)
{
    swap(ph[hp[a]],ph[hp[b]]);
    swap(hp[a], hp[b]);
    swap(h[a], h[b]);
}

void down(int u)
{
    int t = u;
    if (u * 2 <= cnt && h[u * 2] < h[t]) t = u * 2;
    if (u * 2 + 1 <= cnt && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
    if (u != t)
    {
        heap_swap(u, t);
        down(t);
    }
}

void up(int u)
{
    while (u / 2 && h[u] < h[u / 2])
    {
        heap_swap(u, u / 2);
        u >>= 1;
    }
}

int main()
{
    int n, m = 0;//m表示第几个插入的元素
    cin>>n;
    while (n -- )
    {
        string op;
        int k, x;
        cin>>op;
        if (op=="I")
        {
            cin>>x;
            cnt ++ ;
            m ++ ;
            ph[m] = cnt, hp[cnt] = m;
            h[cnt] = x;
            up(cnt);
        }
        else if (op=="PM") cout<>k;//此时的k是第k个插入
            k = ph[k];//现在的k是第k个插入的元素在堆中的下标
            heap_swap(k, cnt);
            cnt -- ;
            up(k);
            down(k);
        }
        else
        {
            cin>>k>>x;
            k = ph[k];
            h[k] = x;
            up(k);
            down(k);
        }
    }

    return 0;
}

哈希表

AcWing 840. 模拟散列表

/*
哈希表:
    存储结构:
        开放寻址法
        拉链法
    字符串哈希方式
把一个比较庞大的空间,值域,映射到比较小的空间,比如把(0,1e9)这些数映射到(0,1e5)
这道题中操作个数是1e5,每一个数的范围最多是1e9,值域较大,从中选一些(最多1e5)数插入或查询
通过一个哈希函数h(x)(输入在-1e9到1e9,输出在0到1e5)
哈希函数怎么写:一般可以写成x mod 1e5
(直接取模就行,模的这个数一般取尽可能离2的整次幂尽可能远的一个质数,数学可证明,这样冲突概率最小)
冲突:因为值域比较大,映射范围比较小,可能把两个不同的数映射为同一个数,所以需要处理冲突
按照冲突处理方式,一般分为开放寻址法  拉链法
之前讲的离散化是一种特殊的哈希方式(需要保序),这里是一般意义的哈希
*/
/*
拉链法:开一个一维数组来存储所有哈希值,比如开一个1e5的数组,如何处理冲突 
如果两个数冲突,就会用一条链全部存下来,平均情况下,每条链的长度可以看做一个常数,所以一般情况下,哈希表
时间复杂度可以看成O(1),算法题中一般情况下只会从哈希表中插入,查找元素,一般不会删除元素
如果要实现删除的话,不会真的把点删掉,一般是开一个数组,打一个标记,开一个布尔变量,记录即可
(1) 拉链法
    int h[N], e[N], ne[N], idx;

    // 向哈希表中插入一个数
    void insert(int x)
    {
        int k = (x % N + N) % N;
        e[idx] = x;
        ne[idx] = h[k];
        h[k] = idx ++ ;
    }

    // 在哈希表中查询某个数是否存在
    bool find(int x)
    {
        int k = (x % N + N) % N;
        for (int i = h[k]; i != -1; i = ne[i])
            if (e[i] == x)
                return true;

        return false;
    }
*/
#include 
#include 
using namespace std;

const int N = 100003;//它是大于1e5的第一个质数,注意如何求质数
int h[N], e[N], ne[N], idx;
//h数组类似于每条拉链的槽或者说头结点编号,拉链就是链表,就是存储图用到的邻接表

void insert(int x)
{   
//首先想一个哈希函数,把x映射到从0到N之间的数 x%N在C++中结果正负由x的正负决定,再+N %N之后必然为正数
    int k = (x % N + N) % N; //这个操作本身和高精度减法很像
    e[idx] = x;//接下来就是链表插入头节点操作
    ne[idx] = h[k];
    h[k] = idx ++ ;
}

bool find(int x)
{
    int k = (x % N + N) % N;
    for (int i = h[k]; i != -1; i = ne[i])
        if (e[i] == x)
            return true;

    return false;
}

int main()
{
    int n;
    cin>>n;

    memset(h, -1, sizeof h);//清空数组

    while (n -- )
    {
        string op;
        int x;
        cin>>op>>x;

        if (op == "I") insert(x);
        else
        {
            if (find(x)) puts("Yes");
            else puts("No");
        }
    }

    return 0;
}
/*
开放寻址法
只开了一个一维数组,没有开链表,但是数组长度经验上来说要开到题目数据范围的两到三倍
比如这道题中输入了1e5个数,数组长度开到2e5到3e5,这是一个经验值,这样冲突概率较低
(2) 开放寻址法
    int h[N];

    // 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
    int find(int x)
    {
        int t = (x % N + N) % N;
        while (h[t] != null && h[t] != x)
        {
            t ++ ;
            if (t == N) t = 0;
        }
        return t;
    }
*/
#include 
#include 
using namespace std;
//大于2e5的一个最小的质数
const int N = 200003, null = 0x3f3f3f3f;
int h[N];

int find(int x)//如果x在哈希表中存在,返回x所在的位置,如果不存在,返回它应该存储的位置
{
    int t = (x % N + N) % N;
    while (h[t] != null && h[t] != x)//循环条件是这个位置存储元素并且这个元素不是x
    {//跳出循环的条件要么是这个位置没有存储元素,要么是存储了元素,并且这个元素就是x
        t ++ ;
        if (t == N) t = 0;//探索到最后一个位置,然后再循环看第一个位置
    }
    return t;
}

int main()
{
    memset(h, 0x3f, sizeof h);
    //h数组存储的是每一个插入的数,初始化为+-1e9以外的数表示这个位置没有存放元素
   //按字节来0x3f,所以int4个字节,每个int是0x3f3f3f3f
    int n;
    cin>>n;

    while (n -- )
    {
        string op;
        int x;
        cin>>op>>x;
        if (op == "I") h[find(x)] = x;
        else
        {
            if (h[find(x)] == null) puts("No");
            else puts("Yes");
        }
    }

    return 0;
}

AcWing 841. 字符串哈希

/*
核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果

typedef unsigned long long ULL;
ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64

// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
    h[i] = h[i - 1] * P + str[i];
    p[i] = p[i - 1] * P;
}

// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

字符串哈希是一个比较重要的哈希方式,很多字符串问题都可以用哈希来做,不一定非要kmp
字符串前缀哈希法:先预处理出来字符串所有前缀的哈希值
h[0]表示前0个字母的哈希值,h[i]表示前i个字母的哈希值
如何定义某一个前缀的哈希值:把字符串看作一个p进制的数
比如“ABCD”看成p进制的1234 所有可以转化成一个数字:1*p^3+..4*p^0得到的数可能很大,所以取模Q
这样就可以把任意一个字符串映射到0(去掉)1到Q-1中的数
注意:一般情况下不能把某一个字母映射成数字0,这样的话比如a映射为0,aaa同样映射为0,
这样会把不同字符串映射成同一个数字
哈希数字可能存在冲突,这里是假定不存在冲突,经验值是p取131或13331,Q取2^64一般不会冲突,这里有一个
小技巧,h数组中所有前缀的哈希值都用unsigned long long来存,这样溢出就相当于取模
配合前缀哈希后,可以利用前缀哈希算出任意一个子串的哈希值
已知从1到l-1,1到r的哈希值h[l-1],h[r]

这道题首先先求出来原字符串所有前缀哈希值,算出两个区间字符串的哈希值,
这两个哈希值都是从0到2^64-1的一个数,哈希值相同就认为两个字符串相同
*/
#include 
#include 
using namespace std;

typedef unsigned long long ULL;//用unsigned long long存储所有h,溢出就相当于取模

const int N = 100010, P = 131;

int n, m;
char str[N];
ULL h[N], p[N];
//前者存储给定字符串所有前缀的哈希值,h[0]前0个字母的哈希值,h[2]前2个字母的哈希值,后者存储次方,这里预处理出来

ULL get(int l, int r)
{//从l到r的哈希值
    return h[r] - h[l - 1] * p[r - l + 1];//把h[l-1]左移若干位与h[r]对齐
}

int main()
{
    cin>>n>>m>>str + 1;//字符串下标从1开始

    p[0] = 1;//p的0次方==1
    for (int i = 1; i <= n; i ++ )
    {//预处理所有前缀的哈希值和次方
        h[i] = h[i - 1] * P + str[i];
        p[i] = p[i - 1] * P;
    }

    while (m -- )
    {
        int l1, r1, l2, r2;
        cin>>l1>>r1>>l2>>r2;

        if (get(l1, r1) == get(l2, r2)) puts("Yes");
        else puts("No");
    }

    return 0;
}

AcWing 139. 回文子串的最大长度

/*
左右两半边子串相等,左边子串正序哈希值和右边子串倒序哈希值相等
回文串两大类,长度分为奇数和偶数
先看奇数类,枚举中心点,二分求出当前中心点的最大半径
对原来字符串变形,使得所有回文串长度都是奇数,在每两个字母之间加上一个没出现的字符即可
*/
#include 
#include 
using namespace std;

typedef unsigned long long ULL;
const int N = 2000010, base = 131;

char str[N];
ULL hl[N], hr[N], p[N];

ULL get(ULL h[], int l, int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

int main()
{
    int T = 1;
    while (scanf("%s", str + 1), strcmp(str + 1, "END"))
    {
        int n = strlen(str + 1);
        for (int i = n * 2; i; i -= 2)
        {
            str[i] = str[i / 2];
            str[i - 1] = 'a' + 26;
        }
        n *= 2;
        p[0] = 1;
        for (int i = 1, j = n; i <= n; i ++, j -- )
        {
            hl[i] = hl[i - 1] * base + str[i] - 'a' + 1; 
            hr[i] = hr[i - 1] * base + str[j] - 'a' + 1;
            p[i] = p[i - 1] * base;
        }

        int res = 0, k=0;
        char str_res[N];
        for (int i = 1; i <= n; i ++ )
        {
            int l = 0, r = min(i - 1, n - i);
            while (l < r)
            {
                int mid = l + r + 1 >> 1;
                if (get(hl, i - mid, i - 1) == get(hr, n - (i + mid) + 1, n - (i + 1) + 1)) l = mid;
                else r= mid-1;
            }

            if (str[i - l] <= 'z') {
                if (l+1>res) res=l+1,k=i;
            }
            else{
                if (l>res) res=l,k=i;
            }
        }

        for(int j=k-res, m=0;j<=k+res;j++){
            if(str[j]<='z') str_res[m++]=str[j];
        }

        cout<

C++ STL简介

/*
vector, 变长数组,倍增的思想 系统为某一程序分配空间时,所需时间与空间大小无关,与申请次数有关
所有变长数组要尽量减少申请空间的次数,每次长度不够时,就把长度乘于2,然后把之前元素copy过来
所有平均情况下,插入一个数的时间复杂度是O(1)
长度为n,开辟空间次数是logn,额外copy次数均摊下来是O(1)
    vector a;//定义一个vector   
    vector b(10);//初始化一个长度为10的vector
    vector c(10,-3);//初始化一个长度为10的vector,里面每个数都是3
    vector a[10];//定义一个数组,里面有10个vector
    size()  返回元素个数
    empty()  返回是否为空
    clear()  清空
    front()/back() 返回vector的第一个和最后一个数
    push_back()/pop_back() 想vector最后插入一个数/把vector最后一个数删掉
    begin()/end() 迭代器 begin是vctor的第0个数,end是vector的最后一个数的后面一个数
    []
    支持比较运算,按字典序

pair  
一般是某个东西有两种不同属性,用一个pair来存,把要排序的关键字放入first,不需要排序的关键字放入second
    first, 第一个元素
    second, 第二个元素
    支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
    pair p;
    p={3,"abc"};//初始化方式

string,字符串
    size()/length()  返回字符串长度
    empty()
    clear()
    substr(起始下标,(子串长度))  返回子串
    c_str()  返回字符串所在字符数组的起始地址
    string a="yxc";
    a+="hgg";

queue, 队列  queue没有clear这个函数
    size()
    empty()
    push()  向队尾插入一个元素
    front()  返回队头元素
    back()  返回队尾元素
    pop()  弹出队头元素

priority_queue, 优先队列,默认是大根堆
    size()
    empty()
    push()  插入一个元素
    top()  返回堆顶元素
    pop()  弹出堆顶元素
    定义成小根堆的方式:priority_queue, greater> q;

stack, 栈
    size()
    empty()
    push()  向栈顶插入一个元素
    top()  返回栈顶元素
    pop()  弹出栈顶元素

deque, 双端队列
    size()
    empty()
    clear()
    front()/back()
    push_back()/pop_back()
    push_front()/pop_front()
    begin()/end()
    []

set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
    size()
    empty()
    clear()
    begin()/end()
    ++, -- 返回前驱和后继,时间复杂度 O(logn)

    set/multiset
        insert()  插入一个数
        find()  查找一个数
        count()  返回某一个数的个数
        erase()
            (1) 输入是一个数x,删除所有x   O(k + logn)
            (2) 输入一个迭代器,删除这个迭代器
        lower_bound()/upper_bound()
            lower_bound(x)  返回大于等于x的最小的数的迭代器
            upper_bound(x)  返回大于x的最小的数的迭代器
    map/multimap
        insert()  插入的数是一个pair
        erase()  输入的参数是pair或者迭代器
        find()
        []  注意multimap不支持此操作。 时间复杂度是 O(logn)
        lower_bound()/upper_bound()

unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
    和上面类似,增删改查的时间复杂度是 O(1)
    不支持 lower_bound()/upper_bound(), 迭代器的++,--

bitset, 圧位
    bitset<10000> s;
    ~, &, |, ^
    >>, <<
    ==, !=
    []

    count()  返回有多少个1

    any()  判断是否至少有一个1
    none()  判断是否全为0

    set()  把所有位置成1
    set(k, v)  将第k位变成v
    reset()  把所有位变成0
    flip()  等价于~
    flip(k) 把第k位取反
*/

搜索与图论

DFS

AcWing 842. 排列数字

/*
两者都可以遍历整个空间搜索
两者对比:
数据结构:DFS(栈) BFS(队列)
使用空间:
DFS只需要记录这条搜索路径上所有点即可,与树的高度成正比O(h),使用空间较少
BFS会把每一层的所有节点存下来,使用空间对于高度而言是指数级别O(2^h),使用空间较大
最短路:
在每条边权值为1的前提下,BFS搜索到的点具有最短路性质,DFS则不具有,一般题目问最短距离,
最少步数,最少操作几次,基本上都是BFS

DFS俗称暴力搜索,最重要的是按照怎样的顺序遍历,以及回溯时注意恢复现场
回溯与剪枝

给定一个数字n,把1-n的所有全排列按照字典序输出,依次判断每一位可以放哪些元素去搜索
*/
#include
using namespace std;
const int N=10;

int n;
int path[N];
bool st[N];//表示当前搜索过程中数字1~n哪些被用过

void dfs(int u){
    if(u==n){
        for(int i=0;i>n;
    
    dfs(0);//从第0个位置开始看
    
    return 0;
}

AcWing 843. n-皇后问题

//按照上题全排列搜索,按行枚举 时间复杂度O(n*n!)
#include
using namespace std;
const int N=10;

int n;
char g[N][N];
bool col[N],dg[2*N],udg[2*N];//对角线个数是2n-1,开两倍

void dfs(int u){
    if(u==n){
        for(int i=0;i>n;
    for(int i=0;i

BFS

AcWing 844. 走迷宫

/*
优势:搜到最短路(只有当所有边的权值为1的前提下,才能用bfs求最短路),一层一层去搜
宽搜常用框架:
初始状态放入queue中
while(queue不空)
{
    t<-队头元素 拿出队头元素
    扩展队列
}
*/
#include
#include
using namespace std;
const int N=110;
int n,m;
int g[N][N],d[N][N];
typedef pair PII;
PII q[N*N],pre[N][N];//前者是自己实现队列,队列中每一个元素都是坐标pair,后者是多记录路径

int bfs(){
    int hh=0,tt=0;
    q[0]={0,0};
    memset(d,-1,sizeof d);//将每个点到起点的距离都置为-1,表示这个点没有探索过
    d[0][0]=0;
    
    int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};
    
    while(hh<=tt){
        auto t=q[hh++];
        
        for(int i=0;i<4;i++){
            // //对于当前节点t,依次探索四个方向是否可行
            int x=t.first+dx[i],y=t.second+dy[i];
            if(x>=0&&x=0&&y>n>>m;
    for(int i=0;i>g[i][j];
    }
    
    cout<

AcWing 845. 八数码

/*
从最初状态到最末状态最少需要走多少步,最短路问题+每步权值为1,所以可以用BFS
状态表示:一般来说,每个状态用一个数字表示就行,这道题每个状态是一个3*3的小矩阵,
用字符串表示
BFS特有:
queue:如何存储状态 queue< string >
distance:如何记录到达每个状态的距离 unordered_map
状态转移:一维坐标转化为二维坐标
*/
#include
#include//存储所有距离
#include
#include
#include

using namespace std;

int bfs(string start){
    string end="12345678x";
    queue q;
    unordered_map d;
    
    q.push(start);
    d[start]=0;
    
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    while(q.size()){
        auto t=q.front();
        q.pop();
        
        int dist=d[t];
        
        if(t==end) return dist;//先判断是否为终点
        
        //状态转移
        int k=t.find('x');//找到字符x在t中的一维坐标
        int x=k/3,y=k%3;//将一维坐标转化为二维坐标
        for (int i = 0; i < 4; i ++ )
        {
            int a = x + dx[i], b = y + dy[i];
            if (a >= 0 && a < 3 && b >= 0 && b < 3)
            {
                swap(t[a * 3 + b], t[k]);//交换(a,b)和(x,y)
                if (!d.count(t))
                {
                    d[t] = dist + 1;
                    q.push(t);
                }
                swap(t[a * 3 + b], t[k]);//恢复现场
            }
        }
    }

    return -1;
}
int main(){
    string start;//初始状态,用字符串存储
    for(int i=0;i<9;i++){
        char s;
        cin>>s;
        start+=s;
    }
    cout<

树与图的存储

/*
树是无环连通图,只考虑图的存储。图分为有向图与无向图,对于无向图来说,存储两条边,
所以只考虑有向图的存储
两大类存储方式
1.邻接矩阵:g[a][b] 存储边a->b的信息,空间复杂度较大,n^2,针对稠密图存储
2.邻接表(常用)
*/
#include 
using namespace std;
const int N = 100010, M = N * 2;
//对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
//e[M], ne[M]表示对于每一条边来说所指向的那个节点的值和next节点
//在树中,每个节点最多可能被两条边所指向,所以M=2*N
int h[N], e[M], ne[M], idx;
// 添加一条边a->b,先找到a对于的单链表,把b插到单链表的头结点
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

int main()
{
    // 初始化
    idx = 0;
    memset(h, -1, sizeof h);//单链表初始化,让所有头结点指向-1
}

树与图的深度优先遍历

AcWing 846. 树的重心

/*
时间复杂度 O(n+m), n 表示点数,m 表示边数
bool st[N];//存储哪些点被遍历过
void dfs(int u)//dfs节点u
{
    st[u] = true; // st[u] 表示点u已经被遍历过

    for (int i = h[u]; i != -1; i = ne[i])//对节点u所指向所有未被遍历过的节点dfs
    {
        int j = e[i];
        if (!st[j]) dfs(j);
    }
}
*/
#include //memset需要
#include 

using namespace std;

const int N = 100010, M = N * 2;

int n;
int h[N], e[M], ne[M], idx;
int ans = N;//全局答案
bool st[N];

void add(int a, int b)
{
//e数组存储的是节点本身,下标表示节点对应的下标
//ne数组下标表示当前节点对应的下标,存储的是当前节点所指向下一个节点的下标
//h数组存储的是节点对应的下标,下标表示节点本身
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
//深度遍历每个节点,找到将这个节点删除后,剩余连通块中点数的最大值
//对于每个节点,找到点数的最小值
//DFS可以算出子树节点数,所以对于每个节点,都可以找到将这个节点删除后剩余连通块数量

//dfs返回以u为根的子树的节点数
int dfs(int u)
{
    st[u] = true;

    int size = 0, sum = 1;//size表示删除节点u之后,每一个连通块节点数最大值
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j]){
            int s = dfs(j);//既可以表示以j为子树的节点数,也可以表示包含j的连通块大小
            size = max(size, s);
            sum += s;
        }
    }
    size = max(size, n - sum);
    ans = min(ans, size);
    return sum;
}

int main()
{
    cin>>n;

    memset(h, -1, sizeof h);
    //cout<>a>>b;
        add(a, b), add(b, a);
    }

    //dfs(1);
    dfs(n);//从哪个点开始搜都一样

    cout<

树与图的广度优先遍历

AcWing 847. 图中点的层次

/*
所有边的长度都是1告诉我们可以用宽搜来求最短路
queue q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);

while (q.size())//当队列不空时
{
    int t = q.front();
    q.pop();//取出队头元素

    for (int i = h[t]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true; // 表示点j已经被遍历过
            q.push(j);
        }
    }
}
*/
#include  //memset
#include 
#include 
//#include 

using namespace std;

const int N = 100010;

int n, m;
int h[N], e[N], ne[N], idx;
int d[N], q[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

int bfs()
{
    int hh=0,tt=0;//定义队头,队尾
    q[0]=1;//输入队头元素
    memset(d, -1, sizeof d);//初始化距离,-1表示未遍历过
    d[1] = 0;

    //queue q;
    //d[1] = 0;
    //q.push(1);

    //while (q.size())
    while(hh<=tt)//当队列不空时
    {
        // int t = q.front();
        // q.pop();
        int t=q[hh++];

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (d[j] == -1)//如果当前点未被遍历
            {
                d[j] = d[t] + 1;
                //q.push(j);
                q[++tt]=j;
            }
        }
    }

    return d[n];
}

int main()
{
    cin>>n>>m;
    memset(h, -1, sizeof h);//初始化表头

    for (int i = 0; i < m; i ++ )
    {//输入所有边
        int a, b;
        cin>>a>>b;
        add(a, b);
    }

    cout << bfs() << endl;

    return 0;
}

拓扑排序

AcWing 848. 有向图的拓扑序列

/*
求拓扑序是图的宽搜很经典的应用,首先拓扑序列是针对有向图来说,无向图没有拓扑序列
对于图中每一条有向边(x,y),x都出现在y的前面,则称这个点序列为图的拓扑序列,
也就是说所有边都是从前指向后的
存在环的话一定不存在拓扑序,可以证明一个有向无环图一定存在拓扑序列,所以有向无环图被称为
拓扑图,可以证明一个有向无环图至少存在一个入度为0的点
当前入度为0的可以作为起点,所以求拓扑序列的第一步是把所有入度为0的点入队,然后就是一个宽搜
的过程
时间复杂度O(n+m), n表示点数,m表示边数
bool topsort()
{
    int hh = 0, tt = -1;

    // d[i] 存储点i的入度
    for (int i = 1; i <= n; i ++ )
        if (!d[i])
            q[ ++ tt] = i;

    while (hh <= tt)//队列不空时
    {
        int t = q[hh ++ ];//取出队头元素

        for (int i = h[t]; i != -1; i = ne[i])//枚举所有出边
        {
            int j = e[i];
            if (-- d[j] == 0)//
                q[ ++ tt] = j;
        }
    }

    // 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
    return tt == n - 1;
}
*/
#include 
#include 

using namespace std;

const int N = 100010;

int n, m;
int h[N], e[N], ne[N], idx;//邻接表的存储方式
int d[N];//点的入度
int q[N];

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

bool topsort()
{
    int hh = 0, tt = -1;//tt为-1还是0取决于你在++t时,队中是否已经插入元素
    //迷宫那道题已经把起点入队了,所以从0开始

    for (int i = 1; i <= n; i ++ )
        if (!d[i])
            q[ ++ tt] = i;//队列中始终放入入度为0的元素

    while (hh <= tt)
    {
        int t = q[hh ++ ];

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (-- d[j] == 0)
                q[ ++ tt] = j;
        }
    }

    return tt == n - 1;//判断是否所有点都入队
}

int main()
{
    cin>>n>>m;

    memset(h, -1, sizeof h);

    for (int i = 0; i < m; i ++ )
    {
        int a, b;
        cin>>a>>b;
        add(a, b);

        d[b] ++ ;
    }

    if (!topsort()) puts("-1");
    else
    {
        for (int i = 0; i < n; i ++ ) cout<

Dijkstra

AcWing 849. Dijkstra求最短路 I

(最终总结)Acwing算法课_第1张图片

/*
单源最短路:起点与终点都确定
多源汇最短路:任意一个起点到任意一个终点

最短路算法考察侧重点是建图,如何定义点和边

朴素dijkstra算法
时间复杂是 O(n^2+m), n 表示点数,m 表示边数,比较适合稠密图,用邻接矩阵来存,
也就是边数较多的图,比如边数m与O(n^2)是一个级别
*/
#include 
#include 

using namespace std;

const int N = 510;

int n, m;
int g[N][N];  // 邻接矩阵存储每条边
int dist[N];  // 存储1号点到每个点的最短距离
bool st[N];   // 存储每个点的最短路是否已经确定

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;//首先初始化距离,只有起点距离为0,其余都为正无穷

    for (int i = 0; i < n - 1; i ++ )
    {
//n个点中除了起点之外,还有n-1个点最短距离未确定,每循环一次就可以确定一个点的最短距离
        int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        // 用t更新其他点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);

        st[t] = true;
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

int main()
{
    cin>>n>>m;

    memset(g, 0x3f, sizeof g);
    while (m -- )//读入n条边
    {
        int a, b, c;
        cin>>a>>b>>c;

        g[a][b] = min(g[a][b], c);//存在重边和自环,对于最短路来说,保留最短的边即可
    }

    cout<

AcWing 850. Dijkstra求最短路 II

/*
因为是稀疏图,所以存储方式改成邻接表形式
朴素版本,因为每次要找最小的数,所以可以用堆去优化,n^2优化成n,m优化成mlogn
(因为修改堆中元素的时间复杂度是logn),所以时间复杂是 O(mlogn), n 表示点数,m 表示边数,
比较适合稀疏图,也就是边数较多的图,比如边数与O(n)是一个级别
堆可以自己实现,用双映射方式支持修改堆里面任何一个元素,能始终保持堆中有n个元素,但较复杂
也可以是stl的优先队列,但优先队列不支持修改任何一个元素,
它是每次修改都往堆里面插入一个新的数,所以堆中有m个元素,所以时间复杂度是mlogm,
但m一般都小于n^2,所以带进去,时间复杂度可以看成mlogn,但这样堆中会存在很多冗余,
对应一个点,可能会有好几个更新前后的距离,因此遍历过程中,
当前已经找到的最小值可能之前已经确定过了,所以用st数组判断一下就行

堆优化版dijkstra  也可以用spfa去做一般,一般都能通过
时间复杂度O(mlogn), n表示点数,m表示边数
*/
#include 
#include 
#include 
#include 

using namespace std;

typedef pair PII;//堆中存储每个点的距离和编号

const int N = 1e6 + 10;

int n,m;      // 点的数量
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储所有点到1号点的距离
bool st[N];     // 存储每个点的最短距离是否已确定

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue, greater> heap;//保证是小根堆
    heap.push({0, 1});      
    // 先放入已知最短距离的1号点,first存储距离,second存储节点编号

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;//说明当前这个点是冗余,跳过
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

int main()
{
    cin>>n>>m;

    memset(h, -1, sizeof h);
    while (m -- )
    {
        int a, b, c;
        cin>>a>>b>>c;
        add(a, b, c);//对于邻接表来说,重边是无所谓的
    }

    cout << dijkstra() << endl;

    return 0;
}

bellman-ford

AcWing 853. 有边数限制的最短路

/*
经过不超过k条边的最短路,时间复杂度 O(nm)
*/
#include 
#include 
#include 

using namespace std;

const int N = 510, M = 10010;

struct Edge// 边,a表示出点,b表示入点,w表示边的权重
{
    int a, b, c;
}edges[M];//结构体存储

int n, m, k;
int dist[N];// dist[x]存储1到x的最短路距离
int last[N];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
void bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);

    dist[1] = 0;
    // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径
    //由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路
    //求最短路时,如果存在负权回路,最短路不一定存在
    for (int i = 0; i < k; i ++ )//迭代不超过k次
    {//比如k次迭代之后的dist数组代表从1号点经过不超过k条边走到每个点的最短距离
        memcpy(last, dist, sizeof dist);//备份dist数组,以防串联
        //也就是在外层迭代经过不超过i条边时,内层迭代更新所有边
        //可能出现先更新了某个点a,再用这个点去更新了其他点b
        //这样就是到达点b经过了i+1条边
        //所有我们必须保证内层每次更新所有边时,只用外层上一次的结果去更新
        for (int j = 0; j < m; j ++ )//每次循环所有边,因此存储方式不限制
        {
            auto e = edges[j];
            dist[e.b] = min(dist[e.b], last[e.a] + e.c);
        }
    }
}

int main()
{
    cin>>n>>m>>k;

    for (int i = 0; i < m; i ++ )
    {
        int a, b, c;
        cin>>a>>b>>c;
        edges[i] = {a, b, c};
    }

    bellman_ford();
//因为存在负权边,最后节点的正无穷可能被其他正无穷更新
    if (dist[n] > 0x3f3f3f3f /2) puts("impossible");
    else cout<

spfa

AcWing 851. spfa求最短路

/*
队列优化的Bellman-Ford算法,时间复杂度 平均情况下 O(m),最坏情况下 O(nm)
图中不存在负环才能用spfa,但存在负环情况较少,它是队列优化的Bellman-Ford算法
前者每次内层迭代遍历所有边,但并不是每条边都会更新,比如点a变小,点b才会变小
spfa对此用队列bfs优化,队列中存储所有要变小,待更新的节点
基本思路是更新过谁,再拿谁去更新别人
和dijkstra非常像,虽然说正权图一般用dijkstra算法,但其实大部分正权图也可以用spfa
*/
#include 
#include 
#include 
#include 

using namespace std;

const int N = 100010;

int n, m;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;//初始化所有点的距离

    queue q;//定义一个队列存储所有待更新的点
    q.push(1);
    st[1] = true;//判断每个点是否在队列当中,防止队列中存储重复的点

    while (q.size())
    {
        auto t = q.front();
        q.pop();//取出队头

        st[t] = false;//此时点t已经不在队列中

        for (int i = h[t]; i != -1; i = ne[i])
        {//遍历更新以t为起点的所有出边
            int j = e[i];
            if (dist[j] > dist[t] + w[i])//如果更新成功的话
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return dist[n];
}

int main()
{
    cin>>n>>m;

    memset(h, -1, sizeof h);

    while (m -- )
    {
        int a, b, c;
        cin>>a>>b>>c;
        add(a, b, c);
    }

    int t = spfa();

    if (t  > 0x3f3f3f3f /2) puts("impossible");
    else cout<

AcWing 852. spfa判断负环

/*
时间复杂度是 O(nm)
*/
#include 
#include 
#include 
#include 

using namespace std;

const int N = 2010, M = 10010;

int n, m;      // 总点数
int h[N], w[M], e[M], ne[M], idx;       // 邻接表存储所有边
int dist[N], cnt[N]; 
// dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中边数
bool st[N];     // 存储每个点是否在队列中


void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

// 如果存在负环,则返回true,否则返回false。
bool spfa()
{
    // 不需要初始化dist数组,因为没有求距离,求的是是否存在负环
    // 原理:如果某条最短路径上有n个点(除了自己)
    //那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。
    queue q;
    for (int i = 1; i <= n; i ++ )
    {
        q.push(i);
        st[i] = true;
    }
//这里最开始把所有元素入队,而不是只入第一个点,因为负环可能从任意一点开始

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;       
// 如果从1号点到x的最短路中经过至少n条边,也就是n+1个点,则说明存在负环
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

int main()
{
    cin>>n>>m;

    memset(h, -1, sizeof h);

    while (m -- )
    {
        int a, b, c;
        cin>>a>>b>>c;
        add(a, b, c);
    }

    if (spfa()) puts("Yes");
    else puts("No");

    return 0;
}

Floyd

AcWing 854. Floyd求最短路

/*
时间复杂度是O(n^3), n表示点数
*/
#include 
#include 
#include 

using namespace std;

const int N = 210, INF = 1e9;

int n, m, Q;
int d[N][N];//邻接矩阵存储所有边

// 算法结束后,d[a][b]表示a到b的最短距离
void floyd()
{
    for (int k = 1; k <= n; k ++ )
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

int main()
{
    cin>>n>>m>>Q;
    //初始化
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

    while (m -- )
    {
        int a, b, c;
        cin>>a>>b>>c;
        d[a][b] = min(d[a][b], c);//有多条边的话保留最短的边
    }

    floyd();

    while (Q -- )
    {
        int a, b;
        cin>>a>>b;

        int t = d[a][b];
        if (t > INF / 2) puts("impossible");
        else cout<

Prim

AcWing 858. Prim算法求最小生成树

/*
稠密图 时间复杂度是 O(n^2+m), n 表示点数,m 表示边数
朴素版prim算法和dijkstra算法非常相似
*/
#include 
#include 
#include 

using namespace std;

const int N = 510, INF = 0x3f3f3f3f;

int n, m;           // n表示点数
int g[N][N];        // 稠密图用邻接矩阵,存储所有边
int dist[N];        // 存储其他点到当前最小生成树的距离
bool st[N];         // 存储每个点是否已经在生成树中

// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
int prim()
{
    memset(dist, 0x3f, sizeof dist);//初始化距离

    int res = 0;//最小生成树所有边长度之和
    for (int i = 0; i < n; i ++ )
//迭代n次,这里的dijkstra是n-1,因为已经先选中了一个点,剩下n-1个点
//而这里一个点都未选中,所以是n
    {
        int t = -1;
        for (int j = 1; j <= n; j ++ )//每次迭代中,找到集合外距离最近的点
        //集合是在当前生成树,连通块中的点
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
    //说明图不连通
        if (i && dist[t] == INF) return INF;

        if (i) res += dist[t];
        st[t] = true;
        //用t更新与t相关联的其他点到集合的距离
        //dijkstra是更新到起点的距离
        for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
    }
    return res;
}

int main()
{
    cin>>n>>m;

    memset(g, 0x3f, sizeof g);//初始化

    while (m -- )
    {
        int a, b, c;
        cin>>a>>b>>c;
        g[a][b] = g[b][a] = min(g[a][b], c);//重边保留最小边,无向图建两次边
    }

    int t = prim();

    if (t == INF) puts("impossible");//所有点不连通,不存在生成树
    else cout<

Kruskal

AcWing 859. Kruskal算法求最小生成树

/*
稀疏图 O(mlogm)
*/
#include 
#include 
#include 

using namespace std;

const int N = 100010, M = 200010, INF = 0x3f3f3f3f;
int n, m;       // n是点数,m是边数
int p[N];       // 并查集的父节点数组

struct Edge     // 不需要复杂数据结构存储边
{
    int a, b, w;

    bool operator< (const Edge &W)const//重载便于排序
    {
        return w < W.w;
    }
    
}edges[M];

int find(int x)     // 并查集核心操作
{
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int kruskal()
{
    sort(edges, edges + m);//将所有边按照权重从小到大排序 O(mlogm)

    for (int i = 1; i <= n; i ++ ) p[i] = i;    // 初始化并查集
//res存储最小生成树中所有边权重之和  cnt表示当前加了多少条边
    int res = 0, cnt = 0;
    for (int i = 0; i < m; i ++ )//枚举每条边O(m)
    {
        int a = edges[i].a, b = edges[i].b, w = edges[i].w;

        a = find(a), b = find(b);
        if (a != b)     // 如果两个连通块不连通,则将这条边加进来
        {
            p[a] = b;
            res += w;
            cnt ++ ;
        }
    }

    if (cnt < n - 1) return INF;
    return res;
}

int main()
{
    cin>>n>>m;

    for (int i = 0; i < m; i ++ )
    {
        int a, b, w;
        cin>>a>>b>>w;
        edges[i] = {a, b, w};
    }

    int t = kruskal();

    if (t == INF) puts("impossible");
    else cout<

染色法判定二分图

AcWing 860. 染色法判定二分图

/*
O(n+m)
*/
#include 
#include 
#include 

using namespace std;

const int N = 100010, M = 200010;
//二分图当且仅当图中不含奇数环
//由于图中不含奇数环,所以染色过程一定没有矛盾
int n,m;      // n表示点数
int h[N], e[M], ne[M], idx;     // 邻接表存储图
int color[N];       // 表示每个点的颜色,-1表示未染色,0表示白色,1表示黑色

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
// 参数:u表示当前节点,c表示当前点要染的颜色
bool dfs(int u, int c)
{
    color[u] = c;//先把点染色
    for (int i = h[u]; i != -1; i = ne[i])//遍历当前染成颜色c的点u的所有邻点
    {
        int j = e[i];//j存储当前点的编号
        if (color[j] == -1)//如果当前点未被染色的话,就尝试把它染成!c颜色
        {
            if (!dfs(j, !c)) return false;
        }
        else if (color[j] == c) return false;//矛盾
    }

    return true;
}

bool check()
{
    memset(color, -1, sizeof color);//初始化,均未被染色
    bool flag = true;
    for (int i = 1; i <= n; i ++ )
        if (color[i] == -1)
            if (!dfs(i, 0))//如果未被染色,就尝试把它染成白色,也就是为0
            {//在此基础上dfs每个点依次染色
                flag = false;//如果染色失败的话
                break;
            }
    return flag;
}

int main()
{
    cin>>n>>m;

    memset(h, -1, sizeof h);

    while (m -- )
    {
        int a, b;
        cin>>a>>b;
        add(a, b), add(b, a);//无向边
    }

    bool flag = true;
    flag=check();


    if (flag) puts("Yes");
    else puts("No");

    return 0;
}

匈牙利算法

AcWing 861. 二分图的最大匹配

//O(nm),但实际运行时间远小于nm,可能是线性
#include 
#include 
#include 

using namespace std;

const int N = 510, M = 100010;

int n1, n2, m;
int h[N], e[M], ne[M], idx;
// 邻接表存储所有边
//匈牙利算法中只会用到从所枚举的第一个集合指向第二个集合的边
//所以这里只用存一个方向的边
int match[N];// 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N];// 对于第一个集合的所有点来说,第二个集合中的每个点是否已经被遍历过

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

bool find(int x)
{
    for (int i = h[x]; i != -1; i = ne[i])//枚举第二部分的相邻点
    {
        int j = e[i];//j表示当前集合中点的编号
        if (!st[j])
        {
            st[j] = true;//这一步一定要先写,这样才是在j已经确定的情况下
            //看match[j]能不能找到下家
            if (match[j] == 0 || find(match[j]))
//如果未匹配或者能在j匹配此时的x的前提下(也就是st[j] = true),为match[j]找到下家
            {
                match[j] = x;
                return true;
            }
        }
    }

    return false;
}

int main()
{
    cin>>n1>>n2>>m;

    memset(h, -1, sizeof h);

    while (m -- )
    {
        int a, b;
        cin>>a>>b;
        add(a, b);
    }

    int res = 0;//当前匹配数量
    for (int i = 1; i <= n1; i ++ )//枚举第一个集合
    {//枚举第一个集合当前节点时,确保第二个集合中每个节点都被考虑到
    //可以这样理解,每个男生去找女朋友时,不管那个女生有没有男朋友
    //这个男生都会考虑她,如果这个男生喜欢她,她没有男朋友最好,哪怕有
    //也看能不能抢过来,给她喜欢的那个男生找好下家
        memset(st, false, sizeof st);
        if (find(i)) res ++ ;
    }

    cout<

数学知识

质数

AcWing 866. 试除法判定质数

/*
质数(素数)是指在大于1的自然数中,除了1和它本身以外不再有其他因数(约数)的自然数
1.严格大于1,本身大于等于2
2.除了1和自身之外没有其他因数,也就是只能整除这两个数
//暴力 O(n)
bool is_prime(int x)
{
    if (x < 2) return false;//严格大于1
    for (int i = 2; i < x; i ++ )//在2到n-1中存在某个数被x整除
        if (x % i == 0)
            return false;
    return true;
}
*/

#include 
using namespace std;
//优化 每个数的约数都是成对出现,如果i是n的约数,则n/i也是n的约数
bool is_prime(int x)
{
    if (x < 2) return false;
/*
可以枚举每一对约数中较小的那个数即可,较小的约数的范围是1到根号n(从2开始枚举)
也可以这样理解,从小到大枚举每个可能是约数的数,循环条件是这个可能是约数的数
小于与它配对的那个约数
i<=sqrt(x)不推荐,因为每次都要执行这个较慢的操作
i*i<=n也不推荐,当n大道接近int最大值时,i*i可能溢出变成负数
时间复杂度sqrt(n)
*/
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
            return false;
    return true;
}

int main()
{
    int n;
    cin >> n;

    while (n -- )
    {
        int x;
        cin >> x;
        if (is_prime(x)) puts("Yes");//输出且换行
        else puts("No");
    }

    return 0;
}

AcWing 867. 分解质因数

/*
//暴力 O(n)
void divide(int x)
{
    for (int i = 2; i <= x; i ++ )
/*
从小到大枚举x的所有数,这里没有枚举质元素,会有问题吗?不会,因为枚举到i时,
x已经把i前面的(2到i-1)质因子全部都除干净了,此时x % i == 0的话,x是i的倍数,
且不包含任何2到i-1中的质因子,i中也不包含任何2到i-1中的质因子,所以i是质数
*/
        if (x % i == 0)
        {
            int s = 0;
            while (x % i == 0) x /= i, s ++ ;
            cout << i << ' ' << s << endl;
        }
    if (x > 1) cout << x << ' ' << 1 << endl;
    cout << endl;
}
*/
/*
判定质数算法时间复杂度一定是根号n,但这道题时间复杂度不一定是根号n,
最好情况下是除一个数就除干净了,也就是除logn次
所以时间复杂度是logn到根号n之间
*/
#include 

using namespace std;

void divide(int x)
{
//任意大于1的自然数,最多只有一个大于sqrt(n)的质因子,反证法可证
//所以先在2到sqrt(n)的范围去找质因子
    for (int i = 2; i <= x / i; i ++ ){
/*
从2开始依次遍历,每次遍历到此时的i时,此时的x是已经把从2到i-1之间可能存在的质因子
全部除干净的x,如果这个时候的i仍然是此时的x的因数的话,那此时的i必然也是质数
*/      if (x % i == 0)
        {
            int s = 0;
            //每次遍历到这个质因子时,x将这个质因子除干净
            while (x % i == 0) x /= i, s ++ ;
            cout << i << ' ' << s << endl;
        }
    }
/*
如果此时已经把sqrt(n)之前的质因子全部除干净的n仍然大于1的话,那此时的n就是
那个大于sqrt(n)的质因子
*/
    if (x > 1) cout << x << ' ' << 1 << endl;
    cout << endl;
}

int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        cin >> x;
        divide(x);
    }

    return 0;
}

AcWing 868. 筛质数

(最终总结)Acwing算法课_第2张图片

/*
朴素筛法 时间复杂度可以看做nlogn
当i==2时,运算n/2次,类推,运算次数
n/2+n/3+...n/n==n(1/2+1/3+...1/n)==n*调和级数==n*(ln n + c)
using namespace std;

const int N= 1000010;

int primes[N], cnt;
bool st[N];

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {//从前往后看,把每一个数的倍数筛掉,这样剩下的数一定是质数
//如果p没被筛掉,说明2到p-1中没有谁的倍数是p,也就是没有p的约数,因此p为质数
        if (!st[i]){ 
            primes[cnt ++ ] = i;
        }
        for (int j = i + i; j <= n; j += i)
            st[j] = true; 
    }
}

int main()
{
    int n;
    cin >> n;

    get_primes(n);

    cout << cnt << endl;//输出cnt即可,不需要再加一,因为cnt已经++

    return 0;
}

(最终总结)Acwing算法课_第3张图片

/*
埃式筛法-O(nloglogn) 时间复杂度约等于O(n)
*/
#include 

using namespace std;

const int N= 1000010;

int primes[N], cnt;
bool st[N];

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
//删掉所有质数的倍数,因为我们判断p是否为质数时
//并不需要判断2到p-1的所有数的倍数是否为p,只要判断这其中的质数的倍数是否为p就行
//因为这其中不是质数的那些书也是前面的质数的倍数
//所有当一个数不是质数的时候,就不需要删掉它的所有倍数
        if (!st[i]){ 
            primes[cnt ++ ] = i;
            for (int j = i + i; j <= n; j += i)
                st[j] = true;
        }
    }
}
int main()
{
    int n;
    cin >> n;

    get_primes(n);

    cout << cnt << endl;

    return 0;
}
/*
线性筛法  O(n) 数据级别为1e7时,比上个方法快一倍
void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}
*/
#include 

using namespace std;

const int N= 1000010;

int primes[N], cnt;// primes[]存储所有素数
bool st[N];// st[x]存储x是否被筛掉

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
    {
//n只会被它的最小质因子删掉
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
//对于任意一个合数x,假设pj为x最小质因子,当外层循环的i枚举到x/pj时,x一定会被筛掉
//也就是说当i枚举到x时,在此之前i一定会枚举到比x小的x/pj,而这个时候,x就会被筛掉
//每次筛的数都是用最小质因子去筛的
//primes[j] <= n / i是因为primes[j] * i要<=n,这样st[primes[j] * i]才有效
//思路是从小到大枚举所有的质数,去这个质数去筛掉它一定作为最小质因子的那个合数
//循环条件是这个合数primes[j] * i<=n也就是primes[j] <= n / i
            st[primes[j]*i] = true;
            //cout<> n;

    get_primes(n);

    cout << cnt << endl;

    return 0;
}

约数

AcWing 869. 试除法求约数

#include 
#include 
#include 

using namespace std;

vector get_divisors(int x)
{
    vector res;
    for (int i = 1; i <= x / i; i ++ )
    //约数成对出现,枚举较小约数即可
        if (x % i == 0)
        {
            res.push_back(i);
            if (i != x / i) res.push_back(x / i);
        }
    sort(res.begin(), res.end());
    return res;
}

int main()
{
    int n;
    cin >> n;

    while (n -- )
    {
        int x;
        cin >> x;
        auto res = get_divisors(x);

        for (auto x : res) cout << x << ' ';
        cout << endl;
    }

    return 0;
}

AcWing 870. 约数个数

/*
如果 N = p1^c1 * p2^c2 * ... *pk^ck
约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)
*/
#include 
#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 110, mod = 1e9 + 7;

int main()
{
    int n;
    cin >> n;

    unordered_map primes;

    while (n -- )
    {
        int x;
        cin >> x;

        for (int i = 2; i <= x / i; i ++ )
            while (x % i == 0)
            {
                x /= i;
                primes[i] ++ ;
            }

        if (x > 1) primes[x] ++ ;
    }

    LL res = 1;
    for (auto p : primes) res = res * (p.second + 1) % mod;//(c1 + 1)

    cout << res << endl;

    return 0;
}

AcWing 871. 约数之和

/*
如果 N = p1^c1 * p2^c2 * ... *pk^ck
约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)
*/
#include 
#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 110, mod = 1e9 + 7;

int main()
{
    int n;
    cin >> n;

    unordered_map primes;

    while (n -- )
    {
        int x;
        cin >> x;

        for (int i = 2; i <= x / i; i ++ )
            while (x % i == 0)
            {
                x /= i;
                primes[i] ++ ;
            }

        if (x > 1) primes[x] ++ ;
    }

    LL res = 1;
    for (auto p : primes)
    {
        LL a = p.first, b = p.second;
        LL t = 1;
        while (b -- ) t = (t * a + 1) % mod;//(p1^0 + p1^1 + ... + p1^c1)
        res = res * t % mod;
    }

    cout << res << endl;

    return 0;
}

AcWing 872. 最大公约数

#include 
#include 

using namespace std;
// (a,b)==(b,a mod b)==(b,a - (a/b)*b)
// d能整除a,能整除b,则d也能整除a-c*b
// d能整除b,能整除a-c*b,则d也能整除能整除a-c*b+c*b==a
int gcd(int a, int b)
{
    return b ? gcd(b, a % b) : a;
}


int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int a, b;
        cin>>a>>b;
        cout<

欧拉函数

AcWing 873. 欧拉函数

#include 
using namespace std;


int phi(int x)
{
    int res = x;
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
        {
            res = res / i * (i - 1);//注意先写除法,再写乘法,避免溢出
            while (x % i == 0) x /= i;
        }
    if (x > 1) res = res / x * (x - 1);

    return res;
}


int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        cin >> x;
        cout << phi(x) << endl;
    }

    return 0;
}

AcWing 874. 筛法求欧拉函数

/*
如果用上道题做法,对每个数直接用公式求解,对每个数都要分解质因数,时间复杂度是n*sqrt(n)
可以用线性筛法用O(n)时间复杂度顺便求出每个数的欧拉函数
线性筛法可以顺便求出来很多东西

用处:欧拉定理
若a与n互质,则a^(n的欧拉函数)模n等于1
推论:当n是质数时 a^(n-1)模n等于1 费马定理
*/
#include 

using namespace std;

typedef long long LL;

const int N = 1000010;


int primes[N], cnt;
int euler[N];
bool st[N];


void get_eulers(int n)
{
    euler[1] = 1;
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i])//如果当前这个数没有被筛过,说明i是一个质数
        {
            primes[cnt ++ ] = i;
            euler[i] = i - 1;//质数i的欧拉函数就是i-1
        }
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            int t = primes[j] * i;
            st[t] = true;
            if (i % primes[j] == 0)
            {
/*
求欧拉函数过程中,只有这个数包含某个质因子,就乘(1-1/pj),与这个质因子的次数没有关系
比如6的欧拉函数==6*(1-1/2)(1-1/3)  
另外一个数n==2^100 * 3^100,它的欧拉函数==n*(1-1/2)(1-1/3),和次数无关

primes[j] * i的欧拉函数与i的欧拉函数的关系:

当i % primes[j] == 0时,primes[j]是i的最小质因子,也是primes[j] * i的最小质因子,
所以primes[j] * i与i的质因子是相同的,所以euler[primes[j] * i] = euler[i] * primes[j]

当i % primes[j] != 0时,primes[j]小于i的最小质因子,是primes[j] * i的最小质因子,
euler[primes[j] * i] == euler[i] * primes[j] * (primes[j] - 1) / primes[j]
                     == euler[i] * (primes[j] - 1)
*/
                euler[t] = euler[i] * primes[j];
                break;
            }
            euler[t] = euler[i] * (primes[j] - 1);
        }
    }
}


int main()
{
    int n;
    cin >> n;

    get_eulers(n);

    LL res = 0;//和可能很大,会报int,所以用ll来存
    for (int i = 1; i <= n; i ++ ) res += euler[i];

    cout << res << endl;

    return 0;
}

快速幂

AcWing 875. 快速幂

/*
快速求出来 a^k mod p,时间复杂度 O(logk)
核心思路是反复平方,预处理出来a^(2^0) mod p,a^(2^1) mod p,...a^(2^logk) mod p
把a^k mod p 拆成前面预处理的logk个数的乘积,k转换成二进制
*/
#include 
#include 

using namespace std;

typedef long long LL;


LL qmi(int a, int b, int p)
{
    LL res = 1 % p;
    while (b)
    {
        if (b & 1) res = res * a % p;//凡是中间结果可能会溢出的地方,都要特殊处理
        a = a * (LL)a % p;
//a * (LL)a % p和(LL)a * a % p 第2个的a∗p可能爆int,所以(LL)写到中间是个好习惯
        b >>= 1;//删掉最后一位
    }
    return res;
}


int main()
{
    int n;
    cin>>n;
    while (n -- )
    {
        int a, b, p;
        cin>>a>>b>>p;
        cout<

AcWing 876. 快速幂求逆元

#include 
#include 

using namespace std;

typedef long long LL;

LL qmi(int a, int b, int p)
{
    LL res = 1;
    while (b)
    {
        if (b & 1) res = res * a % p;
        a = a * (LL)a % p;
        b >>= 1;
    }
    return res;
}

int main()
{
    int n;
    cin>>n;
    while (n -- )
    {
        int a, p;
        cin>>a>>p;
        if (a % p == 0) puts("impossible");// a是p的倍数时无解
        else cout<

扩展欧几里得算法

AcWing 877. 扩展欧几里得算法

/*
求x, y,使得ax + by = gcd(a, b)
裴蜀定理
对于任意正整数a,b 那么一定存在非零整数x,y使得ax + by = gcd(a, b)
如果ax + by = d 则d一定是gcd(a, b)的倍数
*/
#include 
#include 

using namespace std;
/*
int gcd(int a, int b)
{
    if (!b)
    {
        return a;
    }
    return gcd(b, a % b);
}
*/
int exgcd(int a, int b, int &x, int &y)//后两者传引用
{
    if (!b)
    {
        x = 1, y = 0;
        return a;
    }
    int d = exgcd(b, a % b, y, x);
    y -= a / b * x;//x不变,y-= a / b * x
    return d;
}

int main()
{
    int n;
    cin>>n;

    while (n -- )
    {
        int a, b;
        cin>>a>>b;
        int x, y;
        exgcd(a, b, x, y);
        cout<

AcWing 878. 线性同余方程

#include 
#include 

using namespace std;

typedef long long LL;


int exgcd(int a, int b, int &x, int &y)
{
    if (!b)
    {
        x = 1, y = 0;
        return a;
    }
    int d = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return d;
}


int main()
{
    int n;
    cin>>n;
    while (n -- )
    {
        int a, b, m;
        cin>>a>>b>>m;

        int x, y;
        int d = exgcd(a, m, x, y);//扩展欧几里得算法求出最大公约数d
        if (b % d) puts("impossible");//b能够整除最大公约数d有解
        else cout<<(LL)b / d * x % m<

中国剩余定理

(最终总结)Acwing算法课_第4张图片

AcWing 204. 表达整数的奇怪方式

/*
推导见
https://www.acwing.com/solution/content/3539/
https://www.acwing.com/solution/content/23099/
*/

#include

using namespace std;

typedef long long LL;//数据范围比较大,所以用LL来存储

LL exgcd(LL a,LL b,LL &x,LL &y)
{
    if(!b)
    {
        x=1,y=0;
        return a;
    }
    LL d=exgcd(b,a%b,y,x);
    y-=a/b*x;
    return d;
}

int main()
{
    int n;
    LL a1,m1;
    cin>>n>>a1>>m1;
    LL x=0;
    for(int i=1;i>a2>>m2;
        LL k1,k2;
        LL d=exgcd(a1,a2,k1,k2);
        if((m2-m1)%d)//无解
        {
            x=-1;
            break;
        }
        k1*=(m2-m1)/d;
        //因为此时k1是k1*a1+k2*a2=d的解,所以要乘上(m2-m1)/d的倍数大小
        LL t=abs(a2/d);
        k1=(k1%t+t)%t;
        //数据比较极端,所以只求k的最小正整数解
        m1=k1*a1+m1;
        //m1在被赋值之后的值为当前"x"的值,此时赋值是为了方便下一轮的继续使用
        a1=abs(a1*a2/d);
        //循环结束时a1的值为当前所有的a1,a2,……an中的最小公倍数
    }
    if(x!=-1) x=(m1%a1+a1)%a1;
    //当循环结束时,此时的值应该与最小公倍数取模,以求得最小正整数解
    cout<

高斯消元

AcWing 883. 高斯消元解线性方程组

/*
高斯消元一般可以在n^3时间复杂度内求解包含n个方程,n个未知数的多元线性方程组
可能无解,可能无穷多个解,可能存在唯一解
*/
#include
#include
#include

using namespace std;

const int N=110;
const double egs=1e-6;

double a[N][N];
int n;

int gause()
{
    int r,c;//r表示当前所在的行数,c表示当前所指向的列数
    for(r=0,c=0;c=c;i--) a[r][i]/=a[r][c];
        //将当前第c位的值初始化为1,当然其他数也同样需要除以第c位,
        //之所以从第n位开始,是为了不覆盖掉第c位的值

        for(int i=r+1;iegs)//如果当前行数为零,说明不用进行削为零的操作
           for(int j=n;j>=c;j--)
            a[i][j]-=a[i][c]*a[r][j];

        r++;
    }

    if(regs)
          return 2;//无解
        /*
       如果当前列的绝对值最大的值是0,那么r还继续停留,不转到下一行,但c却转至下一行,
       由于循环过程中会进行消元操作,所以循环到该列时,下面所有行的c列就会被消成零.
       假设该列只有零的情况出现一次的话,那么r最终会循环到n-2行,而c却循环到了第n-1列,
       因为循环过程中的消元,所以第n-1行的所有数都会被消成零.如果此时d不为零的话,矛盾,无解;
       如果为零的话,那么就会有无数组解.一列都是零的情况出现两次,三次,甚至更多次都是同理.
        */
        return 1;//无数解
    }

    //将未知数求解出来
    for(int i=n-1;i>=0;i--)
     for(int j=i+1;j>n;
    for(int i=0;i>a[i][j];

    int t=gause();

    if(t==0)
    {
        for(int i=0;i

AcWing 884. 高斯消元解异或线性方程组

/*
异或运算相当于不进位的加法运算,可以把它简单看成一个线性方程组,类似于高斯消元的思路
1.消成上三角矩阵
    1.1枚举列
    1.2找非行
    1.3交换
    1.4下面消0
2.判断
*/
#include 
#include 

using namespace std;

const int N = 110;


int n;
int a[N][N];


int gauss()
{
    int c, r;
    for (c = 0, r = 0; c < n; c ++ )//按列进行枚举
    {
        int t = r;//从当前这一行开始找到非0行
        for (int i = r; i < n; i ++ )
            if (a[i][c]){
                t = i;
                break;
            }
        

        if (!a[t][c]) continue;//在这一列没有找到哪一行为1,继续下一层循环

        for (int i = c; i <= n; i ++ ) swap(a[r][i], a[t][i]);//把第r行的数与第t行交换
        for (int i = r + 1; i < n; i ++ )//用r行把下面所有行的当前列消成0
            if (a[i][c])
                for (int j = n; j >= c; j -- )
                    a[i][j] ^= a[r][j];//之所以从第n位开始,是为了不覆盖掉第c位的值

        r ++ ;
    }

    if (r < n)
    {
        for (int i = r; i < n; i ++ )
            if (a[i][n])
                return 2;
        return 1;
    }

    for (int i = n - 1; i >= 0; i -- )
        for (int j = i + 1; j < n; j ++ )
            a[i][n] ^= a[i][j] * a[j][n];

    return 0;
}


int main()
{
    cin >> n;

    for (int i = 0; i < n; i ++ )//读入n行n+1列
        for (int j = 0; j < n + 1; j ++ )
            cin >> a[i][j];

    int t = gauss();

    if (t == 0)
    {
        for (int i = 0; i < n; i ++ ) cout << a[i][n] << endl;
    }
    else if (t == 1) puts("Multiple sets of solutions");
    else puts("No solution");

    return 0;
}

求组合数

AcWing 885. 求组合数 I

(最终总结)Acwing算法课_第5张图片

/*
数据范围
1≤n≤10000   给定十万组询问
1≤b≤a≤2000  2000*2000==4e6的时间复杂度预处理出来所有C a b的值
在a个苹果中,选出b个苹果出来,现有一个苹果p;
1.选这个苹果的方案相当于在剩余的a-1个苹果中选b-1个苹果
2.不选这个苹果的方案相当于在剩余的a-1个苹果中选b个苹果
将1,2两种方案相加就是在a个苹果中选择b个苹果
*/
#include 
#include 

using namespace std;

const int N = 2010, mod = 1e9 + 7;


int c[N][N];


void init()
{
    for (int i = 0; i < N; i ++ )
        for (int j = 0; j <= i; j ++ )
            if (!j) c[i][j] = 1;
            else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
}


int main()
{
    int n;

    // init();
    for (int i = 0; i < N; i ++ )
        for (int j = 0; j <= i; j ++ )
            if (!j) c[i][j] = 1;
            else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;

    cin>>n;

    while (n -- )
    {
        int a, b;
        cin>>a>>b;

        cout<

AcWing 886. 求组合数 II

(最终总结)Acwing算法课_第6张图片
(最终总结)Acwing算法课_第7张图片

/*
上一道题的数据范围 
1≤n≤10000   给定一万组询问
1≤b≤a≤2000  2000*2000==4e6的时间复杂度预处理出来所有C a b的值,递推求解
这道题的数据范围
1≤n≤10000 
1≤b≤a≤100000 预处理出来阶乘
首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]
如果取模的数是质数,可以用费马小定理求逆元
*/
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 100010, mod = 1e9 + 7;


int fact[N], infact[N];//fact表示阶乘,infact表示阶乘的逆元


int qmi(int a, int k, int p)
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}


int main()
{
    fact[0] = infact[0] = 1;
    for (int i = 1; i < N; i ++ )
    {
        fact[i] = (LL)fact[i - 1] * i % mod;//阶乘运算过程
        infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
        /*
        x=b^(p-2)%p,这里相当于是x=i^(mod-2)%mod
        所以乘x就相当于除i,因为infact表示阶乘逆元和,
        因为infact表示除以i的阶乘的逆元,所以乘infact[i-1]
        相当于乘上1/(i-1)!.又因为乘x相当于除以i,所以infact[i-1]
        乘上x等于1/i!,也就等于infact[i];
        */
    }


    int n;
    cin>>n;
    while (n -- )
    {
        int a, b;
        cin>>a>>b;
        cout<<(LL)fact[a] * infact[b] % mod * infact[a - b] % mod<

AcWing 887. 求组合数 III

(最终总结)Acwing算法课_第8张图片
(最终总结)Acwing算法课_第9张图片

/*
若p是质数,则对于任意整数 1 <= m <= n,有:
    C(n, m) = C(n % p, m % p) * C(n / p, m / p) (mod p)
时间复杂度:logpN * (P + logP)
*/
#include
using namespace std;
typedef long long ll;

int qmi(int a,int k,int p){
    int res=1;
    while(k){
        if(k&1) res=(ll)res*a%p;
        k>>=1;
        a=(ll)a*a%p;
    }
    return res;
}

int C(int a,int b,int p){   //这种写法更优
    if(b>a) return 0;
    //if(b > a - b) b = a - b;//优化

    int up=1,down=1;  //up代表分子,down代表分母
    for(int i=1,j=a;i<=b;i++,j--){
        up=(ll)up*j%p;
        down=(ll)down*i%p;
    }
    return (ll)up*qmi(down,p-2,p)%p;
}

int lucas(ll a,ll b,int p){
    if(a>n;
    while(n--){
        ll a,b,p;
        cin>>a>>b>>p;
        cout<

AcWing 888. 求组合数 IV

(最终总结)Acwing算法课_第10张图片

/*
从定义出发,结果可能很大,需要使用高精度计算。
先将C a b 分解质因数之后变成p1^a1*p2^a2*...,这样只需要实现高精度乘法
当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:
    1. 筛法求出范围内的所有质数
    2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 
    n! 中p的次数是 n / p + n / p^2 + n / p^3 + ...
*/
#include 
#include 
#include 

using namespace std;


const int N = 5010;

int primes[N], cnt;     // 存储所有质数
int sum[N];     // 存储每个质数的次数
bool st[N];     // 存储每个数是否已被筛掉

void get_primes(int n)// 线性筛法求素数
{
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }
}


int get(int n, int p)// 求n!中的次数
{
    int res = 0;
    while (n)
    {
        res += n / p;
        n /= p;
    }
    return res;
}


vector mul(vector a, int b)// 高精度乘低精度模板
{
    vector c;
    int t = 0;
    for (int i = 0; i < a.size(); i ++ )
    {
        t += a[i] * b;
        c.push_back(t % 10);
        t /= 10;
    }
    while (t)
    {
        c.push_back(t % 10);
        t /= 10;
    }
    return c;//不用处理前导零,因为这道题目中一定不会乘0
}


int main()
{
    int a, b;
    cin >> a >> b;

    get_primes(a);// 预处理范围内的所有质数

    for (int i = 0; i < cnt; i ++ )// 求每个质因数的次数
    {
        int p = primes[i];
        sum[i] = get(a, p) - get(a - b, p) - get(b, p);
    }

    vector res;
    res.push_back(1);

    for (int i = 0; i < cnt; i ++ )// 用高精度乘法将所有质因子相乘
        for (int j = 0; j < sum[i]; j ++ )
            res = mul(res, primes[i]);

    for (int i = res.size() - 1; i >= 0; i -- ) cout<

AcWing 889. 满足条件的01序列

(最终总结)Acwing算法课_第11张图片

/*
给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数
的序列的数量为: Cat(n) = C(2n, n) / (n + 1)
将01序列置于坐标系中,起点定于原点。若0表示向右走,1表示向上走,那么任何前缀中0的个数不少
于1的个数就转化为,路径上的任意一点,横坐标大于等于纵坐标。题目所求即为这样的合法路径数量
上图中,表示从 (0,0)走到 (n,n) 的路径,在绿线及以下表示合法,若触碰红线即不合法。
从(0,0)走到 (n,n)一共有 C(2n, n) 种走法
由图可知,任何一条不合法的路径(如黑色路径),都对应一条从 (0,0) 走到 (n−1,n+1) 的一条路
径(如灰色路径)。而任何一条 (0,0) 走到 (n−1,n+1) 的路径,也对应了一条从 (0,0) 走到 (n,n)
的不合法路径。
答案如图,即卡特兰数。
*/
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 100010, mod = 1e9 + 7;


int qmi(int a, int k, int p)
//可以用快速幂是因为取模是质数,所以可以用费马小定理,如果不是质数求逆元,
//只能用扩展欧几里得算法
{
    int res = 1;
    while (k)
    {
        if (k & 1) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}


int main()
{
    int n;
    cin >> n;

    int a = n * 2, b = n;
    int res = 1;
    for (int i = a; i > a - b; i -- ) res = (LL)res * i % mod;

    for (int i = 1; i <= b; i ++ ) res = (LL)res * qmi(i, mod - 2, mod) % mod;

    res = (LL)res * qmi(n + 1, mod - 2, mod) % mod;

    cout << res << endl;

    return 0;
}

容斥原理

(最终总结)Acwing算法课_第12张图片
(最终总结)Acwing算法课_第13张图片

AcWing 890. 能被整除的数

/*
C0n+C1n+C2n+…+Cnn=2^n 从n个数中挑任意多个数的方案数,所以,有2^n−C0n也就是2^n−1项

1~n中能被p整除的个数,也就是p的倍数的个数==n/p下取整
1~n中能被pi和pj整除的个数,也就是pi*pj的倍数的个数==n/(pi*pj)下取整
...

我们要计算的是2^m-1个集合的并,每个集合表示在1~n当中能被某些Pi整除的整数,所以用容斥原理
每一个集合假设有p1到pk这k(1<=k<=m)个数,要计算k次乘法,所以计算每个集合的时间复杂度是O(k)
所以总共时间复杂度是O(2^m * k)==O(2^m * m)

这道题可以爆搜,但是枚举所有集合的情况,一般用位运算枚举

将题目所给出的m个数可以看成是m位的二进制数,例如
当p[N]={2,3}时,此时会有01,10,11三种情况
而二进制的第零位表示的是p[0]上面的数字2,第1位表示p[1]上面的数字3
所以当i=1(01)时表示只选择2的情况,当i=2(10)时,表示只选择3的情况,当i=3(11)时,表示2和3相乘
的情况,在过程中可以用标记变量t记录,可以按照t的值来选择是”+”还是“-”
*/
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 20;

int p[N];


int main()
{
    int n, m;
    cin >> n >> m;

    for (int i = 0; i < m; i ++ ) cin >> p[i];//读入m个质数

    int res = 0;
    for (int i = 1; i < 1 << m; i ++ )//遍历2^m-1个集合
    {
        int t = 1, s = 0;//t表示当前所有容斥数的乘积,s表示当前这个集合选法中有几个1
        for (int j = 0; j < m; j ++ )
            if (i >> j & 1)//判断当前枚举的这个集合的第j位是否为1
            {
                if ((LL)t * p[j] > n)//因为pi范围很大,乘之后可能超过n,所以不用管
                {
                    t = -1;
                    break;
                }
                t *= p[j];
                s ++ ;
            }

        if (t != -1)//说明当前乘积

博弈论

AcWing 891. Nim游戏

/*
给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,
可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。

我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,
第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。
所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取
该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,
即两人均无失误,都采取最优策略行动时游戏的结果。
NIM博弈不存在平局,只有先手必胜和先手必败两种情况。

定理: NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0

公平组合游戏ICG

若一个游戏满足:
由两名玩家交替行动;
在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
不能行动的玩家判负;

则称该游戏为一个公平组合游戏。
NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。
因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。

有向图游戏

给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。
两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。
该游戏被称为有向图游戏。
任何一个公平组合游戏都可以转化为有向图游戏。
具体方法是,把每个局面看成图中的一个节点,并且从每个局面向沿着合法行动能够到达的
下一个局面连有向边。

先手必胜状态:先手操作完,可以走到某一个必败状态
先手必败状态:先手操作完,走不到任何一个必败状态
先手必败状态:a1 ^ a2 ^ a3 ^ ... ^an = 0
先手必胜状态:a1 ^ a2 ^ a3 ^ ... ^an ≠ 0

*/
#include 
#include 

using namespace std;

const int N = 100010;


int main()
{
    int n;
    cin>>n;

    int res = 0;
    while (n -- )
    {
        int x;
        cin>>x;
        res ^= x;
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

AcWing 892. 台阶-Nim游戏

/*
跟Nim游戏类似,如果先手时奇数台阶上的值的异或值为0,则先手必败,反之必胜

证明:
先手时,如果奇数台阶异或非0,根据经典Nim游戏,先手总有一种方式使奇数台阶异或为0,
于是先手留了技术台阶异或为0的状态给后手
于是轮到后手:
1.当后手移动偶数台阶上的石子时,先手只需将对手移动的石子继续移到下一个台阶,
这样奇数台阶的石子相当于没变,于是留给后手的又是奇数台阶异或为0的状态
2.当后手移动奇数台阶上的石子时,留给先手的奇数台阶异或非0,根据经典Nim游戏,
先手总能找出一种方案使奇数台阶异或为0

因此无论后手如何移动,先手总能通过操作把奇数异或为0的情况留给后手,当奇数台阶全为0时,
只留下偶数台阶上有石子。
(核心就是:先手总是把奇数台阶异或为0的状态留给对面,即总是将必败态交给对面)

因为偶数台阶上的石子要想移动到地面,必然需要经过偶数次移动,
又因为奇数台阶全0的情况是留给后手的,因此先手总是可以将石子移动到地面,
当将最后一个(堆)石子移动到地面时,后手无法操作,即后手失败。

因此如果先手时奇数台阶上的值的异或值为非0,则先手必胜,反之必败!

*/
#include 
#include 

using namespace std;

const int N = 100010;

int main()
{
    int n;
    cin>>n;

    int res = 0;
    for (int i = 1; i <= n; i ++ )
    {
        int x;
        cin>>x;
        if (i & 1) res ^= x;
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

AcWing 893. 集合-Nim游戏

/*
Mex运算
设S表示一个非负整数集合。定义mex(S)为求出不属于集合S的最小非负整数的运算,即:
mex(S) = min{x}, x属于自然数,且x不属于S

SG函数
在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1, y2, …, yk,
定义SG(x)为x的后继节点y1, y2, …, yk 的SG函数值构成的集合再执行mex(S)运算的结果,即:
SG(x) = mex({SG(y1), SG(y2), …, SG(yk)})
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即SG(G) = SG(s)。
SG(终点) = 0
SG(x) = 0 必败
SG(x) != 0 必胜,因为一定可以到达终点状态0

有向图游戏的和
设G1, G2, …, Gm 是m个有向图游戏。定义有向图游戏G,它的行动规则是任选某个有向图游戏Gi,
并在Gi上行动一步。G被称为有向图游戏G1, G2, …, Gm的和。
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数值的异或和,即:
SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm)

有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于0。
有向图游戏的某个局面必败,当且仅当该局面对应节点的SG函数值等于0。
*/
#include 
#include 
#include 
#include 

using namespace std;

const int N = 110, M = 10010;

int n, m;
int s[N], f[M];


int sg(int x)
{
    if (f[x] != -1) return f[x];//记忆化搜索,如果f[x]已经被计算过,则直接返回

    unordered_set S;//用一个哈希表来存每一个局面能到的所有情况,便于求mex
    for (int i = 0; i < m; i ++ )//如果可以减去s[i],则添加到S中
    {
        int sum = s[i];
        if (x >= sum) S.insert(sg(x - sum));
    }

    for (int i = 0; ; i ++ )//求mex(),即找到最小并不在原集合中的数
        if (!S.count(i))
            return f[x] = i;
}


int main()
{
    cin >> m;
    for (int i = 0; i < m; i ++ ) cin >> s[i];
    cin >> n;

    memset(f, -1, sizeof f);//sg

    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }

    if (res) puts("Yes");//SG不为0,必胜
    else puts("No");

    return 0;
}

AcWing 894. 拆分-Nim游戏

/*
相比于集合-Nim,这里的每一堆可以变成不大于原来那堆的任意大小的两堆
即a[i]可以拆分成(b[i],b[j]),为了避免重复规定b[i]>=b[j],即:a[i]>=b[i]>=b[j]
相当于一个局面拆分成了两个局面,由SG函数理论,多个独立局面的SG值,
等于这些局面SG值的异或和。
因此需要存储的状态就是sg(b[i])^sg(b[j])(与集合-Nim的唯一区别)
*/
#include 
#include 
#include 
#include 

using namespace std;

const int N = 110;


int n;
int f[N];


int sg(int x)
{
    if (f[x] != -1) return f[x];

    unordered_set S;
    for (int i = 0; i < x; i ++ )
        for (int j = 0; j <= i; j ++ )//规定j不大于i,避免重复
//相当于一个局面拆分成了两个局面,多个独立局面的SG值,等于这些局面SG值的异或和
            S.insert(sg(i) ^ sg(j));

    for (int i = 0;; i ++ )
        if (!S.count(i))
            return f[x] = i;
}


int main()
{
    cin >> n;

    memset(f, -1, sizeof f);

    int res = 0;
    while (n -- )
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

动态规划

背包问题

AcWing 2. 01背包问题

/*
动态规划问题一般从两个角度考虑,分别是状态表示与状态计算
状态表示:用几维表示状态,每一个状态(集合)的含义是什么 
f(i,j)表示的是满足某些条件的集合的某种属性
  DP优化都是对代码方程的等价变形
  状态表示一般从两个角度考虑,分别是集合与属性
    背包问题中集合是指所有选法的集合,这些选法满足两个条件
      1.只考虑前i个物品  2.选出来的物品的总体积不超过j
    属性一般有三种:最大值,最小值,数量  背包问题中是集合中选法的价值最大值
    所以f(i,j)是指只考虑前i个物品,所有物品总体积不超过j的选法中价值的最大值
  状态计算:集合的划分  如何把当前集合不重不漏的划分成若干个更小的子集
  (不漏一定要满足,不重看情况)

背包问题:
for 物品
    for 体积
        for 决策

f(i,j)=max( f(i-1,j) , f(i-1,j-vi)+wi )

二维
f(i,j)是指只考虑前i个物品,所有物品总体积不超过j的选法中价值的最大值
f(i,j)分为两大类,分别是是否包含第i件物品
不包含:f(i-1,j)
包含:f(i-1,j-vi)+wi
f(i,j)=max( f(i-1,j) , f(i-1,j-vi)+wi )
*/
#include
using namespace std;

const int N=1010;
int n,m;
int v[N],w[N];
int f[N][N];

int main(){
    cin>>n>>m;
    
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    //下标从1开始是因为状态计算时需要用到i-1
    
    //初始化时枚举所有状态,也就是f[0~n][0~m],对于f[0][0~m]来说,必然为0
    for(int i=1;i<=n;i++){
        for(int j=0;j<=m;j++){
//对f(i,j)=max( f(i-1,j) , f(i-1,j-vi)+wi )来说,左边一定存在,右边不一定存在
            f[i][j]=f[i-1][j];
            if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }
    }
    
    cout<
using namespace std;
const int N=1010;

int n,m;
int v[N],w[N];
int f[N];

int main(){
    cin>>n>>m;
    
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
//优化:二维变成一维 能够优化是因为f[i]这一层计算只用到的f[i-1]上面这一层
//f[j]用到的j和j-v[i]都小于等于j,所以可以改成一维数组来计算
    for(int i=1;i<=n;i++){
        for(int j=m;j>=v[i];j--){
//由于j-v[i]严格小于j,所以内层循环如果从小到大遍历,所以先去更新j-v[i],然后才是j
//所以计算f[j]时所用到的j-v[i]其实是第i层的,而不是i-1层的j-v[i]
//因此优化时,内层循环的遍历改为从大到小
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }

    cout<
using namespace std;

const int N=1010;

int n,m;
int v[N],w[N];
int f[N];

int main(){
    cin>>n>>m;
  
    for(int i=0;i>v[i]>>w[i];
    
    for(int i=0;i=v[i];j--){
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }

    cout<

AcWing 3. 完全背包问题

/*
对于01背包问题,可以按照第i种物品选0个或1个分类,类似,完全背包问题可按照第i种物品
选0,1,2...k个分类
(注意:虽然每种物品有无限个可选,但背包容量有限,所以最多选k个,其中k的限制条件
是k * v[i] <= 每个状态考虑的背包容量j)
所以状态转移方程 f(i,j)=max(f( i-1 , j- k * v[i] ) + k * w[i])  k>=0&&k*v[i]<=j
*/
//朴素做法 会超时 三维
#include
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int f[N][N];

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    //下标从1开始是因为状态计算时需要用到i-1
    
    //枚举所有状态
    for(int i=1;i<=n;i++){
        for(int j=0;j<=m;j++){
            //k的限制条件是此时的考虑情况的背包容量
            for(int k=0;k*v[i]<=j;k++){
                f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
            }
        }
    }

    cout<
#include
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int f[N][N];

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
    
    for(int i=1;i<=n;i++){
        for(int j=0;j<=m;j++){
            f[i][j]=f[i-1][j];
            if(j>=v[i]) f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
        }
    }

    cout<
#include
using namespace std;
const int N=1010;
int n,m;
int v[N],w[N];
int f[N];

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i];

    for(int i=1;i<=n;i++){
        for(int j=v[i];j<=m;j++){//注意这里与01背包问题的区别
            f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }

    cout<

AcWing 4. 多重背包问题

/*
与完全背包问题类似,多重背包问题可按照第i种物品选0,1,2…k个分类
(注意:虽然每种物品最多有s[i]个可选,再加上背包容量有限,所以最多选k个,
其中k的限制条件是k * v[i] <= 每个状态考虑的背包容量j&&k<=s[i])
所以状态转移方程 f(i,j)=max(f( i-1 , j- k * v[i] ) + k * w[i])
*/
//朴素做法  三维
#include
using namespace std;
const int N=110;
int n,m;
int v[N],w[N],s[N];
int f[N][N];

int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>v[i]>>w[i]>>s[i];

    for(int i=1;i<=n;i++){
        for(int j=0;j<=m;j++){
            //k的限制条件是此时的考虑情况的背包容量
            for(int k=0;k*v[i]<=j&&k<=s[i];k++){
                f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
            }
        }
    }

    cout<
#include
#include
using namespace std;
const int N=110;
int n,m;
int f[N];

int main(){
    cin>>n>>m;

    for(int i=1;i<=n;i++){
        int v,w,s;
        cin>>v>>w>>s;
        for(int j=m;j>=0;j--){
            for(int k=0;k*v<=j&&k<=s;k++){
                f[j]=max(f[j],f[j-k*v]+k*w);
            }
        }
    }

    cout<

AcWing 5. 多重背包问题 II(二进制优化)

/*
先尝试完全背包问题优化思路f(i,j),从转移方程入手

完全:背包最多放k个v  最后始终是-k*wi  求所有前缀最大值
f(i,j)  =max(f(i-1,j),f(i-1,j-v[i])+w[i],...f(i-1,j-k*v[i])+k*w[i])
f(i,j-v)=max(         f(i-1,j-v[i])     ,...f(i-1,j-k*v[i])+(k-1)*w[i])
f(i,j-2v)=max(        f(i-1,j-2v[i])    ,... f(i-1,j-k*v[i])+(k-2)*w[i])

多重:第i类物品有s个  最后始终是+s*wi  求滑动窗口最大值
f(i,j)  =max(f(i-1,j),f(i-1,j-v[i])+w[i],...f(i-1,j-s*v[i])+s*w[i]    )
f(i,j-v)=max(         f(i-1,j-v[i])     ,...f(i-1,j-s*v[i])+(s-1)*w[i],f(i-1,j-(s+1)*v[i])+s*w[i])

尝试失败

二进制优化数量
单调队列优化体积

二进制优化

假设第i种物品有s[i]个,将若干个第i种物品打包放一块儿考虑
比如1023个物品分为10组,每组各有1,2,4,8...512个物品,每一组要么一起选,要么都不选
类似01背包问题,经分析可知,我们可以用这十组拼凑出1023的任意一个数,
这样原来需要枚举1024次,现在只需要枚举10次
第i种物品有s[i]个,先拆分成logs[i]组,对于每一组用01背包问题去解决
时间复杂度:NVS->NlogS V=NVlogS
*/
//二进制做法 二维
#include
#include
using namespace std;
//原来N表示多少种物品,现在N表示总共有多少组物品,所以N=n范围*logS范围
const int N=110010,M=2010;
int n,m;
int v[N],w[N];//存放每一组物品的总共的体积与价值
int f[N];//直接优化到01背包问题的一维

int main(){
    cin>>n>>m;//表示多少种物品与背包的容量
    
    int cnt=0;//表示有多少组物品
    for(int i=1;i<=n;i++){
        int a,b,s;
        cin>>a>>b>>s;//输入当前这一种物品的体积,价值与个数
        int k=1;//从1开始分组,每一组有k个物品
        while(k<=s){
            cnt++;
            v[cnt]=a*k;
            w[cnt]=b*k;
            s-=k;
            k*=2;
        }
        if(s>0) {
            cnt++;
            v[cnt]=a*s;
            w[cnt]=b*s;
        }
    }
    
    n=cnt;//此时n表示当前有多少组
    
    for(int i=1;i<=n;i++){//这儿不能改成从0开始,因为w[i]从下标1开始
        for(int j=m;j>=v[i];j--){
                f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }

    cout<
using namespace std;
const int N=110010,M=2010;
int n,m;
int v[N],w[N];
int f[N];

int main(){
    cin>>n>>m;
    
    int cnt=0;
    for(int i=0;i>a>>b>>s;
        int k=1;
        while(k<=s){
            v[cnt]=a*k;
            w[cnt]=b*k;
            cnt++;
            s-=k;
            k*=2;
        }
        if(s>0) {
            v[cnt]=a*s;
            w[cnt]=b*s;
            cnt++;
        }
    }
    
    n=cnt;
    
    for(int i=0;i=v[i];j--){
                f[j]=max(f[j],f[j-v[i]]+w[i]);
        }
    }

    cout<
using namespace std;
const int N = 2010;

int n, m;
int f[N];

int main()
{
    cin >> n >> m;

    for (int i = 0; i < n; i ++ )
    {
        int v, w, s;
        cin >> v >> w >> s;
            for (int k = 1; k <= s; k *= 2)
            {
                for (int j = m; j >= k * v; j -- )
                    f[j] = max(f[j], f[j - k * v] + k * w);
                s -= k;
            }
            if (s)
            {
                for (int j = m; j >= s * v; j -- )
                    f[j] = max(f[j], f[j - s * v] + s * w);
            }
    }

    cout << f[m] << endl;

    return 0;
}

AcWing 9. 分组背包问题

//与01背包问题类似,分组背包问题可按照第i组物品不选,或者选第1,2…si个去分类
#include 
#include 

using namespace std;

const int N = 110;

int n, m;
int v[N][N], w[N][N], s[N];
int f[N];//直接一维优化版本

int main()
{
    cin >> n >> m;

    for (int i = 1; i <= n; i ++ )//i=0,i> s[i];
        for (int j = 0; j < s[i]; j ++ )
            cin >> v[i][j] >> w[i][j];
    }
//如果转移时候用的是上一层(本层)的状态,就从大到小(从小到大)枚举体积
    for (int i = 1; i <= n; i ++ )//i=0,i= 0; j -- )//从大到小
            for (int k = 0; k < s[i]; k ++ )
                if (v[i][k] <= j)
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);

    cout << f[m] << endl;

    return 0;
}

线性DP

AcWing 898. 数字三角形

/*
动态规划:状态的表示与计算
状态表示:
f(i,j)表示所有从起点走到(i,j)的路径中,数字和最大的路径
(某种集合的某种属性)
状态计算(集合划分):
f(i,j)可分为从左上角走到(i,j)或者从右上角走到(i,j)
f(i,j)=max( f(i-1,j-1)+a[i][j] , f(i-1,j)+a[i][j] )
*/
#include
using namespace std;
const int N=510,INF=1e9;

int n;
int a[N][N];
int f[N][N];

int main(){
    cin>>n;
    
    for(int i=1;i<=n;i++){//读入数字三角形,下标从1开始
        for(int j=1;j<=i;j++) cin>>a[i][j];
    }
// 因为数据中可能有负数,为了防止从边界外转移过来(全局变量默认为0),
// 所以要将边界外赋值为-INF    
    for(int i=0;i<=n;i++){
//为了在计算状态时不处理边界,将所有状态初始化为负无穷
        for(int j=0;j<=i+1;j++) f[i][j]=-INF;
    }    
    
    f[1][1]=a[1][1];
    
    for(int i=2;i<=n;i++){
        for(int j=1;j<=i;j++) f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);
    }
    
    int res=-INF;
    for(int i=1;i<=n;i++) res=max(res,f[n][i]);
    
    cout<
using namespace std;

const int N=510;
int f[N][N];
int n;

int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=i;j++){
            cin>>f[i][j];
        }
    }

    for(int i=n;i>=1;i--){
        for(int j=i;j>=1;j--){
            f[i][j]=max(f[i+1][j],f[i+1][j+1])+f[i][j];
        }
    }
    cout<

AcWing 895. 最长上升子序列

/*
DP
状态表示:
f[i]表示所有以第i个数结尾的上升子序列中长度最大的上升子序列的长度
满足某种条件的某种集合的某种属性
状态计算:
集合划分,这道题f[i]中第i个数一定存在于上升子序列中,
所以根据它的前一个数去分类,前一个数可能没有,可能是数组中第一个元素,
可能是数组中第二个元素,一直到可能是数组第i-1个元素
f[i]=max(f[j]+1) j=0,1,2...i-1;
f[0]到f[i-1]不一定都能取,因为可能有某个数大于f[i]
时间复杂度:O(n^2)
*/
#include
using namespace std;
const int N=1010;

int n;
int a[N],f[N];

int main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    
    for(int i=1;i<=n;i++){
        f[i]=1;//只要a[i]一个数
        for(int j=1;j
#include
using namespace std;
const int N=1010;

int n;
int a[N],f[N],g[N];//g[N]存储转移过程

int main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    
    for(int i=1;i<=n;i++){
        f[i]=1;//只要a[i]一个数
        g[i]=0;//表示第i个数没有从谁转移过来
        for(int j=1;j

AcWing 896. 最长上升子序列 II

/*
与上道题一样,只是数据范围变大,上道题n^2做法会超时

考虑优化

依次遍历数组每个元素,将前面求得的最长上升子序列按长度分类
存储各个长度的上升子序列结尾的最小值,相同长度的上升子序列中,结尾更大的肯定
没有结尾较小的好,因为如果一个数可以接到较大的后面,也一定可以接到较小的后面
所以较大的没必要存下来,它是可被替换的,较小的适用范围更广

可证明各个长度的上升子序列结尾的最小值单调递增

当前遍历元素可以接到某一个上升子序列后面
换句话说,在不考虑边界情况的前提下,当前遍历元素可以替换这个递增的结尾序列中
第一个大于它的元素,也就是更新某个序列的结尾,
更新的意思是当前遍历的这个元素更适合当结尾
时间复杂度:对于每个元素二分查找 O(nlogn)
*/
#include 
#include 

using namespace std;

const int N = 100010;

int n;
int a[N];
int q[N];

int main()
{
    cin>>n;
    for (int i = 0; i < n; i ++ ) cin>>a[i];
//通用做法:两种二分查找都可以做,考虑边界情况
    q[0]=a[0];
//len表示数组q中最后一个元素的下标,因为从0开始,所有输出为len+1
    int len = 0;
    for (int i = 1; i < n; i ++ ){
    //先插入第一个元素,从第二个元素开始遍历
//在结尾递增序列中找第一个大于等于当前遍历的元素,边界情况是最大元素都比它小
        // if(q[len]> 1;
        //         if (q[mid] >= a[i]) r= mid;
        //         else l = mid + 1;
        //     }
        //     q[l] = a[i];
        // }
//在结尾递增序列中找最后一个小于当前遍历的元素,边界情况是最小元素都比它大
        if(q[0]>=a[i]) q[0]=a[i];
        else{
            int l = 0, r = len;
            while (l < r)
            {
                int mid = l + r +1>> 1;
                if (q[mid] < a[i]) l = mid;
                else r = mid - 1;
            }
            len=max(len,l+1);
            q[l+1] = a[i];            
        }
    }

    cout<

AcWing 897. 最长公共子序列

/*
状态表示:
f(i,j)表示所有在第一个序列的前i个字母中出现,且在第二个序列的前j个字母中出现
的子序列(公共子序列)中最长子序列的长度(max)
状态计算:
集合划分最主要的是不漏,如果属性是求最值,划分是否重复不重要,如果求数量,
不能重复
f(i,j)划分成四个部分
包含i和j f(i-1,j-1)+1(这种情况要考虑到需要a[i]和b[j]相等)
只包含i  f(i,j-1)一定不包含j,可能包含i,可能不包含i
只包含j  f(i-1,j)一定不包含i,可能包含j,可能不包含j
均不包含 f(i-1,j-1)
最后一种情况f(i-1,j-1)被f(i-1,j)或f(i,j-1)包含,所以不用单独考虑
中间两种情况的状态表示互有重复,且与只包含i(j)的情况不完全等价,而是包含关系
但因为是求最值,所以不遗漏才最重要
综上所述:
f(i,j)=max( f(i-1,j) , f(i,j-1) , f(i-1,j-1)+1)
*/
#include 
using namespace std;

const int N = 1010;

int n, m;
char a[N], b[N];//注意定义时候别出错,y总这里最开始写成int,很久都没找到错误
int f[N][N];

int main()
{
    cin>>n>>m>>a+1>>b+1;

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            f[i][j] = max(f[i - 1][j], f[i][j - 1]);
            if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
        }

    cout<

AcWing 902. 最短编辑距离

/*
状态表示:
f(i,j)表示所有A中前i个字母转变成B中前j个字母的操作数的最小值
状态计算:
集合划分 三种情况
A中第i个字母增加一个元素后转变成功:f(i,j-1)
A中第i个字母删除后转变成功:f(i-1,j)
A中第i个字母修改后转变成功:f(i-1,j-1)+0/1(取决于a[i]=b[j]是否相等)
*/
#include
using namespace std;
const int N=1010;

int n,m;
char a[N],b[N];
int f[N][N];

int main(){
    cin>>n>>a+1>>m>>b+1;
    //边界情况的确定,一些DP问题f[0][0]等于0就行,一些要单独考虑
    //注意这里从0开始,一直到等于n(m)结束
    for(int i=0;i<=n;i++) f[i][0]=i;
    for(int i=0;i<=m;i++) f[0][i]=i;
    
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1);
            f[i][j]=min(f[i][j],f[i-1][j-1]+(a[i]!=b[j]));
        }
    }
    
    cout<

AcWing 899. 编辑距离

#include
#include
using namespace std;
const int N=15,M=1010;
int n,m;
char str[M][N];
int f[N][N];

int edit_dist(char a[],char b[]){
    int n=strlen(a+1),m=strlen(b+1);
    for(int i=0;i<=n;i++) f[i][0]=i;
    for(int i=0;i<=m;i++) f[0][i]=i;
    
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1);
            f[i][j]=min(f[i][j],f[i-1][j-1]+(a[i]!=b[j]));
        }
    }
    return f[n][m];
}
int main(){
    cin>>n>>m;
    for(int i=0;i>str[i]+1;
    
    while(m--){
        char s[N];
        int limt;
        cin>>s+1>>limt;
        
        int res=0;
        for(int i=0;i

区间DP

AcWing 282. 石子合并

/*
注意这里要求合并的是两堆相邻的石子,所以要考虑区间DP问题,不然这道题是典型的贪心问题
区间DP:状态表示某一个区间
状态表示:
f(i,j)表示区间[i,j]也就是所有合并第i堆到第j堆石子的方法中的最小代价
最后所求为f(1,n)
状态计算:
集合划分  按照最后合并两堆的分界线k分类,也就是最后合并的两堆为
(l,k)和(k+1,r)  
k=l,....,r-1(因为要求最后两个待合并的区间非空,所有k最小取l,最大取r-1)
所以f(l,r)=min( f(l,k) + f(k+1,r) + l到r的前缀和也就是s[r]-s[l-1])
*/
#include 
#include 

using namespace std;

const int N = 310;

int n;
int s[N];//前缀和数组
int f[N][N];//状态数组

int main()
{
    cin>>n;
    for (int i = 1; i <= n; i ++ ) cin>>s[i];
//初始化前缀和数组
    for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1];
//len表示区间中有多少个元素,外层循环枚举区间长度从2开始
//len=1时代价为0,在这道题中,状态数组的边界情况不用特意初始化
    for (int len = 2; len <= n; len ++ )
    //内层循环枚举区间左端点
        for (int i = 1; i + len - 1 <= n; i ++ )
        {
            int l = i, r = i + len - 1;
            f[l][r] = 1e8;//取最小值,要先初始化为最大值
            for (int k = l; k < r; k ++ )
//这里k表示左区间[l,k]的右端点,要保证两个区间非空,所以k可以去到l,不能为r
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
        }

    cout<

计数类DP

AcWing 900. 整数划分

/*
由题知,选择方案n1≥n2≥…≥nk,所以选择方案时不考虑数与数之间的顺序,
比如5=4+1和5=1+4看作一种方案
完全背包做法:
整数n看作一个容量为n的背包,一共有n个物品,物品的体积分别是1,2,3,...,n,
每个物品可以选无数次,所以是一个完全背包问题,求的是恰好装满背包的方案数,
由于不考虑顺序,所以每一种背包问题的选法都可以看作一种划分方式
状态表示:f(i,j)表示只从1~i中选,且总体积恰好等于j的方案数(属性是数量)
状态计算:根据第i个物品选择的数量划分,由于是求方案数,所以每种情况相加
f(i,j)  =f(i-1,j)+f(i-1,j-i)+...+f(i-1,j-s*i)  s*i
using namespace std;
const int N=1010,mod=1e9+7;
int n;
int f[N];

int main(){
    cin>>n;
    f[0] = 1;//当一个数都没有时是一种方案,二维时最左边为1,最上边为0
    for (int i = 1; i <= n; i ++ )
        for (int j = i; j <= n; j ++ )
            f[j] = (f[j] + f[j - i]) % mod;

    cout << f[n] << endl;

    return 0;
}
/*
状态表示:f(i,j)表示总和是i,并且恰好表示成j个数的和的方案数量
状态计算:根据方案中最小值等于1或者大于1分类
方案中最小值等于1:减去最小值的那个1,方案数f(i-1,j-1)
方案中最小值大于1:方案中每个数都减去1,方案数f(i-j,j)
f(i,j)=f(i-1,j-1)+f(i-j,j)
注意最后结果是f(n,1)+f(n,2)+...+f(n,n)
*/
#include 
#include 
using namespace std;

const int N = 1010, mod = 1e9 + 7;

int n;
int f[N][N];

int main()
{
    cin >> n;

    f[0][0] = 1;//边界情况与另外做法对比
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % mod;

    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = (res + f[n][i]) % mod;

    cout << res << endl;

    return 0;
}

数位统计DP

AcWing 338. 计数问题


状态压缩DP

AcWing 291. 蒙德里安的梦想

/*
状态压缩DP,就是用二进制数保存状态。为什么不直接用数组记录呢?
因为用一个二进制数记录方便作位运算。前面做过的八皇后,八数码,也用到了状态压缩。

本题等价于找到所有横放 1 X 2 小方格的合法方案数,因为所有横放确定了,那么竖放方案是唯一的。

如何判断方案是否合法:所有剩余位置能否填满竖着的小方块,所有每一列内部连续的空着的小方块需要是偶数个

状态表示:
用f[i][j]表示已经将前i-1列摆好,且从第i-1列伸出到第i列的状态是j的方案数。
j状态二进制表示中某一位等于1表示在这一行上一列有横放格子,本列有格子捅出来。

状态计算:
本列的每一个状态都由上列所有“合法”状态转移过来,上一列的合法方案一旦确定,本列的摆放也确定。
所以说第i列状态j的方案数为上一列也就是i-1列所有能合法转移到状态j的状态k的方案累加
f[i][j] += f[i - 1][k]

两个转移条件: 
同一行的第 i 列和第i - 1列不同时捅出来 ; 
本列捅出来的状态j和上列捅出来的状态k求或,得到上列的每个格子是否被占用的状态,如果上一列是奇数空行状态,则不转移。
初始化条件f[0][0] = 1,第0列只能是状态0,无任何格子捅出来。返回f[m][0]。第m + 1列不能有东西捅出来。

时间复杂度:先枚举每一列,再枚举每一列的每种状态,再枚举上一列的每种状态来找到合法的状态转移
11*2^11*2^11
优化:
预处理对于每一个状态j,上一列有哪些状态k可以合法更新到j,所以可以少遍历一层循环
*/
#include //memset函数需要引入这个头文件
#include 
#include 
#include//存储合法转移状态

using namespace std;

const int N = 12, M = 1 << N;//N为12是因为会遍历到n+1列,最多为12,M表示有多少种状态

int n, m;
long long f[N][M];//方案数较大,用long long存
bool st[M];//表示每一种状态转移是否合法 
//第i列状态为j,第i-1列状态为k,j|k表示这种状态转移情况下第i-1列每个格子的被占用情况,1表示被占用
vector state[M];//表示每一种状态可以由哪些状态合法转移过来,这是用于优化的预处理过程


int main()
{
    while (cin >> n >> m, n || m)
    {
        //枚举哪些状态转移是合法的
        for (int i = 0; i < 1 << n; i ++ )//一共n行,每个格子是否被占用,枚举2^n种不同的状态
        //0表示00..0(n个0)初始状态,i < 1 << n表示11...1(n个1)最终状态
        {
            int cnt = 0;//用来记录连续的0的个数
            st[i] = true;//记录这个状态被枚举过且可行
            for (int j = 0; j < n; j ++ )//从低位到高位枚举它的每一位
                if (i >> j & 1)//如果状态i的二进制表示中第j位为1的话
                {
                    if (cnt & 1) st[i] = false;//如果之前连续0的个数是奇数,竖的方块插不进来,这种状态不行
                    cnt = 0;//清空计数器
                }
                else cnt ++ ;//如果不为1,计数器+1
//前面判断每一列是否合法是通过找到状态为1的格子,判断它之前的cnt是否为奇数,但如果这一列中一直没有状态为1的格子呢
//所以最后再加一个判断
            if (cnt & 1) st[i] = false;//到末尾再判断一下前面0的个数是否为奇数,同前
        }
        
        // for(int i=0; i<1<

AcWing 91. 最短Hamilton路径

/*
两个关键因素:哪些点被遍历过,当前停留在哪个点上
状态表示:
f(i,j) 表示状态是i,此时停在点j的最短路径
i表示经过哪些点,对应的二进制表示上哪一位为1就代表这一点被遍历过了
j表示当前到达了j点

状态计算:
根据由哪一个点达到j分类
f(state,j)=min( f(state_k , k) + w[k][j] ) state_k表示除掉j,包含k的集合
*/
#include 
#include 
#include 

using namespace std;

const int N = 20, M = 1 << N;

int n;
int w[N][N];
int f[M][N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ )
        for (int j = 0; j < n; j ++ )
            cin >> w[i][j];

    memset(f, 0x3f, sizeof f);//状态全部初始化为正无穷,以便于取最小值
    f[1][0] = 0;//初始时刻只在0号点,所以只经过了0号点,停留在0号点上

    for (int i = 0; i < 1 << n; i ++ )
        for (int j = 0; j < n; j ++ )
            if (i >> j & 1)//从0走到j的话,i中一定得包含j,也就是第j位为1
                for (int k = 0; k < n; k ++ )//枚举从哪儿转移到状态j
                    if (i >> k & 1)
                        f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);

    cout << f[(1 << n) - 1][n - 1];//所有点都走过来,最后停留在点n-1

    return 0;
}

树形DP

AcWing 285. 没有上司的舞会

/*
状态表示:
    集合:
        对于每一个节点u来说有两种情况:
        f[u][0]表示所有从u为根的子树中选择,并且不选u这个点
        f[u][1]表示所有从u为根的子树中选择,并且选u这个点
    属性:max
状态计算:
当前u结点不选,子结点可选可不选
f[u][0]=∑ max(f[si,0],f[si,1])
当前u结点选,子结点一定不能选
f[u][1]=∑ (f[si,0])
有n个节点,总共有2n个状态,每一个状态算的是它所有的儿子,所有节点儿子的
数量加在一块是边的数量也就是n-1,所以总共枚举次数是O(n-1)==O(n)
*/
#include 
#include 
#include 
using namespace std;

const int N = 6010;

int n;
int h[N], e[N], ne[N], idx;//用邻接表存储
int happy[N];//每个人的高兴度
int f[N][2];//每个节点的两个状态
bool has_fa[N];//判断每个点是否有父节点,这道题没有告诉根节点是谁
//所以要判断没有父节点的点就是根节点

void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

void dfs(int u)
{
    f[u][1] = happy[u];

    for (int i = h[u]; ~i; i = ne[i])
    {//枚举相邻点,也就是所有儿子
        int j = e[i];
        dfs(j);//先从儿子开始深搜,其实也就是从下往上计算

        f[u][1] += f[j][0];
        f[u][0] += max(f[j][0], f[j][1]);
    }
}

int main()
{
    cin>>n;

    for (int i = 1; i <= n; i ++ ) cin>>happy[i];

    memset(h, -1, sizeof h);//初始化邻接表表头
    for (int i = 0; i < n - 1; i ++ )//读入n-1条边
    {
        int a, b;
        cin>>a>>b;
        add(b, a);//边由父节点指向子节点
        has_fa[a] = true;
    }

    int root = 1;//找到根节点,也就是找到谁没有父节点
    while (has_fa[root]) root ++ ;

    dfs(root);

    cout<

记忆化搜索

AcWing 901. 滑雪

/*
记忆化搜索,递归求解每一种状态,之前所有的dp问题都是循环求解,
其实都可以递归求解,并且这样更容易理解
状态表示:f(i,j)
    集合:所有从(i,j)这个点开始滑的路径长度
    属性:max
状态计算:
按照第一步往哪个方向滑分为四类,取max

下面代码中的f[i][j] = max(f[i][j],dp(xx,yy)+1);
实际上就是向四个方向判断之后转移:
if(a[i-1][j]b>c>d,绝对不可能存在d>a的情况,所以可以用dp来递归求解
*/
#include 
#include 
#include 

using namespace std;

const int N = 310;

int n, m;
int g[N][N];//每个点的高度
int f[N][N];//状态

int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

int dp(int x, int y)
{
    int &v = f[x][y];
    if (v != -1) return v;//如果计算过,直接返回

    v = 1;//v最小值是1,最次只走当前这个格子
    for (int i = 0; i < 4; i ++ )//枚举四个方向
    {
        int a = x + dx[i], b = y + dy[i];
        if (a >= 1 && a <= n && b >= 1 && b <= m && g[x][y] > g[a][b])
            v = max(v, dp(a, b) + 1);
    }

    return v;
}

int main()
{
    cin>>n>>m;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            cin>>g[i][j];

    memset(f, -1, sizeof f);//要递归求解所有点状态,先全部初始化为-1

    int res = 0;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            res = max(res, dp(i, j));
            //遍历所有点,求出每个点出发得到的最大值

    cout<

贪心

区间问题

AcWing 905. 区间选点

/*
选出来尽可能少的点,使得每个区间至少包含一个点
区间问题一般是先排序,要么按左端点排序,要么按右端点排序,要么双关键字排序

1.将每个区间按照右端点从小到大进行排序
2.从前往后枚举每个区间
    如果当前区间已经包含点,pass
    否则说明需要在这个区间新放一个点,放在当前区间的右端点处
*/
#include 
#include 

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const
    {
        return r < W.r;//运算符重载,按照右端点排序
    }
}range[N];

int main()
{
    cin>>n;
    for (int i = 0; i < n; i ++ ) cin>>range[i].l>>range[i].r;

    sort(range, range + n);

    int res = 0, ed = -2e9;
//因为最开始一个点都没有选,所以可以把上一个点设为负无穷
    for (int i = 0; i < n; i ++ )//遍历区间
        if (range[i].l > ed)
        {
            res ++ ;
            ed = range[i].r;
        }

    cout<

AcWing 908. 最大不相交区间数量

/*
选尽可能多的互不相交的区间,一个区间可以看做一个活动(课程)的开始和结束时间,
有很多个活动(课程),要参加尽可能多的活动(选尽可能多的课程)

这道题和上道题一样也是先将所有区间按照右端点排序
然后从前往后依次枚举每个区间,如果当前区间包含这个点的话,直接pass,
否则选择当前区间的右端点
按照这个方式选择的端点数量就是题中要求的最大互不相交区间数量

为什么最大不相交区间数==最少覆盖区间点数呢?
因为如果几个区间能被同一个点覆盖
说明他们相交了
所以有几个点就是有几个不相交区间
至于为什么是最大的,反证法即可,如果还能找到一个区间互不相交,
则还需要一个点才能覆盖所有区间,此时点数不是最少,矛盾
*/
#include 
#include 

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const
    {
        return r < W.r;//运算符重载,按照右端点排序
    }
}range[N];

int main()
{
    cin>>n;
    for (int i = 0; i < n; i ++ ) cin>>range[i].l>>range[i].r;

    sort(range, range + n);

    int res = 0, ed = -2e9;
//因为最开始一个点都没有选,所以可以把上一个点设为负无穷
    for (int i = 0; i < n; i ++ )//遍历区间
        if (range[i].l > ed)
        {
            res ++ ;
            ed = range[i].r;
        }

    cout<

AcWing 906. 区间分组

/*
首先将所有区间按照左端点从小到大排序
从前往后遍历每个区间,判断当前区间能否放到某个现有的组中
    如果这个区间的左端点比最小组的右端点要小,ranges[i].l<=heap.top() 
    就开一个新组 heap.push(range[i].r)
    如果这个区间的左端点比最小组的右端点要大,则放入该组并更新这一组的右端点
    heap.pop(), heap.push(range[i].r);
*/
#include 
#include 
#include 

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const
    {
        return l < W.l;
    }
}range[N];

int main()
{
    cin>>n;
    for (int i = 0; i < n; i ++ ) cin>>range[i].l>>range[i].r;

    sort(range, range + n);

    priority_queue, greater> heap;
    //小根堆,堆中放的是已经确定的组的最右的端点
    //heap.top()是对应的最小的最右点
    for (int i = 0; i < n; i ++ )
    {
        auto r = range[i];
//如果当前一个组都没有,或者当前区间的左端点比最小的右端点还要小
//放到任何一组都会有相交部分,新开一组
        if (heap.empty() || heap.top() >= r.l) heap.push(r.r);
        else
        {
            heap.pop();
            heap.push(r.r);
        }
    }

    cout<

AcWing 907. 区间覆盖

/*
选择尽可能少的小区间覆盖大区间

1.将所有区间按照左端点从小到大进行排序
2.从前往后枚举每个区间,在所有能覆盖start的区间中(也就是l<=start),
  选择右端点最大的区间,然后将start更新成右端点的最大值
*/
#include 
#include 

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const
    {
        return l < W.l;
    }
}range[N];

int main()
{
    int st, ed;
    cin>>st>>ed;
    cin>>n;
    for (int i = 0; i < n; i ++ ) cin>>range[i].l>>range[i].r;

    sort(range, range + n);

    int res = 0;
    bool success = false;
    for (int i = 0; i < n; i ++ )
    {
        int j = i, r = -2e9;
        while (j < n && range[j].l <= st)
        {//在左端点l<=st的前提下,选右端点尽可能大的小区间
            r = max(r, range[j].r);
            j ++ ;
        }

        if (r < st) break;

        res ++ ;
        if (r >= ed)
        {//初始化为false,只有在最后找到一个区间>=ed时才能设置为true
            success = true;
            break;
        }

        st = r;
        i = j - 1;
    }

    if (!success) res = -1;
    cout<

Huffman树

AcWing 148. 合并果子

/*
经典哈夫曼树的模型,每次合并重量最小的两堆果子即可

使用小根堆维护所有果子,每次弹出堆顶的两堆果子,并将其合并,
合并之后将两堆重量之和再次插入小根堆中。
每次操作会将果子的堆数减一,一共操作n−1次即可将所有果子合并成1堆
每次操作涉及到2次堆的删除操作和1次堆的插入操作,计算量是O(logn)
因此总时间复杂度是O(nlogn)
*/
#include 
#include 
#include 

using namespace std;

int main()
{
    int n;
    cin>>n;

    priority_queue, greater> heap;
    while (n -- )
    {
        int x;
        cin>>x;
        heap.push(x);
    }

    int res = 0;
    while (heap.size() > 1)
    {
        int a = heap.top(); heap.pop();
        int b = heap.top(); heap.pop();
        res += a + b;
        heap.push(a + b);
    }

    cout<

排序不等式

AcWing 913. 排队打水

/*
让打水时间最短的人先打水
*/
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 100010;

int n;
int t[N];

int main()
{
    cin>>n;
    for (int i = 0; i < n; i ++ ) cin>>t[i];

    sort(t, t + n);

    LL res = 0, w = 0;
    for(int i = 0; i < n; i ++)
    {
        res += w;
        w += t[i];
    }
    
    cout<

绝对值不等式

AcWing 104. 货仓选址

//先排序,然后仓库放到中间位置
#include 
#include 

using namespace std;

const int N = 100010;

int n;
int q[N];

int main()
{
    cin>>n;

    for (int i = 0; i < n; i ++ ) cin>>q[i];

    sort(q, q + n);

    int res = 0;
    for (int i = 0; i < n; i ++ ) res += abs(q[i] - q[n / 2]);

    cout<

推公式

AcWing 125. 耍杂技的牛

#include 
#include 

using namespace std;

typedef pair PII;

const int N = 50010;

int n;
PII cow[N];

int main()
{
    cin>>n;
    for (int i = 0; i < n; i ++ )
    {
        int s, w;
        cin>>w>>s;
        cow[i] = {w + s, w};
    }
    //默认按照第一关键字w+s排序,这样得到的最大危险系数最小
    sort(cow, cow + n);

    int res = -2e9, sum = 0;
    for (int i = 0; i < n; i ++ )
    {
        int s = cow[i].first - cow[i].second, w = cow[i].second;
        res = max(res, sum - s);//遍历每头牛的危险值记录其中的最大值即可
        sum += w;
    }

    cout<

你可能感兴趣的:((最终总结)Acwing算法课)