AcWing算法基础班笔记

快速排序

  • 思想:分治
    • 确定分界点:q[l],q[(l+r)/2],q[r],随机
    • 调整范围 :使得第一个区间的值都小于等于x,第二个区间的值都大于等于x(重点)
    • 递归处理左右两端
  • 做法
#include 

using namespace std;

const int N = 1e6 + 10;

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 >> 1];
    while(i < j) {
        while(q[++i] < x);
        while(q[--j] > x);
        if(i < j) swap(q[i], q[j]);
    }
    quick_sort(q, l, j);
    quick_sort(q, j + 1, r);
}

int main() {
    scanf("%d", &n);
    for(int i = 0; i < n; i++) scanf("%d", &q[i]);
    quick_sort(q, 0, n-1);
    for(int i = 0; i < n; i++) printf("%d ", q[i]);
    return 0;
}
  • 分界点处理问题

    1.
     int x = q[l+r>>1];
     quick_sort(q,l,j);
     quick_sort(q,j+1,r);
    
    2.
     int x = q[l+r+1>>1];
     quick_sort(q,l,i-1);
     quick_sort(q,1,r);
    
  • 当题目数据加强时,尽量选择数据中点
    输入数据为1,2时,可以自行判断一下。
    WpAjyQ.png


归并算法

  • 确定分界点 mid=(l+r)/2
  • 递归排序left,right
  • 归并:合二为一
    AcWing算法基础班笔记_第1张图片
#include 

using namespace std;
const int N=1000010;
int n;
int q[N],temp[N];

void mergersort(int l,int r){
   if(l>=r) return;
   int mid = l+r>>1;
   mergersort(l,mid),mergersort(mid+1,r);
   
   int k=0,i=l,j=mid+1;
   while(i<=mid&&j<=r){
       if(q[i]<=q[j]) temp[k++]=q[i++];
       else temp[k++]=q[j++];
   }
   while(i<=mid) temp[k++]=q[i++];
   while(j<=r) temp[k++]=q[j++];
   
   for(i=l,j=0;i<=r;i++,j++) q[i]=temp[j];
}

int main(){
   scanf("%d",&n);
   for(int i=0;i

二分模板(解决边界问题)

整数二分

算法思路:假设目标值在闭区间[l, r]中, 每次将区间长度缩小一半,当l = r时,我们就找到了目标值。

  • 当我们将区间[l, r]划分成[l, mid]和[mid + 1, r]时,其更新操作是r = mid或者l = mid + 1;,计算mid时不需要加1。
 int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;
        else l = mid + 1;
    }
    return l;
}
  • 当我们将区间[l, r]划分成[l, mid - 1]和[mid, r]时,其更新操作是r = mid - 1或者l = mid;,此时为了防止死循环,计算mid时需要加1。
int bsearch_2(int l, int r)
{
    while (l < r)
    {
        int mid = l + r + 1 >> 1;
        if (check(mid)) l = mid;
        else r = mid - 1;
    }
    return l;
}
  • 先写一个check函数
  • 判定在check的情况下(true和false的情况下),如何更新区间。
  • 在check(m)==true的分支下是:
    • l=mid的情况,中间点的更新方式是m=(l+r+1)/2
    • r=mid的情况,中间点的更新方式是m=(l+r)/2
  • 这种方法保证了:
    1. 最后的l==r
    2. 搜索到达的答案是闭区间的,即a[l]是满足check()条件的。

浮点数二分

保留四位小数(1e-6一定要多2)
#include

using namespace std;

double n;

int main(){
    cin >> n;

    double l = -1e4, r = 1e4;
    while(r - l > 1e-8){
        double mid = (l + r) / 2;
        if(mid * mid * mid >= n) r = mid;
        else l = mid;
    }

    printf("%lf", l);

    return 0;
}

高精度问题

指的是大整数运算,包括加减乘除,这算是算法开发面试中经常能看到的一类问题,实际上就是对我们人工真实加减乘除的简单模拟。
AcWing算法基础班笔记_第2张图片

  • 因为C++中的 一般运算变量类型 例如int、long long 等大小限制,无法完成大数字的运算。

高精度加法

  • 用数组存储,数组第0位通常存储大数的个位因为进位时,在末尾添加数组比较方便
  • 加引用的作用是提高效率,这样就不会重新拷贝一遍数组
#include
#include
using namespace std;
vector sum(vector& a,vector& b)
{
    vector result;
    if(a.size()= b.size()
    {
        if(i c,d;
    vector result;//存放结果
    cin>>a>>b;
    //按 个位 十位 百位 ...n位 存放
    for(int i=a.size()-1;i>=0;i--) c.push_back(a[i]-'0');//将字符a[i]转换成数值
    for(int i=b.size()-1;i>=0;i--) d.push_back(b[i]-'0');//将字符b[i]转换成数值
    result=sum(c,d);
    for(int i=result.size()-1;i>=0;i--) cout<

高精度减法

思路

  • 和高精度加法差不多,值得注意的是
    • 减法的借位处理
    • 相减为负数的处理
      AcWing算法基础班笔记_第3张图片
    • 前导0的处理 while(C.size() > 1 && C.back() == 0) C.pop_back(); //去掉前导0去除前导0,123-120=003,实际为300,这样去掉后面的0

收获

  • 对于 t = A[i] - B[i] - t; 可以拆为 t = A[i] - t如果B[i]合法,再t -= B[i] 这么两步来做
  • 相减后t的处理 ,把 t >=0 和 t < 0 用一个式子来表示 t = (t + 10) % 10 这个木有想到
  • A B大小判断,自己写的太冗余,不如单独拎出来
bool cmp(vector& A, vector &B)
{
    if(A.size() != B.size()) return A.size() > B.size();  //直接ruturn 了就不用else

    for(int i = A.size(); i >= 0; i--)
        if(A[i] != B[i])
            return A[i] > B[i];

    return true;
}
#include 
#include 

using namespace std;

bool cmp(vector& A, vector &B)
{
    if(A.size() != B.size()) return A.size() > B.size();  //直接ruturn 了就不用else

    for(int i = A.size(); i >= 0; i--)
        if(A[i] != B[i])
            return A[i] > B[i];

    return true;
}

vector  sub(vector& A, vector &B)
{
    vector C;
    int t = 0;
    for(int i = 0; i < A.size(); i++)
    {
        t = A[i] - t;
        if(i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10 ); // 合而为1
        if(t < 0)  t = 1;
        else t = 0;

    }

    while(C.size() > 1 && C.back() == 0) C.pop_back();  //去掉前导0

    return C;
}

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

    cin >> a >> b ;

    for(int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
    for(int i = b.size() - 1; i >= 0; i--) B.push_back(b[i] - '0');

    if (cmp(A,B)) 
    {
        auto C = sub(A, B);
        for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
        return 0;
    }
    else
    {
        auto C = sub(B, A);
        printf("-");
        for(int i = C.size() - 1; i >= 0; i--) printf("%d", C[i]);
        return 0;
    }


}


高精度乘法

AcWing算法基础班笔记_第4张图片

#include
#include
#include
using namespace std;
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;
    vector A;
    cin>>a>>b;
    for(int i=a.size()-1;i>=0;i--)
        A.push_back(a[i]-'0');
    vector C=mul(A,b);
    for(int i=C.size()-1;i>=0;i--)
        printf("%d",C[i]);
    return 0;
}

高精度除法

#include
#include
#include
using namespace std;
//int r=0;
vector div(vector &A,int B,int &r){//r传入r的地址,便于直接对余数r进行修改
    vector C;
    for(int i=0;i1&&C.back()==0) C.pop_back();
    return C;
}
int main(){
    string a;
    int B,r=0; //代表余数
    cin>>a>>B;
    vector A;
    for(int i=0;i=0;i--) cout<

双指针算法

  • 算法模板
 for(int i=0, j=0; i
#include 

using namespace std;

const int N =1e5;

int n;
int q[N],w[N];

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

前缀和

  • 作用:可以求特定区间里的和
  • AcWing算法基础班笔记_第5张图片
    S[i]从1开始,S[0]=0可以处理边界问题(c++里面全局变量初始就是0了)
  • 原题链接
#include 

using namespace std;

const int N=1e5+10;

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


int main(){
   
        for(int i=1;i<=n;i++) cin>>a[i];
        for(int i=1;i<=n;i++){
            s[i]=s[i-1]+a[i];//前缀和的初始化
        }
        int l,r;
        while(m--){
            cin>>l>>r;
            cout<

二维化的前缀和

AcWing算法基础班笔记_第6张图片
- S[i,j]S[i,j]即为图1红框中所有数的的和为:
S[i,j]=S[i,j−1]+S[i−1,j]−S[i−1,j−1]+a[i,j]
- (x1,y1),(x2,y2)(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 q[N][N],s[N][N];

int n,m,t;

int main(){
    cin>>n>>m>>t;
    
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++){
            cin>>q[i][j];
            s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+q[i][j];
        }
           
   while(t--){
       int x1,y1,x2,y2;
       cin>>x1>>y1>>x2>>y2;
       
       cout<

差分

类似于数学中的求导和积分,差分可以看成前缀和的逆运算

差分数组:
首先给定一个原数组a:a[1], a[2], a[3],,,,,, a[n];
然后我们构造一个数组b : b[1] ,b[2] , b[3],,,,,, b[i];
使得 a[i] = b[1] + b[2 ]+ b[3] +,,,,,, + b[i]
也就是说,a数组是b数组的前缀和数组,反过来我们把b数组叫做a数组的差分数组。换句话说,每一个a[i]都是b数组中从头开始的一段区间和。
考虑如何构造差分b数组?
最为直接的方法
如下:
a[0 ]= 0;
b[1] = a[1] - a[0];
b[2] = a[2] - a[1];
b[3] =a [3] - a[2];

b[n] = a[n] - a[n-1];

  • 作用
    给定区间[l ,r ],让我们把a数组中的[ l, r]区间中的每一个数都加上c,即 a[l] + c , a[l+1] + c , a[l+2] + c ,,,,,, a[r] + c;

暴力做法是for循环l到r区间,时间复杂度O(n),如果我们需要对原数组执行m次这样的操作,时间复杂度就会变成O(n*m)。有没有更高效的做法吗? 考虑差分做法。
始终要记得,a数组是b数组的前缀和数组,比如对b数组的b[i]的修改,会影响到a数组中从a[i]及往后的每一个数。

首先让差分b数组中的 b[l] + c ,a数组变成 a[l] + c ,a[l+1] + c,,,,,, a[n] + c;
然后我们打个补丁,b[r+1] - c, a数组变成 a[r+1] - c,a[r+2] - c,,,,,,,a[n] - c;
AcWing算法基础班笔记_第7张图片
因此我们得出一维差分结论:给a数组中的[ l, r]区间中的每一个数都加上c,只需对差分数组b做 b[l] + = c, b[r+1] - = c。时间复杂度为O(1), 大大提高了效率。

//差分 时间复杂度 o(m)
#include
using namespace std;
const int N = 1e5 + 10;
int a[N], b[N];
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
    {
        scanf("%d", &a[i]);
        b[i] = a[i] - a[i - 1];      //构建差分数组
    }
    int l, r, c;
    while (m--)
    {
        scanf("%d%d%d", &l, &r, &c);
        b[l] += c;     //将序列中[l, r]之间的每个数都加上c
        b[r + 1] -= c;
    }
    for (int i = 1; i <= n; i++)
    {
        a[i] = b[i] + a[i - 1];    //前缀和运算
        printf("%d ", a[i]);
    }
    return 0;
}

差分矩阵

大佬题解
原题链接

#include 

using namespace std;
const int N =1e3+10;
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]);//初始化差分矩阵,和插入二维矩阵类似,想象成1*1矩阵
        }
    }
    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算法基础班笔记_第8张图片
- n>>k //右移
- X&1 看个位是几

  1. lowbit(X) :返回x的最后一位1的位置
    AcWing算法基础班笔记_第9张图片
    lowbit(1010)返回值为10
  • 作用:统计x中1的个数
    [原题链接](https://www.acwing.com/problem/content/description/25/)
class Solution {
public:
    int NumberOf1(int n) {
        int t=0;
        while(n){
            n-=n&-n;//每次减去n的最后一位1
            t++;
        }
        return t;
    }
};

x=1010
原码:000001010 x
反码:111110101 !x
补码:111110110 -x

离散化

AcWing算法基础班笔记_第10张图片

  • 值域很大但是值很少
  • 为什么要离散化呢,因为存储的下标实在太大了,如果直接开这么大的数组,根本不现实,第二个原因,本文是数轴,要是采用下标的话,可能存在负值,所以也不能。
  • 离散化的本质,是映射,将间隔很大的点,映射到相邻的数组元素中。减少对空间的需求,也减少计算量。
    其实映射最大的难点是前后的映射关系,如何能够将不连续的点映射到连续的数组的下标。此处的解决办法就是开辟额外的数组存放原来的数组下标,或者说下标标志,本文是原来上的数轴上的非连续点的横坐标。
    首先要明确find函数的功能,输入一个离散数组的位置(映射前的位置)x返回连续数组的位置+1(映射后的位置+1)。+1的目的是为了求区间和时少一步下标为0的判断。
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=all.size()-1;
    while(l>1;
        if(alls[mid]>=x) r=mid;
        else l=mid+1;
    }
    return r+1;//映射到1.2.3...n。为什么返回r + 1,这是变相的让映射后的数组从1开始。此处描述映射后的数组下标对应的数值用的是a数组。
}
  • 原题链接
  • 分析一下y总的代码。
    主要分为5大步:
    1.读输入。将每次读入的x c push_back()到add中,将每次读入的位置x push_back()到alls中,将每次读入的l r push_back()到query中。
    2.排序、去重。
    3.通过遍历add,完成在离散化的数组映射到的a数组中进行加上c的操作(用到find函数)。
    4.初始化s数组。
    5.通过遍历query,完成求区间[l,r]的和。
#include 
#include 
#include 

using namespace std;

typedef pair PII;

const int N = 300010;

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

vector alls;//首先要明确alls中存放的是位置而不是值,也就是存放的是x而不是c。
vector add, query;

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开始映射
}


int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ )
    {
        int x, c;
        cin >> x >> c;
        add.push_back({x, c});

        alls.push_back(x);
    }

    for (int i = 0; i < m; i ++ )
    {
        int l, r;
        cin >> l >> r;
        query.push_back({l, r});

        alls.push_back(l);//求前缀和就需要下标l r,如果不加入l r到alls中的话,第5步中遍历时query就没有办法通过输入的l r去访问a或者s。因为find函数就是输入映射前的下标,返回在alls中的下标+1。
        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);
        a[x] += item.second;
    }

    // 预处理前缀和
    for (int i = 1; i <= alls.size(); i ++ ) s[i] = s[i - 1] + a[i];//因为映射从1开始到all.size()

    // 处理询问
    for (auto item : query)
    {
        int l = find(item.first), r = find(item.second);
        cout << s[r] - s[l - 1] << endl;
    }

    return 0;
}

  • unique函数的实现(双指针)
vector::iterator unique(vector &a)
{
    int j = 0;
    for (int i = 0; i < a.size(); i ++ )
        if (!i || a[i] != a[i - 1])
            a[j ++ ] = a[i];
    // a[0] ~ a[j - 1] 所有a中不重复的数

    return a.begin() + j;
}

链表和邻接表

  • 第一种方式:结构体(不推荐使用)
    struct Node{
        int val;
        Node *Next;
    }
    new Node();//每次调用这个函数,耗费的时间很长
  • 数组模拟单链表
    AcWing算法基础班笔记_第11张图片
    原题链接
#include 

using namespace std;

const int N = 100010;

int n;
int h[N], e[N], ne[N], head, idx;
//比如ne[idx],你可以理解为idx->next,同理 ne[ne[idx]]:ne->next>next,这样理解方便很多

//对链表进行初始化
void init(){
    head = -1;//最开始的时候,链表的头节点要指向-1,
    //为的就是在后面进行不断操作后仍然可以知道链表是在什么时候结束
    /*
    插句题外话,我个人认为head其实就是一个指针,是一个特殊的指针罢了。
    刚开始的时候它负责指向空结点,在链表里有元素的时候,它变成了一个指向第一个元素的指针

    当它在初始化的时候指向-1,来表示链表离没有内容。
    */
    idx = 0;//idx在我看来扮演两个角色,第一个是在一开始的时候,作为链表的下标,让我们好找
    //第二在链表进行各种插入,删除等操作的时候,作为一个临时的辅助性的所要操作的元素的下标来帮助操作。并且是在每一次插入操作的时候,给插入元素一个下标,给他一个窝,感动!
    }
//将x插入到头节点上
void int_to_head(int x){//和链表中间插入的区别就在于它有head头节点
    e[idx] = x;//第一步,先将值放进去
    ne[idx] = head;//head作为一个指针指向空节点,现在ne[idx] = head;做这把交椅的人换了
   
    head = idx;//head现在表示指向第一个元素了,它不在是空指针了。(不指向空气了)
    idx ++;//指针向下移一位,为下一次插入元素做准备。
}

//将x插入到下标为k的点的后面
void add(int k, int x){
    e[idx] = x;//先将元素插进去
    ne[idx] = ne[k];//让元素x配套的指针,指向它要占位的元素的下一个位置
    ne[k] = idx;//让原来元素的指针指向自己
    idx ++;//将idx向后挪
  
}

//将下标是k的点后面的点个删掉
void remove(int k){
    ne[k] = ne[ne[k]];//让k的指针指向,k下一个人的下一个人,那中间的那位就被挤掉了。
}
int main(){
    cin >> n;
    init();//初始化
    for (int i = 0; i < n; i ++ ) {
        char s;
        cin >> s;
        if (s == 'H') {
            int x;
            cin >> x;
            int_to_head(x);
        }
        if (s == 'D'){
            int k;
            cin >> k;
            if (k == 0) head = ne[head];//删除头节点
            else remove(k - 1);//注意删除第k个输入后面的数,那函数里放的是下标,k要减去1
        }
        if (s == 'I'){
            int k, x;
            cin >> k >> x;
            add(k - 1, x);//同样的,第k个数,和下标不同,所以要减1
        }
    }

    for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ' ;
    cout << endl;

    return 0;
}

双链表

原题链接

#include
using namespace std;
const int N=1e5+10;
int e[N], l[N], r[N], idx;

void insert(int a, int x){
    e[idx]=x;
    l[idx]=a, r[idx]=r[a];
    l[r[a]]=idx, r[a]=idx++;
}
void remove (int a){
    l[r[a]]=l[a];
    r[l[a]]=r[a];
}
int main(){
    r[0]=1, l[1]=0, idx=2;
    int n;
    cin>>n;
    while(n--){
        string op;
        int k,x;
        cin>>op;
        if(op=="L"){
            cin>>x;
            insert(0, x);//! 同理  最左边插入就是 在指向 0的数的左边插入就可以了   也就是可以直接在 0的 有右边插入
        {
        }else if(op=="R"){
            cin>>x;
            insert(l[1], x);//!   0和 1 只是代表 头和尾  所以   最右边插入 只要在  指向 1的 那个点的右边插入就可以了
        }else if(op=="D"){
            cin>>k;
            remove(k+1); //idx从2开始, 所以删除的是k+1
        }else if(op=="IL"){
            cin>>k>>x;
            insert(l[k+1], x);
        }else if(op=="IR"){
            cin>>k>>x;
            insert(k+1, x);
        }
    }
    for(int i=r[0];i!=1;i=r[i]) cout<

栈和队列

单调栈

  • 用于解决离它最近的最大/最小元素
  • 分析性质
    这道题目,最有用的性质,就是离自己最近,而且比自己身高高.
    离自己最近:这个性质其实就是我们所谓的栈的必备性质.
    身高高:看到这种类型的词汇,一定要第一时间反应,这道题目是不是拥有单调性.
    原题链接
#include 

using namespace std;

const int N=1e5;
int st[N],tt;
int n;

 int main(){
     cin>>n;
     for(int i=0;i>x;
        while(tt && st[tt]>=x) tt--;//栈顶元素存在并且小于等于当前输入元素才能入栈                
         if(tt) cout<

单调队列:求滑动窗口最大值和最小值

原题链接

  • 维持滑动窗口的大小
    当队列不为空(hh <= tt) 且 当当前滑动窗口的大小,队列弹出队列头元素以维持滑动窗口的大小
    if(hh <= tt && q[hh] < i - k + 1) hh ++;

  • 构造单调递增队列
    当队列不为空(hh <= tt) 且 当队列队尾元素>=当前元素(a[i])时,那么队尾元素就一定不是当前窗口最小值,删去队尾元素,加入当前元素(q[ ++ tt] = i)
    while(hh <= tt && a[q[tt]] >= a[i]) tt --; q[ ++ tt] = i;

#include 

using namespace std;

const int N =1e5;

int n,k,a[N],q[N];//q[N]存的是数组下标

int main(){
    int tt=-1,hh=0;
    cin>>n>>k;
    for(int i=0;i>a[i];
    }    
    for(int i=0;ik) hh++;//判断队头是否已经滑出滑动窗口
        while(hh<=tt && a[q[tt]]>=a[i]) tt--;//构造递增队列
        q[++tt]=i;
        if(i+1>=k) cout<k) hh++;
        while(hh<=tt && a[q[tt]]<=a[i]) tt--;
        q[++tt]=i;
        if(i+1>=k) cout<

KMP

  • char p[N],s[M];cin>> n >> p+1 >>m >>s+1;字符串的输入输出
    原题链接
#include
using namespace std;
const int N=10010,M=100010;
int n,m,ne[N];
char p[N],s[M];//S是大的串 p是子串
int main()
{
   cin>> n >> p+1 >>m >>s+1;//字符串输入,从数组下标1开始输入
   //对子串 求next数组 解决前后缀的问题 找到最大的前缀和后缀相同的位置 使得快速匹配
   for(int i=2,j=0;i<=n;i++)//j表示匹配成功的长度,i表示q数组中的下标,因为q数组的下标是从1开始的,只有1个时,一定为0,所以i从2开始
   {
       while(j && p[i]!=p[j+1]) j=ne[j];//如果不能匹配就退一步 个人理解:之所以一直是S[i]和P[j+1]而非P[j]是因为:如果j+1不行的话方便直接用ne[j]进行操作。
       if(p[i]==p[j+1])  j++;//如果能就继续
       ne[i]=j;

   }
   //子串和大串的匹配过程
   for(int i=1,j=0;i<=m;i++)
   {
       while(j&&s[i]!=p[j+1]) j=ne[j];匹配不成功时 返回到先前前缀已经匹配到的位置上
       if(s[i]==p[j+1])  j++;
       if(j==n) { printf("%d ",i-n);j=ne[j];}//匹配成功
   }
 return 0;



}

Trie树

  • 基本用法
    1. 快速存储字符串集合
    2. 高速查找字符串的数据结构

原题链接
AcWing算法基础班笔记_第12张图片

  • 这里的☆就相当于代码中cnt[p]
    要对每个字符串结尾的字母标记,比如abc,否则还以为abc不是Trie树中的元素,标记方式就是cnt[p]
    不一定非得是叶子节点才能表☆,只要是字符串结尾的字母都表☆,有多少个不同的字符串就有多少个☆。
  • Trie树还是堆,他们的基本单元都是一个个结点连接构成的,可以成为“链”式结构。
    从y总给出的代码可以看出,idx的操作总是idx++,这就保证了不同的idx值对应不同的结点。因此可以利用idx把结构体内两个属性联系在一起了。因此,idx可以理解为结点。
#include 
#include 

using namespace std;

const int N = 100010;
int son[N][26], cnt[N], idx;
char str[N];
// p 下标是p的点, p点的所有儿子都存在了son[p]中,
// son[p][0] son[p][1] 分别表示p的第一个儿子,第二个儿子...
// cnt[x]以x结尾的单词数量有多少个

// 插入字符串
void insert(char str[]) {
    int p = 0; //从根节点开始,从前往后遍历
    for (int i = 0; str[i]; i++) {//因为字符串结尾时'\0',用'\0'判断字符串是否走到结尾
        int u = str[i] - 'a'; // 每次求出当前字母对应的子节点编号(0~25)
        if (!son[p][u])  son[p][u] = ++idx; //如果当前节点不存在对应“字母”,则创建出来。 p这个节点不存在u号这个儿子,则创建出来
        p = son[p][u]; // p向下更新(如果是走if下来的,则更新为刚创建的点,否则将当前父节点更新为该字符串对应p的儿子节点)(好难描述!)
    }
    cnt[p]++; //往字符串中插入结束时,p对应的点就是该字符串上最后一个点,cnt[p++]表示☆处,以这个点☆结尾的单词数量多了一个。
    //另外cnt[i]中要么是0或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; //如果不存在该子节点,直接return 0;不做插入处理,因为是查询
        p = son[p][u]; //存在该店,更新为子节点的编号
    }
    return cnt[p]; //返回字符串中出现的次数(返回以p结尾的单词数量)
}

int main() {
    int n;
    scanf("%d", &n);
    while (n--) {
        char op[2];
        scanf("%s%s", op, str);
        if (op[0] == 'I') insert(str);
        else printf("%d\n", query(str));
    }
    return 0;
}

并查集

原题链接

  • 基本用法

    1. 将两个集合合并
    2. 询问两个元素是否在一个集合中
  • 基本原理:每个集合用一棵树来表示。树根的编号就是整个集合的编号,每个节点存储它的父节点,p[x]表示x的父节点

  • Q:如何判断一个节点是不是树根呢?
    A:p[x]==x 原因是除根之外,p[x] 都不等于 x。

    Q:如何求 x 的集合编号呢?
    A:while(p[x]!=x) x=p[x];

    Q:如何合并两个集合?
    A:将某一树放在另一树某个位置即可,p[x]=y;

    Q:如何找出一个节点的所有集合?
    A:找那个节点的爹,再判断他爹是不是树根,如果是就返回,不
    是就找它爷爷……(爸爸的爸爸叫爷爷~)。

  • 优化:如果搜一遍找到根节点,则将搜寻路径上所有点直接指向父节点

  • c++读入字符串问题
    scanf("%s%d%d",op,&a,&b); 用%s读入字符串,可以过滤掉空格和回车。

#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]);
    /*
    经上述可以发现,每个集合中只有祖宗节点的p[x]值等于他自己,即:
    p[x]=x;
    */
    return p[x];
    //找到了便返回祖宗节点的值
}

int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) p[i]=i;
    while(m--)
    {
        char op[2];
        int a,b;
        scanf("%s%d%d",op,&a,&b);
        if(*op=='M') p[find(a)]=find(b);//集合合并操作
        else
        if(find(a)==find(b))
        //如果祖宗节点一样,就输出yes
        printf("Yes\n");
        else
        printf("No\n");
    }
    return 0;
}

  • 快速建堆:
    for(int i=n/2;i>=1;i--) down(q[i]);//从堆一半处开始向下调整
    因为n/2处为倒数第二层
    时间复杂度为O(n)
//如何手写一个堆?完全二叉树 5个操作
//1. 插入一个数         heap[ ++ size] = x; up(size);
//2. 求集合中的最小值   heap[1]
//3. 删除最小值         heap[1] = heap[size]; size -- ;down(1);
//4. 删除任意一个元素   heap[k] = heap[size]; size -- ;up(k); down(k);
//5. 修改任意一个元素   heap[k] = x; up(k); down(k);

原题链接

#include
#include
using namespace std;

const int N=1e5+10;
int h[N];   //堆
int ph[N];  //存放第k个插入点的下标
int hp[N];  //存放堆中点的插入次序
int cur_size;   //size 记录的是堆当前的数据多少
//p指下标,h指堆
//这个交换过程其实有那么些绕 但关键是理解 如果hp[u]=k 则ph[k]=u 的映射关系
//之所以要进行这样的操作是因为 经过一系列操作 堆中的元素并不会保持原有的插入顺序
//从而我们需要对应到原先第K个堆中元素
//如果理解这个原理 那么就能明白其实三步交换的顺序是可以互换 
//h,hp,ph之间两两存在映射关系 所以交换顺序的不同对结果并不会产生影响
void heap_swap(int u,int v)
{   
    swap(h[u],h[v]); 
     swap(hp[u],hp[v]);     
     swap(ph[hp[u]],ph[hp[v]]);            

}

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

int main()
{
    int n;
    cin>>n;
    int m=0;      //m用来记录插入的数的个数
                //注意m的意义与cur_size是不同的 cur_size是记录堆中当前数据的多少
                //对应上文 m即是hp中应该存的值
    while(n--)
    {
        string op;
        int k,x;
        cin>>op;
        if(op=="I")
        {
            cin>>x;
            m++;
            h[++cur_size]=x;
            ph[m]=cur_size;
            hp[cur_size]=m;
            //down(size);
            up(cur_size);
        }
        else if(op=="PM")    cout<>k;
            int u=ph[k];                //这里一定要用u=ph[k]保存第k个插入点的下标
            heap_swap(u,cur_size);          //因为在此处heap_swap操作后ph[k]的值已经发生 由于交换完后ph[k]的值变了,为ph[size]了,所以必须要在之前保存ph[k]的值,不然无法进行down和up操作。
            cur_size--;                    //如果在up,down操作中仍然使用ph[k]作为参数就会发生错误
            up(u);
           down(u);
        }
        else if(op=="C")
        {
            cin>>k>>x;
            h[ph[k]]=x;                 //此处由于未涉及heap_swap操作且下面的up、down操作只会发生一个所以
            down(ph[k]);                //所以可直接传入ph[k]作为参数
            up(ph[k]);
        }

    }
    return 0;
}

哈希表

原题链接

  • 开放寻址法
    1. 让人费解的参数:const int N = 200003;
      1.1 开放寻址操作过程中会出现冲突的情况,一般会开成两倍的空间,减少数据的冲突
      1.2如果使用%来计算索引, 把哈希表的长度设计为素数(质数)可以大大减小哈希冲突
      比如
      10%8 = 2 10%7 = 3
      20%8 = 4 20%7 = 6
      30%8 = 6 30%7 = 2
      40%8 = 0 40%7 = 5
      50%8 = 2 50%7 = 1
      60%8 = 4 60%7 = 4
      70%8 = 6 70%7 = 0

      这就是为什么要找第一个比空间大的质数

  • memset()的用法
    void * memset(void *_Dst,int _Val,size_t _Size);
    这是memset的函数声明
    第一个参数为一个指针,即要进行初始化的首地址
    第二个参数是初始化值,注意,并不是直接把这个值赋给一个数组单元(对int来说不是这样)
    第三个参数是要初始化首地址后多少个字节
#include 
#include 

using namespace std;

const int N = 200003; //开发寻找,会出现冲突的情况,一般会开成两倍的空间, 同时去下一个质数
const int null = 0x3f3f3f3f;  //这是一个大于10^9的数


int h[N];

int find(int x){
    int k = (x % N + N) % N;
    //冲突情况:当前位置不为空,并且不为x
    while(h[k] != null && h[k] != x){//符合条件的有两种情况:1.找到x 2.没找到x,但找到一个空闲位置
        k ++; 
        if(k == N) k = 0; //末尾,从头开始
    }
    return k;
}

int main(){
    int n;
    scanf("%d", &n);
    memset(h, 0x3f, sizeof h);
    while(n --){
        char op[2];
        int x;
        scanf("%s%d", op, &x);
        int k = find(x); //找到符合条件的位置
        if(*op == 'I') h[k] = x;
        else{
            if(h[k] != null) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

  • 拉链法
#include
#include
#include
using namespace std;
int const maxn=100003;

int h[maxn],e[maxn],ne[maxn],idx;
h[]是哈希函数的一维数组//e[]是链表中存的值//ne[]是指针存的指向的地址//idx是当前指针
void insert(int x)
{
    int k=(x%maxn + maxn)%maxn; 对负数的处理,k是哈希值

    e[idx]=x;ne[idx]=h[k];h[k]=idx++;    //如果不同单链表的idx都是从0开始单独计数,
   //那么不同链表之间可能会产生冲突。
    //这里的模型是这样的:e[]和ne[]相当于一个大池子,里面是单链表中的节点,会被所有单点表共用,idx相当于挨个分配池子中的节点的指针。
    //比如如果第0个节点被分配给了第一个单链表,那么所有单链表就只能从下一个节点开始分配,所以所有单链表需要共用一个idx。
    //h[k]意义算是一个插入的表头
}
bool find(int x)
{
    int k= (x%maxn+ maxn) %maxn;  ///为了让负数在整数有映射,负数的取模还是负数,加上maxn后为正,再%即可
    for(int i=h[k];i!=-1;i=ne[i])
        if(e[i]==x)
            return true;


return false;
}
int main(void)
{
   cin.tie(0);
   int n;
   cin>>n;
   memset(h,-1,sizeof(h));  ///所有槽都清空,对应的是单链表的头(head)[注:head存的是地址,详见单链表的课]指针为-1
   while(n--)
   {
       char op[2];
       int x;
       scanf("%s%d", op, &x);

        if(op[0]=='I') insert(x);
        else 
        {

            if(find(x)) cout<<"Yes"<

字符串哈希

可用于解决很多字符串难题

原题链接

  • 利用 unsigned long long 自然溢出,相当于自动对2^64−1取模。
    溢出不等于出错,溢出实际上就是取模,这里的溢出就是mod264

  • 全称字符串前缀哈希法,把字符串变成一个p进制数字(哈希值),实现不同的字符串映射到不同的数字。

  • 注意点:

    1. 任意字符不可以映射成0,否则会出现不同的字符串都映射成0的情况,比如A,AA,AAA皆为0
    2. 冲突问题:通过巧妙设置P (131 或 13331) , Q (264)(264)的值,一般可以理解为不产生冲突。
  • 问题是比较不同区间的子串是否相同,就转化为对应的哈希值是否相同。求一个字符串的哈希值就相当于求前缀和,求一个字符串的子串哈希值就相当于求部分和。

AcWing算法基础班笔记_第13张图片

#include 
#include 

using namespace std;

typedef unsigned long long ULL;

const int N = 1e5 + 10, P = 131;

int n, m;
char str[N];
ULL h[N], p[N];

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

int main() {
    cin >> n >> m;
    cin >> (str + 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;
}

常用stl

vector

变长数组,基本思想是倍增(类似于java中的ArrayList)

#include
#include
using namespace std;

int main() {
    vector a; // 最简单的初始化方式
    vector a(10); // 定义一个长度为10的vector
    vector a(10, 3); //定义一个长度为10的vector,并将每个元素初始化为3
    vector a[10]; // 定义一个vector数组,数组大小为10
    
    // vector支持的函数
    a.size(); // vector中的元素个数
    a.empty(); // vector是否为空
    // 上面2个方法的时间复杂度是O(1), 并且其他的容器都有这2个方法
    a.clear(); // 清空
    a.front(); // 返回第一个
    a.back();
    a.push_back();
    a.pop_back();
    a.begin(); // 是第一个元素的位置
    a.end(); // 是最后一个元素的下一个位置
    // vector支持用[]进行随机寻址, 这一点与数组相同
    a[0]; // 取vector中第一个元素
    // vector支持比较运算
    vector a(4, 3), b(3, 4);
    // a = [3,3,3,3]   b = [4,4,4]
    if(a < b) printf("a < b\n"); // 比较大小时是按照字典序
    
    // vector的遍历
    vector a;
    for(int i = 0; i < 10; i++) a.push_back(i);
    for(int i = 0; i ::iterator it = a.begin(); i != a.end(); i++) cout << *i << " ";
    cout << endl;
    
    // C++ 11 的新特性, for each 遍历
    for(auto x : a) cout << x << " ";
    cout << endl;
    return 0;
}
注意:操作系统为某一个程序分配内存空间所需要的时间,与要分配的空间大小无关。只与分配次数有关。比如请求分配一个大小为1的空间,和请求分配一个大小为100的空间,所需时间是一样的。
比如,一次申请大小为1000的数组,与申请1000次大小为1的数组,它们各自所需的时间,就是1000倍的关系。
所以,变长数组,要尽量减少申请空间的次数。
所以vector的倍增,大概就是,每次数组长度不够时,就把大小扩大一倍(新申请一个大小为原先2倍的数组),并把旧数组的元素copy过来

pair

存储一个二元组,二元组的变量类型可以任意

#include
using namespace std;

int main() {
    pair p;
    p.first; //第一个元素
    p.second; //第二个元素
    //pair也支持比较运算,以first为第一关键字,second为第二关键字
    // 构造一个pair
    p = make_pair(10, "hby");
    p = {10, "hby"}; // C++ 11 可以直接这样初始化
    // 当某一个事物有2个属性时,并且需要按照某一个属性进行排序时,
    // 可以将需要排序的属性放到fisrt, 另一个属性放到second
    
    // 当然也可以用pair来存3个属性, 如下
    pair> p;
}

string

字符串,常用的函数substr(),c_str()

#include
#include
using namespace std;

int main() {
    string a = "hby";
    a += "haha"; // 字符串拼接
    a += 'c';
    
    a.size();
    a.length(); // 两种取长度都可以
    
    a.empty();
    a.append("3");
    a.append(10, '3'); // 追加10个3
    
    a.find('b'); // 返回该字符的下标, 从左往右找到的第一个该字符
    
    a.front(); // 字符串第一个字符
    a.back(); // 字符串最后一个字符
    a.substr(1, 3); // 第一个参数是下标起始位置, 第二个参数是长度
    // 上面就是从下标为1的位置开始, 取后面长度为3的子串, 结果就是byh
    // 当第二个参数的长度, 超过了字符串的长度时, 会输出到字符串结尾为止
    a.substr(1); // 也可以省略第二个参数, 则返回下标1之后的子串
    
    a.c_str(); //返回字符串a存储字符串的起始地址
    printf("%s\n", a.c_str());
}

queue

队列,push(),front(),back(),pop()

#include
#include
using namespace std;

int main() {
    queue q;
    q.push(1); // 向队尾插入
    q.pop(); // 弹出队头元素, 注意返回的是void
    q.front(); // 返回队头 
    q.back(); // 返回队尾
    q.size();
    q.empty();
    // queue 没有clear函数
    // 想清空一个queue怎么办? 
    q = queue(); // 直接重新构造一个queue
}

priority_queue

优先队列,本质是个堆。push(),top(),pop()

#include
#include
#include
using namespace std;

int main() {
    // 默认是大根堆
    priority_queue q;
    // 想定义一个小根堆 怎么办?
    // 1. 想插入x时, 直接插入-x
    // 2. 定义时, 直接定义成小根堆, 如下(需要借助vector)
    priority_queue, greater> heap;
    
    q.push();
    q.top(); // 返回堆顶元素
    q.pop(); // 弹出堆顶元素
}

stack栈。push(),top(),pop()

#include
#include
using namespace std;

int main() {
    stack s;
    s.push(); // 压栈
    s.top(); // 返回栈顶
    s.pop(); // 弹出栈顶
}

deque

双端队列。可以在队头队尾进行插入删除,并且支持随机访问

#include
#include
using namespace std;

int main() {
    deque q;
    q.clear(); // 有clear
    
    q.front();
    q.back();
    
    q.push_back();
    q.pop_back();
    
    q.push_front();
    q.pop_front();
    // 并且支持随机寻址
    q[0];
    // 支持begin()和end()迭代器
}

set,map,multiset,multimap

基于平衡二叉树(红黑树),动态维护有序序列。这些set/map支持跟排序相关的操作,如lower_bound/upper_bound方法,也支持迭代器的++和–,但是其增删改查的时间复杂度是O(logn)。

#include
#include
using namespace std;

int main() {
    set s; // 不能有重复元素, 插入一个重复元素, 则这个操作会被忽略
    multiset ms; // 可以有重复元素
    // set 和 multiset 支持的操作
    
    insert(1); // 时间复杂度 O(logn)
    find(1); // 查找一个数, 若不存在, 则返回end迭代器
    count(1); // 返回某个数的个数, set只会返回0或1, multiset则可能返回大于1
    erase(1); // 删除所有1的元素  时间复杂度 O(k + logn), 其中k为元素个数
    erase(??); // 输入一个迭代器, 则只会删迭代器
    // set 比较核心的操作
    lower_bound(x); //返回大于等于x的最小的数的迭代器(注意, 返回的是迭代器)
    upper_bound(x); // 返回大于x的最小的数的迭代器 (注意, 返回的是迭代器)
    // begin() , end() 迭代器
}

unordered_set,unordered_map,unordered_multiset,unordered_multimap

基于哈希表。这些set和map和上面的set/map类似。但是这些unordered的set/map的增删改查的时间复杂度是O(1),效率比上面的更快,但不支持lower_bound()和upper_bound(),也不支持迭代器的++和–
如使用unordered_map,则需要头文件#include

#include
#include
using namespace std;

int main() {
    insert(); // 插入的是一个pair
    erase(); // 输入的参数是一个pair或者迭代器
    find();
    lower_bound();
    upper_bound();
    // 可以像使用数组一样使用map
    // map的几乎所有操作的时间复杂度是 O(logn), 除了size(), empty() 
    
    map m;
    m["hby"] = 1; // 插入可以直接这样操作
    
    cout << m["hby"] << endl; // 查找
}

bitset压位

比如想要开一个1024长度的bool数组,由于C++的bool类型是1个字节。
则需要1024个字节,即1KB。但实际我们可以用位来表示bool,则只需要1024个位,即128字节
bitset支持所有的位运算,以及移位

#include
using namespace std;

int main() {
    bitset<1000> s;
    // 支持 ~, &, |, ^
    // 支持 >>, <<
    // 支持 ==, !=
    // 支持 []
    // count() 返回有多少个1
    // any() 是否至少有一个1
    // none() 是否全为0
    // set() 把所有位置置为1
    // set(k, v)  将第k位变成v
    // reset() 把所有位置变成0
    // flip() 把所有位置取反, 等价于 ~
    // flip(k) 把第k位取反
}

DFS和BFS

  • DFS使用栈(stack)来实现,BFS使用队列(queue)来实现
    DFS所需要的空间是树的高度h,而BFS需要的空间是2h (DFS的空间复杂度较低)
    DFS不具有最短路的特性,BFS具有最短路的特性

DFS

  • 回溯:回溯的时候,一定要记得恢复现场
    剪枝:提前判断某个分支一定不合法,直接剪掉该分支
    全排列
#include 

using namespace std;

cosnt int  N=10;

int n;
int path[N];//用 path 数组保存排列,当排列的长度为 n 时,是一种方案,输出。
int str[N];//用 state 数组表示数字是否用过。当 state[i] 为 1 时:i 已经被用过,state[i] 为 0 时,i 没有被用过。

// x 代表当前是第几个位置的数, x 可能取值为0,1,2,3....,n
void bfs(int x){
    if(x==n){
        for(int i=1;i> n;
    dfs(0);//从0开始
}

N皇后
AcWing算法基础班笔记_第14张图片

  • 方法一:全排列方法
#include 

using namespace std;

const int N = 20; //斜对角线 2n-1 所以开两倍N
int n;
char g[N][N];
bool col[N], dg[N], udg[N];// 同一列、对角线、斜对角线上只能有一个,分别记录的是该位置的列(斜)对角线上是否已经存在过,若均不存在,则填Q,并递归下一行

void dfs(int u) {
    if (u == n) { //有多少种满足条件的摆法就有多少次 u == n
        for (int i =0; i < n; i++) puts(g[i]);
        puts("");
        return ;
    }

    for (int i = 0; i < n; i++) //枚举第u行皇后该放在哪一列
        if (!col[i] && !dg[u + i] && !udg[i - u + n]) { // u+i和n-u+i怎么来的见图片
            g[u][i] = 'Q';
            col[i] = dg[u + i] = udg[n - u + i] = true; //更新状态
            dfs(u + 1);
            col[i] = dg[u + i] = udg[n - u + i] = false; // 恢复现场
            g[u][i] = '.';
        }
}

int main() {
    cin >> n;
    for (int i = 0; i < n; i++ ) 
        for (int j = 0; j < n; j++ )
            g[i][j] = '.';
    dfs(0);
    return 0;
}
  • 方法2:从棋盘的第一个位置[1, 1]依次枚举到最后一个位置[n,n]。每个位置都有两种选择:放或者不放皇后(这就对应了两个分支)。对比解法一,此时我们就需要多维护一个针对行的状态变量了。其余逻辑和解法一类似,只是需要对每个位置的点进行枚举
#include 
#include 

using namespace std;

const int N = 20;
int n;
char g[N][N];
bool row[N], col[N], dg[N], udg[N];

void dfs(int x, int y, int s) {
    if (y == n) x ++, y = 0; // y 出界,转到下一行第一个格子
    if (x == n) { //枚举到最后一行
        if (s == n) { // 如果此时摆的Q个数是n,说明找到了一组解,输出解
            for (int i = 0; i < n; i ++) puts(g[i]); //输出找到的一组解
            puts("");
        }
        return;
    }
    // 枚举下一个格子的两种选择,放皇后和不放皇后
    // 不放皇后
    dfs(x, y + 1, s);
    // 放皇后
    if (!row[x] && !col[y] && !dg[x + y] && !udg[n - x + y]) { //写成 y - x + n 不能AC,当n = 3时,不应该有答案,但是却有了错误答案
        g[x][y] = 'Q';
        row[x] = col[y] = dg[x + y] = udg[n - x + y] = true; //更新状态
        dfs(x , y + 1, s + 1);
        row[x] = col[y] = dg[x + y] = udg[n - x + y] = false; //恢复现场
        g[x][y] = '.'; //恢复现场
    }
}

int main () {
    cin >> n;
    for (int i = 0; i < n; i ++) 
        for (int j = 0; j < n; j ++)
            g[i][j] = '.';
    dfs(0,0,0); //从左上角(0, 0)开始搜,记录当前共有多少个皇后(第三个参数代表当前皇后个数,初始是0)

    return 0;
}

BFS

  • 基本框架
插入一个初始状态到queue中
while(queue非空)
    把队头拿出来
    扩展队头
end

走迷宫834

#include 
using namespace std;

typedef pair PII;

const int N = 1e2 + 7;
int g[N][N], d[N][N];
int n, m;


int bfs() {
    int dx[] = {-1, 0, 1, 0}, dy[] = {0, 1, 0, -1};
    queue  q;

    memset(d, -1, sizeof d);

    d[0][0] = 0;
    q.push({0, 0});

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

        for (int i = 0; i < 4; i++) {
            int x = t.first + dx[i], y = t.second + dy[i];

            if (x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1) {
                d[x][y] = d[t.first][t.second]  + 1;
                q.push({x, y});
            }
        }

    }


        return d[n - 1][m - 1];
}


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

    cout << bfs() << endl;

    return 0;
}

图论

  • 邻接矩阵
    用一个二维数组来存,比如g[a,b]存储的就是a到b的边。邻接矩阵无法存储重复边,比如a到b之间有2条边,则存不了。(用的较少,因为这种方式比较浪费空间,对于有n个点的图,需要n2的空间,这种存储方式适合存储稠密图)
  • 邻接表
    使用单链表来存。对于有n个点的图,我们开n个单链表,每个节点一个单链表。单链表上存的是该节点的邻接点(用的较多)

树与图的深度优先遍历

树的重心846

#include 
#include 
using namespace std;

const int N = 1e5 + 10; //数据范围是10的5次方
const int M = 2 * N; //以有向图的格式存储无向图,所以每个节点至多对应2n-2条边

int h[N]; //邻接表存储树,有n个节点,所以需要n个队列头节点
int e[M]; //存储元素
int ne[M]; //存储列表的next值
int idx; //单链表指针
int n; //题目所给的输入,n个节点
int ans = N; //表示重心的所有的子树中,最大的子树的结点数目

bool st[N]; //记录节点是否被访问过,访问过则标记为true
//a所对应的单链表中插入b  a作为根 
void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

// dfs 框架
/*
void dfs(int u){
    st[u]=true; // 标记一下,记录为已经被搜索过了,下面进行搜索过程
    for(int i=h[u];i!=-1;i=ne[i]){
        int j=e[i];
        if(!st[j]) {
            dfs(j);
        }
    }
}
*/

//返回以u为根的子树中节点的个数,包括u节点
int dfs(int u) {
    int res = 0; //存储 删掉某个节点之后,最大的连通子图节点数
    st[u] = true; //标记访问过u节点
    int sum = 1; //存储 以u为根的树 的节点数, 包括u,如图中的4号节点

    //访问u的每个子节点
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];
        //因为每个节点的编号都是不一样的,所以 用编号为下标 来标记是否被访问过
        if (!st[j]) {
            int s = dfs(j);  // u节点的单棵子树节点数 如图中的size值
            res = max(res, s); // 记录最大联通子图的节点数
            sum += s; //以j为根的树 的节点数
        }
    }

    //n-sum 如图中的n-size值,不包括根节点4;
    res = max(res, n - sum); // 选择u节点为重心,最大的 连通子图节点数
    ans = min(res, ans); //遍历过的假设重心中,最小的最大联通子图的 节点数
    return sum;
}

int main() {
    memset(h, -1, sizeof h); //初始化h数组 -1表示尾节点
    cin >> n; //表示树的结点数

    // 题目接下来会输入,n-1行数据,
    // 树中是不存在环的,对于有n个节点的树,必定是n-1条边
    for (int i = 0; i < n - 1; i++) {
        int a, b;
        cin >> a >> b;
        add(a, b), add(b, a); //无向图
    }

    dfs(1); //可以任意选定一个节点开始 u<=n

    cout << ans << endl;

    return 0;
}

树与图的宽度优先遍历

图中点的层次

#include 
#include 
#include 

using namespace std;

const int N = 1e5 + 10;

int head[N], e[N], ne[N],idx;
int n, m;
int dist[N];

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

int bfs()
{
    queue q;

    memset(dist, -1, sizeof dist);  // todo 初始化距离

    q.push(1);

    dist[1] = 0;  //? 最开始的时候 只有第一个点被遍历过了 他的距离是 0

    while (q.size()) {
        auto t = q.front();  // todo 每一次取得我们的队头
        q.pop();

        for (int i = head[t]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dist[j] == -1)  //!  如果 j 没有被扩展过
            {
                dist[j] = dist[t] + 1;
                q.push(j);
            }
        }
    }
    return dist[n];  //! 返回最后一个搜到的点的距离
}

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

    memset(head, -1, sizeof head);

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

    cout << bfs() << endl;

    return 0;
}

拓扑排序

图的宽度优先搜索的应用,求拓扑序(拓扑序是针对有向图的)

  • 算法思路:
将所有入度为0的点入队。
while(queue非空) {
	t = queue.pop(); // 获取队头
	枚举t的全部出边 t->j
	  删掉边t->j, j节点的入度减一
	  if(j的入度为0) 将j入队
}
#include 
#include 
#include 
#include 
using namespace std;
const int N = 1e5 + 10;
int e[N],ne[N],h[N],idx,d[N],n,m,top[N],cnt = 1;
// e,ne,h,idx 邻接表模板
// d 代表每个元素的入度
// top是拓扑排序的序列,cnt代表top中有多少个元素
void add(int a,int b){
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx ++;
}
bool topsort(){
    queue q;
    int t;
    for(int i = 1;i <= n; ++i)// 将所有入度为0的点加入队列
        if(d[i] == 0) q.push(i);
    while(q.size()){
        t = q.front();//每次取出队列的首部
        top[cnt] = t;//加入到 拓扑序列中
        cnt ++; // 序列中的元素 ++
        q.pop();
        for(int i = h[t];i != -1; i = ne[i]){
            // 遍历 t 点的出边
            int j = e[i];
            d[j] --;// j 的入度 --
            if(d[j] == 0) q.push(j); //如果 j 入度为0,加入队列当中
        }
    }
    if(cnt < n) return 0;
    else return 1;

}
int main(){
    int a,b;
    cin >> n >> m;
    memset(h,-1,sizeof h);
    while(m--){
        cin >> a >> b;
        add(a,b);
        d[b] ++;// a -> b , b的入度++
    }
    if(topsort() == 0) cout << "-1";
    else {
        for(int i = 1;i <= n; ++i){
            cout << top[i] <<" ";
        }
    }
    return 0;
}

最短路问题

在最短路问题中,源点也就是起点,汇点也就是终点。
AcWing算法基础班笔记_第15张图片

单源最短路

单源最短路,指的是求一个点,到其他所有点的最短距离。(起点是固定的,单一的)
根据是否存在权重为负数的边,又分为两种情况

所有边的权重都是正数

通常有两种算法
两者孰优孰劣,取决于图的疏密程度(取决于点数n,与边数m的大小关系)。当是稀疏图(n和m是同一级别)时,可能堆优化版的Dijkstra会好一些。当是稠密图时(m和n2是同一级别),使用朴素Dijkstra会好一些。

朴素Dijkstra

时间复杂度O(n2),其中n是图中点的个数,m是边的个数

  • 算法思路:假设图中一共有n个点,下标为1~n。下面所说的某个点的距离,都是指该点到起点(1号点)的距离。
  1. 算法步骤如下,用一个集合s来存放最短距离已经确定的点。
  2. 初始化距离,d[1] = 0, d[i] = +∞。即,将起点的距离初始化为0,而其余点的距离当前未确定,用正无穷表示。
  3. 循环
  4. 每次从距离已知的点中,选取一个不在s集合中,且距离最短的点(这一步可以用小根堆来优化),遍历该点的所有出边,更新这些出边所连接的点的距离。并把该次选取的点加入到集合s中,因为该点的最短距离此时已经确定。
  5. 当所有点都都被加入到s中,表示全部点的最短距离都已经确定完毕

注意某个点的距离已知,并不代表此时这个点的距离就是最终的最短距离。在后续的循环中,可能用一条更短距离的路径,去更新。

849. Dijkstra求最短路 I

#include
#include
#include
using namespace std;
const int N = 510;
const int INF = 0x3f3f3f3f; // 正无穷
int g[N][N]; // 稠密图采用邻接矩阵存储
int d[N]; // 距离
int n, m;
bool visited[N];

int dijkstra() {
	d[1] = 0;
	// 每次
	for(int i = 1; i <= n; i++) {
		//找到一个距起点距离最小的点
		int t = 0; // d[0]未被使用, 其值一直是 INF
		for(int j = 1; j <= n; j++) {
			if(!visited[j] && d[j] < d[t]) {//该步骤即寻找还未确定最短路的点中路径最短的点
				t = j;
			}
		}
		if(t == 0) break; // 未找到一个点, 提前break
		// 找到该点
		visited[t] = true; // 放入集合s
		// 更新其他所有点的距离
		for(int i = 1; i <= n; i++) {
			d[i] = min(d[i], d[t] + g[t][i]);
		}
	}
	if(d[n] == INF) return -1;
	else return d[n];
}

int main() {
	// 初始化
	memset(d, 0x3f, sizeof d);
	memset(g, 0x3f, sizeof g);
	scanf("%d%d", &n, &m);
	while(m--) {
		int x, y, z;
		scanf("%d%d%d", &x, &y, &z);
		g[x][y] = min(g[x][y], z); // 重复边只需要保留一个权重最小的即可
	}
	printf("%d", dijkstra());
	return 0;
}

堆优化版的Dijkstra

时间复杂度O(mlogn)
算法的主要耗时的步骤是从dist 数组中选出:没有确定最短路径的节点中距离源点最近的点 t。只是找个最小值而已,没有必要每次遍历一遍dist数组。
在一组数中每次能很快的找到最小值,很容易想到使用小根堆。可以使用库中的小根堆(推荐)或者自己编写。
使用小根堆后,找到 t 的耗时从 O(n^2) 将为了 O(1)。每次更新 dist 后,需要向堆中插入更新的信息。所以更新dist的时间复杂度有 O(e) 变为了 O(elogn)。总时间复杂度有 O(n^2) 变为了 O(n + elongn)。适用于稀疏图。

#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];//存储距离
bool st[N];//存储状态

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

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);//距离初始化为无穷大
    dist[1] = 0;
    priority_queue, greater> heap;//小根堆
    heap.push({0, 1});//插入距离和节点编号

    while (heap.size())
    {
        auto t = heap.top();//取距离源点最近的点
        heap.pop();

        int ver = t.second, distance = t.first;//ver:节点编号,distance:源点距离ver 的距离

        if (st[ver]) continue;//如果距离已经确定,则跳过该点
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])//更新ver所指向的节点距离
        {
            int j = e[i];
            if (dist[j] > dist[ver] + w[i])
            {
                dist[j] = dist[ver] + w[i];
                heap.push({dist[j], j});//距离变小,则入堆
            }
        }
    }

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

int main()
{
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    cout << dijkstra() << endl;

    return 0;
}

存在权重为负数的边

通常有两种算法

Bellman-Ford

时间复杂度O(nm):由于循环了n次,每次遍历所有边(m条边)。故Bellman-Ford算法的时间复杂度是O(n×m)。

算法思路:
循环n次
每次循环,遍历图中所有的边。对每条边(a, b, w),(指的是从a点到b点,权重是w的一条边)更新d[b] = min(d[b], d[a] + w)

循环的次数的含义:假设循环了k次,则表示,从起点,经过不超过k条边,走到每个点的最短距离。

该算法能够保证,在循环n次后,对所有的边(a, b, w),都满足d[b] <= d[a] + w。这个不等式被称为三角不等式。上面的更新操作称为松弛操作。
如果有负权回路的话,最短路就不一定存在了。(注意是不一定存在)。当这个负权回路处于1号点到n号点的路径上,则每沿负权回路走一圈,距离都会减少,则可以无限走下去,1到n的距离就变得无限小(负无穷),此时1号点到n号点的最短距离就不存在。而如果负权回路不在1号点到n号点的路径上,则1到n的最短距离仍然存在。

该算法可以求出来,图中是否存在负权回路。如果迭代到第n次,还会进行更新,则说明存在一条最短路,路径上有n条边,n条边则需要n + 1个点,而由于图中一共只有n个点,所以这n + 1个点中一定有2个点是同一个点,则说明这条路径上有环;有环,并且此次进行了更新,说明这个环的权重是负的(只有更新后总的距离变得更小,才会执行更新)。

但求解负权回路,通常用SPFA算法,而不用Bellman-Ford算法,因为前者的时间复杂度更低。

acwing - 853: 有边数限制的最短路

#include
#include

using namespace std;

const int N = 510, M = 10010;

struct Edge {
    int a;
    int b;
    int w;
} e[M];//把每个边保存下来即可
int dist[N];
int back[N];//备份数组防止串联
int n, m, k;//k代表最短路径最多包涵k条边

int bellman_ford() {
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    for (int i = 0; i < k; i++) {//k次循环
        memcpy(back, dist, sizeof dist);
        for (int j = 0; j < m; j++) {//遍历所有边
            int a = e[j].a, b = e[j].b, w = e[j].w;
            dist[b] = min(dist[b], back[a] + w);
            //使用backup:避免给a更新后立马更新b, 这样b一次性最短路径就多了两条边出来
        }
    }
    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    else return dist[n];

}

int main() {
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 0; i < m; i++) {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        e[i] = {a, b, w};
    }
    int res = bellman_ford();
    if (res == -1) puts("impossible");
    else cout << res;

    return 0;
}

SPFA

时间复杂度一般是O(m),最差O(nm),是前者的优化版,但有的情况无法使用SPFA,只能使用前者,比如要求最短路不超过k条边,此时只能用Bellman-Ford

若要使用SPFA算法,一定要求图中不能有负权回路。只要图中没有负权回路,都可以用SPFA,这个算法的限制是比较小的。

SPFA其实是对Bellman-Ford的一种优化。

它优化的是这一步:d[b] = min(d[b], d[a] + w)

我们观察可以发现,只有当d[a]变小了,才会在下一轮循环中更新d[b]

考虑用BFS来做优化。用一个队列queue,来存放距离变小的节点。

#include
#include
#include
using namespace std;

const int N=1e5+10;

#define fi first
#define se second

typedef pair PII;//到源点的距离,下标号

int h[N],e[N],w[N],ne[N],idx=0;
int dist[N];//各点到源点的距离
bool st[N];
int n,m;
void add(int a,int b,int c){
    e[idx]=b;w[idx]=c;ne[idx]=h[a];h[a]=idx++;
}

int spfa(){
    queue q;
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    q.push({0,1});
    st[1]=true;
    while(q.size()){
        PII p=q.front();
        q.pop();
        int t=p.se;
        st[t]=false;//从队列中取出来之后该节点st被标记为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];
                if(!st[j]){//当前已经加入队列的结点,无需再次加入队列,即便发生了更新也只用更新数值即可,重复添加降低效率
                    st[j]=true;
                    q.push({dist[j],j});
                }
            }
        }
    }
    if(dist[n]==0x3f3f3f3f) return -1;
    else return dist[n];
}

int main(){
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof h);
    while(m--){
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    int res=spfa();
    if(res==-1) puts("impossible");
    else printf("%d",res);

    return 0;
}

  • spfa判断负环
#include 
#include 
#include 
#include 

using namespace std;

const int N = 100010;

int n, m;
int h[N], e[N], ne[N], idx;
int w[N];
int dist[N], cnt[N];//记录每个点到起点的边数,当cnt[i] >= n 表示出现了边数>=结点数,必然有环,而且一定是负环!
bool st[N];//判断当前的点是否已经加入到队列当中了;已经加入队列的结点就不需要反复的把该点加入到队列中了
//就算此次还是会更新到起点的距离,那只用更新一下数值而不用加入到队列当中。
//意味着,st数值起着降低效率的作用,不在乎效率的话,去掉也可以

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

int spfa()
{
    //初始化所有非起点和起点的距离
    // memset(dist, 0x3f, sizeof dist);
    // dist[1] = 0;
    // 这里不需要初始化dist数组为 正无穷。不用初始化的原因是, 如果存在负环, 那么dist不管初始化为多少, 都会被更新

    //定义队列,起点进队, 标记进队
    queue q;

    for (int i = 1; i <= n; i ++ )
    {
        //判断负环,只从一个点出发,有可能到达不了负环那里,需要一开始就把所有结点放入队列,且标记进入了队列降低效率
        q.push(i);
        st[i] = true;
    }

    //队列中的点用来更新其他点到起点的距离
    while (q.size())
    {
        //取队头,弹队头
        auto t = q.front();
        q.pop();
        //t出队,标记出队
        st[t] = false;

        //更新与t邻接的边
        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];//结点j可以通过中间点t降低距离
                cnt[j] = cnt[t] + 1;//那么结点j在中间点t的基础上加一条到自己的边

                if (cnt[j] >= n) return true;//边数不小于结点数,出现负环,函数结束

                if (!st[j])//若此时j没在队列中,则进队。已经在队列中了,上面已经更新了数值。重复加入队列降低效率
                {
                    //j进队,标记进队
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;//走到这了,函数还没结束,意味着边数一直小于结点数,不存在负环
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    for (int i = 0; i < m; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

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


    return 0;
}

多源汇最短路

Floyd算法(时间复杂度O(n3))

  • 算法思路:三层循环
for(k = 1; k <= n; k++)
​   for(i = 1; i <= n; i++)
        for(j = 1; j <= n; j++)
            d[i,j] = min(d[i,j] , d[i,k] + d[k,j])//循环结束后,d[i][j]存的就是点i到j的最短距离。

acwing - 854: Floyd求最短路

#include 
using namespace std;

const int N = 210, M = 2e+10, INF = 1e9;

int n, m, k, x, y, z;
int d[N][N];

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 >> k;
    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--) {
        cin >> x >> y >> z;
        d[x][y] = min(d[x][y], z);
        //注意保存最小的边
    }
    floyd();
    while(k--) {
        cin >> x >> y;
        if(d[x][y] > INF/2) puts("impossible");
        //由于有负权边存在所以约大过INF/2也很合理
        else cout << d[x][y] << endl;
    }
    return 0;
}

最小生成树

AcWing算法基础班笔记_第16张图片
首先,给定一个节点数是n,边数是m的无向连通图G。
则由全部的n个节点,和n-1条边构成的无向连通图被称为G的一颗生成树,在G的所有生成树中,边的权值之和最小的生成树,被称为G的最小生成树。

对于最小生成树问题,如果是稠密图,通常选用朴素版Prim算法,因为其思路比较简洁,代码比较短,如果是稀疏图,通常选用Kruskal算法,因为其思路比Prim简单清晰。堆优化版的Prim通常不怎么用。

Prim算法(普利姆)

朴素版Prim(时间复杂度O(n2),适用于稠密图)

  • 联系:Dijkstra算法是更新到起始点的距离,Prim是更新到集合S的距离
#include
#include
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;
int n, m;
int g[N][N], d[N];
bool visited[N];

void prim() {
	memset(d, 0x3f, sizeof d); // 初始化距离为正无穷
	int sum = 0;
	for(int i = 1; i <= n; i++) {
		// 循环n次
		// 选出距离集合s最小的点
		int t = 0;
		for(int j = 1; j <= n; j++) {
			if(!visited[j] && d[j] <= d[t]) t = j; // 这里用<=, 可以避免对第一次选点做特判
		}
		if(i == 1) d[t] = 0;// 第一次加入集合的点, 其到集合的距离为0
		if(d[t] == INF) {
		    // 选中的点距离是正无穷, 无效
		    printf("impossible\n");
		    return;
		}
		// 把这个点放到集合s里
		visited[t] = true; 
		sum += d[t]; // 这次放进来的
		// 更新其他点到集合s的距离, 
		for(int j = 1; j <= n; j++) {
			
			    d[j] = min(d[j],g[t][j]);
			}
		
	}
	printf("%d\n", sum);
}

int main() {
	memset(g, 0x3f, sizeof g);
	scanf("%d%d", &n, &m);
	while(m--) {
		int x, y, w;
		scanf("%d%d%d", &x, &y, &w);
		g[x][y]=g[y][x] = min(g[x][y], w);
		
	}
	prim();
	return 0;
}

堆优化版Prim(时间复杂度O(mlogn),适用于稀疏图)(不怎么用到)

Kruskal算法(克鲁斯卡尔)

适用于稀疏图,时间复杂度O(mlogm)

  • 基本思路:先将所有边,按照权重,从小到大排序
    从小到大枚举每条边(a,b,w)
    若a,b不连通,则将这条边,加入集合中(将a点和b点连接起来)
#include
#include

using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10, INF = 0x3f3f3f3f;
int n, m;
int p[N];

struct Edge {
    int a, b, w;

    bool operator<(const Edge &e) const {
        return w < e.w;
    }
} es[M];

int find(int x) {//并查集的应用
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}

int kruskal() {
    int cnt = 0, res = 0;

    sort(es, es + m);

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

    for (int i = 0; i < m; i++) {
        int a = es[i].a, b = es[i].b, w = es[i].w;
        a = find(a), b = find(b);
        if (a != b) {
            p[a] = b;
            res += w;
            cnt++;
        }
    }

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

int main() {
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        es[i] = {a, b, w};
    }

    int t = kruskal();
    if (t == INF) cout << "impossible";
    else cout << t;
}

二分图

首先,什么是二分图呢?

二分图指的是,可以将一个图中的所有点,分成左右两部分,使得图中的所有边,都是从左边集合中的点,连到右边集合中的点。而左右两个集合内部都没有边。图示如下
AcWing算法基础班笔记_第17张图片
AcWing算法基础班笔记_第18张图片

  • 二分图性质
    图论中的一个重要性质:一个图是二分图,当且仅当图中不含奇数环

    奇数环,指的是这个环中边的个数是奇数。(环中边的个数和点的个数是相同的)

    在一个环中,假设共有4个点(偶数个),由于二分图需要同一个集合中的点不能互相连接。

    则1号点属于集合A,1号点相连的2号点就应当属于集合B,2号点相连的3号点应当属于集合A,3号点相连的4号点应当属于集合B。4号点相连的1号点应当属于集合A。这样是能够二分的。

    而若环中点数为奇数,初始时预设1号点属于集合A,绕着环推了一圈后,会发现1号点应当属于集合B。这就矛盾了。所以存在奇数环的话,这个图一定无法二分。

染色法

  • 可以用染色法来判断一个图是否是二分图,使用深度优先遍历,从根节点开始把图中的每个节点都染色,每个节点要么是黑色要么是白色(2种),只要染色过程中没有出现矛盾,说明该图是一个二分图,否则,说明不是二分图。
#include
#include
#include
using namespace std;
const int N = 1e5 + 10, M = 2e5 + 10; // 由于是无向图, 需要建两条边, 所以边数设为2倍
// 使用邻接表来存储图
int h[N], e[M], ne[M], idx; // 注意这里单链表的实现, 数组大小开为M

int n, m;
int color[N];

void add(int x, int y) {
	// 链表的头插法
	e[idx] = y;
	ne[idx] = h[x];
	h[x] = idx++;
}

bool dfs(int x) {
	// 深搜这个节点的全部子节点
	for(int i = h[x]; i != -1; i = ne[i]) {
		int u = e[i]; // 子节点
		if(color[u] == -1) {
			// 子节点还未染色, 则直接染色, 并深搜
			color[u] = !color[x];
			if(!dfs(u)) return false;
		} else if(color[u] == color[x]) return false; // 若子节点和父节点颜色一致, 则说明矛盾, 自环应该也算矛盾?
	}
	// 深搜结束, 未出现矛盾, 则染色成功, 判定是二分图
	return true;
}

int main() {
	memset(h, -1, sizeof h); // 初始化空链表
	memset(color, -1, sizeof color); // 颜色初始化为-1, 表示还未染色
	scanf("%d%d", &n, &m);
	while(m--) {
		int x, y;
		scanf("%d%d", &x, &y);
		add(x, y);
		add(y, x);
	}
	bool flag = true;
	// 依次对所有点进行染色
	for(int i = 1; i <= n; i++) {
		if(color[i] == -1) {
			// 该点还未染色, 则直接染色, 随便染一个色即可(0或1), 并dfs, dfs完成后, 就对这个点所在的连通块都染上了色
			color[i] = 0;
			// 进行dfs, 若在对这个连通块染色的过程中出现矛盾, 则直接break
			if(!dfs(i)) {
				flag = false;
				break;
			}
		}
	}
	if(flag) printf("Yes\n");
	else printf("No\n");
	return 0;
}

匈牙利算法

匈牙利算法,是给定一个二分图,用来求二分图的最大匹配的。

给定一个二分图G,在G的一个子图M中,M的边集中的任意两天边,都不依附于同一顶点,则称M是一个匹配。就是每个点只会有一条边相连,没有哪一个点,同时连了多条边。(参考yxc的例子:男生女生恋爱配对,最多能凑出多少对)

所有匹配中包含边数最多的一组匹配,被称为二分图的最大匹配。其边数即为最大匹配数

假设一个二分图,左半边部分节点表示男生,右半边部分节点表示女生。一个男生节点和一个女生节点连了一条边,则表示这两个人之间有感情基础,可以发展为情侣。当我们把一对男女凑成一对时,称为这两个节点匹配。

匈牙利算法的核心思想是:我们枚举左半边所有男生(节点),每次尝试给当前男生找对象。我们先找到这个男生看上的全部女生。(即找到这个节点连接的所有右侧的节点)。遍历这些女生,当一个女生没有和其他男生配对时,直接将这个女生和这个男生配对。则该男生配对成功。当这个女生已经和其他男生配对了,则尝试给这个女生的男朋友,找一个备胎。如果这个女生的男朋友有其他可选择的备胎。则将这个女生的男朋友和其备胎配对。然后将这个女生和当前男生配对。如此找下去…

acwing - 861: 二分图的最大匹配

#include
#include
#include
using namespace std;
const int N = 1010,  M = 1e5 + 10;

int h[N], e[M], ne[M], idx;
int match[N];//对应的女生 match[j]=a 表示女孩的现在配对的男友是a
bool st[N]; //st[]数组我称为临时预定数组,st[j]=a表示一轮模拟匹配中,女孩j被男孩a预定了。

int n1, n2, m;

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 u = e[i]; // 女生节点编号
        if(st[u]) continue; // 如果这个女生已经被标记, 则跳过
        st[u] = true; // 先把这个女生标记, 使得后续递归时时跳过这个女生
        if(match[u] == 0 || find(match[u])) {
            // 如果当前这个女生没有被匹配, 或者能够给这个女生已匹配的男生另外找个备胎, 则可以
            match[u] = x;
            return true;
        }
    }
    return false; // 找了一圈还是不行
}

int main() {
    memset(h, -1, sizeof h);
    scanf("%d%d%d", &n1, &n2, &m);
    while(m--) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b); // 只从左半边连到右半边
    }

    // 枚举左半边所有点
    int res = 0;
    for(int i = 1; i <= n1; i++) {
        // 每次给一个男生找女朋友时, 清空状态变量
        memset(st, false, sizeof st);
        if(find(i)) res++;
    }
    printf("%d\n", res);
    return 0;
}

数学知识

质数

  • 定义:对于大于1的自然数,如果这个数的约数只包含1和它本身,则这个成为质数,或者素数

质数的判定

  • 判定:
    • 试除法:对于一个数n,从2枚举到n-1,若有数能够整除n,则说明除了1和n本身,n还有其他约数,则n不是质数;否则,n是质数
      • 优化:优化:由于一个数的约数都是成对出现的。比如12的一组约数是3,4,另一组约数是2,6。则我们只需要枚举较小的那一个约数即可
        AcWing算法基础班笔记_第19张图片
        bool is_prime(int x){
            if(n<2) return false;
            for(int i=2;i<=n/i;i++){
                if(n%i==0) return false;
            }
            return true;
        }
    
    • 细节:for循环的结束条件,推荐写成i <= n / i。
      有的人可能会写成i <= sqrt(n),这样每次循环都会执行一次sqrt函数,而这个函数是有一定时间复杂度的。而有的人可能会写成i * i < =n,这样当i很大的时候(比如i比较接近int的最大值时),i * i可能会溢出,从而导致结果错误。
      acwing - 866: 试除法判定质数
#include 

using namespace std;

const int N=1e5+10;
int n,ai;

bool is_prime(int n){
    if(n<2) return false;
    for(int i=2;i<=n/i;i++){
        if(n%i==0) return false;
    }
    return true;
}
int main(){
    cin>>n;
    while (n--)
    {
        cin>>ai;
        if(is_prime(ai)) cout<<"Yes"<

求解质因子

AcWing算法基础班笔记_第20张图片

void divide(int n) {
    for(int i = 2; i <= n / i; i++) {
        if(n % i == 0) {
            int s = 0;
            while(n % i == 0) {
                s++;
                n /= i;
            }
            printf("%d %d\n", i, s);
        }
    }
    // 如果除完之后, n是大于1的, 
    // 说明此时的n就是那个大于 原根号n 的最大的质因子, 单独输出一下
    if(n > 1) printf("%d %d\n", n, 1);
}
  • 注意:比如 n=39,由于枚举时的条件是i<=n/i,则只会枚举到6,for循环就结束了,而39有一个质因子是13

筛选质数

埃氏筛法

其实不需要把全部数的倍数删掉,而只需要删除质数的倍数即可。

对于一个数p,判断其是否是质数,其实不需要把2到p-1全部数的倍数删一遍,只要删掉2到p-1之间的质数的倍数即可。因为,若p不是个质数,则其在2到p-1之间,一定有质因数,只需要删除其质因数的倍数,则p就能够被删掉。优化后的代码如下

#include 
using namespace std;

const int N=1e5+10;

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


void get_primes(int x){
    for(int i=2;i<=n;i++){
        if(!st[i]){//没有被筛选掉,说明是质数
            primes[cnt++]=i;
            for(int j=i+i;j<=n;j+=i)
                st[j]=true;
        }
    }
}

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

    cout << cnt << endl;

    return 0;
}

线性筛法 o(n)

  • 原理
    用最小质因子筛去合数,保证了每个合数只被筛一次。

  • 核心 :为什么每个合数只被筛一次?
    答:每个合数可以分解为若干个质因数相乘,其中一定存在一个唯一的最小的质因数。
    关键代码,这句代码保证了每个合数只会被最小质因数筛去,将时间复杂度降到了线性:

if(i%primes[j] == 0) break;

  • 证明
    首先primes存放的质数是递增的。
    当i%pj不为零,pj小于所有i的质因子,并且pj是pj本身的最小质因子。因此,pj是ipj这个合数的最小质因子。
    当i%pj为零,因为pj是从小到大被枚举的,因此pj就是i的最小质因子,并且pj是pj本身的最小质因子。因此,pj是i
    pj这个合数的最小质因子。
    为什么这个时候要break呢?如果不break,iprimes[j+1]这个合数就会被筛去,但是这个合数的最小质因子不是primes[j+1]。
    因为i中含有prime[j],prime[j]比prime[j+1]小,即i=k(某个数)
    prime[j].
    那么i * prime[j+1]=(k*prime[j]) * prime[j+1]=k * prime[j]

  • 因此,i*primes[j+1]。它的最小质因子不是primes[j+1],它应该被prime[j]乘上一个不是i的某个数k筛去。所以咯,这个时候应该break,防止后面的合数被重复筛去。也就是说,在满足i%primes[j] == 0这个条件之前以及第一次满足时,pj是pj * i的最小质因子。

void get_primes(){
    //外层从2~n迭代,因为这毕竟算的是1~n中质数的个数,而不是某个数是不是质数的判定
    for(int i=2;i<=n;i++){
        if(!st[i]) primes[cnt++]=i;
        for(int j=0;primes[j]<=n/i;j++){//primes[j]<=n/i:变形一下得到——primes[j]*i<=n,把大于n的合数都筛了就
        //没啥意义了
            st[primes[j]*i]=true;//确保第j个质数和i相乘不会爆n

            //1)当i%primes[j]!=0时,说明此时遍历到的primes[j]不是i的质因子,那么只可能是此时的primes[j]

试除法求约数

AcWing 869. 试除法求约数

#include 
#include 
#include 

using namespace std;

int n;

void get_divisors(int n)
{
    vector res;

    for (int i = 1; i <= n / i; i++) {
        if (n % i == 0) {
            res.push_back(i);

            if (i != n / i) {  // 避免 i==n/i, 重复放入 (n是完全平方数
                res.push_back(n / i);
            }
        }
    }

    sort(res.begin(), res.end());
    for (auto item : res) {
        cout << item << " ";
    }
    puts("");
}

int main()
{
    cin >> n;
    while (n--) {
        int x;
        cin >> x;
        get_divisors(x);
    }
    return 0;
}

求约数个数

AcWing 870. 约数个数

  • 约数定理
    AcWing算法基础班笔记_第21张图片

#include 
using namespace std;
typedef long long LL; 
const int mod = 1e9 + 7;
int main(){
    int n,x;
    LL ans = 1;
    unordered_map hash;
    cin >> n;
    while(n--){
        cin >> x;
        for(int i = 2;i <= x/i; ++i){
            while(x % i == 0){
                x /= i;
                hash[i] ++;
            }
        }
        if(x > 1) hash[x] ++;
    }
    for(auto i : hash) ans = ans*(i.second + 1) % mod;
    cout << ans;
    return 0;
}

约数之和

AcWing 871. 约数之和
AcWing算法基础班笔记_第22张图片

#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;
        res = res * t % mod;
    }

    cout << res << endl;

    return 0;
}

辗转相除法

AcWing 872. 最大公约数
AcWing算法基础班笔记_第23张图片

#include
using namespace std;

// 写代码时可以假设一定满足 a > b 
// 就算 a < b , 也会在第一次递归时调转位置
int gcd(int a, int b) {
    // b == 0 时, 直接返回a, 否则进行辗转相除
    return b ? gcd(b, a % b) : a;
}

int main() {
    int m;
    scanf("%d", &m);
    while(m--) {
        int a, b;
        scanf("%d%d", &a, &b);
        printf("%d\n", gcd(a, b));
    }
    return 0;
}

欧拉函数

欧拉函数用φ(n)来表示,它的含义是,1到n中与n互质的数的个数
比如ϕ(6)=2,解释:1到6当中,与6互质的数只有1,5,共两个数。
AcWing 873. 欧拉函数
两个数 a b 互质的含义是gcd(a,b)=1

  • 求解
    AcWing算法基础班笔记_第24张图片
#include
using namespace std;

int euler(int n) {
	int res = n;
	for(int i = 2; i <= n / i; i++) {
		if(n % i == 0) {
			res = res / i * (i  - 1);
			while(n % i == 0) n /= i;
		}
	}
	if(n > 1) res = res / n * (n - 1);
	return res;
}

int main() {
	int m;
	scanf("%d", &m);
	while(m--) {
		int n;
		scanf("%d", &n);
		printf("%d\n", euler(n));
	}
	return 0;
}

线性筛法求欧拉函数

AcWing算法基础班笔记_第25张图片

#include
using namespace std;

typedef long long LL;

const int N = 1e6 + 10;

int primes[N], ctn;

bool st[N];

LL phi[N];

void get_eulers(int n) {
	phi[1] = 1;
	for(int i = 2; i <= n; i++) {
		if(!st[i]) {
			primes[ctn++] = i;
			phi[i] = i - 1;
		}
		for(int j = 0; primes[j] <= n / i; j++) {
			st[primes[j] * i] = true;
			if(i % primes[j] == 0) {
				phi[primes[j] * i] = primes[j] * phi[i];
				break;
			}
			phi[primes[j] * i] = (primes[j] - 1) * phi[i];
		}
	}
}

int main() {
	int n;
	scanf("%d", &n);
	get_eulers(n);
	LL sum = 0;
	for(int i = 1; i <= n; i++) sum += phi[i];
	printf("%lld", sum);
	return 0;
}
  • 欧拉函数的用处
    AcWing算法基础班笔记_第26张图片
  • 费马定理
    AcWing算法基础班笔记_第27张图片

快速幂

AcWing 875. 快速幂

  • 核心:反复平方法(思想上有点类似逆向二分。二分是每次在当前基础上减一半,快速幂是每次在当前基础上扩大一倍)。
  • 思想:
    AcWing算法基础班笔记_第28张图片
  • a b m o d p = ( a m o d p ) ( b m o d p ) abmodp=\left( amodp\right) \left( bmodp\right) abmodp=(amodp)(bmodp)
#include
using namespace std;

typedef long long LL;

// 快速幂求解 a^k mod p
int qmi(int a, int k, int p) {
	int res = 1;
	// 求 k 的二进制表示
	while(k > 0) {
		if(k & 1 == 1) res = (LL) res * a % p;
		k = k >> 1;//把k的末尾删掉
		a = (LL)a * a % p;
	}
	return res;
}

int main() {
	int n;
	scanf("%d", &n);
	while(n--) {
		int a, k, p;
		scanf("%d%d%d", &a, &k, &p);
		printf("%d\n", qmi(a, k, p));
	}
	return 0;
}

快速幂求逆元

AcWing算法基础班笔记_第29张图片
AcWing 876. 快速幂求逆元

#include
using namespace std;

typedef long long LL;

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

int main() {
	int n;
	scanf("%d", &n);
	while(n--) {
		int a, p;
		scanf("%d%d", &a, &p);
		if(a % p == 0) printf("impossible\n"); // a 和 p 不互质
		else printf("%d\n", qmi(a, p - 2, p));
	}
	return 0;
}

拓展欧几里德算法

AcWing 877. 扩展欧几里得算法  
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hIPnOupG-1652106821835)(https://z3.ax1x.com/2021/10/09/5idFyQ.png)]

  • 设ax1+by1=gcd(a,b), bx2+(a%b)y2=gcd(b,a%b);
    由gcd(a,b)=gcd(b,a%b),可得:
    ax1+by1=bx2+(a%b)y2;
    即:ax1+by1=bx2+(a-(a/b)*b)y2
    =ay2+bx2-(a/b)*by2;
    即:ax1+by1=ay2 + b(x2-(a/b)*y2)
    根据恒等定理,对应项相等,得:x1=y2; y1=x2-(a/b)*y2;
    这样我们就得到了:x1,y1的值基于x2,y2,所以我们可以通过递归求解。
#include
using namespace std;

int gcd(int a, int b, int &x, int &y) {
	if(b == 0) {
		x = 1;
		y = 0;
		return a;
	} else {
		 int d = gcd(b, a % b, y, x); // 注意这里要交换 x 和 y 的位置
		 y -= a / b * x;
		 return d;
	}
}

int main() {
	int n;
	scanf("%d", &n);
	while(n--) {
		int a, b, x, y;
		scanf("%d%d", &a, &b);
		gcd(a, b, x ,y);
		printf("%d %d\n", x, y);
	}
	return 0;
}

线性同余方程

AcWing 878. 线性同余方程

#include
// a * x ≡ b (mod m)

// 变形为拓展欧几里得形式:a * x + b * y = gcd(a, b)
// 原式变为:   a * x = m * y + b  (注:mod m 为 b, 则相当于结果为 m 的倍数和 b 的和)
//              a * x - m * y = b
// 另y1 = -y得:a * x + m * y1 = b
// 根据拓展欧几里得定理,只要 b 是 gcd(a, m)的倍数即有解!
// 另d = gcd(a, m), 我们得到的式子其实是:a * x + m * y1 = gcd(a, m) = d (注;上面的b其实就是d的倍数)
// 所以左右同乘 b / d 即可转化为:a * x * b / d + m * y1 * b / d = b * b / d = b
// 即最后答案为:res = x * d / b % m


using namespace std;

typedef long long LL;

int gcd(int a, int b, int &x, int &y) {
	if(b == 0) {
		x = 1;
		y = 0;
		return a;
	} else {
		 int d = gcd(b, a % b, y, x); // 注意这里要交换 x 和 y 的位置
		 y -= a / b * x;
		 return d;
	}
}

int main() {
	int n;
	scanf("%d", &n);
	while(n--) {
		int a, b, m, x, y;
		scanf("%d%d%d", &a, &b, &m);
		int d = gcd(a, m, x ,y);
		if(b % d != 0) printf("impossible\n");
        else printf("%d\n", (LL)x * b / d % m);
	}
	return 0;
}

中国剩余定理

AcWing 204. 表达奇怪的整数
AcWing算法基础班笔记_第30张图片
AcWing算法基础班笔记_第31张图片

#include 
#include 
using namespace std;

typedef long long LL;

// 扩展欧几里得算法
LL exgcd(LL a, LL b, LL &x, LL &y) {
	if (b == 0) {
		x = 1;
		y = 0;
		return a;
	}
	LL d = exgcd(b, a % b, y, x);
	y -= a / b * x;
	return d;
}

int main() {
	int n;
	scanf("%d", &n);
	bool no_ans = false;
	LL a1, m1;

	cin >> a1 >> m1;
	for (int i = 0; i < n - 1; i++) {
		LL a2, m2;
		cin >> a2 >> m2;
		LL k1, k2;
		LL d = exgcd(a1, a2, k1, k2);
		if ((m2 - m1) % d != 0) {
			no_ans = true;
			break;
		}
		k1 *= (m2 - m1) / d;
		LL t = a2 / d;
		k1 = (k1 % t + t) % t; // 将k1置为最小的正整数解
		// 更新a1和m1, 准备下一轮的合并
		m1 = a1 * k1 + m1;
		a1 = abs(a1 / d * a2);
	}
	if (no_ans) printf("-1");
	else printf("%lld", (m1 % a1 + a1) % a1);
}

组合数

AcWing算法基础班笔记_第32张图片

递推(dp味道)

AcWing算法基础班笔记_第33张图片

  • C a b 表示从a个苹果中选择b个苹果,假设有一个苹果分开,这个苹果就有选和不选两种方案,分别那么从剩下的a-1个苹果中选择b-1和b个,即 C a-1 b-1 和C a-1 b
    AcWing 885. 求组合数 I
#include
using namespace std;
const int mod=1e9+7,N=2010;
int n;
int c[N][N];

void init(){
    for(int i=0;i>n;
    while(n--){
        int a,b;
        cin>>a>>b;

        cout<

快速幂逆元求法

AcWing算法基础班笔记_第34张图片

#include
using namespace std;
const int mod=1e9+7,N=1e5+10;
typedef long long LL;
long long fac[N],infac[N];
int quick_pow(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;
    fac[0]=infac[0]=1;
    for(int i=1;i<=1e5;i++)
    {
        fac[i]=fac[i-1]*i%mod;
        infac[i]=(LL)infac[i - 1] * quick_pow(i,mod-2,mod)%mod;
    }
    cin>>n;
    while(n--)
    {
        int a,b;
        cin>>a>>b;
        cout<<(LL)fac[a] * infac[b] % mod * infac[a - b] % mod<

lucas定理

AcWing算法基础班笔记_第35张图片

#include 

using namespace std;

typedef long long ll;

int quick_power(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;
    }
    int res = 1;
    for (int i = 1, j = a; i <= b; i++, j--) {
        res = (ll)res * j % p;
        res = (ll)res * quick_power(i, p - 2, p) % p;
    }
    return res;
}

int lucas(ll a, ll b, int p)
{
    if (a < p && b < p) {
        return C(a, b, p);
    }
    return (ll)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}

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

    while (n--) {
        ll a, b;
        int p;
        cin >> a >> b >> p;
        cout << lucas(a, b, p) << endl;
    }
    return 0;
}

高精度-素数组合

AcWing算法基础班笔记_第36张图片
AcWing 888. 求组合数 IV

  • 筛素数(1~5000)

  • 求每个质数的次数

  • 用高精度乘把所有质因子乘上

  • 分解质因数
    为什么分解质因数?这个问题真的是难到我自己了。
    因为当时看录像没有听太明白所以一下子真想不到,看所有题解也并不是很懂。
    你既然能分解N!的质因子那理所当然M!的质因子也能分解,那这么说的话(N-M)!的质因子也一定能分解。
    看到这应该也看明白了,C(N,M)=N!/M!/(N-M)!
    求出质因子的用途就很想当然了我们可以把它的所有质因子乘起来这样既可得到最终答案。
    而直接求出上面式子有些许困难我们不如直接求出N!的质因子接着减去这些除数的质因子。
    这样就得出了答案所有的质因子。

#include 
#include 

using namespace std;

typedef long long LL;

using namespace std;

const int N = 5010;

int cnt , primes[N];
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 a , int p)//求a!中p的次数
{
    int s = 0;
    while(a)
    {
        s += a / p;
        a /= p;
    }
    return s;
}

vector mul(vector a , int b)//高精度乘法
{
    vector ans;
    int t = 0;
    for(int i = 0 ; i < a.size() || t ; i++)
    {
        if(i < a.size()) t += b * a[i];
        ans.push_back(t % 10);
        t /= 10;
    }
    return ans;
}

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

    get_primes(a);//筛质数

    for(int i = 0 ; i < cnt ; i ++)//求出每一个p最后的次数
    {
        int p = primes[i];
        sum[i] = get(a , p) - get(b , p) - get(a - b , p);//m1~mk是其中对应的每个素数的次数,是在a中的次数 - b 中的次数 - (a - b)中的次数;
    }

    vector res;
    res.push_back(1);
    for(int i = 0 ; i < cnt ; i++)
    {
        int p = primes[i];
        for(int j = 0 ; j < sum[i] ; j++)//累乘
            res = mul(res , p);
    }

    for(int i = res.size() - 1 ; i >= 0 ; i--) cout << res[i];
    cout << endl;
    return 0;
}

卡特兰数

  • 卡特兰数 C 2 n n n + 1 \dfrac{C_{2n}^{n}}{n+1} n+1C2nn
    AcWing 889. 满足条件的01序列
    AcWing算法基础班笔记_第37张图片
  • 解法:将 01 序列置于坐标系中,起点定于原点。若 0 表示向右走,1 表示向上走,那么任何前缀中 0 的个数不少于 1 的个数就转化为,路径上的任意一点,横坐标大于等于纵坐标。题目所求即为这样的合法路径数量。
    图中,表示从(0,0) 走到(n,n) 的路径,在绿线及以下表示合法,若触碰红线即不合法。
    由图可知,任何一条不合法的路径(如黑色路径),都对应一条从 (0,0) 走到 (n−1,n+1)的一条路径(如灰色路径)。而任何一条(0,0) 走到 (n−1,n+1) 的路径,也对应了一条从 (0,0) 走到(n,n) 的不合法路径。即从 (0, 0) 走到 (n - 1, n + 1) 的所有方案数,一共要走 2n 步,其中向右走 n-1 步,向上走 n+1 步,组合数就是 C(2n, n - 1) 或者 C(2n, n + 1).
  • 求逆元
    如果取模的数是一个质数,可以采用费马小定理求逆元
    如果不是质数,只能采用拓展欧几里得求逆元
#include 

using namespace std;

const int mod = 1e9 + 7;

using LL = long long;

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

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

    int res = 1;

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

    for(int i = n ; i ; i --) res = (LL) res * qmi(i , mod - 2) % mod;

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

    cout << res << endl;
    return 0;
}

容斥原理 O(2^m)

容斥原理是一种重要的组合数学方法,可以让你求接任意大小的集合,或者计算复合事件的概率

  • 描述:要计算几个集合并集的大小,我们要先将所有单个集合的大小计算出来,然后减去所有两个集合相交的部分,再加回所有三个集合相交的部分,再减去所有四个集合相交的部分,依此类推,一直计算到所有集合相交的部分。
    AcWing算法基础班笔记_第38张图片
    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)//说明当前乘积

博弈论

NIM游戏

AcWing 891. Nim游戏

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

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

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

  • 公平组合游戏ICG
    若一个游戏满足:
    由两名玩家交替行动;
    在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;
    不能行动的玩家判负;
    则称该游戏为一个公平组合游戏。
    NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏。
    因为围棋交战双方分别只能落黑子和白子,胜负判定也比较复杂,不满足条件2和条件3。

  • 在解决这个问题之前,先来了解两个名词:
    必胜状态,先手进行某一个操作,留给后手是一个必败状态时,对于先手来说是一个必胜状态。即先手可以走到某一个必败状态。
    必败状态,先手无论如何操作,留给后手都是一个必胜状态时,对于先手来说是一个必败状态。即先手走不到任何一个必败状态。

假设n堆石子,石子数目分别是a1,a2,…,an,如果a1⊕a2⊕…⊕an≠0,先手必胜;否则先手必败。
AcWing 891. Nim游戏
AcWing算法基础班笔记_第39张图片

/*

*/
#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;
}

动态规划

从两个角度进行讲解

  • 常用的DP模型
    背包问题
  • DP的不同类型
    线性DP
    区间DP
    状态压缩DP
    树形DP
    计数类DP
    数位统计DP

0-1 背包

什么是背包问题?

背包问题的本质是,给定一堆物品和一个背包,每个物品有 体积 和 价值两种属性,在一些限制条件下,将一些物品装入背包,使得在不超过背包体积的情况下,能够得到的最大价值。根据不同的限制条件,分为不同类型的背包问题。

给定 N个物品,和一个容量为 V的背包,每个物品有2个属性,分别是它的体积 vi(v for volume),和它的价值 wi(w for weight),每件物品只能使用一次(0-1背包的特点,每件物品要么用1次(放入背包),要么用0次(不放入背包),问往背包里放入哪些物品,能够使得物品的总体积不超过背包的容量,且总价值最大。

你可能感兴趣的:(算法学习,算法,排序算法,c++)