[TOC]

基础算法

进制转化

int get(string s, int b)  // 将b进制的数转化成十进制
{
    int res = 0;
    // 秦九韶算法
    for (auto c: s)
        res = res * b + c - '0';
    return res;
}

排序

快速排序

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

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

归并排序

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

    int k = 0, i = l, j = mid + 1;
    while (i <= mid && j <= r)
        if (q[i] <= q[j]) tmp[k ++ ] = q[i ++ ];
        else tmp[k ++ ] = q[j ++ ];

    while (i <= mid) tmp[k ++ ] = q[i ++ ];
    while (j <= r) tmp[k ++ ] = q[j ++ ];

    for (i = l, j = 0; i <= r; i ++, j ++ ) q[i] = tmp[j];
}

二分

整数二分

bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
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;
}
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
    while (l < r)
    {
        int mid = l + r >> 1;
        if (check(mid)) r = mid;    // check()判断mid是否满足性质
        else l = mid + 1;
    }
    return l;
}

浮点数二分

bool check(double x) {/* ... */} // 检查x是否满足某种性质
double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,一般题目要求是1e-6时,eps是1e-8
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

前缀和与差分

一维前缀和

#include <iostream>
using namespace std;
const int N = 100060;
int n, m, l, r, a[N], b[N];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        b[i] = b[i - 1] + a[i];
    }
    while (m--) {
        cin >> l >> r;
        cout << b[r] - b[l - 1] << endl;
    }
}

二维前缀和

#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int a[N][N], b[N][N];
int n, m, q;
int main() 
{
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cin >> a[i][j];
            b[i][j] = b[i][j - 1] + b[i - 1][j] - b[i - 1][j - 1] + a[i][j];
        }
    }
    while (q--) {
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        cout << b[x2][y2] - b[x2][y1 - 1] - b[x1 - 1][y2] + b[x1 - 1][y1 - 1] << endl;
    }
}

一维差分

#include <iostream>
using namespace std;
const int N = 100010;
int n, m, a[N], b[N];
int main() 
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        b[i] = a[i] - a[i - 1];
    }
    while (m--) {
        int l, r, x;
        cin >> l >> r >> x;
        b[l] += x, b[r + 1] -= x;
    }
    for (int i = 1; i <= n; i++) {
        a[i] = a[i - 1] + b[i];
    }
    for (int i = 1; i <= n; i++) {
        cout << a[i] << " ";
    }
}

二维差分

/*
a[][]是差分矩阵,对差分矩阵求前缀和就得到了原数组,这和一维是完全一样的
不同于一维差分的是,二维差分进行修改区间需要修改四个点
a[x1][y1]+=d;进行求前缀和的时候,这会让所有x1,y1之后求的点都改变
所以要进行三个操作抵消
a[x2+1][y1]-=d,a[x1][y2+1]-=d,a[x2+1][y2+1]+=d;
insert函数便诞生了
最终的数组对差分数组求二维前缀和就行
*/
#include <bits/stdc++.h>
const int N = 1005;
using namespace std;
int n, m, q;
int a[N][N], b[N][N];
void insert(int x1, int y1, int x2, int y2, int d) {
    a[x1][y1] += d;
    a[x2 + 1][y1] -= d;
    a[x1][y2 + 1] -= d;
    a[x2 + 1][y2 + 1] += d;
}
int main() 
{
    cin >> n >> m >> q;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            int x;
            cin >> x;
            insert(i, j, i, j, x);//可以看成在一个小区间进行insert
        }
    }
    while (q--) {
        int x1, y1, x2, y2, d;
        cin >> x1 >> y1 >> x2 >> y2 >> d;
        insert(x1, y1, x2, y2, d);
    }
    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] + a[i][j];//二维前缀和
        }
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cout << b[i][j] << " ";
        }
        cout << endl;
    }
    return 0;
}

高精度

加法

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
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(t);
    return C;
}
int main() 
{
    string a, b;
    cin >> a >> b;
    vector<int> 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 = add(A, B);
    for (int i = C.size() - 1; i >= 0; i--) cout << C[i];
    return 0;
}

减法

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
bool cmp(vector<int> a, vector<int> 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;
    int t = 0;//借位1
    for (int i = 0; i < a.size(); i++) {
        t = a[i] - t;
        if (i < b.size()) t -= b[i];
        c.push_back((t + 10) % 10);//有可能产生负数,这样就借1当10
        if (t < 0) t = 1;
        else t = 0;
    }
    while (c.size() > 1 && c.back() == 0) c.pop_back();//去除前导0
    return c;
}
int main() 
{
    vector<int> A, B, C;
    string a, b;
    cin >> a >> b;
    for (int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
    for (int i = b.size() - 1; i >= 0; i--) B.push_back(b[i] - '0');
    if (cmp(A, B)) C = sub(A, B);
    else {
        cout << "-";
        C = sub(B, A);
    }
    for (int i = C.size() - 1; i >= 0; i--) cout << C[i];
    return 0;
}

乘法

大数乘小数

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
vector<int> mul(vector<int> A, int b) {
    vector<int> c;
    int t = 0;//进位
    for (int i = 0; i < A.size(); i++) {
        t = A[i] * b + t;
        c.push_back(t % 10);
        t /= 10;
    }
    while (t) {
        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');
    vector<int> c = mul(A, b);
    for (int i = c.size() - 1; i >= 0; i--) cout << c[i];
    return 0;
}

除法

大数除小数,先输出商,在输出余数

#include<bits/stdc++.h>
using namespace std;
vector<int> div(vector<int> &A, int B, int &r) {//r传入r的地址,便于直接对余数r进行修改
    vector<int> C;
    for (int i = 0; i < A.size(); i++) {//对A从最高位开始处理
        r = r * 10 + A[i];//将上次的余数*10在加上当前位的数字,便是该位需要除的被除数
        C.push_back(r / B);//所得即为商在这一位的数字
        r = r % B;
    }
    //由于在除法运算中,高位到低位运算,因此C的前导零都在vector的前面而不是尾部
    //vector只有删除最后一个数字pop_back是常数复杂度,而对于删除第一位没有相应
    //的库函数可以使用,而且删除第一位,其余位也要前移,
    //因此我们将C翻转,这样0就位于数组尾部,可以使用pop函数删除前导0
    reverse(C.begin(), C.end());
    while (C.size() > 1 && C.back() == 0) C.pop_back();
    return C;
}
int main() 
{
    string a;
    int B, r = 0; //代表余数
    cin >> a >> B;
    vector<int> A;
    for (int i = 0; i < a.size(); i++) A.push_back(a[i] - '0');//注意这次的A是由高为传输至低位,
    //由于在除法的手算过程中,发现从高位进行处理
    //for(int i=0;i<A.size();i++) cout<<A[i];
    //cout<<B;
    auto C = div(A, B, r);
    for (int i = C.size() - 1; i >= 0; i--) cout << C[i];//将C从最高位传给最低位
    cout << endl << r;//输出余数
    cout << endl;
    return 0;
}

离散化

离散化其实就是排序+去重,然后把值映射成[0,n-1]或者[1,n],注意此时的n是去重后的n。

映射其实就是把值放入数组(vector)中,下标就是数组下标。

alls表示所有待离散化的数。

注意处理离散化问题的时候,往往需要先把所有用到的坐标存起来并且离散化,之后在进行各种操作。

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
vector<PII> add, query;
vector<int> alls;
int a[300010], b[300010];//最多这么多下标
int find(int x) {
    int l = 0, r = alls.size() - 1;//因为就是存在vector里面
    while (l < r) {
        int mid = (l + r) >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return l + 1;
}
int main() 
{
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        int x, c;
        cin >> x >> c;
        add.push_back({x, c});
        alls.push_back(x);
    }
    for (int i = 1; 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 (int i = 0; i < add.size(); i++) {
        int x = find(add[i].first), c = add[i].second;
        a[x] += c;
    }
    for (int i = 1; i <= alls.size(); i++) {
        b[i] = b[i - 1] + a[i];
    }
    for (int i = 0; i < query.size(); i++) {
        int l = find(query[i].first), r = find(query[i].second);
        cout << b[r] - b[l - 1] << endl;
    }
    return 0;
}

读入

string s;
getline(cin,s);//空格不断行输入
stringstream ss(s);
int x;//也可以改为string,double,char;
while(ss >> x) cout<<x<<endl;

//注意使用getline()之前,若使用了cin/scanf,必须要getchar()一次,过滤掉换行符,一次就行,多次getline()之间不需要getchar()
#include <bits/stdc++.h>
using namespace std;
int main() {
    int a;
    cin >> a;
    getchar();
    for (int i = 1; i <= 3; i++) {
        string s;
        getline(cin, s);
        cout << s << endl;
    }
    return 0;
}

//cin.get()不同于getline()应该在每次读入后都getchar()一次
#include <bits/stdc++.h>
using namespace std;
int main() {
    int a;
    cin >> a;
    getchar();
    for (int i = 1; i <= 3; i++) {
        char s[105];
        cin.get(s, 105);
        getchar();
        cout << s << endl;
    }
    return 0;
}

矩阵

#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
#define endl "\n"
typedef long long LL;
const int N = 1e5 + 10, M = N, mod = 1e9 + 7;
using namespace std;
struct Mat{
    int a[105][105];
    int r, c;
    Mat(int _r, int _c){
        r = _r, c = _c;
        memset(a, 0, sizeof a);
    }
    //单位矩阵
    void unit(){
        memset(a, 0, sizeof a);
        for(int i = 1; i <= r; i ++){
            a[i][i] = 1;
        }
    }
    Mat operator+(const Mat t) const{
        Mat ans = *this;
        for(int i = 1; i <= r; i ++){
            for(int j = 1; j <= c; j ++){
                ans.a[i][j] += t.a[i][j];
            }
        }
        return ans;
    }
    Mat operator-(const Mat t) const{
        Mat ans = *this;
        for(int i = 1; i <= r; i ++){
            for(int j = 1; j <= c; j ++){
                ans.a[i][j] -= t.a[i][j];
            }
        }
        return ans;
    }
    Mat operator*(const Mat t) const{
        Mat ans(r, t.c);
        int n = r, m = t.c;
        for(int i = 1; i <= n; i ++){
            for(int j = 1; j <= m; j ++){
                for(int k = 1; k <= c; k ++){
                    ans.a[i][j] += a[i][k] * t.a[k][j];
                    ans.a[i][j] %= mod;
                }
            }
        }
        return ans;
    }
    Mat pow(LL b){
        Mat ans(r, c), _a = *this;
        ans.unit();
        while(b){
            if(b & 1) ans = ans * _a;
            _a = _a * _a;
            b >>= 1;
        }
        return ans;
    }
    void output(){
        for(int i = 1; i <= r; i ++){
            for(int j = 1; j <= c; j ++){
                cout << a[i][j] << " ";
            }
            cout << endl;
        }
    }
};
int main()
{
    int n1, m1, n2, m2;
    cin >> m1 >> n1 >> m2 >> n2;
    Mat A(n1, m1), B(n2, m2);
    for(int i = 1; i <= n1; i ++){
        for(int j = 1; j <= m1; j ++){
            cin >> A.a[i][j];
        }
    }
    for(int i = 1; i <= n2; i ++){
        for(int j = 1; j <= m2; j ++){
            cin >> B.a[i][j];
        }
    }
    Mat ans = A * B;
    ans.output();
    return 0;
}

STL

Set/multiset

set和multiset会根据特定的排序原则将元素排序。两者不同之处在于,multisets允许元素重复,而set不允许重复。

s.insert() //log(n)
s.erase() //log(n)
s.begin() //返回第一个元素的地址
s.end() //返回最后一个元素下一个元素的地址
s.find(x) //log(n) 返回一个地址(迭代器),如果找不到当前元素x返回s.end()
s.lower_bound(x)// log(n) 返回一个元素>=x的地址(迭代器),如果找不到当前元素x返回s.end()
s.upper_bound(x)// log(n) 返回一个元素>x的地址(迭代器),如果找不到当前元素x返回s.end()

二分函数

lower_bound(首地址,尾地址,x)  //左闭右开,查找>=x的第一个数,若查找不到,返回尾地址
upper_bound(首地址,尾地址,x)  //左闭右开,查找>x的第一个数 ,若查找不到,返回尾地址 
//注意此函数返回的是地址,若要变成下标,若是数组a只需要-a,若是vector只需要-v.begin()

数据结构

字符串哈希

不能把字符映射成0,否则$A,AA,AAA$都是一样的。经验值$P=131$,或者$P=13331$,Q取成$2^{64}$,这样冲突的可能性很小。

/* 
全称字符串前缀哈希法,把字符串变成一个p进制数字(哈希值),实现不同的字符串映射到不同的数字。
对形如 X1X2X3⋯Xn−1XnX1X2X3⋯Xn−1Xn 的字符串,采用字符的ascii 码乘上 P 的次方来计算哈希值。
映射公式 (X1×Pn−1+X2×Pn−2+⋯+Xn−1×P1+Xn×P0)modQ  Q一般qu公式 (X1×Pn−1+X2×Pn−2+⋯+Xn−1×P1+Xn×P0)modQ  Q一般
注意
1. 任意字符不可以映射成0,否则会出现不同的字符串都映射成0的情况,比如A,AA,AAA皆为0
2. 冲突问题:通过巧妙设置P (131 或 13331) , Q (2的64次方)的值,一般可以理解为不产生冲突。

公式:前缀hash  h[i]=h[i-1]*P+str[i];
      区间hash  h[l,r]=h[r]−h[l−1]×P(r−l+1)次方
*/
#include <bits/stdc++.h>
typedef unsigned long long ULL;//代替对2的64次方取模
const int N = 1e6, P = 131;//P一般取131或者13331,
using namespace std;
ULL h[N], p[N];//h放的是前缀hash值,p放的是P的多少次方;
int l1, r1, l2, r2, n, m;
char str[N];
ULL query(int l, int r)//类似于前缀和,只不过需要一个偏移量
{
    return h[r] - h[l - 1] * p[r - l + 1];//公式
}
int main() 
{
    scanf("%d%d%s", &n, &m, str + 1);//注意s从数组的第一位开始存,类似于前缀和
    h[0] = 0, p[0] = 1;//初始化,P的0次方为1
    for (int i = 1; i <= n; i++) {
        h[i] = h[i - 1] * P + str[i];//计算前缀哈希,类似P进制,只要保证str[i]不是0就行
        p[i] = P * p[i - 1];//计算p的i次方,因为会用到一个P的(r−l+1)次方
    }
    while (m--) {
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
        if (query(l1, r1) == query(l2, r2)) printf("Yes\n");
        else printf("No\n");
    }
}

KMP

复杂度:$O(n)$

$p$是模板串,长度为$m$,$s$是要匹配的串,长度为$n$

$next[j]$的含义,以$p[j]$为结尾,最大的后缀和前缀相等的长度,当然长度要小于$j$,否则肯定是$k$

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int ne[N], n, m;
char s[N], p[N];
int main() {
    cin >> m >> (p + 1) >> n >> (s + 1);
    ne[0] = ne[1] = 0;
    for (int i = 2, j = 0; i <= m; i++)//和自己匹配
    {
        while (j && p[i] != p[j + 1]) j = ne[j]; 
        if (p[i] == p[j + 1]) j++;
        ne[i] = j;
    }
    for (int i = 1, j = 0; i <= n; i++)//匹配
    {
        while (j && s[i] != p[j + 1]) j = ne[j];//我们是用s[i]和p[j+1]比较,不相等就回退
        if (s[i] == p[j + 1]) j++;//如果相等,则i++,j++进行下一次匹配
        if (j == m)//匹配完成,不完成的话就匹配p[j+1]了
        {
            cout << i - m << " ";
            j = ne[j];//不要忘了
        }
    }
    return 0;
}

单调栈和队列

给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。

#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
using namespace std;
typedef long long ll;
typedef pair<int,int>PII;
const int N=1e6;
stack <int> stk;
int main()
{
    CLOSE;
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int x;
        cin>>x;
        while(!stk.empty()&&stk.top()>=x) stk.pop();//当前这个点一定不会被用到,因为x更优
        if(!stk.empty()) cout<<stk.top()<<" ";
        else cout<<-1<<" ";
        stk.push(x);
    }
    return 0;
}

单调队列

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
deque<int> q;//双端队列
int a[N];
int main() {
    int n, k;
    cin >> n >> k;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= n; i++) {
        while (!q.empty() && a[q.back()] >= a[i]) q.pop_back();//制作队列
        q.push_back(i);
        while (!q.empty() && i - q.front() + 1 > k) q.pop_front();//因为要求长度是k
        if (i >= k) cout << a[q.front()] << " ";
    }
    cout << endl;
    q.clear();//别忘了
    for (int i = 1; i <= n; i++) {
        while (!q.empty() && a[q.back()] <= a[i]) q.pop_back();
        q.push_back(i);
        while (!q.empty() && i - q.front() + 1 > k) q.pop_front();
        if (i >= k) cout << a[q.front()] << " ";
    }
    return 0;
}

并查集

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
int p[N], siz[N];
int find(int x) {
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}
int merge(int x, int y) {
    int fx = find(x);
    int fy = find(y);
    if (fx != fy) {
        p[fx] = fy;
        siz[fy] += siz[fx];
    }
}
int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        p[i] = i;
        siz[i] = 1;
    }
    while (m--) {
        string op;
        int a, b;
        cin >> op;
        if (op == "C") {
            cin >> a >> b;
            merge(a, b);
        } else if (op == "Q1") {
            cin >> a >> b;
            if (find(a) == find(b)) cout << "Yes" << "\n";
            else cout << "No" << "\n";
        } else {
            cin >> a;
            cout << siz[find(a)] << "\n";
        }
    }
    return 0;
}

DFS序

树上单点修改,子树求和问题。

dfs把子树转换成区间,子树对应的区间一定是连续的。

#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
#define endl "\n"
typedef long long LL;
const int N = 1e6 + 10, M = 2 * N, mod = 1e9 + 7;
using namespace std;
int n, m, k, a[N], t[N];
int h[N], e[M], ne[M], idx;
int l[N], r[N], timestamp;
void add(int x, int y){
    e[idx] = y, ne[idx] = h[x], h[x] = idx ++;
}
int lowbit(int x){return x & -x;}
void addT(int x, int c){
    for(int i = x; i <= n; i += lowbit(i)){
        t[i] += c;
    }
}
LL getSum(int x){
    LL ans = 0;
    for(int i = x; i; i -= lowbit(i)){
        ans += t[i];
    }
    return ans;
}
//u子树对应的区间就是l[u],r[u]
void dfs(int u, int fa){
    l[u] = ++ timestamp;
    for(int i = h[u]; ~ i; i = ne[i]){
        int j = e[i];
        if(j == fa) continue;
        dfs(j, u);
    }
    r[u] = timestamp;
}
int main()
{
    CLOSE;
    cin >> n >> m >> k;
    memset(h, -1, sizeof h);
    for(int i = 1; i <= n; i ++) cin >> a[i];
    for(int i = 1; i < n; i ++){
        int x, y;
        cin >> x >> y;
        add(x, y), add(y, x);
    }
    dfs(k, -1);
    for(int i = 1; i <= n; i ++){
        addT(l[i], a[i]);
    }
    while(m --){
        int op, x, y;
        cin >> op >> x;
        if(op == 1){
            cin >> y;
            addT(l[x], y);
        } else{
            cout << getSum(r[x]) - getSum(l[x] - 1) << endl;
        }
    }
    return 0;
}

ST表

#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
#define endl "\n"
typedef long long LL;
const int N = 2e5 + 10, M = 19, mod = 1e9 + 7;
using namespace std;
int f[N][M], a[N];
void init(int n){
    int x = log2(n);
    for(int j = 0; j <= x; j ++){
        for(int i = 1; i + (1 << j) - 1 <= n; i ++){
            if(!j) f[i][j] = a[i];
            else f[i][j] = max(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
        }
    }
}
int query(int l, int r){
    int k = log2(r - l + 1);
    return max(f[l][k], f[r - (1 << k) + 1][k]);
}
int main()
{
    int n, m;
    cin >> n;
    for(int i = 1; i <= n; i ++) cin >> a[i];
    init(n);
    cin >> m;
    while(m --){
        int l, r;
        cin >> l >> r;
        cout << query(l, r) << endl;
    }
    return 0;
}

树状数组

单点修改,区间求和

#include <bits/stdc++.h>
typedef long long LL;
const int N = 5e5 + 10, M = N;
using namespace std;
int a[N], n, m;
LL t[N];
int lowbit(int x){return x & -x;}
void add(int x, int c){
    for(int i = x; i <= n; i += lowbit(i)){
        t[i] += c;
    }
}
LL query(int x){
    LL sum = 0;
    for(int i = x; i; i -= lowbit(i)){
        sum += t[i];
    }
    return sum;
}
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++){
        cin >> a[i];
        add(i, a[i]);
    }
    while(m --){
        int op, x, y;
        cin >> op >> x >> y;
        if(op == 1) add(x, y);
        else cout << query(y) - query(x - 1) << endl;
    }
    return 0;
}

区间修改,单点查询,即维护差分数组

#include <bits/stdc++.h>
typedef long long LL;
const int N = 5e5 + 10, M = N;
using namespace std;
int a[N], n, m;
LL t[N];
int lowbit(int x){return x & -x;}
void add(int x, int c){
    for(int i = x; i <= n; i += lowbit(i)){
        t[i] += c;
    }
}
LL query(int x){
    LL sum = 0;
    for(int i = x; i; i -= lowbit(i)){
        sum += t[i];
    }
    return sum;
}
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++){
        cin >> a[i];
        add(i, a[i] - a[i - 1]);
    }
    while(m --){
        int op;
        cin >> op;
        if(op == 1) {
            int l, r, k;
            cin >> l >> r >> k;
            add(l, k), add(r + 1, -k);
        }
        else {
            int x;
            cin >> x;
            cout << query(x) << endl;
        }
    }
    return 0;
}

备注 2020年7月25日.png

#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
#define endl "\n"
typedef long long LL;
const int N = 1e6 + 10, M = N, mod = 1e9 + 7;
using namespace std;
int n, q;
LL  t1[N], t2[N], a[N], b[N];
int lowbit(int x) {return x & -x;}
void add(int x, LL c, LL t[]){
    for(int i = x; i <= n; i += lowbit(i)){
        t[i] += c;
    }
}
LL getsum(int x, LL t[]){
    LL sum = 0;
    for(int i = x; i; i -= lowbit(i)){
        sum += t[i];
    }
    return sum;
}
LL query(int x){
    return getsum(x, t1) * (x + 1) - getsum(x, t2); 
}
int main()
{
    CLOSE;
    cin >> n >> q;
    for(int i = 1; i <= n; i ++){
        cin >> a[i];
    }
    for(int i = 1; i <= n; i ++){
        b[i] = a[i] - a[i - 1];
        add(i, b[i], t1);
        add(i, b[i] * i, t2);
    }
    while(q --){
        int op;
        cin >> op;
        if(op == 1){
            int l, r;
            LL k;
            cin >> l >> r >> k;
            add(r + 1, -k, t1), add(l, k, t1);//改差分数组
            add(r + 1, -k * (r + 1), t2), add(l, k * l, t2);//改i*差分数组
        } else{
            int l, r;
            cin >> l >> r;
            cout << query(r) - query(l - 1) << endl;
        }
    }
    return 0;
}

线段树

建树复杂度 $O(n)$,其他操作$O(log(n))$

父节点:x/2 int会自动下取整 x>>1; 左儿子:2x x<<1; 右儿子:2x+1 x<<1|1

凡是只修改单点的,是不需要懒标记的,修改区间的需要懒标记。

pushup()放在build和modify的下面

pushdown放在query和modify的上面,并且pushdown放在的是要分裂的地方;

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e5 + 100;
struct node {
    LL l, r, sum, add;
} t[N * 4];//开四倍空间
LL a[N];
void pushup(LL u)//用子节点的信息更新父亲节点
{
    t[u].sum = t[u << 1].sum + t[u << 1 | 1].sum;
};
void pushdown(LL u)//用父节点去更新子节点
{
    //更新的信息包括值和懒标记,不要忘了清空父节点的信息,这更像传递!!!
    t[u << 1].sum += (t[u << 1].r - t[u << 1].l + 1) * t[u].add;//更新左儿子的值
    t[u << 1 | 1].sum += (t[u << 1 | 1].r - t[u << 1 | 1].l + 1) * t[u].add;//更新右儿子的值
    t[u << 1].add += t[u].add;//更新左儿子的懒标记
    t[u << 1 | 1].add += t[u].add;//更新右儿子的值
    t[u].add = 0;//清空父节点的懒标记
}
void build(int u, int l, int r) {
    t[u].l = l, t[u].r = r;//这个一定不要忘了
    if (l == r)//初始化
    {
        t[u].sum = a[l];
        t[u].add = 0;
    } else {
        t[u].add = 0;//最好也加上
        int mid = (l + r) >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
        pushup(u);//上面的代码修改了子节点,父节点的值必须更新
    }
}
void modify(int u, int l, int r, LL d)//l,r是要修改的区间
{
    if (l <= t[u].l && t[u].r <= r)//当前区间是修改区间的子区间
    {
        t[u].sum += (t[u].r - t[u].l + 1) * d;//直接修改当前区间,不在递归修改后面区间了
        t[u].add += d;//更新懒标记
    } else//修改部分区间的情况
    {
        pushdown(u);//要修改部分区间了,分裂了,要pushdown()
        int mid = (t[u].r + t[u].l) >> 1;//这里一定不要错了,l,r是要修改的区间
        if (l <= mid) modify(u << 1, l, r, d);//左儿子有交集,递归修改左半边
        if (r > mid) modify(u << 1 | 1, l, r, d);//右儿子有交集,递归修改右半边
        pushup(u);//修改了子节点,必然要更新父节点
    }
}
LL query(int u, int l, int r) {
    if (l <= t[u].l && t[u].r <= r) {
        return t[u].sum;//直接返回
    } else//查询部分区间情况
    {
        pushdown(u);//查询的时候不要忘了pushdown()
        int mid = (t[u].l + t[u].r) >> 1;//不要用l,r, l,r是要查询的区间左右端点
        LL ans = 0;
        if (l <= mid) ans += query(u << 1, l, r);//左儿子有交集,递归查询左半边
        if (r > mid) ans += query(u << 1 | 1, l, r);//右儿子有交集,递归查询右半边
        return ans;
    }
}
int n, m;
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) scanf("%lld", &a[i]);
    build(1, 1, n);
    while (m--) {
        char op;
        LL l, r, d;
        cin >> op >> l >> r;
        if (op == 'Q') {
            cout << query(1, l, r) << endl;
        } else {
            cin >> d;
            modify(1, l, r, d);
        }
    }
}

字典树

void insert(string s){
    int p = 0;
    for(int i = 0; i < s.size(); i ++){
        int u = s[i] - '0';
        if(!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
    }
    cnt[p] ++;
}
int query(string s){
    int p = 0;
    for(int i = 0; i < s.size(); i ++){
        int u = s[i] - '0';
        if(!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];
}

01字典树

cnt[p] ++,保证只用一次,用完了就不会跳到x!!

#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
#define endl "\n"
typedef long long LL;
const int N = 3e5 + 10, M = N, mod = 1e9 + 7;
using namespace std;
int son[N][2], cnt[N], idx;
int a[N];
void insert(int x){
    int p = 0;
    for(int i = 30; i >= 0; i --){
        int u = (x >> i) & 1;
        if(!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
        cnt[p] ++;
    }
}
int query(int x){
    int p = 0, ans = 0;
    for(int i = 30; i >= 0; i --){
        int u = (x >> i) & 1;
        if(cnt[son[p][u]]){
            p = son[p][u];
        } else {
            p = son[p][u ^ 1];
            ans += 1 << i;
        }
        cnt[p] --;
    }
    return ans;
}
int main()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i ++){
        cin >> a[i];
    }
    for(int i = 1; i <= n; i ++){
        int x;
        cin >> x;
        insert(x);
    }
    for(int i = 1; i <= n; i ++){
        cout << query(a[i]) << " ";
    }
    return 0;
}

搜索

BFS

bfs中的队列有两个性质

1:两段性 距离: x x x x (x+1) (x+1) (x+1)

2:单调性 bfs每次取出的队头都是距离最近的点,类似于Dijisitla,

3:只要能时时刻刻满足两段性和单调性,呢么bfs就相当于Dijisitla,所以bfs是正确的;

双端队列广搜

1:双端队列广搜解决的问题是边权是0和边权是1的问题,普通的BFS是解决边权都是1(边权相等)的问题;

2:双端队列可以说是简化版的堆优化Dijisitla,但是复杂度更优为O(n);

3:因为有两种权重,第一次搜到的点不一定就是最短距离,与堆优化Dijkstra 一样,必须在出队时才知道每个点最终的最小值,而和一般的bfs不一样,原因是如下图所示

4.与Dijisitla不同是,他是搜索的时候用,不需要建边,不需要建图,与普通的bfs题的不同是有两种边。

2b9afb6b5c690928a81e855d477ed37.png

#include <bits/stdc++.h>

const int N = 1006;
using namespace std;
struct node {
    int x, y;
};
int a[N][N], dist[N][N], st[N][N];
int wy[4][2] = {1, 0},{-1, 0},{0, 1},{0,-1};
int qdx, qdy;
int bfs() {
    memset(dist, 0x3f, sizeof dist);
    deque<node> q;
    q.push_front({qdx, qdy});
    dist[qdx][qdy] = 0;
    while (!q.empty()) {
        node one = q.front();
        q.pop_front();
        if (st[one.x][one.y]) continue;
        else st[one.x][one.y] = 1;
        for (int i = 0; i <= 3; i++) {
            int xx = one.x + wy[i][0];
            int yy = one.y + wy[i][1];
            if (xx >= 0 && xx < N && yy >= 0 && yy <= N) {
                if (dist[xx][yy] > dist[one.x][one.y] + a[xx][yy]) {
                    dist[xx][yy] = dist[one.x][one.y] + a[xx][yy];
                    if (a[xx][yy] == 1)q.push_back({xx, yy});
                    else q.push_front({xx, yy});
                }
            }
        }
    }
    return dist[0][0];
}
int main() {
    int n;
    cin >> n >> qdx >> qdy;
    for (int i = 1; i <= n; i++) {
        int x, y;
        cin >> x >> y;
        a[x][y] = 1;
    }
    cout << bfs();
    return 0;
}

双向广搜

一般用于最小步数模型,最短路模型一般用不到,因为本来就不会超时,比如我们要看一个字符串十步之内是不是能变到另一个字符串,如果有六个变化方式,直接BFS是6^10,如果是双向广搜的话,只有3*6的五次方,显然是一个很大的优化,而不是简单的/2。

//字符串函数  s.substr(qd,len);
#include <bits/stdc++.h>
using namespace std;
string A, B, x, y;
string a[7], b[7];
unordered_map<string, int> da, db;
queue<string> qa, qb;
int idx = 0;
int extend(queue<string> &q, unordered_map<string, int> &da, unordered_map<string, int> &db, string a[],
           string b[])//别忘了加引用符号
{
    string s = q.front();
    q.pop();
    for (int i = 0; i < s.size(); i++)//遍历起点
    {
        for (int j = 1; j <= idx; j++)//遍历规则
        {
            if (s.substr(i, a[j].size()) == a[j])//函数
            {
                string ss = s.substr(0, i) + b[j] + s.substr(i + a[j].size());//变换后的字符串
                if (db.count(ss))//如果另一个遍历到了这个,就要汇合
                {
                    return da[s] + db[ss] + 1;//返回距离
                }
                if (da.count(ss)) continue;//如果a搜过了,就不入队了
                q.push(ss);
                da[ss] = da[s] + 1;
            }
        }
    }
    return 11;//搜素一层都没有返回,直接返回11
}
int bfs() {
    if (A == B) return 0;//特判
    da[A] = 0, db[B] = 0;
    qa.push(A), qb.push(B);
    while (!qa.empty() && !qb.empty())//如果有一个为空了,说明是不连通的,无法从A->B
    {
        int bs;
        if (qa.size() <= qb.size())//优化,优先搜索队列长度短的
        {
            bs = extend(qa, da, db, a, b);//进行一次扩展看看双向搜索是否能接在一起
        } else bs = extend(qb, db, da, b, a);//进行一次扩展看看双向搜索是否能接在一起
        if (bs <= 10) return bs;//如果距离小于10,就找到了,不用继续搜索了
    }
    return -1;//全部情况都搜索了,没有<10的,呢么无解
}
int main() {
    cin >> A >> B;
    while (cin >> x >> y) {
        idx++;
        a[idx] = x, b[idx] = y;
    }
    int ans = bfs();
    if (ans == -1) puts("NO ANSWER!");
    else cout << ans << endl;
    return 0;
}

图和树

二叉树

先序+中序

#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
using namespace std;
typedef long long LL;
typedef pair<int, int> PII;
const int N = 1e3;
map<int, PII> node;
map<int, int> mp;
int n;
int a[N], b[N];
vector<int> v;
int dfs(int l1, int r1, int l2, int r2)//先序+中序
{
    if (l1 > r1 || l2 > r2) return 0;
    int root = a[l1];
    int k = mp[root];
    int sum = k - l2;
    node[root].first = dfs(l1 + 1, l1 + sum, l2, k - 1);
    node[root].second = dfs(l1 + sum + 1, r1, k + 1, r2);
    return root;
}
int dfs(int l1, int r1, int l2, int r2)//后序+中序
{
    if (l1 > r1 || l2 > r2) return 0;
    int root = a[r1];
    int k = mp[root];
    int sum = k - l2;
    node[root].first = dfs(l1, l1 + sum - 1, l2, k - 1);
    node[root].second = dfs(l1 + sum, r1 - 1, k + 1, r2);
    return root;
}
void bfs()//层次
{
    queue<int> q;
    q.push(a[1]);
    int cnt = 0;
    while (!q.empty()) {
        int t = q.front();
        q.pop();
        cnt++;
        if (cnt != n) cout << t << " ";
        else cout << t << endl;
        if (node[t].first) {
            q.push(node[t].first);
        }
        if (node[t].second) {
            q.push(node[t].second);
        }
    }
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> b[i];
        mp[b[i]] = i;
    }
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    dfs(1, n, 1, n);
    bfs();
    return 0;
}

树的直径和重心

直径

#include <bits/stdc++.h>
using namespace std;
const int N = 10010, M = 20010;
int h[N], ne[M], w[M], e[M], idx;
int ans; // 保存最长路径
int t;  // 保存找到的最远点
int n;
void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void dfs(int u, int father, int dist) {
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (j == father) continue;
        dfs(j, u, dist + w[i]);
    }
    // 找到最大的dist用来更新答案ans和点t
    if (dist > ans) {
        ans = dist;
        t = u;
    }
}
int main() {
    memset(h, -1, sizeof h);
    scanf("%d", &n);
    for (int i = 0; i < n - 1; ++i) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
        add(b, a, c);
    }
    dfs(1, -1, 0);  // 先找到点t
    dfs(t, -1, 0);  // 根据t找到结果ans
    printf("%d", ans);
    return 0;
}

重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。树的重心不唯一。

https://kdyfiles.oss-cn-hangzhou.aliyuncs.com/kdyb60d5214d84c40bc9fc22546b18d0255.png

定义函数$dfs(u)$,表示以$u$为根的子树的节点个数。

#include <bits/stdc++.h>
using namespace std;
const int N = 100010, M = N * 2;
int n;
int h[N], e[M], ne[M], idx;//存图
int ans = 0x3f3f3f3f;//最终的答案
bool st[N];//表示哪些点已经被遍历过
//加边函数
void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
//dfs(u)返回的结果是以u为根的子树的节点个数
int dfs(int u) {
    st[u] = true;//u节点被访问过
    int size = 0;//size存的是若删除u节点,剩余各个连通块中点数的最大值的最小是多少。
    int sum = 1;// sum存的是以u为根的子树的节点个数,也就是dfs函数最后返回的值,初值是1
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];//j是子节点的编号
        if (st[j]) continue;//只访问没有访问过的点
        int s = dfs(j);//子树的节点个数
        size = max(size, s);//取一个max
        sum += s;//以u为根的子树的节点个数需要加上s
    }
    size = max(size, n - sum);//不要忘了父节点也构成了一个联通块
    ans = min(ans, size);//更新答案
    return sum;
}
int main() {
    scanf("%d", &n);
    memset(h, -1, sizeof h);//别忘了初始化图
    for (int i = 0; i < n - 1; i++) {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b), add(b, a);
    }
    dfs(1);//从任意一个点搜都可以
    printf("%d\n", ans);
    return 0;
}

拓扑排序

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 100, M = N;
int h[N], e[M], ne[M], idx, din[N], n, m;
vector<int> v;
void add(int x, int y) {
    e[idx] = y, ne[idx] = h[x], h[x] = idx++;
}
void topsort() {
    queue<int> q;
    for (int i = 1; i <= n; i++) {
        if (din[i] == 0) q.push(i);
    }
    while (!q.empty()) {
        int t = q.front();
        q.pop();
        v.push_back(t);
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            din[j]--;
            if (din[j] == 0) q.push(j);
        }
    }
}
int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y;
        cin >> x >> y;
        add(x, y);
        din[y] ++;
    }
    topsort();
    if (v.size() != n) cout << -1;
    else {
        for (int i = 0; i < v.size(); i++) cout << v[i] << " ";
    }
    return 0;
}

最小生成树

Kruskal复杂度 $O(n*log(n))$

#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
using namespace std;
typedef long long ll;
typedef pair<int,int>PII;
const int N=1e5+100,M=2e5+100;
struct node{
    int x,y,w;
}edge[M];
bool cmp(node a,node b)
{
    return a.w<b.w;
}
int n,m,p[N];
int find(int x)
{
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}
int main()
{
    CLOSE;
    cin>>n>>m;
    for(int i=1;i<=n;i++) p[i]=i;
    for(int i=1;i<=m;i++)
    {
        cin>>edge[i].x>>edge[i].y>>edge[i].w;
    }
    sort(edge+1,edge+1+m,cmp);//这里排序别排错了
    int sum=0,cnt=0;
    for(int i=1;i<=m;i++)
    {
        int x=find(edge[i].x),y=find(edge[i].y),w=edge[i].w;
        if(x!=y)
        {
            cnt++;
            sum+=w;
            p[x]=y;
        }
    } 
    if(cnt==n-1) cout<<sum<<endl;
    else cout<<"impossible"<<endl;
    return 0;
}

次小生成树

严格次小生成树:边权之和必须小于最小生成树

非严格次小生成树,边权之和可以与最小生成树相等

//离谱,卡vector!!!
#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 1e5 + 10, M = 6e5 + 100, inf = 0x3f3f3f3f;
int h[N], e[M], ne[M], w[M], idx;
int f[N][18], depth[N], d1[N][18], d2[N][18], p[N];
int n, m;
ll sum;
void add(int x, int y, int z) {
    e[idx] = y, w[idx] = z, ne[idx] = h[x], h[x] = idx++;
}
int find(int x) {
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}
struct node {
    int x, y, w;
    bool used;
} edge[M];
bool cmp(node a, node b) {
    return a.w < b.w;
}
void bfs() {
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0;
    depth[1] = 1;
    queue<int> q;
    q.push(1);
    while (!q.empty()) {
        int t = q.front();
        q.pop();
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (depth[j] > depth[t] + 1) {
                q.push(j);
                depth[j] = depth[t] + 1;
                f[j][0] = t;
                d1[j][0] = w[i], d2[j][0] = -inf;
                for (int k = 1; k <= 17; k++) {
                    int anc = f[j][k - 1];
                    f[j][k] = f[anc][k - 1];
                    int distance[4] = {d1[j][k - 1], d1[anc][k - 1], d2[j][k - 1], d2[anc][k - 1]};
                    d1[j][k] = d2[j][k] = -inf;
                    for (int s = 0; s <= 3; s++) {
                        if (distance[s] > d1[j][k]) {
                            d2[j][k] = d1[j][k], d1[j][k] = distance[s];
                        } else if (distance[s] < d1[j][k] && distance[s] > d2[j][k]) d2[j][k] = distance[s];
                    }
                }
            }
        }
    }
}
int lca(int x, int y, int w) {
    int distance[M];
    int cnt = 0;
    if (depth[x] < depth[y]) swap(x, y);
    for (int i = 17; i >= 0; i--) {
        if (depth[f[x][i]] >= depth[y]) {
            distance[cnt++] = d1[x][i];
            distance[cnt++] = d2[x][i];
            x = f[x][i];
        }
    }
    if (x != y) {
        for (int i = 17; i >= 0; i--) {
            if (f[x][i] != f[y][i]) {
                distance[cnt++] = d1[x][i];
                distance[cnt++] = d2[x][i];
                distance[cnt++] = d1[y][i];
                distance[cnt++] = d2[y][i];
                x = f[x][i], y = f[y][i];
            }
        }
        distance[cnt++] = d1[x][0];
        distance[cnt++] = d2[x][0];
        distance[cnt++] = d1[y][0];
        distance[cnt++] = d2[y][0];
    }
    int dist1 = -inf, dist2 = -inf;
    for (int i = 0; i < cnt; i++) {
        int it = distance[i];
        if (it > dist1) {
            dist2 = dist1, dist1 = it;
        } else if (it < dist1 && it > dist2) dist2 = it;
    }
    if (w > dist1) return w - dist1;
    else if (w > dist2) return w - dist2;
    else return inf;
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) p[i] = i;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        edge[i] = {x, y, z, false};
    }
    sort(edge + 1, edge + 1 + m, cmp);
    for (int i = 1; i <= m; i++) {
        int fx = find(edge[i].x), fy = find(edge[i].y), w = edge[i].w;
        if (fx != fy) {
            p[fx] = fy;
            sum += w;
            edge[i].used = true;
            add(edge[i].x, edge[i].y, w);
            add(edge[i].y, edge[i].x, w);
        }
    }
    bfs();
    ll ans = 1e18;
    for (int i = 1; i <= m; i++) {
        if (!edge[i].used) {
            int x = edge[i].x, y = edge[i].y;
            ans = min(ans, sum + lca(x, y, edge[i].w));
        }
    }
    printf("%lld\n", ans);
    return 0;
}

最短路

Dijkstra

只能正权

#include<bits/stdc++.h>
typedef long long LL;
const int N = 1e5 + 10, M = 2 * N;
using namespace std;
int h[N], e[M], ne[M], w[M], dist[N], idx;
bool st[N];
int n, m;
struct node{
    int x, dist;
};
struct cmp{
    bool operator()(node a, node b){
        return a.dist > b.dist;
    }
};
void add(int x, int y, int z){
    e[idx] = y, w[idx] = z, ne[idx] = h[x], h[x] = idx ++;
}
void dij(){
    priority_queue<node, vector<node>, cmp> q;
    q.push({1, 0});
    dist[1] = 0;
    while(!q.empty()){
        node t = q.top();
        q.pop();
        if(st[t.x]) continue;
        st[t.x] = true;
        for(int i = h[t.x]; ~ i; i = ne[i]){
            int j = e[i];
            if(dist[j] > dist[t.x] + w[i]){
                dist[j] = dist[t.x] + w[i];
                q.push({j, dist[j]});
            }
        }
    }
}
int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    memset(dist, 0x3f, sizeof dist);
    for(int i = 1; i <= m; i ++){
        int x, y, z;
        cin >> x >> y >> z;
        add(x, y, z);
    }
    dij();
    cout << (dist[n] > 0x3f3f3f3f / 2 ? -1 : dist[n]);
    return 0;    
}

朴素版

#include <bits/stdc++.h>
using namespace std;
const int N = 600;
int a[N][N], bj[N], s[N];
int n, m, x, y, z;
int f() {
    s[1] = 0;
    for (int i = 1; i <= n; i++) {
        int t = -1;
        for (int j = 1; j <= n; j++) {
            if (bj[j] == 0 && (t == -1 || s[j] < s[t])) {
                t = j;
            }
        }
        bj[t] = 1;
        for (int j = 1; j <= n; j++) {
            s[j] = min(s[j], s[t] + a[t][j]);
        }

    }
    if (s[n] > 0x3f3f3f3f / 2) return -1;
    else return s[n];
}
int main() {
    memset(a, 0x3f, sizeof a);
    memset(s, 0x3f, sizeof s);
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        cin >> x >> y >> z;
        a[x][y] = min(a[x][y], z);
    }
    int t = f();
    cout << t;
}

Bellman-ford

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。

注意:图中可能存在负权回路 。

#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 10010;
struct Edge{
    int a, b, c;
}edges[M];
int n, m, k, dist[N], last[N];
void bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    for (int i = 0; i < k; i ++ ){
        memcpy(last, dist, sizeof dist);
        for (int j = 0; j < m; j ++ ){
            auto e = edges[j];
            dist[e.b] = min(dist[e.b], last[e.a] + e.c);
        }
    }
}
int main()
{
    scanf("%d%d%d", &n, &m, &k);
    for (int i = 0; i < m; i ++ ){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edges[i] = {a, b, c};
    }
    bellman_ford();
    if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
    else printf("%d\n", dist[n]);
    return 0;
}

Spfa

负权可以

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 100, M = N;
int h[N], e[M], ne[M], w[M], idx, n, m, st[N], dist[N];
void add(int x, int y, int z) {
    e[idx] = y, w[idx] = z, ne[idx] = h[x], h[x] = idx++;
}
void spfa() {
    queue<int> q;
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    st[1] = 1;
    q.push(1);
    while (!q.empty()) {
        int t = q.front();
        q.pop();
        st[t] = 0;
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                if (!st[j]) {
                    st[j] = 1;
                    q.push(j);
                }
            }
        }
    }
}
int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        cin >> x >> y >> z;
        add(x, y, z);
    }
    spfa();
    if (dist[n] >= 0x3f3f3f3f / 2) cout << "impossible";
    else cout << dist[n];
    return 0;
}

Floyd

Floyd可以解决的问题:不含负环

1:最短路

floyd实际上是一种dp,d[k,i,j]表示从i出发到达j,只经过节点编号不超过k的最短路。

呢么可以分为两种,包含节点k和不包含节点k,因为没有负环,所以floyd算的的最短路中一定没有重复点,否则就有负环。

呢么d[k,i,j]=min(d[k-1,i,j],d[k-1,i,k]+d[k-1,k,j];发现可以去掉一维

d[i,j]=min(d[i,j],d[i,k]+d[k,j]);

注意floyd中有时候要用到一个函数memcpy(d,g,sizeof g);//d是dist数组,g是原图,

因为我们跑最短路最好不要在原图上面跑,后面可能会用到原图。

memset(g,0x3f,sizeof g);//初始化
for(int i=1;i<=n;i++) g[i][i]=0;
for(int i=1;i<=m;i++)
{
    int x,y,z;
    cin>>x>>y>>z;
    g[x][y]=g[y][x]=min(g[x][y],z);//防止有重边
}
for(int k=1;k<=n;k++)//floyd
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(g[i][j]>g[i][k]+g[k][j])
                g[i][j]=g[i][k]+g[k][j];

2:传递背包:离散数学i可以到k,k可以到j,呢么i就可以到j。初始化化可以到达的点为1,不能到达的点为0。

3:最小环

我们把环分类,按照环中最大节点的编号分类。 我们开始枚举k的时候,已经求得了任意两点只经过编号为1~k-1的最短路径,这恰好可以帮助我们求环。 环一定是i-k-j这是边,然后j-…-i,这个路径只能包含1~k-1,否则环还可以更短。 所以当k固定的时候,我们只需要枚举i和j(i和j应该都小于k)。 pos[i,j]表示i到j需要经过的中间节点

#include <bits/stdc++.h>
using namespace std;
const int N = 105;
int g[N][N], pos[N][N], d[N][N];
int n, m;
vector<int> v;
void get_pos(int x, int y)//输出x到y之间不包括x和y的道路
{
    if (pos[x][y] == 0) return;//x和y之间没有中间点
    get_pos(x, pos[x][y]);
    v.push_back(pos[x][y]);
    get_pos(pos[x][y], y);
}
int main() {
    cin >> n >> m;
    memset(g, 0x3f, sizeof g);//初始化
    for (int i = 1; i <= n; i++) g[i][i] = 0;
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        cin >> x >> y >> z;
        g[x][y] = g[y][x] = min(g[x][y], z);
    }
    memcpy(d, g, sizeof g);//一个存图,一个跑最短路!!!
    int ans = 1e9;
    //如果存在一个最小环的话,呢么环上的点必定不重复
    for (int k = 1; k <= n; k++) {
        //这里已经求得了从i->j,最大点编号为k-1的最短路
        //我们把环分类,以环中最大点的编号分类,呢么环一定是 i-k-j-...-i,注意i-k-j为两个边,
        //我们枚举i到j就行,所以只要i-j(只经过1~k-1个点)最小就行
        for (int i = 1; i < k; i++)//注意范围
        {
            for (int j = i + 1; j < k; j++)//i和j不相同
            {
                if (ans > (long long) d[i][j] + g[i][k] + g[k][j])//有可能0x3f3f3f3f累加爆int
                {
                    v.clear();
                    ans = d[i][j] + g[i][k] + g[k][j];
                    v.push_back(i);
                    v.push_back(k);
                    v.push_back(j);
                    get_pos(j, i);
                }
            }
        }
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                if (d[i][j] > d[i][k] + d[k][j]) {
                    d[i][j] = d[i][k] + d[k][j];
                    pos[i][j] = k;
                }
            }
        }
    }
    if (ans == 1e9) puts("No solution.");
    else {
        for (auto i: v) {
            cout << i << " ";
        }
    }
    return 0;
}

4:恰好经过k条边的最短路

判负环

(1)由于负环可能并不包含1号点,或者图不连通。所以要建立一个超级源点,从这个点向各个点连一条边权是0的边,然后进行Spfa的第一次出队更新,这等价于初始就将所有点入队。

(2)所以当存在超级源点的时候,求负环时不需要将所有点全部入队了。

(3)求负环或者正环,与dist[i]初始值无关,因为有负环dist就会被不断更新为负无穷,反之为正无穷。但是求最短路顺便判断负环的时候,dist[i]设为正无穷。求最长路顺便判断正环的时候,dist[i]设为负无穷。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 100, M = N;
int h[N], e[M], ne[M], w[M], idx, n, m, st[N], dist[N], cnt[N];
void add(int x, int y, int z) {
    e[idx] = y, w[idx] = z, ne[idx] = h[x], h[x] = idx++;
}
bool spfa() {
    queue<int> q;
    memset(dist, 0x3f, sizeof dist);
    for (int i = 1; i <= n; i++) {
        q.push(i);
        st[i] = 1;
    }
    while (!q.empty()) {
        int t = q.front();
        q.pop();
        st[t] = 0;
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[t] + w[i]) {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;
                if (!st[j]) {
                    st[j] = 1;
                    q.push(j);
                }
            }
        }
    }
    return false;
}
int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y, z;
        cin >> x >> y >> z;
        add(x, y, z);
    }
    if (spfa()) cout << "Yes" << endl;
    else cout << "No" << endl;
    return 0;
}

差分约束

只要存在一个点可以走到所有点,呢么一定可以走到所有边。

当建立超级源点的边权为0时,等价于将所有点入队。边权不为0还是得建立一个超级源点。

有超级源点了,判断负环不需要将所有点入队,这个超级源点可能是题目中有,或者你自己建立。

如果所有边权都是>=0,并且要求最长路,呢么可以用强连通分量解决,正环等价于强连通分量不存再一条>0的边

最近公共祖先

#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
#define endl "\n"
typedef long long LL;
const int N = 1e5 + 10, M = 2 * N, mod = 1e9 + 7;
using namespace std;
int h[N], e[M], ne[M], idx, root, n, m;
int f[N][18], depth[N];
void add(int x, int y){
    e[idx] = y, ne[idx] = h[x], h[x] = idx ++;
}
void dfs(int u, int fa){
    for(int i = h[u]; ~i; i = ne[i]){
        int j = e[i];
        if(j == fa) continue;
        depth[j] = depth[u] + 1;
        f[j][0] = u;
        for(int k = 1; k <= 17; k ++){
            f[j][k] = f[f[j][k - 1]][k - 1];
        }
        dfs(j, u);
    }
}
int lca(int x, int y){
    if(depth[x] < depth[y]) swap(x, y);
    for(int k = 17; k >= 0; k --){
        if(depth[f[x][k]] >= depth[y]){
            x = f[x][k];
        }
    }
    if(x == y) return x;
    //没有depth了
    for(int k = 17; k >= 0; k --){
        if(f[x][k] != f[y][k]){
            x = f[x][k];
            y = f[y][k];
        }
    }
    return f[x][0];
}
int main()
{
    cin >> n;
    memset(h, -1, sizeof h);
    for(int i = 1; i <= n; i ++){
        int x, y;
        cin >> x >> y;
        if(y == -1) {
            root = x;
            continue;
        }
        add(x, y), add(y, x);
    }
    depth[root] = 1;
    dfs(root, -1);
    cin >> m;
    while(m --){
        int x, y;
        cin >> x >> y;
        int z = lca(x, y);
        if(z == x) cout << 1 << endl;
        else if(z == y) cout << 2 << endl;
        else cout << 0 << endl;
    }
    return 0;
}

树上差分

将a到b路径上的所有边全部+c,可以先d[a]+=c,d[b]+=c , d[lca(a,b)]-=2c;

最后在一遍dfs求出最终的各边权值

以x节点为根,子树的点权值之和就是x节点与之父节点的边权。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 100010, M = 2 * N;
int h[N], e[M], ne[M], idx;
int depth[N], d[N], f[N][18];
int n, m;
LL ans;
void add(int x, int y) {
    e[idx] = y, ne[idx] = h[x], h[x] = idx++;
}
void bfs() {
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[1] = 1;
    queue<int> q;
    q.push(1);
    while (!q.empty()) {
        int t = q.front();
        q.pop();
        for (int i = h[t]; ~i; i = ne[i]) {
            int j = e[i];
            if (depth[j] > depth[t] + 1) {
                depth[j] = depth[t] + 1;
                q.push(j);
                f[j][0] = t;
                for (int k = 1; k <= 17; k++) {
                    f[j][k] = f[f[j][k - 1]][k - 1];
                }
            }
        }
    }
}
int lca(int x, int y) {
    if (depth[x] < depth[y]) swap(x, y);
    for (int i = 17; i >= 0; i--) {
        if (depth[f[x][i]] >= depth[y]) {
            x = f[x][i];
        }
    }
    if (x == y) return x;
    for (int i = 17; i >= 0; i--) {
        if (f[x][i] != f[y][i]) {
            x = f[x][i];
            y = f[y][i];
        }
    }
    return f[x][0];
}
int dfs(int x, int fa) {
    LL res = d[x];
    for (int i = h[x]; ~i; i = ne[i]) {
        int j = e[i];
        if (j != fa) {
            res += dfs(j, x);
        }
    }
    if (x == 1) return 0;
    if (res == 0) ans += m;
    else if (res == 1) ans += 1;
    return res;
}
int main() {
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for (int i = 1; i < n; i++) {
        int x, y;
        cin >> x >> y;
        add(x, y), add(y, x);
    }
    bfs();
    for (int i = 1; i <= m; i++) {
        int x, y;
        cin >> x >> y;
        d[x]++, d[y]++, d[lca(x, y)] -= 2;
    }
    dfs(1, -1);
    cout << ans << endl;
    return 0;
}

强连通分量

有向图

时间复杂度线性

强连通分量一定有环,但是不一定就是一个环

环一定是强连通分量

/*
1. 加时间戳;
2. 放入栈中,做好标记;
3. 遍历邻点
    1)如果没遍历过,tarjan一遍,用low[j]更新最小值low
    2) 如果在栈中,用dfn[j]更新最小值low
4.找到最高点
    1)scc个数++
    2)do-while循环:
        从栈中取出每个元素;标志为出栈;
        对元素做好属于哪个scc;该scc中点的数量++
*/
// tarjan 算法求强连通分量
// 时间复杂度O(n+ m)
void tarjan(int u){
    // 初始化自己的时间戳
    dfn[u] = low[u] = ++ timestamp;
    //将该点放入栈中
    stk[++ top] = u, in_stk[u] = true;
    // 遍历和u连通的点
    for(int i = h[u]; ~i; i = ne[i]){
        int j = e[i];
        if(!dfn[j]){
            tarjan(j);
            // 更新u所能遍历到的时间戳的最小值
            low[u] = min(low[u], low[j]);
        }
        // 如果当前点在栈中
        //注意栈中存的可能是树中几个不同分支的点,因为有横叉边存在
        // 栈中存的所有点,是还没搜完的点,同时都不是强连通分量的最高点
        // 这里表示当前强连通分量还没有遍历完,即栈中有值
        else if(in_stk[j])
            //更新一下u点所能到的最小的时间戳
            //此时j要么是u的祖先,要么是横叉边的点,时间戳小于u
            low[u] = min(low[u], dfn[j]);
    }
    // 找到该连通分量的最高点
    if(dfn[u] == low[u]){
        int y;
        ++ scc_cnt; // 强连通分量的个数++
        do{// 取出来该连通分量的所有点
            y = stk[top --];
            in_stk[y] = false;
            id[y] = scc_cnt; // 标记点属于哪个连通分量
            size_scc[scc_cnt] ++;
        } while(y != u);
    }
}
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 100, M = 5e4 + 100;
int h[N], e[M], ne[M], idx, n, m;
int timestamp, dfn[N], low[N], scc_size[N], id[N], scc_cnt, dout[N];
bool in_stk[N];
stack<int> stk;
void add(int x, int y) {
    e[idx] = y, ne[idx] = h[x], h[x] = idx++;
}
void tarjan(int u) {
    dfn[u] = low[u] = ++timestamp;
    stk.push(u), in_stk[u] = true;
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (!dfn[j]) {
            tarjan(j);
            low[u] = min(low[j], low[u]);
        } else if (in_stk[j]) {
            low[u] = min(low[u], dfn[j]);
        }
    }
    if (dfn[u] == low[u]) {
        int y;
        scc_cnt++;
        do {
            y = stk.top();
            stk.pop();
            in_stk[y] = false;
            id[y] = scc_cnt;
            scc_size[scc_cnt]++;
        } while (y != u);
    }
}
int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    for (int i = 1; i <= m; i++) {
        int x, y;
        cin >> x >> y;
        add(x, y);
    }
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) tarjan(i);
    }
    for (int i = 1; i <= n; i++) {
        for (int j = h[i]; ~j; j = ne[j]) {
            int x = id[i], y = id[e[j]];
            if (x != y) {
                dout[x]++;
            }
        }
    }
    int sum = 0, nb;
    for (int i = 1; i <= scc_cnt; i++) {
        if (!dout[i]) {
            sum++;
            nb = i;
        }
    }
    if (sum == 1) cout << scc_size[nb];
    else cout << 0 << endl;
    return 0;
}

缩点

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 100, M = 2e5 + 100;//记得边数翻两倍
int h[N], e[M], ne[M], idx, n, m;
int hh[N];
int dfn[N], low[N], in_stk[N], timestamp, scc_cnt, scc_value[N], a[N], f[N], id[N];
stack<int> s;
void add(int h[], int x, int y) {
    e[idx] = y, ne[idx] = h[x], h[x] = idx++;
}
void tarjan(int u) {
    dfn[u] = low[u] = ++timestamp;
    s.push(u);
    in_stk[u] = 1;
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (!dfn[j]) {
            tarjan(j);
            low[u] = min(low[u], low[j]);
        } else if (in_stk[j]) {
            low[u] = min(low[u], dfn[j]);
        }
    }
    if (dfn[u] == low[u]) {
        int y;
        scc_cnt++;
        do {
            y = s.top();
            s.pop();
            in_stk[y] = 0;
            id[y] = scc_cnt;
            scc_value[scc_cnt] += a[y];
        } while (y != u);
    }
}
int main() {
    memset(h, -1, sizeof h);
    memset(hh, -1, sizeof hh);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    for (int i = 1; i <= m; i++) {
        int x, y;
        cin >> x >> y;
        add(h, x, y);
    }
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) tarjan(i);
    }
    for (int i = 1; i <= n; i++) {
        for (int j = h[i]; ~j; j = ne[j]) {
            int x = i, y = e[j];
            if (id[x] != id[y]) {
                add(hh, id[x], id[y]);
            }
        }
    }
    for (int i = 1; i <= scc_cnt; i++) {
        f[i] = scc_value[i];
    }
    for (int i = scc_cnt; i >= 1; i--) {
        for (int j = hh[i]; ~j; j = ne[j]) {
            int k = e[j];
            f[k] = max(f[k], f[i] + scc_value[k]);
        }
    }
    int ans = -0x3f3f3f3f;
    for (int i = 1; i <= scc_cnt; i++) {
        ans = max(ans, f[i]);
    }
    cout << ans << endl;
    return 0;
}

双联通分量

无向图

割边(桥)

边的双连通分量 : 极大的不包含桥的连通块

边的双连通分量 <=> 任何两个点之间至少存在两个不相交路径

x和y之间是桥 <=> low[y] >dfn[x] y无论如何往上走不到x

将一个图变为边的双联通分量最少要添加几条边:缩点后,(度为1的点的个数+1)/2向下取整

void tarjan(int u, int from)
{
    dfn[u] = low[u] = ++ timestamp;
    stk[ ++ top] = u;
    for (int i = h[u]; i!=-1; i = ne[i])
    {
        int j = e[i];
        if (!dfn[j])//j未遍历过
        {
            tarjan(j, i);//dfs(j)
            low[u] = min(low[u], low[j]);//用j更新u
            if (dfn[u] < low[j])//j到不了u
                // 则x-y的边为桥,
                //正向边is_bridge[i] 反向边is_bridge[i ^ 1]都是桥
                is_bridge[i] = is_bridge[i ^ 1] = true;
                // 这里i==idx 如果idx==奇数 则反向边=idx-1 = idx^1
                //            如果idx==偶数 则反向边=idx+1 = idx^1
        }
        // j遍历过 且i不是反向边(即i不是指向u的父节点的边)
        // 因为我们不能用u的父节点的时间戳更新u
        else if (i != (from ^ 1))//不能往回
            low[u] = min(low[u], dfn[j]);
    }
    if (dfn[u] == low[u])
    {
        ++ dcc_cnt;
        int y;
        do {
            y = stk[top -- ];
            id[y] = dcc_cnt;
        } while (y != u);
    }
}
#include <bits/stdc++.h>
using namespace std;
const int N = 5010, M = 20010;
int h[N], e[M], ne[M], idx, n, m;
int dfn[N], low[N], id[N], dcc_cnt, timestamp;
stack<int> stk;
bool is_bridge[M];
int d[N];
void add(int x, int y) {
    e[idx] = y, ne[idx] = h[x], h[x] = idx++;
}
void tarjan(int u, int from)//from表示从哪个边来的
{
    dfn[u] = low[u] = ++timestamp;
    stk.push(u);
    for (int i = h[u]; ~i; i = ne[i]) {
        int j = e[i];
        if (!dfn[j]) {
            tarjan(j, i);
            low[u] = min(low[u], low[j]);
            if (low[j] > dfn[u]) is_bridge[i] = is_bridge[i ^ 1] = true;//j无论如何也走不到u,说明只有一条路径,是割边(桥)
        } else if (i != (from ^ 1))//肯定不能往回更新,相当于只能往下
        {
            low[u] = min(low[u], dfn[j]);
        }
    }
    if (low[u] == dfn[u]) {
        int y;
        dcc_cnt++;
        do {
            y = stk.top();
            stk.pop();
            id[y] = dcc_cnt;
        } while (y != u);
    }
}
int main() {
    cin >> n >> m;
    memset(h, -1, sizeof h);
    for (int i = 1; i <= m; i++) {
        int x, y;
        cin >> x >> y;
        add(x, y), add(y, x);
    }
    for (int i = 1; i <= n; i++) {
        if (!dfn[i]) tarjan(i, -1);
    }
    for (int i = 0; i < idx; i += 2)//缩点,但是没必要真的缩点,i+=2防止重复计算
    {
        if (is_bridge[i])//i是桥的话
        {
            d[id[e[i]]]++;
            d[id[e[i ^ 1]]]++;
        }
    }
    int sum = 0;
    for (int i = 1; i <= dcc_cnt; i++) {
        if (d[i] == 1) sum++;
    }
    cout << (sum + 1) / 2 << endl;//结论
    return 0;
}

二分图

判断

二分图存在的充分必要条件是没有奇数边的环,二分图一般指的都是无向图

时间复杂度:$O(n+m)$

棋盘问题,很多都是二分图,$(i+j)$为偶数染成1,为奇数染成2

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;
int h[N], e[N], ne[N], col[N], idx, n, m, x, y;//col中,0表示未染色,1,2表示两种颜色
void add(int x, int y) {
    e[idx] = y;
    ne[idx] = h[x];
    h[x] = idx++;
}
bool dfs(int x, int q)//x表示正在搜索哪个点,q表示要染色的颜色
{

    col[x] = q;//染色
    for (int i = h[x]; i != -1; i = ne[i]) {
        int j = e[i];
        if (col[j] == 0)//如果这个点没被染过色的话
        {
            if (dfs(j, 3 - q) == false) return false;//对这个点染色失败了,就可以返回false
        }//3-q可以让1和2相互转化
        else if (col[j] == q) return false;//这个点已经被染过了,并且颜色和父节点相同
    }
    return true;//不矛盾,染色成功
}
int main() {
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while (m--) {
        scanf("%d%d", &x, &y);
        add(x, y), add(y, x);
    }
    int flag = 0;
    for (int i = 1; i <= n; i++) {
        if (col[i] == 0)//如果这个点没被染过色的话,直接染色
        {
            if (dfs(i, 1) == false)//有一个点染色失败(发生矛盾),就直接可以break
            {
                flag = 1;
                break;
            }
        }
    }
    if (flag == 0) cout << "Yes" << endl;
    else cout << "No" << endl;
}

最大匹配

增广路径,从一个非匹配点出发,依次走非匹配边、匹配边、非匹配边、匹配边,最后走到一份非匹配边的算法

二分图的最大匹配等价于不存在增广路径。

实际复杂的$O(nm)$,但是实际上复杂度远小于$O(nm)$

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;//match放的是女朋友对应的男朋友是谁,bj是为了防止男生重复匹配一个人
int h[N], e[N], ne[N], idx, bj[N], match[N], n1, n2, m, x, y, ans;
void add(int x, int y) {
    e[idx] = y, ne[idx] = h[x], h[x] = idx++;
}
bool find(int x) {
    for (int i = h[x]; i != -1; i = ne[i])//遍历与这名男生相联系的女生
    {
        int j = e[i];
        if (bj[j] == 1) continue;//如果这个女生已经匹配过了,就不在匹配了
        bj[j] = 1;
        if (match[j] == 0 || find(match[j]) == true)//如果女生没有对象,或者女生的对象还可以匹配另一个女生
        {
            match[j] = x;//以这位女生建立关系
            return true;//成功匹配,返回true
        }
    }
    return false;//没有找到对象,返回false
}
int main() {
    cin >> n1 >> n2 >> m;
    memset(h, -1, sizeof h);
    while (m--) {
        scanf("%d%d", &x, &y);
        add(x, y);
    }
    for (int i = 1; i <= n1; i++) {
        memset(bj, 0, sizeof bj);//每一次匹配都要初始化一下
        if (find(i) == true) ans++;//成功匹配答案就加1
    }
    cout << ans;
    return 0;
}

二分图中:

最小点覆盖定义: 二分图中,选出最少的点,使每一条边的两个端点,至少有一个被选中。

最大独立集:二分图中,选出最多的点,使得选出的点之间没有边

最少路径覆盖:用最少的互不相交的路径,将所有点都覆盖住。

最少路径重复的覆盖:

欧拉路径(回路)

欧拉路径:是否存在一种路径,可以使所有的边恰好走一次;

欧拉回路:是否存在一种回路,是所有边恰好走一次并且回到原点

欧拉路径也就是一笔画问题,起点的度必然是奇数,终点的度也是奇数,其他点的度必须是偶数

存在欧拉路径和欧拉回路的前提是所有边联通!!!

无向图存在欧拉路径的充分必要条件:度数为奇数的点只能为2个或者0个(起点=终点);

无向图存在欧拉回路的充分必要条件:度数为奇数的点只能为0个;

有向图存在欧拉路径的充分必要条件:要么所有点的入度都等于出度(起点=终点);要么除了两个点外其余的所有点,入度等于出度,这两个点,一个出度比入度多1(起点),另一个入度比出度多1(终点);

有向图存在欧拉回路的充分必要条件:所有点的入度都等于出度;

数论

质数定理:1~n中质数的个数为$n/ln(n)$

$1~n$中能被$x$整除的个数为$\lfloor$n/x$\rfloor$$下取整

质数

试除法判定质数

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
bool f(int x) {
    if (x <= 1) return false;
    for (int i = 2; i <= x / i; i++) {
        if (x % i == 0) return false;
    }
    return true;
}
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        if (f(x)) cout << "Yes" << endl;
        else cout << "No" << endl;
    }
    return 0;
}

埃氏筛

bool st[N];
int primes[N], cnt;
void init(int n) {
    st[0] = st[1] = true;
    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;
        }
    }
}

线性筛法

#include <bits/stdc++.h>
#define endl "\n"
const int N = 1e8 + 10, M = N, mod = 1e9 + 7;
using namespace std;
int primes[N], cnt;
bool st[N];
void init(int n){
    st[0] = st[1] = true;
    for(int i = 2; i <= n; i ++){
        if(!st[i]) primes[++ cnt] = i;
        for(int j = 1; j <= cnt && i * primes[j] <= n; j ++){
            st[i * primes[j]] = true;
            if(i % primes[j] == 0) break;
        }
    }
}
int main()
{
    int n, q;
    cin >> n >> q;
    init(n);
    while(q --){
        int k;
        cin >> k;
        cout << primes[k] << endl;
    }
    return 0;
}

质因数和约数

质因数

#include <bits/stdc++.h>
using namespace std;
void f(int n) {
    for (int i = 2; i <= n / i; i++) {
        if (n % i == 0) {
            int s = 0;
            while (n % i == 0) {
                n = n / i;
                s ++;
            }
            cout << i << " " << s << endl;
        }
    }
    if (n > 1) cout << n << " " << 1 << endl;
}
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        f(x);
        cout << endl;
    }
}

约数

试除法

#include <bits/stdc++.h>
using namespace std;
void f(int x) {
    vector<int> v;
    for (int i = 1; i <= x / i; i++) {
        if (x % i == 0) {
            v.push_back(i);
            if (i != x / i) v.push_back(x / i);
        }
    }
    sort(v.begin(), v.end());
    for(auto it : v) cout << it << " ";
    cout << endl;
}
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        f(x);
    }
    return 0;
}

求约数个数

$N=p_1^{\alpha_1}p_1^{\alpha_2}\cdots*p_1^{\alpha_k}$

$cnt=(\alpha_1+1)(\alpha_2+1)\cdots*(\alpha_k+1)$

#include <bits/stdc++.h>
typedef long long LL;
using namespace std;
const int N = 1e6, mod = 1e9 + 7;
map<int, int> mp;
void solve(int n) {
    for (int i = 2; i <= n / i; i++) {
        while (n % i == 0) {
            n /= i;
            mp[i]++;
        }
    }
    if (n > 1) mp[n]++;
}
int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        solve(n);
    }
    LL sum = 1;
    for (auto it: mp) {
        sum = sum * (it.second + 1) % mod;
    }
    cout << sum << endl;
    return 0;
}

约数之和

$N=p_1^{\alpha_1}p_1^{\alpha_2}\cdots*p_1^{\alpha_k}$

$sum=(p_1^0+p_1^1+\cdots+p_1^{\alpha_1})\cdots(p_2^0+p_2^1+\cdots+p_2^{\alpha_2})\cdots(p_k^0+p_k^1+\cdots +p_k^{\alpha_k}) $

如何求出$p_k^0+p_k^1+\cdots +p_k^{\alpha_k}$,当成$p$进制,就可以,每一位都是$1*权重$

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e6, mod = 1e9 + 7;
map<int, int> mp;
void solve(int n) {
    for (int i = 2; i <= n / i; i++) {
        while (n % i == 0) {
            n /= i;
            mp[i]++;
        }
    }
    if (n > 1) mp[n]++;
}
int main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        solve(n);
    }
    LL sum = 1;
    for (auto it: mp) {
        LL x = 0;
        for (int i = it.second; i >= 0; i--)//类似于进制转化
        {
            x = (x * it.first + 1) % mod;
        }
        sum = sum * x % mod;
    }
    cout << sum << endl;
    return 0;
}

最大公约数

欧几里得算法: $gcd(a,b)=gcd(b,a%b)$

性质:$ d|a$,$d|b$则 $d|(a+b)$ d能整除a,d能整除b,则d能整除(a+b)很好证明,直接设就行

int gcd(int a, int b) {
    return b ? gcd(b, a % b) : a;
}

扩展欧几里得:求$ax+by=gcd(a,b)$的一组解

通解$x=x_0-b/d*k$ $k$是整数

​ $y=y_0+a/d*k$

x最小非负数是多少?另$t=b/d$ 则 $x=x_0-t*k$

$x_{min}=(x_0\%t+t)\%t$

如何求$ax+by=d$的解?

有解的充分必要条件是$gcd(a,b)|d$

扩展欧几里得求出$ax+by=gcd(a,b)$的一组解,乘上系数即可。

#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 1e6;
int exgcd(int a, int b, int &x, int &y) {
    if (b == 0) {
        x = 1, y = 0;
        return a;
    }
    int d = exgcd(b, a % b, y, x);//交换一下顺序
    y = y - a / b * x;
    return d;
}
int main() {
    int n;
    scanf("%d", &n);
    while (n--) {
        int a, b, x, y;
        scanf("%d%d", &a, &b);
        int d = exgcd(a, b, x, y);
        printf("%d %d\n", x, y);
    }
    return 0;
}

欧拉函数

$\phi(n)$为$1$~$n$直接中与$n$互质数的个数



快速幂

快速幂求逆元,要求逆元存在(a和p互质),并且p是质数。

LL qpow(LL a, LL b, LL p) {
    LL ans = 1;
    while (b) {
        if (b & 1) ans = ans * a % p;
        b >>= 1;
        a = a * a % p;//这里别忘了mod
    }
    return ans;
}

矩阵快速幂

#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
#define endl "\n"
typedef long long LL;
const int N = 1e5 + 10, M = N, mod = 1e9 + 7;
using namespace std;
struct Mat{
    LL a[105][105];
    int r, c;
    Mat(int _r, int _c){
        r = _r, c = _c;
        memset(a, 0, sizeof a);
    }
    //单位矩阵
    void unit(){
        memset(a, 0, sizeof a);
        for(int i = 1; i <= r; i ++){
            a[i][i] = 1;
        }
    }
    Mat operator+(const Mat t) const{
        Mat ans = *this;
        for(int i = 1; i <= r; i ++){
            for(int j = 1; j <= c; j ++){
                ans.a[i][j] += t.a[i][j];
            }
        }
        return ans;
    }
    Mat operator-(const Mat t) const{
        Mat ans = *this;
        for(int i = 1; i <= r; i ++){
            for(int j = 1; j <= c; j ++){
                ans.a[i][j] -= t.a[i][j];
            }
        }
        return ans;
    }
    Mat operator*(const Mat t) const{
        Mat ans(r, t.c);
        int n = r, m = t.c;
        for(int i = 1; i <= n; i ++){
            for(int j = 1; j <= m; j ++){
                for(int k = 1; k <= c; k ++){
                    ans.a[i][j] += a[i][k] * t.a[k][j];
                    ans.a[i][j] %= mod;
                }
            }
        }
        return ans;
    }
    Mat pow(LL b){
        Mat ans(r, c), _a = *this;
        ans.unit();
        while(b){
            if(b & 1) ans = ans * _a;
            _a = _a * _a;
            b >>= 1;
        }
        return ans;
    }
    void output(){
        for(int i = 1; i <= r; i ++){
            for(int j = 1; j <= c; j ++){
                cout << a[i][j] << " ";
            }
            cout << endl;
        }
    }
};
int main()
{
    LL n;
    cin >> n;
    Mat a(2, 2), temp(1, 2);
    temp.a[1][1] = temp.a[1][2] = 1,
    a.a[1][1] = 0, a.a[1][2] = a.a[2][1] = a.a[2][2] = 1;
    Mat ans = temp * a.pow(n - 2);
    cout << ans.a[1][2];
    return 0;
}

龟速乘

和快速幂思想一样,快速幂是把乘法变成乘法,龟速乘是把乘法变成加法,速度变慢了,但是不会爆long long

LL qadd(LL a, LL b, LL p) {
    LL ans = 0;
    while (b) {
        if (b & 1) ans = (ans + a) % p;
        b >>= 1;
        a = (a + a) % p;
    }
    return ans;
}

组合数

$C_n^m$ $=$ $n(n-1)(n-2)\cdot\cdot\cdot(n-m+1) \over m(m-1)\cdot\cdot*1$

$C_a^b=$$a!\over b!*(a-b)!$

C[a,b]=C[a-1,b]+C[a-1,b-1];

预处理时间复杂度:$O(N^2)$

const int N = 2005, 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 == 0) c[i][j] = 1;
            else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
        }
    }
}

预处理时间复杂度$O(N*log(N))$

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL N = 1e5 + 8, mod = 1e9 + 7;
LL fact[N], infact[N];
LL qmi(LL a, LL k, LL p) {
    LL ans = 1;
    while (k != 0) {
        if (k & 1) ans = ans * a % p;
        k = k >> 1;
        a = a * a % p;
    }
    return ans;
}
void init() {
    fact[0] = infact[0] = 1;//0的阶乘为1,除0的阶乘也为1
    for (int i = 1; i < N; i++) {
        fact[i] = fact[i - 1] * i % mod;
        infact[i] = infact[i - 1] * qmi(i, mod - 2, mod) % mod;
    }
}
LL n, a, b;
int main() {
    cin >> n;
    init();
    while (n--) {
        scanf("%lld%lld", &a, &b);
        printf("%lld\n", fact[a] * infact[b] % mod * infact[a - b] % mod);//要及时取模
    }
}

$1<=b<=a<=1e18$

lucas

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int n;
LL qmi(LL a, LL k, LL p) {
    LL ans = 1;
    while (k) {
        if (k & 1) ans = ans * a % p;
        k = k >> 1;
        a = a * a % p;
    }
    return ans;
}
LL p;
LL c(LL a, LL b) {
    if (a < b) return 0;//使用lucas定理有可能a<b
    LL ans = 1;
    LL sum = 1;
    for (int i = 1, j = a; i <= b; i++, j--)//就是高中求组合数的方法
    {
        ans = ans * j % p;
        sum = sum * i % p;
    }
    return ans * qmi(sum, p - 2, p) % p;//为什么可以用快速幂求逆元,因为sum和p一定互质,且p为质数
    //由此可见,p为素数的时候不一定存在逆元,因为不一定互质
}
LL lucas(LL a, LL b) {
    if (a < p && b < p) {
        return c(a, b);//直接用定义求,也是递归的终点
    }
    return c(a % p, b % p) * lucas(a / p, b / p) % p;//一定注意要继续递归,因为a/p可能很大
    //a/p要是大于p的话会出现大麻烦,因为阶乘要mod p
    //a/p=6,p=2的话 a/p%p==0,阶乘怎么可能为0
}
int main() {
    cin >> n;
    while (n--) {
        LL a, b;
        cin >> a >> b >> p;
        cout << lucas(a, b) << endl;
    }
}

容斥原理

时间复杂度$O(2^n)$

分析,有$C_n^1+C_n^3+C_n^2+\cdot\cdot\cdot+C_n^n$,可以补上一个$C_n^0$,所以有$2^n-1$项

QQ浏览器截图20200807160444.png

/*容斥原理的应用
能被p1,p2,…,pm 中的至少一个数整除的整数数量
就是p1∪p2∪p3---∪pm,可以由容斥原理算出;
[1,n]中能被x整除的数有[n/x]个;
其中因为p是素数,所以既能被p1整除又能被p2整除的话,
就相当于被p1*p2整除,如何枚举呢?
采用二进制枚举法,需要枚举m个位置,所以从1枚举到pow(2,m-1);不能是从0开始枚举,因为
全是0表示都没选,不符合题意
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 20;
int a[N];
int n, m;
int main() {
    cin >> n >> m;
    for (int i = 0; i < m; i++)//从0开始存的话方便后面二进制枚举
    {
        cin >> a[i];
    }
    LL ans = 0;
    for (int i = 1; i <= pow(2, m) - 1; i++)//枚举所有的方案
    {
        LL sum = 0, t = 1;//sum为奇数符号为+,sum为偶数符号为-
        int flag = 0;//有可能枚举的数的乘积>n,这样不合法,但是最终答案是对的;
        for (int j = 0; j < m; j++) {
            if (i >> j & 1) {
                sum++;
                if (t * a[j] > n) {
                    flag = 1;
                    break;
                }
                t = t * a[j];
            }

        }
        if (flag == 0) {
            if (sum % 2 == 1) ans += n / t;
            else ans -= n / t;
        }
    }
    cout << ans << endl;
}

DP

背包问题

01背包

#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int dp[N][N], v[N], w[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++){
            if(j < v[i]) dp[i][j] = dp[i - 1][j];//背包容量不够,选不了
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);//决策选还是不选
        }
    }
    cout << dp[n][m];
    return 0;
}

空间优化

#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int dp[N], v[N], w[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 --){//从大到小枚举,注意j >= v[i],并且是j --
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m];
    return 0;
}

求方案数

#include <bits/stdc++.h>
#define CLOSE ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
using namespace std;
typedef long long ll;
typedef pair<int,int>PII;
const int N=110;
int f[N][10010];//从前i个里面选,恰好可以表示出来j
int a[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>a[i];
    f[0][0]=1;
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=m;j++)
        {
            if(a[i]>j)  f[i][j]=f[i-1][j];
            else f[i][j]=f[i-1][j]+f[i-1][j-a[i]];
        }
    }
    cout<<f[n][m];
    return 0;
}

完全背包

时间复杂度$O(n^2)$

#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int dp[N][N], v[N], w[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++){
            if(j < v[i]) dp[i][j] = dp[i - 1][j];//背包容量不够,一个都选不了
            else dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + w[i]);
        }
    }
    cout << dp[n][m];
    return 0;
}

优化版本

#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int dp[N], v[N], w[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 ++){//注意j从v[i]开始枚举
            dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m];
    return 0;
}

求方案数

#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int f[2][N], w[N], v[N], n, m;
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++) {
            if (j < v[i]) f[i & 1][j] = f[(i - 1) & 1][j];
            else f[i & 1][j] = max(f[(i - 1) & 1][j], f[i & 1][j - v[i]] + w[i]);
        }
    }
    cout << f[n & 1][m];
    return 0;
}

多重背包

暴力写法时间复杂度$O(n^3)$

#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n, m;
int v[N], w[N], s[N], 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 - v[i] * k] + w[i] * k);
            }
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

二进制优化写法 时间复杂度$O(n^2*log(s))$

建议数组开$10*n$

#include <bits/stdc++.h>
using namespace std;
const int N = 12010, M = 2010;//建议N = 10 * n
int n, m, cnt;//cnt是打包后的物品数量
int v[N], w[N], f[M];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ){
        int a, b, s;
        cin >> a >> b >> s;
        int k = 1;
        while (k <= s){
            cnt ++;
            //k个物品进行打包,体积是k*a, 价值是k*b
            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;//新物品的数量是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;
}



#include <bits/stdc++.h>
using namespace std;
const int N = 2010;
int n, m, f[N];
int main()
{
    cin >> n >> m;
    for(int i = 1; i<= n; i ++){
        int a, b, s;
        cin >> a >> b >> s;
        //开始打包
        for(int k = 1; k <= s; k *= 2){
            for(int j = m; j >= k * a; j --){
                f[j] = max(f[j], f[j - k * a] + k * b);
            }
            s -= k;
        }
        //剩下的就自己打包成一份
        if(s){
            for(int j = m; j >= s * a; j --){
                f[j] = max(f[j], f[j - s * a] + s * b);
            }
        }
    }
    cout << f[m];
    return 0;
}

分组背包

时间复杂度$O(n^3)$

#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n, m;
int v[N][N], w[N][N], s[N];
int f[N][N];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ){
        cin >> s[i];
        for (int j = 0; j < s[i]; j ++ ){
            cin >> v[i][j] >> w[i][j];
        }
    }
    for (int i = 1; i <= n; i ++ ){
        for (int j = m; j >= 0; j -- ){
            f[i][j] = f[i - 1][j];//第i组一个都不选
            for (int k = 0; k < s[i]; k ++ ){
                if (v[i][k] <= j){
                    f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
                }
            }
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

优化空间

#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n, m;
int v[N][N], w[N][N], s[N];
int f[N];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i ++ ){
        cin >> s[i];
        for (int j = 0; j < s[i]; j ++ ){
            cin >> v[i][j] >> w[i][j];
        }
    }
    for (int i = 1; i <= n; i ++ ){
        for (int j = m; j >= 0; j -- ){
            for (int k = 0; k < s[i]; k ++ ){
                if (v[i][k] <= j){
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
                }
            }
        }
    }
    cout << f[m] << endl;
    return 0;
}

混合背包

混合背包就是将前面三种的背包问题混合起来,有的只能取一次,有的能取无限次,有的只能取$k$次。

先将01背包和完全背包转化成多重背包。

  • 01背包,物品数量为1的多重背包,
  • 完全背包,物品数量为$V_{背包体积}/v_i$的多重背包,虽然物品数量是无限的,但是背包容量是有限的。

再按照多重背包来做即可。

#include <bits/stdc++.h>
using namespace std;
const int N = 1005 * 20;
int f[N], w[N], v[N], n, m;
int main()
{
    int cnt = 0;
    cin >> n >> m;
    for (int i = 1; i <= n; i ++)
    {
        int a, b, s;
        cin >> a >> b >> s;
        if (s == 0) s = m / a;//如果是完全背包,则看成多重背包做
        if (s == -1)//01背包
        {
            cnt ++;
            v[cnt] = a, w[cnt] = b;
        }
        else//多重背包二进制拆分
        {
            int k = 1;
            while (k <= s)
            {
                cnt ++;
                v[cnt] = k * a;
                w[cnt] = k * b;
                s -= k;
                k *= 2;
            }
            if (s)
            {
                cnt ++;
                v[cnt] = s * a;
                w[cnt] = s * b;
            }
        }
    }
    for (int i = 1; i <= cnt; i ++)
        for (int j = m; j >= v[i]; j --)
            f[j] = max(f[j], f[j - v[i]] + w[i]);

    cout << f[m];
    return 0;
}

思路和代码2

01背包当成数量为1的多重背包,然后进行二进制优化。

完全背包的求法与01背包不同,所以单独算完全背包部分。

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int f[N];
int main()
{
    cin >> n >> m;
    for (int i = 0; i < n; i ++)
    {
        int v, w, s;
        cin >> v >> w >> s;
        if (!s)//完全背包
        {
            for (int j = v; j <= m; j ++)
                f[j] = max(f[j], f[j - v] + w);
        }
        else
        {
            if (s == -1) s = 1;//01背包当成数量为1的多重背包
            for (int k = 1; k <= s; k *= 2)
            {
                for (int j = m; j >= k * v; j --)
                    f[j] = max(f[j], f[j - k * v] + k * w);
                s -= k;
            }
            if (s)
            {
                for (int j = m; j >= s * v; j --)
                    f[j] = max(f[j], f[j - s * v] + s * w);
            }
        }
    }
    cout << f[m] << endl;
    return 0;
}

二维费用背包

#include <iostream>
using namespace std;
const int N = 1010, K = 110;
int n, V, M;
int v[N], m[N], w[N];
int f[K][K];
int main()
{
    cin >> n >> V >> M;
    for (int i = 1; i <= n; ++ i)
        cin >> v[i] >> m[i] >> w[i];
    for (int i = 1; i <= n; ++ i)
    {
        for (int j = V; j >= v[i]; -- j)
        {
            for (int k = M; k >= m[i]; -- k)
            {
                f[j][k] = max(f[j][k], f[j - v[i]][k - m[i]] + w[i]);
            }
        }
    }
    cout << f[V][M] << endl;
    return 0;
}

区间dp

按照区间长度递增枚举,才可以dp

#include <bits/stdc++.h>
using namespace std;
const int N = 505;
int s[N], f[N][N];
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        s[i] = s[i - 1] + x;
    }
    //长度为1都是0
    for (int len = 2; len <= n; len++) {
        for (int i = 1; i + len - 1 <= n; i++)//枚举左端点
        {
            int l = i, r = i + len - 1;
            f[l][r] = 0x3f3f3f3f;//初始化
            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];
    return 0;
}

数位dp

数位DP笔记(DFS做法) - AcWing

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
int a[30];
int f[30][15];
int dfs(int pos, int pre, int limit, int lead) {
    if (!pos) return 1;//递归终点
    else if (!limit && !lead && f[pos][pre] != -1) return f[pos][pre];//记忆化,只有无限制、无前导零才算,不然都是未搜索完的情况。
    int res = 0, up = limit ? a[pos] : 9;
    for (int i = 0; i <= up; i++) {
        if (abs(i - pre) < 2) continue;
        if (lead && !i) res += dfs(pos - 1, -5, limit && i == up, 1);
        else res += dfs(pos - 1, i, limit && i == up, 0);
    }
    if (!limit && !lead) f[pos][pre] = res;
    return res;
}
int calc(int x) {
    memset(f, -1, sizeof f);
    int len = 0;
    while (x) {
        a[++len] = x % 10;
        x /= 10;
    }
    return dfs(len, -5, 1, 1);
}
int main() {
    int a, b;
    cin >> a >> b;
    cout << calc(b) - calc(a - 1);
    return 0;
}

状态压缩DP

一般要用long long

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 12;
ll n, s, k, f[N][105][1 << 10];//第一层为枚举到了第i层,第二层为恰好拜访国王的数量,第三层为当前的状态,最坏有pow(2,10)
vector<int> state;//合法的状态
vector<int> h[1 << 10];//当前状态可以转移到哪些状态
bool check(int x) {
    for (int i = 0; i < n; i++) {
        if ((x >> i) & 1 && (x >> (i + 1)) & 1)//相邻的1
        {
            return false;
        }
    }
    return true;
}
int count(int x) {
    int res = 0;
    for (int i = 0; i < n; i++) res += (x >> i) & 1;
    return res;
}
int main() {
    cin >> n >> k;
    for (int i = 0; i < (1 << n); i++) {
        if (check(i)) state.push_back(i);
    }
    for (int i = 0; i < state.size(); i++) {
        for (int j = 0; j < state.size(); j++) {
            if ((state[i] & state[j]) == 0 && check(state[i] | state[j])) {
                h[i].push_back(j);
            }
        }
    }
    f[0][0][0] = 1;
    for (int i = 1; i <= n + 1; i++)//为什么枚举到n+1呢,最后算答案的时候要枚举状态
    {
        for (int j = 0; j <= k; j++) {
            for (int a = 0; a < state.size(); a++) {
                for (auto b: h[a]) {
                    int cnt = count(state[b]);
                    if (cnt > j) f[i][j][b] = f[i - 1][j][b];//类似于01背包,当前状态选不选
                    else f[i][j][b] += f[i - 1][j - cnt][a];
                }
            }
        }
    }
    cout << f[n + 1][k][0];//n+1行,最多是k个,第n+1行一个都不选就是最终的答案
    return 0;
}

博弈论

先手必胜状态:存在一种方式让对手变成先手必败状态。

先手必败状态:无论采取哪种方式,留给对手的都是先手必胜状态。

Nim游戏

先手必胜:$a_1\bigoplus a_2 \bigoplus \cdots \bigoplus a_n \neq 0$

先手必败:$a_1\bigoplus a_2 \bigoplus \cdots \bigoplus a_n=0$

证明:https://www.acwing.com/solution/content/14269/

#include<bits/stdc++.h>
using namespace std;
int main() {
    int n;
    cin >> n;
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        ans = ans ^ x;
    }
    if (ans) cout << "Yes";
    else cout << "No";
}

台阶-Nim游戏

先手必胜:$a1\bigoplus a_3 \bigoplus \cdots \bigoplus a{2n+1} \neq 0$

先手必败:$a2\bigoplus a_4 \bigoplus \cdots \bigoplus a{2n}=0$

证明:和NIm游戏类似,若奇数台阶异或不为0,我一定可以操作让奇数台阶异或变为0,之后若对手拿奇数台阶,呢么留给我的必然是异或不为0,若对手拿偶数台阶,我把他往下拿的,在往下拿,这样局面不改变。一直进行下去,必然是对手先遇到奇数台阶为0,之后对手怎么拿石子下去,我把他的石子往下拿,这样我必胜。

SG函数

$mex$运算:找到一个集合里面不存在的最小的自然数,比如$mex{1,2,3}=0$

$sg(x)=k$,则由定义地$x$可以走到的点的$sg$一定包含$[0,k-1]$里面的任何数

$sg$函数性质:多个独立局面的$sg$值,等于这些局面$sg$值的异或和

$sg=0$是先手必败状态,它只能走下$sg$不为0的状态。

$sg$不等于0,是先手必胜状态,它可以走向一个$sg$为0(必败状态)

#include<bits/stdc++.h>
using namespace std;
int a[105], f[10005];
int k, n;
int sg(int x) {
    if (~f[x]) return f[x];
    unordered_set<int> s;
    for (int i = 1; i <= k; i++) {
        if (x - a[i] >= 0) s.insert(sg(x - a[i]));
    }
    for (int i = 0;; i++) {
        if (!s.count(i)) return f[x] = i;
    }
}
int main() {
    memset(f, -1, sizeof f);
    cin >> k;
    for (int i = 1; i <= k; i++) cin >> a[i];
    cin >> n;
    int res = 0;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        res ^= sg(x);
    }
    if (res) cout << "Yes";
    else cout << "No";
    return 0;
}
Copyright © 版权信息 all right reserved,powered by aspire-zero and Gitbook该文件修订时间: 2025-02-22 18:26:10

results matching ""

    No results matching ""