基础算法模板

由数据范围反推算法复杂度以及算法内容#

  1. n30, 指数级别, dfs+剪枝,状态压缩dp
  2. n100 => O(n3)floyddp,高斯消元
  3. n1000 => O(n2)O(n2logn)dp,二分,朴素版Dijkstra、朴素版PrimBellmanFord
  4. n10000 => O(n×n),块状链表、分块、莫队
  5. n100000 => O(nlogn) => 各种sort,线段树、树状数组、set/mapheap、拓扑排序、dijkstra+heapprim+heapKruskalspfa、求凸包、求半平面交、二分、CDQ分治、整体二分、后缀数组、树链剖分、动态树
  6. n1000000 => O(n), 以及常数较小的 O(nlogn) 算法 => 单调队列、 hash、双指针扫描、并查集,kmp、AC自动机,常数比较小的 O(nlogn) 的做法:sort、树状数组、heapdijkstraspfa
  7. n10000000 => O(n),双指针扫描、kmpAC自动机、线性筛素数
  8. n109 => O(n),判断质数
  9. n1018 => O(logn),最大公约数,快速幂,数位DP
  10. n101000 => O((logn)2),高精度加减乘除
  11. n10100000 => O(logk×loglogk)k表示位数,高精度加减、FFT/NTT

1.排序#

1.快速排序#

  • 算法内容:

    1. 确定分界点x(一般为q[r+l<<1]
    2. 调整区间,将[l,r]区间划分为左右两段,利用双指针使得左边的数都小于等于x,右边的数都大于等与x
    3. 递归处理左右两边
  • 时间复杂度O(nlogn)

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 1e5 + 10;

int q[N];

void quick_sort(int q[], int l, int r)
{
    if (l >= r) return;
	
    //我们要实现的是两个指针每次比较完之后,如果符合条件就向中间移动一格。
	//可以用do while实现:先向中间移动一格再去比较,这一次的移动实际上是上一次的结果,
	//所以最后开始两个指针要指在边界外 l - 1 和 r + 1,执行完do语句之后就会变为l和r
    int i = l - 1, j = r + 1, x = q[l + r >> 1];
    while (i < j)
    {
        do i ++ ; while (q[i] < x);
        do j -- ; 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()
{
    int n;
    cin >> 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;
}

延申:快速选择求第k小的数

  • 算法内容:

    每一次调整区间之后只递归处理第k个数所在的区间

  • 时间复杂度:O(n)

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 1e5 + 10;

int q[N], n, k;

int quick_select(int q[], int l, int r, int k)
{
    if (l >= r) return q[l];
    
    int x = q[(l + r) >> 1], i = l - 1, j = r + 1;
    
    while (i < j)
    {
        do i ++ ; while (q[i] < x);
        do j -- ; while (q[j] > x);
        if (i < j) swap (q[i], q[j]);
    }
    
    int sl = j - l + 1; //[l, j]区间上由多少个数
    if (k <= sl) return quick_select(q, l, j, k);
    else return quick_select(q, j + 1, r, k - sl);
}

int main()
{
    cin >> n >> k;
    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
    
    cout << quick_select(q, 0, n - 1, k);
    
    return 0;
}

2.归并排序#

  • 算法内容:
    1. 确定分界点mid=l+r>>1
    2. mid[l,r]区间分为左右两段,递归排序左边和右边,得到左边和右边两段有序序列
    3. 用双指针将两段序列合并为一个序列
  • 时间复杂度:nlogn
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1e5 + 10;

int a[N], tmp[N];

void merge_sort(int q[], int l, int r)
{
    if (l >= r) return;

    int mid = l + r >> 1;
	
    //递归排序
    merge_sort(q, l, mid), merge_sort(q, mid + 1, r);
	
    //归并[l, mid]和[mid + 1, r]两段有序序列
    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
    {
        //把小的元素放到tmp数组
        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 (i = l, j = 0; i <= r; i ++, j ++ ) 
        q[i] = tmp[j];
}

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

    merge_sort(a, 0, n - 1);

    for (int i = 0; i < n; i ++ ) printf("%d ", a[i]);

    return 0;
}

延申:求逆序对的数量

  • 算法内容:

    1. mid为分界点递归排序左右两个区间

    2. 将逆序对(i,j)分为三类:

      1. i,j都在左边区间,数量为merge_sort(q, l, mid)
      2. i在左边,j在右边,数量为merge_sort(q, mid + 1, r)
      3. i,j都在右边
    3. 先令res = merge_sort(q, l, mid) + merge_sort(q, mid + 1, r);。然后在归并的过程中,每一次q[i] 开始大于 q[j]的时候res += mid - i + 1

      如图:

  • 时间复杂度:O(nlogn)

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

typedef long long LL;

const int N = 1e5 + 10;

int n;
int q[N], tmp[N];
LL res;

LL merge_sort(int q[], int l, int r) //返回[l, r]区间上的逆序对数量
{
    if (l >= r) return 0;
	
    //递归排序
    int mid = l + r >> 1;
    LL res = 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 ++ ];
            res += mid - i + 1;
        }
    }
    
    //收尾过程
    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];
    
    //物归原主
    for (i = l, j = 0; i <= r; i ++, j ++ )  q[i] = tmp[j];
    
    return res;
}

int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);

    cout << merge_sort(q, 0, n - 1) << endl;
    
    return 0;
}

3.堆排序#

  • 算法内容:
  • 时间复杂度:
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m;
int h[N], cnt;

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)
    {
        swap(h[u], h[t]);
        down(t);
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
    cnt = n;

    for (int i = n / 2; i; i -- ) down(i);

    while (m -- )
    {
        printf("%d ", h[1]);
        h[1] = h[cnt -- ];
        down(1);
    }

    puts("");

    return 0;
}

4.计数排序#

5.基数排序#

6.冒泡排序#

  • 算法内容:

    枚举数组中的元素,比较相邻元素,如果左端元素大于有段元素,则交换两个数。这样操作后数组最右端的元素即为该数组中所有元素的最大值。接着对该数组除最右端的n1个元素进行同样的操作,再接着对剩下的n2个元素做同样的操作,直到整个数组有序排列。

  • 时间复杂度:O(n2)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 10010;

int n, q[N];

void bubble_sort(int q[], int n)
{
    for (int i = 0; i < n - 1; i ++) //枚举比较的趟数,一共需要进行n - 1趟
        for (int j = 0; j < n - i - 1; j ++) //枚举数列中的元素(除去已经确定最大值的元素)
            if (q[j] > q[j + 1])
                swap(q[j], q[j + 1]);
}

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++) cin >> q[i];
    bubble_sort(q, n);
    for (int i = 0; i < n; i ++) cout << q[i] << " ";
    return 0;
}

7.选择排序#

  • 算法内容:

    假定数组最左端元素x最小。接着从他的右边开始遍历,找到它右边最小的数minv,如果minv!=x则将minvx交换,直到排序完整个数组。

  • 时间复杂度:O(n2)

#include <bits/stdc++.h>

using namespace std;

const int N = 1e3 + 10;

int n;
int a[N];

void insertion_sort(int a[], int n)
{
    int j = 0, key;
    for (int i = 1; i < n; i ++ )
    {
        key = a[i];
        j = i - 1;
        while ((j >= 0) && (a[j] > key))
        {
            a[j + 1] = a[j];
            j -- ;
        }
        a[j + 1] = key;
    }
}

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

    insertion_sort(a, n);

    for (int i = 0; i < n; i ++ ) cout << a[i] << ' ';

    return 0;
}

8.插入排序#

  • 算法内容:

    通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。例如要将数组arr=[4,2,8,0,5,1]排序,可以将4看做是一个有序序列,将[2,8,0,5,1]看做一个无序序列。无序序列中24小,于是将2插入到4的左边,此时有序序列变成了[2,4],无序序列变成了[8,0,5,1]。无序序列中84大,于是将8插入到4的右边,有序序列变成了[2,4,8],无序序列变成了[0,5,1]。以此类推,最终数组按照从小到大排序。

  • 时间复杂度:O(n2)

#include <bits/stdc++.h>

using namespace std;

const int N = 1e5 + 10;

int n;
int a[N];

void select_sort(int a[], int n)
{
    for (int i = 0; i < n; i ++ )
    {
        int minpos = i; //假定i位置的数最小
        for (int j = i + 1; j < n; j ++ )
        {
            if (a[j] < a[minpos])
            {
                minpos = j;
            }
        }
        if (minpos != i) swap(a[i], a[minpos]);
    }
}

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> a[i];
    
    select_sort(a, n);
    
    for (int i = 0; i < n; i ++ ) cout << a[i] << ' ';
    
    return 0;
}

9.希尔排序#

10.桶排序#

2.二分#

  • 只要具有二段性,就可以二分。

1.整数二分#

  • 模板一

    /*当我们将区间[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;
    }
    
  • 模板题

    //在有序数组中查找某个数出现的范围(Acwing 789)
    #include <iostream>
    
    using namespace std;
    
    const int N = 100010;
    
    int q[N];
    int n, m;
    
    int main()
    {
        cin >> n >> m;
        for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);
        
        while (m -- )
        {
            int x;
            cin >> x;
            int l = 0, r = n - 1;
            
            //二分左端点
            
            //写法一:
            // while (l < r)
            // {
            //     int mid = (l + r) >> 1;
            //     if (q[mid] >= x) r = mid;
            //     else l = mid + 1;
            // }
            
            //写法二:
            while (l < r)
            {
                int mid = (l + r) >> 1;
                if (q[mid] < x) l = mid + 1;
                else r = mid;
            }
            
            if (q[l] != x) cout << "-1 -1" << endl;
            else
            {
                cout << l << ' ';
                l = 0, r = n - 1;
                
                //二分右端点
                
                //写法一:
                // while (l < r)
                // {
                //     int mid = (l + r + 1) >> 1;
                //     if (q[mid] <= x) l = mid;
                //     else r = mid - 1;
                // }
                
                //写法二:
                while (l < r)
                {
                    int mid = (l + r + 1) >> 1;
                    if (q[mid] > x) r = mid - 1;
                    else l = mid;
                }
                
                cout << l << endl;
            }
        }
        return 0;
    }
    

2.浮点数二分#

  • 模板

    //不需要考虑取整问题,更新操作为l = mid, 或r = mid
    double bsearch(int l, int r)
    {
        while (r - l > 1e-8)
        {
            double mid = (l + r) / 2.0;
            if (check(mid)) l = mid;
            else r = mid;
        }
        
        return l;
    }
    
  • 模板题

    //求浮点数平方根
    #include <iostream>
    #include <algorithm>
    #include <cstring>
    
    using namespace std;
    
    int main()
    {
        double x;
        cin >> x;
    
        double l = 0, r = x;
        while (r - l > 1e-8)
        {
            double mid = (l + r) / 2;
            if (mid * mid >= x) r = mid;
            else l = mid;
        }
    
        printf("%lf\n", l);
    
        return 0;
    }
    

3. 高精度#

1.高精度加法#

#include <iostream>
#include <vector>

using namespace std;

vector<int> add(vector<int> &A, vector<int> &B) //加引用后不复制数组节省时间
{
    vector<int> C;
    int t = 0;//进位
    for (int i = 0; i < A.size() || i < B.size(); i ++ )
    {
        if (i < A.size()) t += A[i];
        if (i < B.size()) t += B[i];
        C.push_back(t % 10);
        t /= 10;
    }

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

int main()
{
    string a, b;
    vector<int> 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');

    auto C = add(A, B);

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

    return 0;
}

2.高精度减法#

#include <iostream>
#include <vector>

using namespace std;

bool cmp(vector<int> &A, vector<int> &B)//判断是否有A>=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;
}

vector<int> sub(vector<int> &A, vector<int> &B)
{
    vector<int> C;
    for (int i = 0, t = 0; i < A.size(); i ++ )
    {
        t = A[i] - t;
        if (i < B.size()) t -= B[i];
        C.push_back((t + 10) % 10);
        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<int> 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');

    vector<int> 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 << C[i];
    cout << endl;

    return 0;
}

3.高精度乘法#

#include <iostream>
#include <vector>

using namespace std;


vector<int> mul(vector<int> &A, int b)
{
    vector<int> C;

    int t = 0;
    for (int i = 0; i < A.size() || t; i ++ )
    {
        if (i < A.size()) t += A[i] * b;
        C.push_back(t % 10);
        t /= 10;
    }

    while (C.size() > 1 && C.back() == 0) C.pop_back();

    return C;
}


int main()
{
    string a;
    int b;

    cin >> a >> b;

    vector<int> 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 -- ) printf("%d", C[i]);

    return 0;
}

4.高精度除法#

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

vector<int> div(vector<int> &A, int b, int &r)
{
    vector<int> C;
    r = 0;
    for (int i = A.size() - 1; i >= 0; i -- )
    {
        r = r * 10 + A[i];
        C.push_back(r / b);
        r %= b;
    }
    reverse(C.begin(), C.end());
    while (C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}

int main()
{
    string a;
    vector<int> A;

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

    int r;
    auto C = div(A, B, r);

    for (int i = C.size() - 1; i >= 0; i -- ) cout << C[i];

    cout << endl << r << endl;

    return 0;
}

4. 前缀和与差分#

1. 一维前缀和#

  • 算法内容:

    预处理:循环一遍a数组,s[i]=s[i1]+a[i],得到前缀和数组ss[k]=i=1kai

    查询:i=lrai=s[r]s[l1]

  • 时间复杂度:

    预处理:O(n)

    查询:O(1)

//求区间和
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m;
int a[N], s[N]; //a为原数组,b为前缀和数组

int main()
{
    scanf("%d%d", &n, &m);
    
    //读入原数组
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
	
    //前缀和的初始化
    for (int i = 1; i <= n; i ++ ) s[i] = s[i - 1] + a[i];
	
    //通过查询前缀和数组得到区间和
    while (m -- )
    {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", s[r] - s[l - 1]); // 区间和的计算
    }

    return 0;
}

2. 二维前缀和#

  • 算法内容:

    预处理:循环一遍a矩阵,令s[i][j]=s[i1][j]+s[i][j1]s[i1][j1]+a[i][j],得到前缀和矩阵s,此时s[i][j]表示的是左上角是(1,1)右下角是(i,j)的矩阵的所有元素的和

image

查询:左上角是(x1,y1),右下角是(x2,y2)的矩阵的所有元素的和为s[x2][y2]s[x11][y2]s[x2][y11]+s[x11][y11]

image

  • 时间复杂度:

    预处理:O(n2)

    查询:O(1)

//求子矩阵的和
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m, q;
int a[N][N], s[N][N]; //a为原矩阵,s为前缀和矩阵

int main()
{
    scanf("%d%d%d", &n, &m, &q);
	
    //读入原矩阵并初始化前缀和矩阵
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            scanf("%d", &a[i][j]);
            s[i][j] = s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i][j];
        }
	
    //通过查询前缀和矩阵得到子矩阵的和
    while (q -- )
    {
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", s[x2][y2] - s[x1 - 1][y2] - s[x2][y1 - 1] + s[x1 - 1][y1 - 1]);
    }

    return 0;
}

3. 一维差分#

  • 算法内容:

    差分数组b的定义:bi=aiai1(b1=a1,1in)

    a数组的[l,r]区间上都加上一个数cb[r + 1] -= c, b[l] += c

  • 时间复杂度:

    预处理:O(n)

    查询:O(1)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m;
int a[N], b[N]; //a为原数组,b为差分数组

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

int main()
{
    scanf("%d%d", &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]);
	
    //通过差分数组实现对原数组的增添元素
    while (m -- )
    {
        int l, r, c;
        scanf("%d%d%d", &l, &r, &c);
        insert(l, r, c);
    }
	
    //对差分数组求前缀和
    for (int i = 1; i <= n; i ++ ) b[i] += b[i - 1];
	
    //输出操作后的数组
    for (int i = 1; i <= n; i ++ ) printf("%d ", b[i]);

    return 0;
}

4. 二维差分#

  • 算法内容:

    预处理:用insert函数初始化差分矩阵b,然后对差分矩阵b求前缀和

image

查询:b矩阵即为处理后得到的新的矩阵

  • 时间复杂度:

    预处理:O(n2)

    查询:O(1)

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010;

int n, m, q;
int a[N][N], b[N][N]; //a为原矩阵,b为差分矩阵

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()
{
    scanf("%d%d%d", &n, &m, &q);
	
    //读入原矩阵
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            scanf("%d", &a[i][j]);
    
	//初始化差分矩阵
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; 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];
	
    //输出操作后的矩阵
    for (int i = 1; i <= n; i ++ )
    {
        for (int j = 1; j <= m; j ++ )
            printf("%d ", b[i][j]);
        puts("");
    }

    return 0;
}


双指针

模板#

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

    // 具体问题的逻辑
}

常见问题分类:
(1) 对于一个序列,用两个指针维护一段区间
(2) 对于两个序列,维护某种次序,比如归并排序中合并两 个有序序列的操作

例题#

1.输入abc def ghi,输出abc和def和ghi#

#include<iostream>
#include<string.h>
using namespace std;

char str[1000];
int j = 0;

int main()
{
    gets(str);
    int n = strlen(str);
    for (int i = 0; i < n; i ++)//i每次都指向每个单词的第一个字母
    {
        j = i;
        while (j < n && str[j] != ' ')
        {
            j ++;
        }
        //这道题的具体逻辑
        for (int k = i; k < j; k ++) cout << str[k];
        cout << endl;

        i = j;
    }

    return 0;
}

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

#include <iostream>

using namespace std;

const int N = 100010;

int n;
int q[N], s[N];
//i指的是每段序列的终点
//j指的的是j向左最远能走多少
int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);

    int res = 0;
    for (int i = 0, j = 0; i < n; i ++ )
    {
        s[q[i]] ++ ;
        while (j < i && s[q[i]] > 1) s[q[j ++ ]] -- ;
        res = max(res, i - j + 1);
    }

    cout << res << endl;

    return 0;
}

位运算

1.统计x的二进制表示里面有多少1#

#include<iostream>

using namespace std;

int lowbit(int x)
{
    return (x & - x);
}
int main()
{
    int res = 0;
    int x = 18;    //10010
    int tmp = x;
    printf("lowbit(x) = %d\n", lowbit(x));
    while (x)
    {
        x -= lowbit(x);
        res ++;
    }
    printf("%d的二进制表示中有%d个1出现\n",tmp, res);
    return 0;
}

2.一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数#

#include<iostream>

using namespace std;

const int N = 10010;

int arr[N], n;

void printOddTimesNum1(int *arr, int len)
{
    int eor = 0;
    for (int i = 0; i < n; i ++)
    {
        eor = eor ^ arr[i];
    }
    printf("%d", eor);
}

int main()
{
    cin >> n;
    for (int i = 0; i < n; i ++) scanf("%d", &arr[i]);
    printOddTimesNum1(arr, n);
    return 0;
}

3.一个数组中有俩种数出现了奇数次,#

其他数都出现了偶数次,怎么找到并打印这两种数

#include<iostream>

using namespace std;

const int N = 10010;

int lowbit(int x)
{
    return (x & - x);
}

void printOddNum2(int *arr, int len)
{
    int eor = 0;
    for (int i = 0; i < len; i ++)
    {
        eor ^= arr[i];
    }
    //eor = a ^ b
    //eor != 0 (因为a 和 b一定不相等,如果相等则a和b出现了偶数次矛盾)
    //eor必然有一个位置上是1
    //  假设eor = 0110010000
//则lowbit(eor) = 0000010000
    int rightone = lowbit(eor); //提取出eor最右的1
    int onlyone = 0; //eor'
    for (int i = 0; i < len; i ++)
    {
        if ((arr[i] & rightone) != 0)//arr[i]的那个位置上一定是1,
        //所以找出了那个位置上为1的所有数字,也意味着将a 和 b分开了
        //此时符合if的arr[i]只可能包含a或者只可能包含b
        {
            onlyone ^= arr[i];
        }
    }
    printf("%d %d", onlyone, (eor ^ onlyone));
    //假设onlyone是a,则eor ^ eor' = a ^ b ^ a = b
}


int main()
{
    int n, arr[N] = {0};
    cin >> n;
    for (int i = 0; i < n; i ++) scanf("%d", &arr[i]);
    printOddNum2(arr, n);
    return 0;
}

不创建额外变量实现两个数的交换#

#include<iostream>

using namespace std;

int main()
{
    int a = 3, b = 6;
    a = a ^ b;
    b = a ^ b;
    a = a ^ b;
    cout << a << " " << b;
    return 0;
}

离散化

vector<int> 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
    //或者 return r 映射到0, 1, 2, ..., n
}
  • 例题

    区间和

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

typedef pair<int, int> PII;

const int N = 300010;

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

vector<int> alls;
vector<PII> add, query;

int find(int 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;
}

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);
        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];

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

    return 0;
}

区间合并

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e5 + 10, INF = 2e9;

int n;
vector<PII> segs;

void merge(vector<PII> &segs)
{
    vector<PII> res;
    
    sort(segs.begin(), segs.end()); //按照区间的左端点排序
    
    int st = -INF, ed = -INF; //维护当前区间
    
    for (auto seg : segs) //枚举当前区间的下一个区间
    {
        //当前区间和下一个区间没有重叠,将当前区间加到答案里
        //并且将当前区间更新为下一个区间
        if (ed < seg.first)
        {
            if (st != -INF) res.push_back(seg);
            st = seg.first, ed = seg.second;
        }
        //当前区间和下一个区间有重叠,将当前区间的右端点更新为较大的右端点,
        //实现区间合并,并准备枚举下一个区间
        else
        {
            ed = max(ed, seg.second);
        }
    }
    
    //将最后一个区间加到答案里
    if (st != -INF) res.push_back({st, ed});
    segs = res;
}


int main()
{
    cin >> n;
    
    while (n -- )
    {
        int l, r;
        cin >> l >> r;
        segs.push_back({l, r});
    }
    
    merge(segs);
    
    cout << segs.size() << endl;
    
    return 0;
}

KMP

  • 写法一

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    
    using namespace std;
    
    const int N = 1000;
    const int M = 100;
    
    int ne[M] = {0};
    
    
    //求ne数组 O(M)
    void getne(char ms[], int ne[])
    {
        int len = strlen(ms);
        if (len == 1)
            ne[0] = - 1;
    
        ne[0] = - 1;
        ne[1] = 0;
    
        int i = 2;//next数组的位置
        int cn = 0;
        while (i < len)
        {
            if (ms[i - 1] == ms[cn])
                ne[i ++] = ++ cn;
            else if (cn > 0) //当前跳到cn的位置,和i - 1位置的字符配不上
                cn = ne[cn];
            else ne[i ++] = 0;
        }
    }
    
    
    //匹配过程 O(N)
    int getindex(char s[], char p[], int res)
    {
        int i1 = res;
        int i2 = 0;
        int len1 = strlen(s);
        int len2 = strlen(p);
    
        if (s == "" || p == "" || len1 < len2)
            return - 1;
    
        for (i1 = res)
        while (i1 < len1 && i2 < len2) //都不越界
        {
            if(s[i1] == p[i2])
            {
                i1 ++;
                i2 ++;
            }
            else if(ne[i2] == - 1)
                i1 ++;
            else i2 = ne[i2];//回跳
        }
        //i1越界或者i2越界
        return i2 == len2 ? (i1 - i2) : - 1;
    }
    
    
    int main()
    {
        int start = 0;
        char s[N];
        char p[M];
        scanf("%s", s);
        scanf("%s", p);
        getne(p, ne);
        //for (int i = 0; i < strlen(m); i ++) printf("%d", ne[i]);
        while ( (start = getindex(s, p, start)) != - 1)
        {
            printf("%d ", getindex(s, p, start++));
        }
        if (getindex(s, p, start) == - 1) printf("Not Found\n");
        return 0;
    }
    
  • 写法二

    #include <iostream>
    
    using namespace std;
    
    const int N = 100010, M = 1000010;
    
    int n, m; //s串的长度为m, p串的长度为n
    int ne[N];
    char s[M], p[N];
    
    int main()
    {
        cin >> n >> p + 1 >> m >> s + 1;
    
        //求next数组 (ne[0] = 0, ne[1] = 0)
        for (int i = 2, j = 0; i <= n; 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 <= 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 + 1 - 1);
                j = ne[j];
            }
        }
    
        return 0;
    }
    

基础数据结构

1.单链表#

#include <iostream>

using namespace std;

const int N = 100010;


// head 表示头结点的下标
// e[i] 表示节点i的值
// ne[i] 表示节点i的next指针是多少
// idx 存储当前已经用到了哪个点
int head, e[N], ne[N], idx;

// 初始化
void init()
{
    head = -1;
    idx = 0;
}

// 将x插到头结点
void add_to_head(int x)
{
    e[idx] = x, ne[idx] = head, head = idx ++ ;
}

// 将x插到下标是k的点后面
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();

    while (m -- )
    {
        int k, x;
        char op;

        cin >> op;
        if (op == 'H')
        {
            cin >> x;
            add_to_head(x);
        }
        else if (op == 'D')
        {
            cin >> k;
            if (!k) head = ne[head];
            else remove(k - 1);
        }
        else
        {
            cin >> k >> x;
            add(k - 1, x);
        }
    }

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

    return 0;
}

2.双链表#

#include <iostream>

using namespace std;

const int N = 100010;

int m;
int e[N], l[N], r[N], idx;

// 在节点a的右边插入一个数x
void insert(int a, int x)
{
    e[idx] = x;
    l[idx] = a, r[idx] = r[a];
    l[r[a]] = idx, r[a] = idx ++ ;
}

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

int main()
{
    cin >> m;

    // 0是左端点,1是右端点
    r[0] = 1, l[1] = 0;
    idx = 2;

    while (m -- )
    {
        string op;
        cin >> op;
        int k, x;
        if (op == "L")
        {
            cin >> x;
            insert(0, x);
        }
        else if (op == "R")
        {
            cin >> x;
            insert(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 << e[i] << ' ';
    cout << endl;

    return 0;
}

3.栈#

#include <iostream>

using namespace std;

const int N = 100010;

int m;
int stk[N], 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;
}

单调栈#

#include <iostream>

using namespace std;

const int N = 100010;

int stk[N], tt;

int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        while (tt && stk[tt] >= x) tt -- ;
        if (!tt) printf("-1 ");
        else printf("%d ", stk[tt]);
        stk[ ++ tt] = x;
    }

    return 0;
}

4.队列#

#include <iostream>

using namespace std;

const int N = 100010;

int m;
int q[N], hh, tt = -1;

int main()
{
    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;
}

单调队列#

#include <iostream>

using namespace std;

const int N = 1000010;

int a[N], q[N];

int main()
{
    int n, k;
    scanf("%d%d", &n, &k);
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);

    int hh = 0, tt = -1;
    for (int i = 0; i < n; i ++ )
    {
        if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;

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

        if (i >= k - 1) printf("%d ", a[q[hh]]);
    }

    puts("");

    hh = 0, tt = -1;
    for (int i = 0; i < n; i ++ )
    {
        if (hh <= tt && i - k + 1 > q[hh]) hh ++ ;

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

        if (i >= k - 1) printf("%d ", a[q[hh]]);
    }

    puts("");

    return 0;
}

5.Trie树#

#include <iostream>

using namespace std;

const int N = 100010;

int son[N][26], cnt[N], idx;
char str[N];

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

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;
    scanf("%d", &n);
    while (n -- )
    {
        char op[2];
        scanf("%s%s", op, str);
        if (*op == 'I') insert(str);
        else printf("%d\n", query(str));
    }

    return 0;
}

6.并查集#

  • 支持的操作

    1. 将两个集合合并

    2. 询问两个元素是否在一个集合当中

  • 基本原理

    每个集合用一棵树来表示。树根的编号就是整个集合的编号。每个节点存储它的父节点,p[x]表示x的父节点

  • 问题

    1. 如何判断树根:if (p[x] == x)
    2. 如何求x的集合编号:while (p[x] != x) x = p[x];
    3. 如何合并两个集合:px是x的集合编号,py是y的集合编号。p[px] = py;
  • 优化:路径压缩

#include <iostream>

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;
    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)) puts("Yes");
            else puts("No");
        }
    }

    return 0;
}

7.堆#

  • 定义:用一维数组存储小根堆(完全二叉树),对于节点x,它的左儿子是2x,右儿子是2x+1

  • 支持的操作

    1. 插入一个数O(logn)heap[++ size] = x; up(size);
    2. 求集合当中的最小值O(1)heap[1];
    3. 删除最小值O(logn) heap[1] = heap[size]; size --; down(1);
    4. 删除下标为k的元素O(logn)heap[k] = heap[size]; size --; down(k); up(k);(优先队列无法实现,想要修改某个元素只能向堆里插入这个元素)
    5. 修改下标为k的元素O(logn)heap[k] = x; down(k); up(k);(优先队列无法实现,想要修改某个元素只能向堆里插入这个元素)
  • up函数

    void up(int u)
    {
        while (u / 2 && h[u / 2] > h[u]) //有父节点并且当前点比父节点更小
        {
            swap(h[u / 2], h[u]);
            u /= 2;
        }
    }
    
  • down函数

    void down(int u)
    {
        int t = u; //t为三个数中最小的数的编号
        if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2; //如果左儿子存在并且左儿子比当前点小
        if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1; //如果右儿子存在并且右儿子比当前点小
        if (u != t) //根节点不是三个数中最小的
        {
            swap(h[t], h[u]);
            down(t);
        }
    }
    

1. 堆排序#

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m;
int h[N], cnt;

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)
    {
        swap(h[u], h[t]);
        down(t);
    }
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &h[i]);
    cnt = n;

    for (int i = n / 2; i; i -- ) down(i); 
    //O(n)

    while (m -- )
    {
        printf("%d ", h[1]);
        h[1] = h[cnt -- ];
        down(1);
    }

    puts("");

    return 0;
}

2. 模拟堆#

#include <iostream>
#include <algorithm>
#include <string.h>

using namespace std;

const int N = 100010;

int h[N], ph[N], hp[N], cnt;

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;
    scanf("%d", &n);
    while (n -- )
    {
        char op[5];
        int k, x;
        scanf("%s", op);
        if (!strcmp(op, "I"))
        {
            scanf("%d", &x);
            cnt ++ ;
            m ++ ;
            ph[m] = cnt, hp[cnt] = m;
            h[cnt] = x;
            up(cnt);
        }
        else if (!strcmp(op, "PM")) printf("%d\n", h[1]);
        else if (!strcmp(op, "DM"))
        {
            heap_swap(1, cnt);
            cnt -- ;
            down(1);
        }
        else if (!strcmp(op, "D"))
        {
            scanf("%d", &k);
            k = ph[k];
            heap_swap(k, cnt);
            cnt -- ;
            up(k);
            down(k);
        }
        else
        {
            scanf("%d%d", &k, &x);
            k = ph[k];
            h[k] = x;
            up(k);
            down(k);
        }
    }

    return 0;
}

8.哈希表#

1. 拉链法#

#include <cstring>
#include <iostream>

using namespace std;

const int N = 100003;//质数可以降低冲突概率

int h[N], e[N], ne[N], idx;

void insert(int x)
{
    int k = (x % N + N) % N;//哈希函数
    //头插法插入一个数,把h[k]看成head头指针,
    //h[k]=idx++的原因是由于头插法导致头指针在不断改变.
    //从后往前赋值
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx ++ ;
}

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

    return false;
}

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

    memset(h, -1, sizeof h);

    while (n -- )
    {
        char op[2];//即使读入1个字符也应该用字符串存储,因为scanf读入字符串时自动忽略空格换行制表符等
        int x;
        scanf("%s%d", op, &x);

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

    return 0;
}

2. 开放寻址法#

#include <cstring>
#include <iostream>

using namespace std;

const int N = 200003, null = 0x3f3f3f3f;//一个大于10的九次方的数

int h[N];

//如果x在哈希表中已经存在,返回x的位置;
//如果x在哈希表中不存在,返回x应该存储的位置
int find(int x)
{
    int k = (x % N + N) % N;
    while (h[k] != null && h[k] != x)
    {
        k ++ ;
        if (k == N) k = 0;
    }
    return k;
}

int main()
{
    memset(h, 0x3f, sizeof h);//按字节将所有位置初始化为空

    int n;
    scanf("%d", &n);

    while (n -- )
    {
        char op[2];
        int x;
        scanf("%s%d", op, &x);
        if (*op == 'I') h[find(x)] = x;
        else
        {
            if (h[find(x)] == null) puts("No");
            else puts("Yes");
        }
    }

    return 0;
}

3. 字符串哈希#

//字符串前缀哈希法
#include <iostream>
#include <algorithm>

using namespace std;

typedef unsigned long long ULL;
//用ULL存储会溢出等价于模上2 ^ 64

const int N = 100010, 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];//[l, r]区间内字符串的哈希值
}

//哈希方式:把每一个字符串看成一个P进制的数然后模Q,实现了把任何一个字符串映射到从0到Q - 1之间的数
//注意不能映射为0,并且假设不会出现冲突情况(P = 131或13331, Q = 2 ^ 64)
int main()
{
    scanf("%d%d", &n, &m);
    scanf("%s", str + 1);

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

    while (m -- )
    {
        int l1, r1, l2, r2;
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);

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

    return 0;
}

高级数据结构

1. 树状数组#

  • 支持的操作:
    1. 在某个位置加上一个数(单点修改) O(logn)
    2. 求某一个前缀和(区间查询) O(logn)
  1. 初始化

        for (int i = 1; i <= n; i ++ ) add(i, a[i]); //初始化树状数组
    
  2. 单点修改

    void add(int x, int v) //在树状数组的x位置上加上v
    {
        for (int i = x; i <= n; i += lowbit(i))
        {
            tr[i] += v;
        }
    }
    
  3. 区间查询

    int query(int x) //查询x位置的前缀和
    {
        int res = 0;
        for (int i = x; i > 0; i -= lowbit(i))
        {
            res += tr[i];
        }
        return res;
    }
    
  4. lowbit操作

    int lowbit(int x) //返回二进制的x中的最后一位一,返回值为十进制
    {
        return x & (-x);
    }
    

2. 线段树#

  1. 存储

    struct Node
    {
        int l, r;
        int sum;
    }tr[N * 4];
    
  2. 用子节点信息更新当前节点信息

    void pushup(int u)
    {
        tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
    }
    
  3. 在一段区间上初始化线段树

    void build(int u, int l, int r)
    {
        if (l == r) tr[u] = {l, r, w[r]};
        else
        {
            tr[u] = {l, r};
            int mid = l + r >> 1;
            build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
            pushup(u);
        }
    }
    
  4. 查询

    int query(int u, int l, int r)
    {
        if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
        int mid = tr[u].l + tr[u].r >> 1;
        int sum = 0;
        if (l <= mid) sum += query(u << 1, l, r);
        if (r > mid) sum += query(u << 1 | 1, l, r);
        return sum;
    }
    
  5. 修改

    void modify(int u, int x, int v)
    {
        if (tr[u].l == tr[u].r) tr[u].sum += v;
        else
        {
            int mid = tr[u].l + tr[u].r >> 1;
            if (x <= mid) modify(u << 1, x, v);
            else modify(u << 1 | 1, x, v);
            pushup(u);
        }
    }
    

STL

1.vector#

DFS

  1. 指数型枚举
#include <iostream>

using namespace std;

const int N = 16;

int n;
int st[N];  // 状态,记录每个位置当前的状态:0表示还没考虑,1表示选它,2表示不选它

void dfs(int u)
{
    if (u > n)
    {
        for (int i = 1; i <= n; i ++ )
        {
            if (st[i] == 1)
            {
                cout << i << ' ';
            }
        }
        puts("");
        return;
    }

    st[u] = 2;
    dfs(u + 1);     // 第一个分支:不选
    st[u] = 0;  // 恢复现场

    st[u] = 1;
    dfs(u + 1);     // 第二个分支:选
    st[u] = 0;
}

int main()
{
    cin >> n;

    dfs(1);

    return 0;
}
  1. 排列型枚举
#include <iostream>

using namespace std;

const int N = 10;

int n;
int st[N];   // 0 表示还没放数,1~n表示放了哪个数
bool used[N];   // true表示用过,false表示还未用过

void dfs(int u)
{
    if (u > n)  // 边界
    {
        //枚举n个位置
        for (int i = 1; i <= n; i ++ ) cout << st[i] << ' '; // 打印方案
        puts("");

        return;
    }

    // 依次枚举每个分支,即当前位置可以填哪些数
    for (int i = 1; i <= n; i ++ ) //枚举1~n的所有数
    {
        if (!used[i])
        {
            st[u] = i;
            used[i] = true;
            dfs(u + 1);

            // 恢复现场
            st[u] = 0;
            used[i] = false;
        }
    }
}

int main()
{
    cin >> n;

    dfs(1);

    return 0;
}
  1. 组合型枚举
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 30;

int n, m;
int way[N];
//为了避免重复,保持升序选择,只需保证每次新加的数大于前面的数
void dfs(int u, int start)
{
    if (u + n - start < m) return;  // 剪枝
    if (u == m + 1)
    {
        for (int i = 1; i <= m; i ++ ) printf("%d ", way[i]);
        puts("");
        return;
    }

    for (int i = start; i <= n; i ++ )
    {
        way[u] = i;
        dfs(u + 1, i + 1);
        way[u] = 0; // 恢复现场
    }
}

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

    dfs(1, 1);

    return 0;
}

BFS

  • 例题
    1. AcWing 844. 走迷宫
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 110;

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

int bfs()
{
    queue<PII> q;

    memset(d, -1, sizeof d);
    d[0][0] = 0;
    q.push({0, 0});

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

    while (q.size())
    {
        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;
}

或者

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 110;

typedef pair<int, int> PII;

int n, m;
PII q[N * N];
int g[N][N];
int d[N][N];

int bfs()
{
    int hh = 0, tt = -1;
    q[ ++ tt] = {0, 0};
    memset(d, -1, sizeof d);
    d[0][0] = 0;
    
    int dx[4] = {-1, 0, 1, 0};
    int dy[4] = {0, 1, -0, -1};
    
    while (hh <= tt)
    {
        auto t = q[hh ++];  //取出队头, 并弹出队头
        
        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[ ++ tt] = {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;
}

图论

  • 无向图是有两个方向的有向图,树是无环有向连通图

1. 图的存储#

  • 稀疏图用邻接表
  • 稠密图用邻接矩阵(g[a][b]ab)
(邻接表)
#include <iostream>

using namespace std;

int h[N], e[N], ne[N], idx;

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

int main()
{
    memset(h, -1, sizeof h);
    return 0;
}

2. 图的深度优先遍历#

  • 模板

    bool st[N];
    int h[N], e[N], ne[N], idx;
    void dfs(int u)  //u表示正在搜的点的编号
    {
        st[u] = true; //表示点u已经被搜过了
        for (int i = h[u]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (!st[j]) dfs(j);
        }
    }
    
  • 例题

    1.AcWing 846. 树的重心

    #include <iostream>
    #include <cstring>
    #include <algorithm>
    
    using namespace std;
    
    const int N = 100010;
    
    int n;
    int ans = N;
    int h[N], e[N * 2], ne[N * 2], idx;
    bool st[N];
    
    void add(int a, int b)
    {
        e[idx] = b;
        ne[idx] = h[a];
        h[a] = idx ++;
    }
    
    
    //返回以u为根的子树中点的数量
    int dfs(int u)
    {
        st[u] = true;
        int sum = 1, res = 0;
        for (int i = h[u]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (!st[j])
            {
                int s = dfs(j);
                res = max(res, s);
                sum += s;
            }
        }
        res = max(res, n - sum);
        ans = min(ans, res);
        return sum;
    }
    
    int main()
    {
        cin >> n;
        memset(h, -1, sizeof h);
        
        for (int i = 0; i < n - 1; i ++ )
        {
            int a, b;
            cin >> a >> b;
            add(a, b), add(b, a);
        }
        
        dfs(1);
        
        cout << ans << endl;
        return 0;
    }
    

3. 图的广度优先遍历#

  • 例题

    1. AcWing 847. 图中点的层次
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    #include <queue>
    
    using namespace std;
    
    const int N = 100010;
    
    int n, m;
    int h[N], e[N], ne[N], idx;
    int d[N];
    
    void add(int a, int b)
    {
        e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
    }
    
    int bfs()
    {
        memset(d, -1, sizeof d);
    
        queue<int> q;
        d[1] = 0;
        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 (d[j] == -1)
                {
                    d[j] = d[t] + 1;
                    q.push(j);
                }
            }
        }
    
        return d[n];
    }
    
    int main()
    {
        scanf("%d%d", &n, &m);
        memset(h, -1, sizeof h);
    
        for (int i = 0; i < m; i ++ )
        {
            int a, b;
            scanf("%d%d", &a, &b);
            add(a, b);
        }
    
        cout << bfs() << endl;
    
        return 0;
    }
    

4. 有向图的拓扑序列#

  • 有向无环图一定存在拓扑序列,被称为拓扑图

  • 一个有向无环图一定至少存在一个入度为0的点

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int e[N], ne[N], idx;//邻接表存储图
int h[N];
int q[N], hh = 0, tt = -1;//队列保存入度为0的点,也就是能够输出的点,
int n, m;//保存图的点数和边数
int d[N];////保存各个点的入度

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

void topsort(){
    for(int i = 1; i <= n; i++){//遍历一遍顶点的入度。
        if(d[i] == 0)//如果入度为 0, 则可以入队列
            q[++tt] = i;
    }
    while(tt >= hh){//循环处理队列中点的
        int a = q[hh++];
        for(int i = h[a]; i != -1; i = ne[i]){//循环删除 a 发出的边
            int b = e[i];//a 有一条边指向b
            d[b]--;//删除边后,b的入度减1
            if(d[b] == 0)//如果b的入度减为 0,则 b 可以输出,入队列
                q[++tt] = b;
        }
    }
    if(tt == n - 1){//如果队列中的点的个数与图中点的个数相同,则可以进行拓扑排序
        for(int i = 0; i < n; i++){//队列中保存了所有入度为0的点,依次输出
            cout << q[i] << " ";
        }
    }
    else//如果队列中的点的个数与图中点的个数不相同,则不可以进行拓扑排序
        cout << -1;//输出-1,代表错误
}


int main(){
    cin >> n >> m;//保存点的个数和边的个数
    memset(h, -1, sizeof h);//初始化邻接矩阵
    while (m -- ){//依次读入边
        int a, b;
        cin >> a >> b;
        d[b]++;//顶点b的入度+1
        add(a, b);//添加到邻接矩阵
    }
    topsort();//进行拓扑排序
    return 0;
}

5. 最短路#

1. 单源最短路#

1. 所有边权都是正数#

  1. 朴素Dijkstra算法O(n2)(稠密图)

    //注意存在重边和自环,但是因为所有边权都是正数,
    //所以自环一定不会出现在最短路之中,重边只需要保留一条最短的边
    
    #include <iostream>
    #include <algorithm>
    #include <cstring>
    
    using namespace std;
    
    const int N = 510;
    
    int n, m;
    int g[N][N]; //稠密图
    int dist[N]; //表示从一号点走到当前点的最短距离
    bool st[N]; //表示某个点的最短路是否已经确定
    
    int dijkstra()
    {
        memset(dist, 0x3f, sizeof dist);
        dist[1] = 0;
        
        //每迭代一次就会确定一个点的最短路,迭代n次后就会找到所有点的最短路
        for (int i = 0; i < n; i ++ )
        {
            //找到所有还未确定最短路的点中的到起点距离最小的点
            int t = -1;
            for (int j = 1; j <= n; j ++ ) //枚举所有点
                if (!st[j] && (t == -1 || dist[t] > dist[j]))
                    t = j;
            
            if (t == n) break;
            
            //点t已经被确定
            st[t] = true;
            
            //用t更新其他点
            for (int j = 1; j <= n; j ++ ) //枚举所有点
                dist[j] = min(dist[j], dist[t] + g[t][j]);
        }
        
        if (dist[n] == 0x3f3f3f3f) return -1; //1和n不连通,不存在最短路
        
        return dist[n];
    }
    
    
    int main()
    {
        cin >> n >> m;
        
        memset(g, 0x3f, sizeof g);
        
        while (m -- )
        {
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            g[a][b] = min(g[a][b], c);//重边只需要保留一条最短的边
        }
        
        cout << dijkstra() << endl;
        
        return 0;
    }
    
  2. 堆优化Dijkstra算法O(mlogn)(稀疏图)

    • 用堆优化了每一次迭代时找最小距离的点的操作,将这一步从O(n)优化为O(1)
    #include <iostream>
    #include <algorithm>
    #include <cstring>
    #include <queue>
    
    using namespace std;
    
    typedef pair<int, int> PII; //<距离,编号>
    
    const int N = 1000010;
    
    int n, m;
    int h[N], e[N], w[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<PII, vector<PII>, greater<PII>> heap; //小根堆
        heap.push({0, 1}); //距离是0,编号为1
        
        while (heap.size())
        {
            auto t = heap.top(); //找到最小
            heap.pop();
            
            int ver = t.second, distance = t.first;
            
            if (st[ver]) continue; //当前这个点是冗余备份,没必要再操作
            
            st[ver] = true;
            
            //用t更新其他点
            for (int i = h[ver]; i != -1; i = ne[i]) //枚举t的下一层
            {
                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()
    {
        cin >> 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;
    }
    

2. 存在负权边#

  1. bellman-fordO(nm)

    //如果在1~n的路径中存在负权回路,则不存在最短路
    #include <iostream>
    #include <algorithm>
    #include <cstring>
    
    using namespace std;
    
    const int N = 510, M = 10010;
    
    int n, m, k;
    int dist[N], backup[N];
    
    struct edge
    {
        int a, b, w;
    }edges[M];
    
    int bellman_ford()
    {
        memset(dist, 0x3f, sizeof dist);
        dist[1] = 0;
        
        for (int i = 0; i < k; i ++ )
        {
            memcpy(backup, dist, sizeof dist); //防止串联
            for (int j = 0; j < m; j ++ )
            {
                int a = edges[j].a, b = edges[j].b, w = edges[j].w;
                dist[b] = min(dist[b], backup[a] + w);
            }
        }
        
        if (dist[n] > 0x3f3f3f3f / 2) return -0x3f3f3f3f;
        return dist[n];
    }
    
    
    int main()
    {
        cin >> n >> m >> k;
        
        for (int i = 0; i < m; i ++ )
        {
            int a, b, w;
            scanf("%d%d%d", &a, &b, &w);
            edges[i] = {a, b, w};
        }
        
        int t = bellman_ford();
        if (t == -0x3f3f3f3f) puts("impossible");
        else cout << t << endl;
        
        return 0;
    }
    
  2. spfa 一般O(m),最坏O(nm)

    #include <iostream>
    #include <algorithm>
    #include <cstring>
    #include <queue>
    
    using namespace std;
    
    typedef pair<int, int> PII; //<距离,编号>
    
    const int N = 1000010;
    
    int n, m;
    int h[N], e[N], w[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 spfa()
    {
        memset(dist, 0x3f, sizeof dist);
        dist[1] = 0;
        
        queue<int> q;
        q.push(1);
        st[1] = true;
        
        while (q.size())
        {
            int 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];
                    if (!st[j])
                    {
                        q.push(j);
                        st[j] = true;
                        
                    }
                }
            }
        }
        
        if (dist[n] == 0x3f3f3f3f) return -0x3f3f3f3f;
        return dist[n];
    }
    
    int main()
    {
        cin >> 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 t = spfa();
        
        if (t == -0x3f3f3f3f) puts("impossible");
        else cout << t << endl;
        
        return 0;
    }
    
  3. spfa判断负环

    #include <iostream>
    #include <algorithm>
    #include <cstring>
    #include <queue>
    
    using namespace std;
    
    typedef pair<int, int> PII; //<距离,编号>
    
    const int N = 1000010;
    
    int n, m;
    int h[N], e[N], w[N], ne[N], idx;
    int dist[N], cnt[N]; //cnt[]表示当前点所对应的最短路径的有多少条边
    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 spfa()
    {
        queue<int> q;
        
        //初始把所有点放到队列里
        for (int i = 1; i <= n; i ++ )
        {
            q.push(i);
            st[i] = true;
        }
        
        while (q.size())
        {
            int 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;
                    
                    //边数>=n时说明有n + 1个点说明有负环
                    if (cnt[j] >= n) return true;
                    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;
            scanf("%d%d%d", &a, &b, &c);
            add(a, b, c);
        }
        
        if (spfa()) puts("Yes");
        else puts("No");
        
        return 0;
    }
    

2. 多源汇最短路#

floyd O(n3)

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 210, INF = 1e9;

int n, m, q;
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 >> 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, w;
        scanf("%d%d%d", &a, &b, &w);
        
        d[a][b] = min(d[a][b], w); //重边
    }
    
    floyd();
    
    while (q -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        if (d[a][b] > INF / 2) puts("impossible");
        else printf("%d\n", d[a][b]);
    }
    
    return 0;
}

6.最小生成树#

  1. 朴素版prim O(n2)(稠密图)

    #include <iostream>
    #include <algorithm>
    #include <cstring>
    
    using namespace std;
    
    const int N = 510, INF = 0x3f3f3f3f;
    
    int n, m;
    int g[N][N];
    int dist[N];
    bool st[N];
    
    int prim()
    {
        memset(dist, 0x3f, sizeof dist);
        
        int res = 0;
        for (int i = 0; i < n; i ++ )
        {
            //找到集合外到当前集合距离最近的点
            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];
            
            //用t更新其他点到集合的距离
            for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
    
            st[t] = true;
        }
        return res;
    }
    
    int main()
    {
        cin >> n >> m;
        memset(g, 0x3f, sizeof g);
        while (m -- )
        {
            int a, b, w;
            scanf("%d%d%d", &a, &b, &w);
            g[a][b] = g[b][a] = min(g[a][b], w); //重边
        }
        
        int t = prim();
        
        if (t == INF) puts("impossible");
        else cout << t << endl;
        
        return 0;
    }
    
  2. 堆优化版的prim O(mlogn)(稀疏图)

    
    
  3. kruskal O(mlogm)

    #include <iostream>
    #include <algorithm>
    #include <cstring>
    
    using namespace std;
    
    const int N = 200010;
    
    int n, m;
    int p[N];
    
    struct Edge
    {
        int a, b, w;
        
        bool operator < (const Edge &t) const
        {
            return  w < t.w;
        }
    }edges[N];
    
    int find(int x) //返回x的祖宗节点
    {
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    
    int main()
    {
        cin >> n >> m;
        for (int i = 0; i < m; i ++ )
        {
            int a, b, w;
            scanf("%d%d%d", &a, &b, &w);
            edges[i] = {a, b, w};
        }
        
        //把所有边排序
        sort(edges, edges + m);
        
        //初始化并查集
        for (int i = 1; i <= n; i ++ ) p[i] = i;
        
        //从小到大枚举所有边
        int res = 0, cnt = 0;
        for (int i = 0; i < m; i ++ )
        {
            int a = edges[i].a, b = edges[i].b, w = edges[i].w;
            if (find(a) != find(b)) //如果不连通
            {
                p[find(a)] = find(b); //将这条边加入集合中
                res += w;
                cnt ++ ;
            }
        }
        
        if (cnt < n - 1) puts("impossible");
        else cout << res << endl;
        
        return 0;
    }
    

7. 二分图#

1.染色法判定二分图 O(n+m)

  • 性质:一个图是二分图当且仅当它不含奇数环
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 100010, M = 200010;

int n, m;
int h[N], e[M], ne[M], idx;
int color[N]; //0表示未染色,1表示1号色,2表示2号色

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

bool dfs(int u, int c) //u号点,颜色为c。返回false表示染色发生矛盾
{
    color[u] = c;
    
    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!color[j]) //如果当前点没染过颜色,就染成与u号点不同的颜色
        {
            //3 - c 表示 1变2,2变1
            if (!dfs(j, 3 - c)) return false; //染色发生矛盾
        }
        //如果当前点已经染过颜色,直接判断它的颜色与u号点的颜色是否相同
        else if (color[j] == c) return false;
    }
    
    return true;
}

int main()
{
    cin >> n >> m;
    
    memset(h, -1, sizeof h);
    
    while (m -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a); //无向图
    }
    
    bool flag = true;
    for (int i = 1; i <= n; i ++ )
        if (!color[i]) //如果没染色就染成1号色
        {
            if (!dfs(i, 1)) //如果染色发生矛盾
            {
                flag = false;
                break;
            }
        }
    
    if (flag) puts("Yes");
    else puts("No");
    
    return 0;
}

2.匈牙利算法O(mn)

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 510, M = 100010;

int n1, n2, m;
int h[N], e[M], ne[M], idx;
int match[N]; //女生匹配的男生是谁, 是0表示没有匹配
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]) //枚举男生x喜欢的女生
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true;
            //这个女生没有匹配或者她匹配的男生能找到下家
            if (match[j] == 0 || find(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;
        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 ++ ;
    }
    
    cout << res << endl;
    
    return 0;
}

数学知识

1. 质数#

  • 质数的定义:在大于1的整数中,如果只包含1和本身这两个约数,就被称为质数,或者叫素数。
  • 质因数的定义:能整除给定正整数的质数。
  1. 质数的判定

    //试除法 O(sqrt(n))
    //因为 如果d | n,则n/d | n,所以只需枚举数值小的,即d < d/n
    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;
    }
    
  2. 分解质因数

    //试除法 O(sqrt(n))
    void divide(int x)
    {
        for (int i = 2; i <= x / i; i ++ )
        {
            //n不包含任何从2到i-1之间的质因子(已经被除干净了),所以i也不包含何从2到i-1之间的质因子
            if (x % i == 0) //i一定是质数
            {
                int s = 0;
                while (x % i == 0)
                {
                    x /= i;
                    s ++ ;
                }
                cout << i << ' ' << s << endl;
            }
        }
        if (x > 1) cout << x << ' ' << 1 << endl;//n中最多只包含一个大于sqrt(n)的质因子
        puts("");
    }
    
  3. 筛质数

    • 埃氏筛法

      int primes[N], cnt;
      
      bool st[N];
      
      void get_primes(int n)
      {
          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;
          }
      }
      
    • 线性筛法

      //每个数都会被它的最小质因子筛掉,并且每个数只有一个最小质因子,所以只会被筛一次,所以是线性的
      //任何一个合数一定会被筛掉。因为对于一个合数x,假设primes[j]是x的最小质因子,当i枚举到x/primes[j]时,x会被筛掉
      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; //prime[j]一定是primes[j] * i的最小质因子。
      //当i % primes[j] == 0时, primes[j]一定是i的最小质因子, 也一定是primes[j] * i的最小质因子。
      //当i % primes[j] != 0时, primes[j]一定小于i的所有质因子, 也一定是primes[j] * i的最小质因子
                  if (i % primes[j] == 0) break;
              }
          }
      }
      

2. 约数#

  1. 试除法求一个数的所有约数

    // O(sqrt(n))
    vector<int> get_divisors(int n)
    {
        vector<int> res;
        
        for (int i = 1; i <= n / i; i ++ )
        {
            if (n % i == 0)
            {
                res.push_back(i);
                if (i != n / i) res.push_back(n / i);
            }
        }
        sort(res.begin(), res.end());
        return res;
    }
    
  2. 约数个数

    #include <iostream>
    #include <algorithm>
    #include <cstring>
    #include <unordered_map>
    
    using namespace std;
    
    typedef long long LL;
    
    const int mod = 1e9 + 7;
    
    int main()
    {
        int n;
        cin >> n;
        
        unordered_map<int, int> 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 prime : primes) res = res * (prime.second + 1) % mod;
        
        cout << res << endl;
        
        return 0;
    }
    
  3. 约数之和

    #include <iostream>
    #include <algorithm>
    #include <cstring>
    #include <unordered_map>
    
    using namespace std;
    
    typedef long long LL;
    
    const int mod = 1e9 + 7;
    
    int main()
    {
        int n;
        cin >> n;
        
        unordered_map<int, int> 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 prime : primes)
        {
            int p = prime.first, a = prime.second;
            LL t = 1;
            while (a -- ) t = (t * p + 1) % mod;
            res = res * t % mod;
        }
        
        cout << res << endl;
        
        return 0;
    }
    
  4. 求最大公约数(欧几里得算法,也叫辗转相除法)

    //b不是0的时候a和b的最大公约数为gcd(b, a % b)
    //b是0的时候a和b的最大公约数是a
    int gcd(int a, int b)
    {
        return b ? gcd(b, a % b) : a;
    }
    

3. 欧拉函数#

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int a;
        cin >> a;
        
        int res = a;
        for (int i = 2; i <= a / i; i ++ )
            if (a % i == 0)
            {
                res = res / i * (i - 1);
                while (a % i == 0) a /= i;
            }
            
        if (a > 1) res = res / a * (a - 1);
        
        cout << res << endl;
    }
    
    return 0;
}
//筛法求欧拉函数
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

typedef long long LL;

const int N = 1000010;

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

LL get_eulers(int n)
{
    phi[1] = 1;
    for (int i = 2; i <= n; i ++ )
    {
        if (!st[i])
        {
            primes[cnt ++ ] = 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] = phi[i] * primes[j];
                break;
            }
            phi[primes[j] * i] = phi[i] * (primes[j] - 1);
        }
    }
    
    LL res = 0;
    for (int i = 1; i <= n; i ++ ) res += phi[i];
    return res;
}

int main()
{
    int n;
    cin >> n;
    cout << get_eulers(n) << endl;
    return 0;
}

4. 快速幂#

  • akmodp
  • (a * a) % p = (a % p) * (b % p) % p
LL qmi(int a, int k, int p)
{
    LL res = 1 % p; //k = 0, p = 1时输出结果应该为0
    while (k)
    {
        if (k & 1) res = res * a % p;
        k >>= 1;
        a = (LL)a * a % p;
    }
    return res;
}

5. 扩展欧几里得算法#

6. 中国剩余定理#

7. 高斯消元#

8. 求组和数#

  1. 105,1ba2000

    递推

    #include <iostream>
    #include <algorithm>
    
    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();
    
        scanf("%d", &n);
    
        while (n -- )
        {
            int a, b;
            scanf("%d%d", &a, &b);
    
            printf("%d\n", c[a][b]);
        }
    
        return 0;
    }
    
  2. 104,1ba105

9. 容斥原理#

10. 博弈论#

DP

1. 背包问题#

1. 01背包问题#

  • 状态表示:

    • 集合:f(i, j)表示只考虑前i个物品,且总体积不大于j的所有选法。
    • 属性:最大价值
  • 集合划分:第i个物品选或者不选(第i个物品选0个和1个)

    不选第i个物品:f[i - 1, j]

    选第i个物品:f[i - 1, j - v[i]] + w[i] (去掉第i个物品之后不影响最大值)

//朴素做法
#include <iostream>
#include <algorithm>

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 - 1][j - v[i]] + w[i]);
        }

    cout << f[n][m] << endl;

    return 0;
}
/*
滚动数组优化关键在于看是否已经循环过,如果已经循环过,则代表已经被更新,所以是第i层;没更新过的就是第i - 1层

对于方程f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]) 只需要保存第i - 1层的结果,由于j - v[i] < j,所以如果从小到大循环,则f[j - v[i]]在第i层已经被更新过,保存的结果并不是第i - 1层,所以采取从大到小循环体积
*/
#include <iostream>
#include <algorithm>

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 = m; j >= v[i]; j -- )
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m] << endl;

    return 0;
}

2. 完全背包问题#

  • 状态表示:

    • 集合:f(i, j)表示只考虑前i个物品,且总体积不大于j的所有选法。
    • 属性:最大价值
  • 集合划分:第i个物品选多少个。0个,1个,2个,3个,4个,... ,k个,用k枚举(装k个第i个物品不超过背包体积)

//朴素做法(TLE)
#include <iostream>
#include <algorithm>

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 ++ )
            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 << f[n][m] << endl;

    return 0;
}
/*
对方程进行变形优化时间
f[i, j] = max(f[i - 1, j], f[i - 1, j - v] + w, f[i - 1, j - 2v] + 2w, f[i - 1, j - 3v] + 3w + ...)

f[i, j - v] = max(f[i - 1, j - v],f[i - 1, j - 2v] + w, f[i - 1, j - 3v] + 2w + ...)

所以 f[i, j] = max(f[i - 1, j], f[i, j - v] + w)
*/

#include <iostream>
#include <algorithm>

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 << f[n][m] << endl;

    return 0;
}
/*
优化空间
f[i][j] = max(f[i - 1][j], f[i][j - v] + w)中j - v < j,从小到大循环体积,所以f[i][j - v] 先被更新,代表第i层结果
*/
#include <iostream>
#include <algorithm>

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 ++ )
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m] << endl;

    return 0;
}

3. 多重背包问题#

  • 状态表示:
    • 集合:f(i, j)表示只考虑前i个物品,且总体积不大于j的所有选法。
    • 属性:最大价值
  • 集合划分:第i个物品选多少个。0个,1个,2个,3个,4个,... ,s[i]个,用k枚举(装k个第i个物品不超过背包体积)
//朴素做法
#include <iostream>

using namespace std;

const int N = 1010;

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 ++ )
        {
            for (int k = 0; k <= s[i] && k * v[i] <= j; k ++ )
            {
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
            }
        }
    }
    
    cout << f[n][m] << endl;
    return 0;
}
//空间优化
#include <iostream>

using namespace std;

const int N = 110;

int n, m;
int f[N], v[N], w[N], s[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 = m; j >= 0; j -- )
            for (int k = 0; k <= s[i] && j >= k * v[i]; k ++ )
                f[j] = max(f[j], f[j - k * v[i]] + w[i] * k);
    cout << f[m] << endl;
    return 0;
}
/*
0<N≤1000
0<V≤2000
0<vi,wi,si≤2000
*/

/*
无法对方程进行类似完全背包问题的时间优化,所以进行二进制优化,实现了将时间复杂度从NVS优化为NVlogs

优化过程:
将每一层i中的s个物品打包为1, 2, 4, 8, 16, 32, 64, ...
含义是第i个物品选1个, 第i个物品选2个, 第4个物品选8个...
对打包后的物品进行01选择可以拼凑出所有的s个物品
例如:
s = 1023
打包为1, 2, 4, 8, ..., 512
s = 200
打包为1, 2, 4, 8, 16, 32, 64, 73
*/
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 12010, M = 2010;

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

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

    int cnt = 0; //记录产生的新的物品的编号
    for (int i = 1; i <= n; i ++ )
    {
        int a, b, s;
        cin >> a >> b >> s;
        //每次将k个第i个物品打包在一起
        int k = 1;
        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;
	
    //转化为01背包问题
    for (int i = 1; i <= n; i ++ )
        for (int j = m; j >= v[i]; j -- )
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m] << endl;

    return 0;
}
/*
0<N≤1000
0<V≤20000
0<vi,wi,si≤20000
*/

/*
进行单调队列优化

*/

4. 混合背包问题#

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 1010;

int n, m;
int f[N];

struct Thing
{
    int kind;
    int v, w;
};

vector<Thing> things;

int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++ )
    {
        int v, w, s;
        cin >> v >> w >> s;
        if (s < 0) things.push_back({-1, v, w}); //01背包
        else if (s == 0) things.push_back({0, v, w}); //完全背包
        else //多重背包问题的二进制优化转化为01背包
        {
            for (int k = 1; k <= s; k ++ )
            {
                s -= k;
                things.push_back({-1, v * k, w * k});
            }
            if (s > 0) things.push_back({-1, v * s, w * s});
        }
    }
    
    for (auto thing : things)
    {
        if (thing.kind < 0) //01背包,从大到小循环
        {
            for (int j = m; j >= thing.v; j -- )
                f[j] = max(f[j], f[j - thing.v] + thing.w);
        }
        else //完全背包,从小到大循环
        {
            for (int j = thing.v; j <= m; j ++ )
                f[j] = max(f[j], f[j - thing.v] + thing.w);
        }
    }
    
    cout << f[m] << endl;
    
    return 0;
}

5. 二维费用的背包问题#

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 110;

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

int main()
{
    cin >> n >> v >> m;
    
    for (int i = 0; i < n; i ++ )
    {
        int a, b, c;
        cin >> a >> b >> c; //v[i], m[i], w[i]
        for (int j = v; j >= a; j -- )
            for (int k = m; k >= b; k -- )
                f[j][k] = max (f[j][k], f[j - a][k - b] + c);
    }
    
    cout << f[v][m] << endl;
    
    return 0;
}

6. 分组背包问题#

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 110;

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

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

7. 背包问题求方案数#

#include <iostream>
#include <algorithm>

using namespace std;

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

int n, m;
int f[N], g[N]; //f[j]表示体积为j的最大价值,g[j]表示体积为j的方案数

int main()
{
    cin >> n >> m;
    
    g[0] = 1; //体积为0的方案数为1
    for (int i = 1; i <= m; i ++ ) f[i] = -INF; //体积为0的最大价值不存在
    
    for (int i = 0; i < n; i ++ )
    {
        int v, w;
        cin >> v >> w;
        for (int j = m; j >= v; j -- )
        {
            int t = max(f[j], f[j - v] + w); //记录最优解
            int s = 0;
            //找到由谁转移而来,加上其方案数
            if (t == f[j]) s += g[j];
            if (t == f[j - v] + w) s += g[j - v];
            if (s >= mod) s -= mod;
            f[j] = t;
            g[j] = s;
        }
    }
    
    //找到最优解
    int maxw = 0;
    for (int i = 0; i <= m; i ++ ) maxw = max(maxw, f[i]);
    
    //求方案数
    int res = 0;
    for (int i = 0; i <= m; i ++ )
    {
        if (f[i] == maxw)
        {
            res += g[i];
            if (res >= mod) res -= mod;
        }
    }
    
    cout << res << endl;
    
    return 0;
}

8. 背包问题求具体方案#

#include <iostream>

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 = n; i >= 1; 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 + 1][j - v[i]] + w[i]);
        }

    int j = m;
    for (int i = 1; i <= n; i ++ )
        if (j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i])
        {
            cout << i << ' ';
            j -= v[i];
        }

    return 0;
}

9. 有依赖的背包问题#

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 110;

int n, m;
int h[N], e[N], ne[N], idx;
int f[N][N]; //f[u, j]表示在以u为根的子树中选,总体积不超过j的最大价值
int v[N], w[N];

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

void dfs(int u)
{
    for (int i = h[u]; ~i; i = ne[i]) //循环物品组
    {
        int son = e[i];
        dfs(son);
        
        //分组背包
        for (int j = m - v[u]; j >= 0; j -- ) //循环体积,因为一定要选u,所以要给u留空间,从m - v[u]开始循环
            for (int k = 0; k <= j; k ++ ) //循环决策(为了减少枚举次数,所以对每一棵子树按照体积分类,而不按照方案数分类)
                f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
    }
    
    //将物品u加进去
    for (int i = m; i >= v[u]; i -- ) f[u][i] = f[u][i - v[u]] + w[u];
    for (int i = 0; i < v[u]; i ++ ) f[u][i] = 0;
}

int main()
{
    cin >> n >> m;
    
    memset(h, -1, sizeof h);
    
    int root;
    for (int i = 1; i <= n; i ++ )
    {
        int p;
        cin >> v[i] >> w[i] >> p;
        if (p == -1) root = i;
        else add(p, i);
    }
    
    dfs(root);
    
    cout << f[root][m] << endl;
    
    return 0;
}

2. 线性dp#

1. 数字三角形#

/*
DP
状态表示:
    集合:所有从起点走到(i, j)的路径
    属性:max
状态计算:
    集合划分:1. 来自左上:f[i - 1][j - 1] + a[i][j]
              2. 来自右上:f[i - 1][j] + a[i][j]
    转移方程:f[i][j] = max (f[i - 1][j - 1] + a[i][j], f[i - 1][j] + a[i][j])
*/
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 510, INF = 1e9;
int n;
int a[N][N], f[N][N];

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            scanf("%d", &a[i][j]);
    
    //因为求最大值,所以初始化为计算的状态为负无穷
    for (int i = 0; i <= n; i ++ ) //第0行实际不存在,也要初始化为负无穷
        for (int j = 0; j <= i + 1; j ++ ) //第0列和第i + 1列实际不存在,也要初始化为负无穷
            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 << res << endl;
    
    return 0;
}

2. 最长上升子序列#

  • O(n2)
/*
DP
状态表示:
    集合:f[i]表示所有以第i个数结尾的上升子序列
    属性:最大长度
状态计算:
    集合划分:第i - 1个数是 没有,a[1], a[2], ..., a[i - 1] 共i类
    (每一类不一定都存在, 只有a[j] < a[i]时才存在)
    转移方程:f[i] = max(f[j] + 1) (j = 0, 1, 2, ... , i - 1)
*/

#include <iostream>
#include <algorithm>
#include <cstring>

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; //只有i一个数,所以f[i]起码等与1
        for (int j = 1; j < i; j ++ )
            if (a[j] < a[i])
                f[i]= max(f[i], f[j] + 1);
    }
    
    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = max(res, f[i]);
    cout << res << endl;
    
    return 0;
}

贪心优化O(nlogn)

  • 贪心策略:因为上升子序列的结尾越小,在下一次在其结尾增添元素时,越有利于增加长度。
    所以用q数组预处理出不同长度下的上升子序列的结尾的最小数
  • 求最优解的过程:枚举a数组,每次找到q数组中小于a[i]的最大的数,将a[i]接到后面,即求出了一个以a[i]结尾的最长上升子序列;然后更新len为它的长度。因此枚举完a数组之后,len即为最长的上升子序列。
  • q数组一定单调递增的证明: 因为q[5]所对应的是上升子序列,所以倒数第二个数肯定比倒数第一个数小,
    即一定存在倒数第二个数 < q[5],那么就会存在以这个倒数第二个数为结尾的长度为4的上升子序列,就会推出q[4] < q[5],所以可以二分查找
  • 每次更新时,由于q[r]是小于a[i]的最大的数,所以可以推出q[r] < a[i], q[r + 1] >= a[i], 所以当把a[i]接到后面之后,a[i]才是长度为r + 1的上升子序列的最小结尾
//优化
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 100010;

int n;
int a[N];
int q[N]; //q[k]表示所有长度为k的上升子序列的结尾的最小值

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

    int len = 0; //len表示当前的最大的长度(q里面的元素个数)
    q[0] = -2e9; //为了保证小于当前数的最大数一定存在,让q为哨兵,赋值为负无穷
    for (int i = 0; i < n; i ++ ) //枚举每一个数
    {
        //二分出小于当前数的最大数的下标
        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;
        }
        //找到r之后,要把a[i]接到r的后面,所以q的长度加1
        len = max(len, r + 1);
        q[r + 1] = a[i];
    }

    cout << len << endl;

    return 0;
}

3. 最长公共子序列#

/*
DP
状态表示:
    集合:f[i, j]表示所有在a序列的前i个字母中出现,
          并且在b序列的前j个字母出现的子序列
    属性:Max
状态计算:
    集合划分:1.不包含a[i], 不包含b[j] (f[i - 1, j - 1])
              2.不包含a[i], 包含b[j] (f[i - 1, j])
              3.包含a[i], 不包含b[j] (f[i, j - 1])
              4.包含a[i], 包含b[j] (f[i - 1, j - 1] + 1)
    转移方程:f[i, j] = max(f[i - 1, j], f[i, j - 1], f[i - 1, j - 1] + 1)
    注意:第2类是f[i - 1, j]的子集,第3类是f[i, j - 1]的子集,
          第1类是f[i - 1, j]和f[i, j - 1]的子集。
          由于是求最大值,所以有重复不影响答案,只要都覆盖了就行
*/

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 1010;

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

int main()
{
    cin >> n >> m;
    scanf("%s%s", 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 << f[n][m] << endl;

    return 0;
}

4. 最短编辑距离#

/*DP
状态表示:
    集合:f[i, j]表示所有将a[1~i]变为b[1~j]的操作方式
    属性:Min
状态计算:
    集合划分:按照最后一步的操作方式分为3类
        1. 删 f[i - 1, j] + 1
        2. 增 f[i, j - 1] + 1
        3. 改 f[i - 1, j - 1] + 1或者0
    转移方程:f[i, j] = min(f[i - 1, j] + 1, f[i, j - 1] + 1, f[i - 1, j - 1] + 1或者0)
*/

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

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

int main()
{
    scanf("%d%s", &n, a + 1);
    scanf("%d%s", &m, b + 1);

    //初始化边界情况
    for (int i = 0; i <= m; i ++ ) f[0][i] = i; //a长度为0时,对a只能添加
    for (int i = 0; i <= n; i ++ ) f[i][0] = i; //a长度为整个字串时,对a只能删除

    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);
            if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);
            else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);
        }

    printf("%d\n", f[n][m]);

    return 0;
}

3. 区间dp#

1. 石子合并#

/*
状态表示:
    集合:f[i, j]表示所有将第i堆石子到第j堆石子合并为一堆石子的合并方式
    属性:Min
状态计算:
    集合划分:以最后一次合并的分界线分类。
              将区间[i, j]分为[i, k]和[k + 1, j](k从i到j - 1枚举)
    转移方程:f[i, j] = min(f[i, k] + f[k + 1, j] + s[j] - s[i - 1])
    (合并的最后一步必然存在,所以将最后一步的合并去掉,并不影响整体的最小值)
*/

#include <iostream>
#include <algorithm>

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];

    //因为区间长度为1时只有一堆石子,合并代价为0,所以len从2开始枚举
    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; //用到了min需要初始化为正无穷,不然永远是0
            for (int k = l; k < r; k ++ ) //枚举此区间合并到最后一步时的划分线
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
        }
    }

    cout << f[1][n] << endl;

    return 0;
}

4. 计数类dp#

1. 整数划分#

/*DP
转化为完全背包问题:有一个总体积为n的背包,有n件物品,
每个物品的体积为1~n,求恰好将背包装满的方案数

状态表示:
    集合:f[i, j]表示从1~i中选并且体积恰好为j的所有选法
    属性:数量
状态计算:
    集合划分:按照第i个物品选k个划分
    转移方程:f[i, j] = f[i - 1, j] + f[i, j - v]
*/

#include <iostream>

using namespace std;

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

int n;
int f[N];

int main()
{
    cin >> n;
    f[0] = 1; //什么都不选时的方案数为1
    
    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;
}

5. 数位统计dp#

1. 计数问题#

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 10;

/*

001~abc-1, 999

abc
    1. num[i] < x, 0
    2. num[i] == x, 0~efg
    3. num[i] > x, 0~999

*/

int get(vector<int> num, int l, int r)
{
    int res = 0;
    for (int i = l; i >= r; i -- ) res = res * 10 + num[i];
    return res;
}

int power10(int x)
{
    int res = 1;
    while (x -- ) res *= 10;
    return res;
}

int count(int n, int x)
{
    if (!n) return 0;

    vector<int> num;
    while (n)
    {
        num.push_back(n % 10);
        n /= 10;
    }
    n = num.size();

    int res = 0;
    for (int i = n - 1 - !x; i >= 0; i -- )
    {
        if (i < n - 1)
        {
            res += get(num, n - 1, i + 1) * power10(i);
            if (!x) res -= power10(i);
        }

        if (num[i] == x) res += get(num, i - 1, 0) + 1;
        else if (num[i] > x) res += power10(i);
    }

    return res;
}

int main()
{
    int a, b;
    while (cin >> a >> b , a)
    {
        if (a > b) swap(a, b);

        for (int i = 0; i <= 9; i ++ )
            cout << count(b, i) - count(a - 1, i) << ' ';
        cout << endl;
    }

    return 0;
}

6. 状态压缩dp#

1. 蒙德里安的梦想#

/*
DP
状态表示:
    集合:f[i, j]表示第i列的状态为j的所有方案,
          其中j是一个二进制数,含义是第i - 1列有没有支出来的横向方格
    属性:数量
状态计算:
    转移条件:1. 第i列与第i - 1列的状态没有冲突
              2. 第i - 1列的纵向连续空白方格为偶数
    转移方程:f[i, j] += f[i - 1, k](k枚举第i - 1列的所有状态)
*/

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

typedef long long LL;

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

int n, m;
LL f[N][M];
bool st[M]; //此状态是否满足纵向的连续空白方格数为偶数

int main()
{
    while (cin >> n >> m, n || m)
    {
        memset(f, 0, sizeof f);
        
        //预处理st数组
        for (int i = 0; i < 1 << n; i ++ ) //枚举每一列的所有状态
        {
            st[i] = true;
            int cnt = 0; //cnt表示当前状态的连续空白方格数
            for (int j = 0; j < n; j ++ ) //枚举二进制数状态的每一位
                if (i >> j & 1) //当前连续块结束
                {
                    if (cnt & 1) st[i] = false;
                    cnt = 0;
                }
                else cnt ++ ;
                
            //判断最后一次连续块
            if (cnt & 1) st[i] = false;
        }
        
        //dp
        f[0][0] = 1;
        for (int i = 1; i <= m; i ++ ) //枚举所有列
            for (int j = 0; j < 1 << n; j ++ ) //枚举第i列的所有状态
                for (int k = 0; k < 1 << n; k ++ ) //枚举第i - 1列的所有状态
                    if (((j & k) == 0 && st[j | k]))
                        f[i][j] += f[i - 1][k];
                        
        cout << f[m][0] << endl;
    }
    
    return 0;
}

2. 最短Hamilton路径#

/*DP
状态表示:
    集合:f[i, j]表示所有从0走到j,走过的所有点是i的所有路径
    属性:路径长度的最小值

状态计算:
    集合划分:按照倒数第二个点分类(设为k)分为n类
    转移方程:f[i, j] = min(f[i, j], f[(i - (1 << j))][k] + w[k][j])
*/

#include<iostream>
#include <cstring>
#include <algorithm>

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
    
    for (int i = 0; i < 1 << n; i ++ ) //枚举所有状态
        for (int j = 0; j < n; j ++ )
            if (i >> j & 1) //状态i中包含第j位
                for (int k = 0; k < n; k ++ ) //枚举倒数第二位是k号点
                    if ((i - (1 << j)) >> k & 1) //去除最后一个点后包含k号点
                        f[i][j] = min(f[i][j], f[(i - (1 << j))][k] + w[k][j]);
                    
    cout << f[(1 << n) - 1][n - 1];
    
    return 0;
}

7. 树形dp#

1. 没有上司的舞会#

/*DP
状态表示:
    集合:f[u, 0]表示所有从以u为根节点的子树中选择,并且不选u这个点的所有方案
          f[u, 1]表示所有从以u为根节点的子树中选择,并且选u这个点的所有方案
    属性:最大的快乐指数
状态计算:
    (设si为u的子树的根节点)
    集合划分:f[u, 0] = Σmax(f[si, 0], f[si, 1]); //因为没选u,所以子树选不选根都可以
              f[u, 1] = Σmax(f[si, 0]); //因为选了u,所以子树就不能选根
    转移方程:ans = max(f[u, 0], f[u, 1])
*/

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 6010;

int n;
int w[N];
int f[N][2];
             
int h[N], ne[N], e[N], idx;
bool has_fa[N];

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

//递归枚举每一个状态
void dfs(int u)
{
    f[u][1] = w[u]; //选择u这个点就要加上这个点的高兴度

    for (int i = h[u]; i != -1; i = ne[i]) //枚举u的所有子树的根
    {
        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 >> w[i];
    memset(h, -1, sizeof h);
    for (int i = 0; i < n - 1; i ++ )
    {
        int a, b;
        cin >> a >> b;
        add(b, a);
        has_fa[a] = true;
    }
    
    //找到整个树的根节点的编号
    int root = 1;
    while (has_fa[root]) root ++ ;

    dfs(root);

    printf("%d\n", max(f[root][0], f[root][1]));

    return 0;
}

8. 记忆化搜索#

1. 滑雪#

/*DP
状态表示:
    集合:f[i, j]表示所有以(i, j)为起点开始滑的路径
    属性:路径最大长度
状态计算:
    集合划分:以第一步向上,向右,向下,向左分为四类
    转移方程:以第一步向右为例 f[i, j] = max(f[i, j], f[i, j + 1] + 1),其他同理
*/

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 310;

int n, m;
int h[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]; //所有写v的地方等价于写的是f[x][y]
    if (v != -1) return v; //(x, y)已经算过
    
    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 && h[a][b] < h[x][y])
            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 >> h[i][j];
    
    memset(f, -1, sizeof(f)); //表示(x, y)没被算过
    
    int res = 0;
    //枚举从哪个点出发
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            res = max(res, dp(i, j));
    
    cout << res << endl;
    
    return 0;
}

贪心

1. 区间选点#


做题技巧总结

1. 矩阵乘法#

#include <iostream>

using namespace std;

const int N = 110;

int m, n, p;

int a[N][N], b[N][N], c[N][N];

void getmatrix(int a[N][N], int m, int n)
{
    for (int i = 0; i < m; i ++ )
    {
        for (int j = 0; j < n; j ++ )
        {
            scanf("%d", &a[i][j]);
        }
    }
}

void mul(int m, int n, int p)
{
    for (int i = 0; i < m; i ++ )
        for (int j = 0; j < p; j ++ )
            for (int k = 0; k < n; k ++ )
            {
                c[i][j] += a[i][k] * b[k][j];
            }
}

void putmatrix(int a[N][N], int m, int n)
{
    for (int i = 0; i < m; i ++ )
    {
        for (int j = 0; j < n; j ++ )
        {
            printf("%d", a[i][j]);
            if (j < n - 1) printf(" ");
        }
        if (i < m - 1) puts("");
    }
}

int main()
{
    scanf("%d%d%d", &m, &n, &p);
    getmatrix(a, m, n);
    getmatrix(b, n, p);
    mul(m, n, p);
    putmatrix(c, m, p);
    return 0;
}

2. 有序数组中二分查找某数的范围#


3. 进位制转化#

  1. 十进制数转化为b进制数(短除法)

    短除法的正确性证明

    char get(int x) //将数字转化为字符
    {
        if (x <= 9) return x + '0';
        return x - 10 + 'A';
    }
    
    string base(int num, int b) //将一个十进制数num转化为b进制数
    {
        string res;
        while (num)
        {
            res += get(num % b);
            num /= b;
        }
        reverse(res.begin(), res.end());
        return res;
    }
    
    • AcWing 1346. 回文平方
  2. b进制数转为为十进制数(秦九韶算法)

    anxn+an1xn1+···+a1x1+a0x0=(···((anx+an1)x+an2)x+···+a1)x+a0

    int get(char c) //将字符转化为数字
    {
        if (c <= '9') return c - '0';
        return c - 'A' + 10;
    }
    
    int base(string num, int b) //将一个b进制数num转化为十进制数
    {
        int res = 0;
        for (auto c : num)
        {
            res = res * b + get(c);
        }
        return res;
    }
    
  3. a进制数转化为b进制数(短除法)

    reverse(num.begin(), num.end());
    vector<int> res;
    while (num.size())
    {
        int r = 0;
        for (int i = num.size() - 1; i >= 0; i -- )
        {
            num[i] += r * a;
            r = num[i] % b;
            num[i] /= b;
        }
        res.push_back(r);
        //去除前导0
        while (num.size() && num.back() == 0) num.pop_back();
    }
    reverse(res.begin(), res.end());
    

4. 有关回文#

  1. 判断回文串

    bool check(string s)
    {
        for (int i = 0, j = s.size() - 1; i < j; i ++, j -- )
        {
            if (s[i] != s[j]) return false;
        }
        return true;
    }
    
  2. 判断回文数

    bool check(int num)
    {
        int t = num, res = 0;
        while (t != 0)
        {
            res = res * 10 + t % 10;
            t /= 10;
        }
        if (res == num) return true;
        else return false;
    }
    

5. 去重#


6. 日期问题#

  1. 判断闰年

    bool is_leap(int y)
    {
        if ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0) return true;
        else return false;
    }
    

7. 求字符串中ASSIC最大的字符的坐标#

int get(string s)
{
    int p = 0;
    for (int i = 1; i < s.size(); i ++ )
    {
        if (s[i] > s[p]) p = i;
    }
    return p;
}

8. 取整问题转化#

  • n/s(上取整)等价于 (n + s - 1) / s(下取整)

9. 筛素数#

#include<stdio.h>

#define MAX_SIZE 500001

void get_prime_table(int is_not_prime[])
{
    int num, i;
    is_not_prime[0] = is_not_prime[1] = 1;
    is_not_prime[2] = 0;
    for (num = 2; num <= MAX_SIZE / num; num ++)
    {
        if(is_not_prime[num] == 0)  //没被筛过
            for (i = num * 2; i < MAX_SIZE; i += num) is_not_prime[i] = 1;
    }
}

int main()
{
    int i, begin, end, flag = 1, flag1 = 0;
    int is_not_prime[MAX_SIZE] = {0}; // 1 -> 不是素数
    get_prime_table(is_not_prime);
    while (scanf("%d %d", &begin, &end)!=EOF)
    {
        if (flag1) printf("\n");
        for (i = begin; i <= end; i ++)
        {
            if (is_not_prime[i] == 0)
            {
                printf("%d\n", i);
                flag = 0;
            }
        }
        if (flag) printf("\n");
        flag = 1;
        flag1 = 1;
    }
    return 0;
}
posted @   LiHaoyu  阅读(63)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示
主题色彩