数据结构与算法(AcWing),持续更新中

学习笔记,仅供参考。
如有错误,欢迎探讨。

To Me

1.例题说明

1.整篇笔记的例题都是AcWing上的。

2.例题模块的第一行为例题题号名称,随后的注释为题目内容,再下面是正确的可以运行的代码。

2.模板说明

1.y总给的模板,或者有我自己的一些修改,但是是正确的。

2.都是伪代码。

3.做题细节总结

1.2分中求mid的时候要用mid = l + (r - l) / 2来做。

2.当输入字符的时候可以用字符数组来代替,因为输入有的时候会有空格,而%c严格读入一个字符,所以有可能把空格读入,导致输入结果不对。%s可以过滤掉空格和回车。

3.memset是以字节赋值的,取数的后八位二进制数,然后把数组里所有数的每个字节都赋值成这样。一般为0或-1。

4.让cin和scanf一样快的方法

cin.tie(0);
ios::sync_with_stdio(false);

5.INT_MAIX一般为0x3f3f3f3f。

一、基础算法

排序

1.快速排序——分治

1.找分界点,一般为数组最左边,最右边,中间,或者随机点。

2.使分界点左边的数都小于等于分界点,右边的数都大于等于分界点。

3.递归

模板:

void quick_sort(int q[], int l, int r)
{
    if (l >= r) return;
int i = l - 1, j = r + 1, x = q[l];
while (i < j)
{
    do i ++ ; while (q[i] < x);
    do j -- ; while (q[j] > x);
    if (i < j) swap(q[i], q[j]);
    else break;
}
	quick_sort(q, l, j), quick_sort(q, j + 1, r);
}

例题:

785 快速排序
//给定你一个长度为 n 的整数数列。
//请你使用快速排序对这个数列按照从小到大进行排序。
//并将排好序的数列按顺序输出。

#include<iostream>
using namespace std;

int n;
const int N = 1e6 + 10;
int q[N];

void quick_sort(int l, int r){
    if(l >= r) return;
    
    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]);
    }
    quick_sort(l, j);
    quick_sort(j+1, r);
}

int main(){
    scanf("%d", &n);
    for(int i = 0; i < n; i++){
        scanf("%d", &q[i]);
    }
    quick_sort(0, n-1);
    for(int i = 0; i < n; i++){
        printf("%d ", q[i]);
    }
    return 0;
}
2.归并排序——分治

1.找分界点,为数组中间的那个点。

2.递归排序:L->mid,mid+1->R

3.归并——合二为一。

模板:

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

例题:

787 归并排序
//给定你一个长度为 n 的整数数列。
//请你使用归并排序对这个数列按照从小到大进行排序。
//并将排好序的数列按顺序输出。

#include<iostream>
using namespace std;

int n;
const int N = 1e6 + 10;
int q[N], temp[N]

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

int main(){
    scanf("%d", &n);
    for(int i = 0; i < n; i++){
        scanf("%d", &q[i]);
    }
    merge_sort(0, n-1);
    for(int i = 0; i < n; i++){
        printf("%d ", q[i]);
    }
}

二分法

1.整数二分

基础的mid写法为:mid = (l + r) / 2

但这种写法当l和r特别大的时候mid会得到一个负数,所以求mid时一般写成:

mid = l + (r - l) / 2,上取整为mid = l + (r - l + 1) / 2

模板1:

// 区间[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;
}

模板2:

// 区间[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;
}

例题:

789 数的范围
//给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
//对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 0 开始计数)。
//如果数组中不存在该元素,则返回 -1 -1。

#include<iostream>
using namespace std;

int n, q;
const int N = 1e6 + 10;
int arr[N];

int main(){
    cin>>n>>q;
    for(int i = 0; i < n; i++){
        cin>>arr[i];
    }
    while(q--){
        int k;
        cin>>k;
        
        int l = 0, r = n - 1;
        while(l < r){
            int mid = l + (r - l) / 2;
            if(arr[mid] >= k) r = mid;
            else l = mid + 1;
        }
        if(k != arr[l]) cout<<"-1 -1"<<endl;
        else{
            cout<<l<<" ";
            int l = 0, r = n - 1;
            while(l < r){
                int mid = l + (r - l + 1) / 2;
                if(arr[mid] <= k) l = mid;
                else r = mid - 1;
            }
            cout<<l<<endl;
        }
    }
    return 0;
}
2.实数(浮点数)二分

一般eps取比题目要求精度多乘0.01就行

模板:

double bsearch_3(double l, double r)
{
    const double eps = 1e-6;   // eps 表示精度,取决于题目对精度的要求
    while (r - l > eps)
    {
        double mid = (l + r) / 2;
        if (check(mid)) r = mid;
        else l = mid;
    }
    return l;
}

例题:

790 数的三次方根
//给定一个浮点数 n,求它的三次方根。

#include<bits/stdc++.h>
using namespace std;

int main(){
    double n;
    cin>>n;
    double l = -10000, r = 10000;
    while(r - l > 1e-8){
        double mid = l + (r - l) / 2;
        if(mid*mid*mid >= n) r = mid;
        else l = mid;
    }
    printf("%lf", l);
    return 0;
}

高精度

1.高精度加法

1.用vector去储存两个数,把两个数定义成String,然后拆分每一位给vector。

2.由于在vector后添加数据更为简单,所以在存储时将数的低位放到数组前面。

模板:

vector<int> add(vector<int> &A, vector<int> &B){
    vector<int> C;
    
    int temp = 0; //进位数
    for(int i = 0; i < A.size() || i < B.size(); i++){ //只要A,B长度有一个没加完,就继续加
        if(i < A.size()) temp += A[i];
        if(i < B.size()) temp += B[i]; 
        C.push_back(temp % 10); //要用push_back,不能数组用数组下标赋值
        temp /= 10;
    }
    
    if(temp) C.push_back(temp); //最后一个进位肯定为1
    return C;
}

例题:

791 高精度加法
//给定两个正整数(不含前导 0),计算它们的和。

#include<iostream>
#include<vector>
using namespace std;

vector<int> add(vector<int> &A, vector<int> &B){
    vector<int> C;
    
    int temp = 0; //进位数
    for(int i = 0; i < A.size() || i < B.size(); i++){ //只要A,B长度有一个没加完,就继续加
        if(i < A.size()) temp += A[i];
        if(i < B.size()) temp += B[i]; 
        C.push_back(temp % 10); //要用push_back,不能数组用数组下标赋值
        temp /= 10;
    }
    
    if(temp) C.push_back(temp); //最后一个进位肯定为1
    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');
    auto C = add(A, B);
    for(int i = C.size() - 1; i >= 0; i--) cout<<C[i];
    return 0;
}
2.高精度减法

默认给的两个整数都是正整数。

A, B, t(下一位找本位借位的数)

t = 0表示没有借位,t = 1表示借位了

t也表示当前位的值。

1.先判断A和B谁大,如果A - B >= 0,那就是A - B;如果A - B < 0,那就是B - A。

2.如果A - B - t >= 0,那结果就是A - B - t;如果A - B - t < 0,那结果就是A - B - t + 10。

模板:

// 判断A和B谁大
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;
}

// 高精度减法
// C = A - B, 满足A >= B, A >= 0, B >= 0
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;
}

例题:

792 高精度减法
//给定两个正整数(不含前导 0),计算它们的差,计算结果可能为负数。

#include<iostream>
#include<vector>
using namespace std;

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;
    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();
    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');
    
    if(cmp(A, B)){
        auto C = sub(A, B);
        for(int i = C.size() - 1; i >= 0; i--) cout<<C[i];
    }else{
        auto C = sub(B, A);
        cout<<"-";
        for(int i = C.size() - 1; i >= 0; i--) cout<<C[i];
    }
    return 0;
}
3.高精度乘法

高精度*低精度

1.用A的每一位去乘以b,得出来的数%10是当前位的值,/10是进位的值,用一个t既代表了进位,又代表了当前的值,每一轮循环最后再更新t的值,t /= 10。

模板:

// 高精度乘低精度
// C = A * b, A >= 0, b > 0
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(); //处理前导0
    
    return C;
}

例题:

793 高精度乘法
//给定两个非负整数(不含前导 0) A 和 B,请你计算 A×B 的值。

#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--) cout<<C[i];
    
    return 0;
}
4.高精度除法

高精度/低精度

1.除法与其它三种算法不同,它不需要倒序存储,因为除法是从高位开始除的,但是为了保持和那三种算法的一致,我们还是用倒序存储高精度的数。

2.在处理商C的时候,需要将其反转一下,因为这样可以更好的消去它的前导0,并且不会在主函数输出的时候写法和前三种不同。

模板:

// 高精度除以低精度
// A / b = C ... r, A >= 0, b > 0
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;
}

例题:

794 高精度除法
//给定两个非负整数(不含前导 0) A,B,请你计算 A/B 的商和余数。
    
#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;
    int b;
    cin>>a>>b;
    vector<int> A;
    
    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;
    return 0;
}

前缀和与拆分

1.一维前缀和

模板:

// 一维前缀和
// S[i] = a[1] + a[2] + ... a[i]
// a[l] + ... + a[r] = S[r] - S[l - 1]

例题:

795 前缀和
// 输入一个长度为 n 的整数序列。
//接下来再输入 m 个询问,每个询问输入一对 l,r。
//对于每个询问,输出原序列中从第 l 个数到第 r 个数的和。

#include<iostream>
using namespace std;

const int N = 1e6 + 10;
int a[N], S[N];

int main(){
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);     // 这里都从下标1开始,因为题目给的l,r都是第几个数,而不是数组下标
    for(int i = 1; i <= n; i++) S[i] = S[i - 1] + a[i];

    while(m--){
        int l, r;
        scanf("%d%d", &l, &r);
        cout<<S[r] - S[l - 1]<<endl;
    }
    return 0;
}
2.二维前缀和

模板:

// 二维前缀和
// S[i, j] = 第i行j列格子左上部分所有元素的和
// 以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵的和为 S[x2, y2] - S[x1 - 1, y2] - S[x2, y1 - 1] + S[x1 - 1, y1 - 1]

例题:

796 子矩阵的和
//输入一个 n 行 m 列的整数矩阵,再输入 q 个询问,每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。
//对于每个询问输出子矩阵中所有数的和。

#include<iostream>
using namespace std;

const int N = 1e3 + 10;
int a[N][N], S[N][N];
int n, m, q;

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++)      
            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);
        cout<<S[x2][y2] - S[x2][y1 - 1] - S[x1 - 1][y2] + S[x1 - 1][y1 - 1]<<endl;
    }        
    return 0;
}
3.一维差分

一个思想就是一开始认为a数组和b数组都为0,后面只需要插入一遍就可以了。

to me:看的时候如果看不明白就列一下递推式子看。bn = an - an-1

模板:

// 一维前缀和
// S[i] = a[1] + a[2] + ... a[i]
// a[l] + ... + a[r] = S[r] - S[l - 1]

例题:

797 差分
//输入一个长度为 n 的整数序列。
//接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。
//请你输出进行完所有操作后的序列。

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int n, m;
int a[N], b[N];

void insert(int l, int r, int c){
    b[l] += c; // 下标为l的差分数组+c就可以改变其前缀和数组l及以后的数了
    b[r + 1] -= c; // 对应的要在区间范围外的数减去c
}

int main(){
    cin>>n>>m;
    for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
    for(int i = 1; i <= n; i++) insert(i, i, a[i]); // 这一步已经构造完差分数组了
    
    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]; // 这一步其实就是求a数组,即b的前缀和数组
    for(int i = 1; i <= n; i++) cout<<b[i]<<" ";
    return 0;
}
4.二维差分

参考一位差分和二维前缀和的思想。

模板:

// 二维差分
// 给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
// S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c

例题:

798 差分矩阵
//输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1) 和 (x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。
//每个操作都要将选中的子矩阵中的每个元素的值加上 c。
//请你将进行完所有操作后的矩阵输出。

#include<iostream>
using namespace std;

const int N = 1e3 + 10;
int n, m, q;
int a[N][N], b[N][N];

// 参考一维差分和二维前缀和的思想
void insert(int x1, int y1, int x2, int y2, int c){
    b[x1][y1] += c;
    b[x2 + 1][y1] -= c;
    b[x1][y2 + 1] -= c;
    b[x2 + 1][y2 + 1] += c;
    
}

int main(){
    cin>>n>>m>>q;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            scanf("%d", &a[i][j]);
    // 把数组视作0,然后往里面插入数据,做完以后就是一个差分矩阵了
    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;
        scanf("%d%d%d%d%d", &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]; //其实就是求a[i][j]
    
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++)
            printf("%d ", b[i][j]);
        cout<<endl;
    }  
    return 0;
}

双指针算法

1.先用暴力解法写出来,然后再去优化。

2.找题的单调性,比如下面的例题800,两个序列都是升序的有序数组,且有唯一解,当i增大时,只有j减小才能保证值不会一直增大而错过x,同时也可以找到内层循环的判别条件。

模板:

// 双指针算法
for (int i = 0, j = 0; i < n; i ++ )
	{
		while (j < i && check(i, j)) j ++ ;
		
		// 具体问题的逻辑
	}
	常见问题分类:
		(1) 对于一个序列,用两个指针维护一段区间
		(2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作

例题:

799 最长连续不重复子序列
//给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度。

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int a[N], S[N];
int n;
int res = 0;

int main(){
    cin>>n;
    for(int i = 0; i < n; i++) scanf("%d", &a[i]);
    
    for(int i = 0, j = 0; i < n; i++){
        S[a[i]]++;
        
        while(j < i && S[a[i]] > 1){
            S[a[j]]--;
            j++;
        }
        res = max(res, i - j + 1);
    }
    cout<<res<<endl;
    return 0;
}

800 数组元素的目标和
// 给定两个升序排序的有序数组 A 和 B,以及一个目标值 x。
//数组下标从 0 开始。
//请你求出满足 A[i]+B[j]=x 的数对 (i,j)。
//数据保证有唯一解。

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int n, m, x;
int a[N], b[N];

int main(){
    cin>>n>>m>>x;
    for(int i = 0; i < n; i++) scanf("%d", &a[i]);
    for(int i = 0; i < m; i++) scanf("%d", &b[i]);
    
    int i = 0;
    int j = m - 1;
    for(i = 0; i < n; i++){
        while( j >= 0 && a[i] + b[j] > x){
            j--;
        }
        if(a[i] + b[j] == x) break;
    }
    cout<<i<<" "<<j;
    return 0;
}

位运算

1.lowbit方法 : 计算出来以后,n和-n总是最后一个1位置相同,1往后都是0,1往前全相反,所以&了以后就是1及以后的数,也就知道最后一位1在哪。

2.看不明白的话就写个数的原码和其负数的原码比一下就知道了。

3.负数的原码就是其正数的补码

模板:

//位运算
//求n的第k位数字: n >> k & 1
//返回n的最后一位1:lowbit(n) = n & -n

例题:

801 二进制中1的个数
//给定一个长度为 n 的数列,请你求出数列中每个数的二进制表示中 1 的个数。

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int a[N];
int n;

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

int main(){
    cin>>n;
    for(int i = 0; i < n; i++) cin>>a[i];
    
    int res = 0;
    for(int i = 0; i < n; i++){
        int x = a[i];
        while(x){
            x -= lowbit(x);
            res++;
        }
        cout<<res<<" ";
        res = 0;
    }
    return 0;
}

离散化

1.映射以后的数组中可能存在重复的元素,要先去重。

2.用二分法算出x离散后的值。

3.太难了。

模板:

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

// 二分求出x对应的离散化的值
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;
}

例题:

802 区间和
//假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。
//现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。
//接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r] 之间的所有数的和。

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
typedef pair<int, int> PII;

const int N = 3e5 + 10;
int a[N], S[N];
vector<int> alls; //存放的是原数组或者说原坐标轴上的位置,alls每一个下标对应一个具体的位置
vector<PII> add;//存放的是具体位置和要加的数
vector<PII> query;//询问

int find(int x){
    int l = 0, r = alls.size() - 1;
    while(l < r){
        int mid = l + (r - l) / 2;
        if(alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return l + 1;//这里返回l+1是为了让数组下标从1开始映射
}

int main(){
    int n, m;
    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); //这里调用find是因为去重和排序后,下标发生了一些变化
        cout << S[r] - S[l - 1] << endl;
    }
    return 0;
    
}

区间合并

合并的时候有三种情况:

1.完全包含

2.有交集

3.无交集

在写代码实现的时候可以就分成有无交集两种情况去判断。在遍历整个区间的时候,把所有有交集的区间合并起来。

模板:

区间合并
	
// 将所有存在交集的区间合并
void merge(vector<PII> &segs){
    vector<PII> res;
    
    sort(segs.begin(), segs.end());//先排序,pair的排序是先第一个数,后第二个数
    
    int st = -2e9, ed = -2e9;//初始区间设置成负的无穷大
    for(auto seg : segs)
        //如果上一个区间的终点小于新一个区间的起点,那么上个区间已经维护完,现在又有了一个新的区间
        if(ed < seg.first){
            if(st != -2e9) res.push_back({st, ed});//只要不是初始区间,就加入res
            st = seg.first; ed = seg.second;//更新正在维护区间的起点和终点
        }else ed = max(ed, seg.second);//如果不是新的区间那么就代表有交集,求二者最远的终点,就是求并集
    
    if(st != -2e9) res.push_back({st, ed});//因为代码缘故,还要把最后一个区间添加进去
    
    segs = res;
}

例题:

803 区间合并
//给定 n 个区间 [li,ri],要求合并所有有交集的区间。
//注意如果在端点处相交,也算有交集。
//输出合并完成后的区间个数。
//例如:[1,3] 和 [2,6] 可以合并为一个区间 [1,6]。

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

typedef pair<int, int> PII;

int n;

void merge(vector<PII> &segs){
    vector<PII> res;
    
    sort(segs.begin(), segs.end());//先排序,pair的排序是先第一个数,后第二个数
    
    int st = -2e9, ed = -2e9;//初始区间设置成负的无穷大
    for(auto seg : segs)
        //如果上一个区间的终点小于新一个区间的起点,那么上个区间已经维护完,现在又有了一个新的区间
        if(ed < seg.first){
            if(st != -2e9) res.push_back({st, ed});//只要不是初始区间,就加入res
            st = seg.first; ed = seg.second;//更新正在维护区间的起点和终点
        }else ed = max(ed, seg.second);//如果不是新的区间那么就代表有交集,求二者最远的终点,就是求并集
    
    if(st != -2e9) res.push_back({st, ed});//因为代码缘故,还要把最后一个区间添加进去
    
    segs = res;
}

int main(){
    cin>>n;
    vector<PII> segs;
    for(int i = 0; i < n; i++){
        int l, r;
        cin>>l>>r;
        segs.push_back({l, r});
    }
    
    merge(segs);
    
    cout<<segs.size();
    return 0;
}

二、数据结构

链表

1.单链表

模板:

1. 单链表
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
int head, e[N], ne[N], idx;

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

// 在链表头插入一个数a
void insert(int a)
{
    e[idx] = a, ne[idx] = head, head = idx ++ ;
}

// 将头结点删除,需要保证头结点存在
void remove()
{
    head = ne[head];
}

例题:

826 单链表
//实现一个单链表,链表初始为空,支持三种操作:

//向链表头插入一个数;
//删除第 k 个插入的数后面的数;
//在第 k 个插入的数后插入一个数。
//现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。

//注意:题目中第 k 个插入的数并不是指当前链表的第 k 个数。例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int e[N], ne[N];
int head, idx;

void init(){
    head = -1;
    idx = 0;
}
// 表头插入一个节点
void add_to_head(int x){
    e[idx] = x; 
    ne[idx] = head; 
    head = idx++;
}
// 删除k后面的那个节点
void remove(int k){
    ne[k] = ne[ne[k]];
}
// 向k后面插入一个数
void add(int k, int x){
    e[idx] = x;
    ne[idx] = ne[k];
    ne[k] = idx++;
}

int main(){
    
    init();
    
    int m;
    cin>>m;
    while(m--){
        char op;
        int k, x;
        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);// 第k个数对应的数组下标应为k - 1
        }else{
            cin>>k>>x;
            add(k - 1, x);// 第k个数对应的数组下标应为k - 1
        }
    }
    for(int i = head; i != -1; i = ne[i]){
        cout<<e[i]<<" ";
    }
    return 0;
}
2.双链表

copy的题解评论:

  1. 之所以在 “D”, “IL”, “IR” 要用 k+1 的原因是 双链表的起始点是2. 所以,每个插入位置k的真实位置应该为 k-1+2 = k+1 (在单链表中为 k-1)。
  2. 0, 1 节点的作用是边界。0为左边界,1为右边界。他俩在这里有点类似保留字的作用。正因如此,我们的idx也是从2开始
  3. 最后遍历输出结果的 for (int i = rn[0]; i != 1; i = rn[i])。从 rn[0] 开始是因为 0 为左边界,而终止条件 i==1是因为1为右边界(如果碰到,说明已经遍历完毕)

模板:

双链表
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
int e[N], l[N], r[N], idx;

// 初始化
void init()
{
    //0是左端点,1是右端点
    r[0] = 1, l[1] = 0;
    idx = 2;
}

// 在节点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];
}

例题:

827 双链表
//实现一个双链表,双链表初始为空,支持 5 种操作:
//1.在最左侧插入一个数;
//2.在最右侧插入一个数;
//3.将第 k 个插入的数删除;
//4.在第 k 个插入的数左侧插入一个数;
//5.在第 k 个插入的数右侧插入一个数
//现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int e[N], l[N], r[N], idx;

// 初始化
void init(){
    r[0] = 1; l[1] = 0;
    idx = 2;
}

// 在第k个点的右侧插入一个数
void add(int k, int x){
    e[idx] = x;
    r[idx] = r[k];
    l[idx] = k;
    l[r[k]] = idx;
    r[k] = idx;
    idx++;
}

// 删除第k个点
void remove(int k){
    r[l[k]] = r[k];
    l[r[k]] = l[k];
}

int main(){
    init();
    
    int m;
    cin>>m;
    while(m--){
        string op;
        cin>>op;
        int k, x;
        if(op == "L"){
            cin>>x;
            add(0, x);
        }else if(op == "R"){
            cin>>x;
            add(l[1], x);
        }else if(op == "D"){
            cin>>k;
            remove(k + 1);
        }else if(op == "IL"){
            cin>>k>>x;
            add(l[k + 1], x);
        }else{
            cin>>k>>x;
            add(k + 1, x);
        }
    }
    for(int i = r[0]; i != 1; i = r[i]) cout<<e[i]<<" ";
    return 0;
}

栈与队列

1.栈

模板:

int stk[N], tt = 0;

// 在栈顶插入一个元素,我的栈顶指针tt是从1开始的
void push(int x){
    stk[++tt] = x;
}
// 从栈顶退出一个元素
void pop(){
    tt--;
}
// 判空
void empty(){
    if(tt >= 1) cout<<"NO"<<endl;
    else cout<<"YES"<<endl;
}
// 查询栈顶元素
void query(){
    cout<<stk[tt]<<endl;    
}

例题:

828 模拟栈
//实现一个栈,栈初始为空,支持四种操作:
//push x – 向栈顶插入一个数 x;
//pop – 从栈顶弹出一个数;
//empty – 判断栈是否为空;
//query – 查询栈顶元素。
//现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int stk[N], tt = 0;

// 在栈顶插入一个元素,我的栈顶指针tt是从1开始的
void push(int x){
    stk[++tt] = x;
}
// 从栈顶退出一个元素
void pop(){
    tt--;
}
// 判空
void empty(){
    if(tt >= 1) cout<<"NO"<<endl;
    else cout<<"YES"<<endl;
}
// 查询栈顶元素
void query(){
    cout<<stk[tt]<<endl;    
}

int main(){
    int m;
    cin>>m;
    
    while(m--){
        string op;
        cin>>op;
        if(op == "push"){
            int x;
            cin>>x;
            push(x);
        }else if(op == "pop"){
            pop();
        }else if(op == "empty"){
            empty();
        }else{
            query();
        }
    }
    return 0;
}
2.队列

模板:

int q[N], hh = 1, tt = 0;// 还是以1这个下标作为队列的一开始指针的位置
// 在队尾插入一个元素
void push(int x){
    q[++tt] = x;
}
// 从对头弹出一个元素
void pop(){
    hh++;
}
// 判空
void empty(){
    if(hh <= tt) cout<<"NO"<<endl;
    else cout<<"YES"<<endl;
}
// 查询队头元素
void query(){
    cout<<q[hh]<<endl;
}

例题:

829 模拟队列
//实现一个队列,队列初始为空,支持四种操作:
//1.push x – 向队尾插入一个数 x;
//2.pop – 从队头弹出一个数;
//3.empty – 判断队列是否为空;
//4.query – 查询队头元素。
//现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int q[N], hh = 1, tt = 0;// 还是以1这个下标作为队列的一开始指针的位置
// 在队尾插入一个元素
void push(int x){
    q[++tt] = x;
}
// 从对头弹出一个元素
void pop(){
    hh++;
}
// 判空
void empty(){
    if(hh <= tt) cout<<"NO"<<endl;
    else cout<<"YES"<<endl;
}
// 查询队头元素
void query(){
    cout<<q[hh]<<endl;
}

int main(){
    int m;
    cin>>m;
    while(m--){
        string op;
        cin>>op;
        if(op == "push"){
            int x;
            cin>>x;
            push(x);
        }else if(op == "pop"){
            pop();
        }else if(op == "empty"){
            empty();
        }else{
            query();
        }
    }
    return 0;
}
3.单调栈

1.保持当前栈顶元素小于要插入的元素,如果栈顶元素大于等于要插入的元素,那么就可以弹出栈顶元素。

2.当一直遵守1的规律去操作,那么整个栈就是一个单调递增的栈了。

模板:

单调栈
//常见模型:找出每个数左边离它最近的比它大/小的数
 while(n--){
        int x;
        scanf("%d", &x);
        while(tt && stk[tt] >= x) tt--;
        if(tt) cout<<stk[tt]<<" ";
        else cout<<-1<<" ";
        
        stk[++tt] = x;
}

例题:

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

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int stk[N], tt = 0;

int main(){
    int n;
    cin>>n;
    while(n--){
        int x;
        scanf("%d", &x);
        while(tt && stk[tt] >= x) tt--; // 当栈不为空,且当前栈顶的数大于等于要插入的数,则把栈顶的数弹出
        // 如果栈不为空,则输出栈顶这个数
        if(tt) cout<<stk[tt]<<" ";
        else cout<<-1<<" ";
        // 往栈顶加入这个数
        stk[++tt] = x;
    }
    return 0;
}
4.单调队列

1.先暴力思考怎么做。

2.把没有用的元素删掉。

3.看有没有单调性。

4.有单调性再去优化问题。

模板:

单调队列
//常见模型:找出滑动窗口中的最大值/最小值
int hh = 1, tt = 0;
for(int i = 0; i < n; i++){
	if(hh <= tt && i - k + 1 > q[hh]) hh++;// 判断队头是否滑出窗口
	while(hh <= tt && a[i] <= a[q[tt]]) tt--;// 判断当前元素是否比队尾元素要小
	q[++tt] = i; // 往队尾插入新元素
	if(i - k + 1 >= 0) cout<<a[q[hh]]<<' ';// 只有窗口中有k个元素的时候才去输出
}

例题:

解题思路:

最大值和最小值类似,只有判队尾元素大于还是小于不同。

1.先看队首是否滑出窗口。

2.判断当前要入队元素a[i]与队尾元素的大小问题,使队列具有单调性。

3.将当前元素a[i]加入队尾。

4.满足条件输出。

注意:

1.一定要先步骤3,再4,因为新加入的元素有可能是这个窗口的最小值(最大值)。

2.q[N]存的是a数组下标,a[N]是元素值。

154 滑动窗口
//给定一个大小为 n≤10^6 的数组。
//有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
//你只能在窗口中看到 k 个数字。
//每次滑动窗口向右移动一个位置。
//以下是一个例子:
//该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。
//窗口位置			最小值	最大值
//[1 3 -1] -3 5 3 6 7		-1	3
//1 [3 -1 -3] 5 3 6 7		-3	3
//1 3 [-1 -3 5] 3 6 7		-3	5
//1 3 -1 [-3 5 3] 6 7		-3	5
//1 3 -1 -3 [5 3 6] 7		3	6
//1 3 -1 -3 5 [3 6 7]		3	7
//你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

#include<iostream>
using namespace std;

const int N = 1e6 + 10;
int a[N], q[N];// 这题队列里存放的是数组下标,a里面存放的才是值


int main(){
    int n, k;
    scanf("%d%d", &n, &k);
    for(int i = 0; i < n; i++) scanf("%d", &a[i]);
    
    int hh = 1, tt = 0;
    for(int i = 0; i < n; i++){
        if(hh <= tt && i - k + 1 > q[hh]) hh++;// 判断队头是否滑出窗口
        while(hh <= tt && a[i] <= a[q[tt]]) tt--;// 判断当前元素是否比队尾元素要小
        q[++tt] = i; // 往队尾插入新元素
        if(i - k + 1 >= 0) cout<<a[q[hh]]<<' ';// 只在窗口中有k个元素的时候才去输出
    }
    cout<<endl;
    
    hh = 1, tt = 0;
    for(int i = 0; i < n; i++){
        if(hh <= tt && i - k + 1 > q[hh]) hh++;
        while(hh <= tt && a[i] >= a[q[tt]]) tt--;// 其它步骤与判最小相同,只有比较队尾元素是不同的,这个需要大于
        q[++tt] = i;
        if(i - k + 1 >= 0) cout<<a[q[hh]]<<' ';
    }
    cout<<endl;
    
    return 0;
}

KMP

不太会,等二刷了。

模板:

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

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

例题:

831 KMP字符串
//给定一个模式串 S,以及一个模板串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
//模板串 P 在模式串 S 中多次作为子串出现。
//求出模板串 P 在模式串 S 中所有出现的位置的起始下标。

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
const int M = 1e6 + 10;
char s[M], p[N];
int m, n;
int ne[N];

int main(){
    cin>>n>>p + 1>>m>>s + 1;
    
    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){
            cout<<i - n<<' ';
            j = ne[j];
        }
    }
    return 0;
}

Trie树

copy的第一个大佬的题解

Trie树中有个二维数组 son[N][26],表示当前结点的儿子,如果没有的话,可以等于++idx。Trie树本质上是一颗多叉树,对于字母而言最多有26个子结点。所以这个数组包含了两条信息。比如:son[1][0]=2表示1结点的一个值为a的子结点为结点2;如果son[1][0] = 0,则意味着没有值为a子结点。这里的son[N][26]相当于链表中的ne[N]。

模板:

Trie树

	int son[N][26], cnt[N], idx;
	// 0号点既是根节点,又是空节点
	// son[][]存储树中每个节点的子节点
	// cnt[]存储以每个节点结尾的单词数量

	// 插入一个字符串
	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];
	}

例题:

835 Trie字符串统计
//维护一个字符串集合,支持两种操作:
//I x 向集合中插入一个字符串 x;
//Q x 询问一个字符串在集合中出现了多少次。
//共有 N 个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
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;
}

并查集

并查集思想:1.将两个集合合并在一起。2.询问两个元素是否在一个集合中。

解题的基本原理:每个集合用一颗树来表示。树根的编号就是整个集合的编号。每个节点表示它的父节点,p[x]表示x的父节点。

步骤1:如何判断树根:if(p[x] == x)

步骤2:如何求x的集合编号:while(p[x] != x) x = [px];

步骤3:如何合并两个集合:px是x的集合编号,py是y的集合编号,px = y;

朴素并查集,维护size的并查集,维护到祖宗节点距离的并查集

模板:

并查集

	(1)朴素并查集:

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

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

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

		// 合并a和b所在的两个集合:
		p[find(a)] = find(b);
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
(2)维护size的并查集:

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

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

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

		// 合并a和b所在的两个集合:
		p[find(a)] = find(b);
		size[b] += size[a];
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
  (3)维护到祖宗节点距离的并查集:

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

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

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

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

例题:

836 合并集合
//一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。
//现在要进行 m 个操作,操作共有两种:
//M a b,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
//Q a b,询问编号为 a 和 b 的两个数是否在同一个集合中;

#include<iostream>
using namespace std;

const int N = 1e5 + 10;
int p[N];// 表示当前节点的父亲节点
int n, m;

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

int main(){
    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)) cout<<"Yes"<<endl;
            else cout<<"No"<<endl;
        }
    }
    return 0;
}

1.存储的数据结构为一维数组,逻辑结构为完全二叉树。

思路:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PaeFF9gr-1660545578268)(…/Study_images/image-20220810134659043.png)]

模板:

// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
	// ph[k]存储第k个插入的点在堆中的位置
	// hp[k]存储堆中下标是k的点是第几个插入的
	int h[N], ph[N], hp[N], size;

	// 交换两个点,及其映射关系
	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 <= 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)
		{
			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;
		}
	}
	
	// O(n)建堆
	for (int i = n / 2; i; i -- ) down(i);

例题:

838 堆排序
//输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。

#include <iostream>

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

    return 0;
}

哈希表

1.一般哈希
拉链法

1.开一个一维数组存储,每个下标相当于一个头节点,然后指向一个链表,每个节点插入的时候都是插在数组后面。

2.哈希函数的N一般取N及N以后最小的一个质数。

模板:

拉链法
			int h[N], e[N], ne[N], idx;

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

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

				return false;
			}

例题:

840 模拟散列表
//维护一个集合,支持如下几种操作:
//I x,插入一个数 x;
//Q x,询问数 x 是否在集合中出现过;
//现在要进行 N 次操作,对于每个询问操作输出对应的结果。

#include<iostream>
#include<cstring>
using namespace std;

const int N =1e5 + 1;
int h[N], e[N], ne[N], idx = 1;

void insert(int x){
    int k = ( (x % 100003) + N ) % N;
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx++;
}

bool find(int x){
    int k = ( (x % 100003) + N) % N;
    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];
        int x;
        scanf("%s%d", op, &x);
        if(*op == 'I'){
            insert(x);
        }else{
            if( find(x) ) cout<<"Yes"<<endl;
            else cout<<"No"<<endl;
        }
    }
    return 0;
}
开放寻址法

1.N一般开要求的2倍到3倍。

2.只用开一个数组,数组里每个值初始化为一个很大的数null,然后再去比较,如果这个位置的数不等于null,那么就代表这个位置有数,插入操作的话就需要按顺序往后面找,查找操作的话看x是否等于这个位置的数,不等于就接着往后,如果这个位置的值等于null的话就为空。

模板:

开放寻址法
			int h[N];

			// 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
			int find(int x)
			{
				int t = (x % N + N) % N;
				while (h[t] != null && h[t] != x)
				{
					t ++ ;
					if (t == N) t = 0;
				}
				return t;
			}

例题:

840 模拟散列表
//维护一个集合,支持如下几种操作:
//I x,插入一个数 x;
//Q x,询问数 x 是否在集合中出现过;
//现在要进行 N 次操作,对于每个询问操作输出对应的结果。

#include<iostream>
#include<cstring>
using namespace std;

const int N = 2e5 + 3;
int h[N], null = 0x3f3f3f3f;

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);
        int k = find(x);
        if(*op == 'I') h[k] = x;
        else{
            if(h[k] == null) puts("No");
            else puts("Yes");
        }
    }
    return 0;
}
2.字符串哈希

841第一个题解:

(字符串哈希) O(n)+O(m)O(n)+O(m)
全称字符串前缀哈希法,把字符串变成一个p进制数字(哈希值),实现不同的字符串映射到不同的数字。
对形如 X1X2X3⋯Xn−1XnX1X2X3⋯Xn−1Xn 的字符串,采用字符的ascii 码乘上 P 的次方来计算哈希值。

映射公式 (X1×Pn−1+X2×Pn−2+⋯+Xn−1×P1+Xn×P0)modQ

前缀和公式 h[i+1]=h[i]×P+s[i]h[i+1]=h[i]×P+s[i] i∈[0,n−1]i∈[0,n−1] h为前缀和数组,s为字符串数组
区间和公式 h[l,r]=h[r]−h[l−1]×Pr−l+1h[l,r]=h[r]−h[l−1]×Pr−l+1
区间和公式的理解: ABCDE 与 ABC 的前三个字符值是一样,只差两位,
乘上 P2P2 把 ABC 变为 ABC00,再用 ABCDE - ABC00 得到 DE 的哈希值。

模板:

字符串哈希
		核心思想:将字符串看成P进制数,P的经验值是13113331,取这两个值的冲突概率低
		小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
		注意: 1.不能映射成0
    		   2.不存在冲突。
		typedef unsigned long long ULL;
		ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64
		
		// 初始化
		p[0] = 1;
		for (int i = 1; i <= n; i ++ )
		{
			h[i] = h[i - 1] * P + str[i];
			p[i] = p[i - 1] * P;
		}
		
		// 计算子串 str[l ~ r] 的哈希值
		ULL get(int l, int r)
		{
			return h[r] - h[l - 1] * p[r - l + 1];
		}

例题:

841 字符串哈希
给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1][l2,r2] 这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
    
#include <iostream>
using namespace std;

typedef unsigned long long ULL;

const int N = 1e5 + 10, P = 131;
char str[N];
ULL h[N], p[N];

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

int main(){
    int n, m;
    scanf("%d%d", &n, &m);
    scanf("%s", str + 1);
    
    p[0] = 1;//如果为0,那么所有的权重就都为0了
    for(int i = 1; i <= n; i++){
        p[i] = p[i - 1] * P;// 处理权重的数组
        h[i] = h[i - 1] * P + str[i];// 前缀和数组,之前的每一项都乘以P,相当于左移1位,再加上新的一个字符
    }
    
    while(m--){
        int l1, r1, l2, r2;
        scanf("%d%d%d%d", &l1, &r1, &l2, &r2);
        if(query(l1, r1) == query(l2, r2)) puts("Yes");
        else puts("No");
    }
    return 0;
}

C++STL

模板:

C++ STL简介
	vector, 变长数组,倍增的思想
		size()  返回元素个数
		empty()  返回是否为空
		clear()  清空
		front()/back()
		push_back()/pop_back()
		begin()/end()
		[]
		支持比较运算,按字典序

	pair<int, int>
		first, 第一个元素
		second, 第二个元素
		支持比较运算,以first为第一关键字,以second为第二关键字(字典序)

	string,字符串
		szie()/length()  返回字符串长度
		empty()
		clear()
		substr(起始下标,(子串长度))  返回子串
		c_str()  返回字符串所在字符数组的起始地址

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

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

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

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

	set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
		size()
		empty()
		clear()
		begin()/end()
		++, -- 返回前驱和后继,时间复杂度 O(logn)
		
		set/multiset
			insert()  插入一个数
			find()  查找一个数
			count()  返回某一个数的个数
			erase()
				(1) 输入是一个数x,删除所有x   O(k + logn)
				(2) 输入一个迭代器,删除这个迭代器
			lower_bound()/upper_bound()
				lower_bound(x)  返回大于等于x的最小的数的迭代器
				upper_bound(x)  返回大于x的最小的数的迭代器
		map/multimap
			insert()  插入的数是一个pair
			erase()  输入的参数是pair或者迭代器
			find()
			[]   时间复杂度是 O(logn)
			lower_bound()/upper_bound()

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

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

三、搜索与图论

DFS

1.一直搜到头。

模板:

(1) 深度优先遍历
		int dfs(int u)
		{
			st[u] = true; // st[u] 表示点u已经被遍历过

			for (int i = h[u]; i != -1; i = ne[i])
			{
				int j = e[i];
				if (!st[j]) dfs(j);
			}
		}

例题:

842 排列数字
//给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。
//现在,请你按照字典序将所有的排列方法输出。

#include<iostream>
using namespace std;

const int N = 10;
int path[N];// 记录其中一条路径
bool st[N];// 状态数组,判断某个数是否被用过了
int n;

void dfs(int u){
    if(u == n){
        for(int i = 0; i < n; i++) printf("%d ", path[i]);
        puts("");
        return;
    }
    
    for(int i = 1; i <= n; i++){
        if(!st[i]){
            path[u] = i;
            st[i] = true;
            dfs(u + 1);
            st[i] = false;// 状态恢复在每一次深搜以后
        }
    }
    
}

int main(){
    scanf("%d", &n);
    
    dfs(0);// u要从0开始,因为dfs是先判断u是否是叶子节点,如果从1开始,那么n就永远遍历不到
    
    return 0;
}

BFS

模板:

宽度优先遍历
手写队列的模板	
		queue<int> q;
		st[1] = true; // 表示1号点已经被遍历过
		q.push(1);

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

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

例题:

844 走迷宫
//给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。

//最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。

//请问,该人从左上角移动至右下角 (n,m) 处,至少需要移动多少次。

//数据保证 (1,1) 处和 (n,m) 处的数字为 0,且一定至少存在一条通路。

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
typedef pair<int, int> PII;

const int N = 1e2 + 10;
int g[N][N], d[N][N];// d代表走到当前位置已经用了多少步了
int n, m;

int bfs(){
    queue<PII> q;
    q.push({0, 0});
    memset(d, -1, sizeof d);// 初始化为-1,代表没有走到过这里
    d[0][0] = 0;// 给起点初始化为0, 代表走到起点用了0步
    
    int dx[4] = {0, 1, 0, -1}, dy[4] = {1, 0, -1, 0};
    
    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 && d[x][y] == -1 && g[x][y] == 0){
                d[x][y] = d[t.first][t.second] + 1;
                q.push({x, y});
            }
        }
    }
    return d[n - 1][m - 1];
}

int main(){
    scanf("%d%d", &n, &m);
    for(int i = 0; i < n; i++)
        for(int j = 0; j < m; j++)
            scanf("%d", &g[i][j]);
            
    cout<< bfs() << endl;
    return 0;
}

树与图的存储

树是一种特殊的图,与图的存储方式相同。
对于无向图中的边ab,存储两条有向边a->b, b->a。
因此我们可以只考虑有向图的存储。

邻接表中,点的存储顺序是没关系的,只要存储每个点能到哪个点就行。

遍历的时候,一层循环其实是遍历某一个顶点能到的所有点。

模板:

树与图的存储
	(1) 邻接矩阵:g[a][b] 存储边a->b
	
	(2) 邻接表:
		
		// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
		int h[N], e[N], ne[N], idx;
		
		// 添加一条边a->b
		void add(int a, int b)
		{
			e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
		}

树与图的遍历

深度优先遍历

模板:

深度优先遍历
		int dfs(int u)
		{
			st[u] = true; // st[u] 表示点u已经被遍历过

			for (int i = h[u]; i != -1; i = ne[i])
			{
				int j = e[i];
				if (!st[j]) dfs(j);
			}
		}

例题:

846 树的重心
给定一颗树,树中包含 n 个结点(编号 1∼n)和 n−1 条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N =1e5 + 10, M = 2 * N;
int h[N], e[M], ne[M], idx;
bool st[N];
int n;
int ans = N;

// 在a, b间插入一条边,a是根,b是链表
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 res = 0;// 存储删掉某个节点后,最大的连同子图节点数
    int sum = 1;// 存储以u为根的子树的节点数,包括u
    
    for(int i = h[u]; i != -1; i = ne[i]){
        int j = e[i];
        if(!st[j]){
            int s = dfs(j);// u的单棵子树的节点数
            res = max(res, s);// 最大联通子图节点数
            sum += s;// 以j为根的子树的节点数,包括j
        }
    }
    
    res = max(res, n - sum);
    ans = min(ans, res);

    return sum;
}

int main(){
    scanf("%d", &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;
}
宽度优先遍历

就用bfs的框架。

模板:

宽度优先遍历
	
		queue<int> q;
		st[1] = true; // 表示1号点已经被遍历过
		q.push(1);

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

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

例题:

847 图中点的层次
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。
所有边的长度都是 1,点的编号为 1∼n。
请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 −1#include <iostream>
#include <cstring>
#include <queue>
using namespace std;

const int  N = 1e5 + 10;
queue<int> q;
int h[N], e[N], ne[N], idx;
int d[N];
int n, m;

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

int bfs(){
    memset(d, -1, sizeof d);
    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(){
    memset(h, -1, sizeof h);
    scanf("%d%d", &n, &m);
    while(m--){
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }
    
    cout << bfs() << endl;
    return 0;
}

拓扑排序

1.有向无环图一定有拓扑序,有向有环图没有拓扑序。

2.将入度为0的点加入队列,再删去该点指向的所有点;当有新的点入度为0,将该点入队。

3.队列入队的顺序就是拓扑序。

模板:

拓扑排序
	bool topsort()
	{
		int hh = 0, tt = -1;

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

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

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

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

例题:

848 有向图的拓扑序列
给定一个 n 个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。

#include<iostream>
#include<cstring>
using namespace std;

const int N = 1e5 + 10;
int h[N], e[N], ne[N], idx;
int n, m;
int q[N];
int d[N];

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

bool topsort(){
    int hh = 0, tt = -1;
    for(int i = 1; i <= n; i++){
        if(!d[i]) q[++tt] = i; // 如果入度为0,则入队
    }
    
    while(hh <= tt){
        int t = q[hh++];
        
        for(int i = h[t]; i != -1; i = ne[i]){
            int j = e[i];
            --d[j];// 删除t这个点指向j的边
            if(d[j] == 0)// 如果点j入度为0则入队
                q[++tt] = j;
        }
    }
    return tt == n - 1;// 如果只有n-1个点就代表没有环,则可以输出
}

int main(){
    cin >> n >> m;    
    memset(h, -1, sizeof h);
    for(int i = 0; i < m; i++){
        int x, y;
        scanf("%d%d", &x, &y);
        add(x, y);
        
        d[y]++;// d数组表示入度,因为是x指向y,所以y的入度加1
    }
    
    if(topsort()){
        for(int i = 0; i < n; i++) cout << q[i] << ' ';
        puts(" ");
    }else{
        puts("-1");
    }
    return 0;
}

最短路算法

1.约定n为点数,m为边数,当m和n^2是一个数量级的时候,就说这张图是稠密图;如果m和n是一个数量级的话就说明这张图是稀疏图。

2.单源最短路,没有负权边,且是稠密图,用朴素Dijkstra算法,稀疏图用堆优化Dijkstra算法。

3.单源最短路,有负权边,一般用spfa算法;如果求不超过k条边的题,用bellman—ford算法.

4.多源最短路,Floyd算法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rvRUAy2u-1662635838719)(…/Study_images/image-20220823181141554.png)]

1.朴素Dijkstra算法

1.循环n次,在未确定最短路的点中找出距离起点最短的点,让它确定,然后再用这个点去更新其它还未确定的点到起点的最短距离。

2.Dijkstra算法不能被用于有负权边的情况,是因为这个算法是基于贪心的一种算法,每次都会遍历当前点的最小距离的点,而如果另外一条路径上,后面的点有负权边,那么就有可能使其不是最短路。

模板:

朴素dijkstra算法

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

	// 求1号点到n号点的最短路,如果不存在则返回-1
	int dijkstra()
	{
		memset(dist, 0x3f, sizeof dist);
		dist[1] = 0;
		
		for (int i = 0; i < n - 1; i ++ )
		{
			int t = -1;		// 在还未确定最短路的点中,寻找距离最小的点
			for (int j = 1; j <= n; j ++ )
				if (!st[j] && (t == -1 || dist[t] > dist[j]))
					t = j;
			
			// 用t更新其他点的距离
			for (int j = 1; j <= n; j ++ )
				dist[j] = min(dist[j], dist[t] + g[t][j]);
			
			st[t] = true;
		}
		
		if (dist[n] == 0x3f3f3f3f) return -1;
		return dist[n];
	}

例题:

849 Dijkstra求最短路 I
//给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
//请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

#include <iostream>
#include <cstring>
using namespace std;

const int N = 510;
int g[N][N];// 稠密图,用邻接矩阵表示
int dis[N];// 代表每个点到起点的最短距离
bool st[N];// 代表当前这个点是否已经有了最小值
int n, m;

int dijkstra(){
    memset(dis, 0x3f, sizeof dis);
    dis[1] = 0;// 将起点初始化为0,其它点为正无穷
    
    for(int i = 0; i < n; i++){// 一共n个点,要迭代n轮
        int t = -1;// 用t来存储当前点
        
        for(int j = 1; j <= n; j++){
            if(!st[j] && (t == -1 || dis[t] > dis[j]))// 从还未确定最短路的点中,找出来距离最小的点
                t = j;
        }
        st[t] = true;
        
        for(int j = 1; j <= n; j++)// 用t来更新其它还未确定最短路的点的距离
            if(!st[j])
                dis[j] = min(dis[j], dis[t] + g[t][j]);
    }
    
    if(dis[n] == 0x3f3f3f3f) return -1;
    return dis[n];
}

int main(){
    memset(g, 0x3f, sizeof g);
    
    scanf("%d%d", &n, &m);
    
    while(m--){
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        g[x][y] = min(g[x][y], z);// 有重边的可能性,要选出最短的那条边
    }
            
    cout << dijkstra() << endl;
    return 0;
}
2.堆优化Dijkstra算法

用优先队列来维护,本质上是小根堆。

这道题相对于朴素版算法优化的原理理解的不好。

模板:

堆优化版dijkstra
	typedef pair<int, int> PII;

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

	// 求1号点到n号点的最短距离,如果不存在,则返回-1
	int dijkstra()
	{
		memset(dist, 0x3f, sizeof dist);
		dist[1] = 0;
		priority_queue<PII, vector<PII>, greater<PII>> heap;
		heap.push({0, 1});		// first存储距离,second存储节点编号
		
		while (heap.size())
		{
			auto t = heap.top();
			heap.pop();
			
			int ver = t.second, distance = t.first;
			
			if (st[ver]) continue;
			st[ver] = true;
			
			for (int i = h[ver]; i != -1; i = ne[i])
			{
				int j = e[i];
				if (dist[j] > distance + w[i])
				{
					dist[j] = distance + w[i];
					heap.push({dist[j], j});
				}
			}
		}
		
		if (dist[n] == 0x3f3f3f3f) return -1;
		return dist[n];
	}

例题:

850 Dijkstra求最短路 II
//给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
//请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

#include <iostream>
#include <queue>
#include <cstring>
using namespace std;

typedef pair<int, int> PII;
const int N = 150010;
int h[N], e[N], ne[N], idx;
int w[N];
int dis[N];// 存储每个点到起点的最短距离
bool st[N];
int n, m;

// 邻接表插入
void add(int x, int y, int z){
    w[idx] = z;
    e[idx] = y;
    ne[idx] = h[x];
    h[x] = idx++;
}

int dijkstra(){
    memset(dis, 0x3f, sizeof dis);
    dis[1] = 0;// 起点的距离为0
    priority_queue<PII, vector<PII>, greater<PII>> heap;// 用优先队列存储,第一个点存储距离,第二点存储时哪个点,这个代码来看的话不需要用到距离的点,但是不这么写会wa
    heap.push({0, 1});
    
    while(heap.size()){
        auto t = heap.top();
        heap.pop();
        
        int ver = t.second;
        
        if(st[ver]) continue;
        st[ver] = true;
        // 用当前距离最短点更新所有它能到的点
        for(int i = h[ver]; i != -1; i = ne[i]){
            int j = e[i];
            if(dis[j] > dis[ver] + w[i]){
                dis[j] = dis[ver] + w[i];
                heap.push({dis[j], j});
            }
        }
    }
    if(dis[n] == 0x3f3f3f3f) return -1;
    return dis[n];
}

int main(){
    memset(h, -1, sizeof h);
    scanf("%d%d", &n, &m);
    while(m--){
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        add(x, y, z);
    }
    
    cout<< dijkstra() << endl;
    return 0;
}
3.bellman-ford算法

n为点数,m为边数,k < m

1.路径里如果存在负环,那么最短路就不一定存在了,因为有可能会一直减权重,直到负无穷。

2.那么存在负环但最短路还是存在的情况也是有的,比如整个路径不需要经过这个负环,那么最短路还是存在的。

3.负环:权重和为负数的环。

4.bellman-ford算法用于存在负权边,且限制最多经过k条边的情况下。

5.若在n - 1次松弛(不知道啥意思)后还能更新,那就说明存在负环。因为n个点,就有n-1条边,n-1次循环的话一定能到达n,没到达就说明存在负环。

6.因为限制最多经过k条边,也就是最多松弛k次,要开一个last数组,记录上一次循环时候各个点的最短距离。

如果再看的时候不知道为什么要开last数组的话,可以csdn搜一下,策马奔腾向前冲这个博主写的挺好。

模板:

Bellman-Ford算法
	int n, m;		// n表示点数,m表示边数
	int dist[N];		// dist[x]存储1到x的最短路距离

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

	// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
	int bellman_ford()
	{
		memset(dist, 0x3f, sizeof dist);
		dist[1] = 0;
		
		// 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
		for (int i = 0; i < n; i ++ )
		{
			for (int j = 0; j < m; j ++ )
			{
				int a = edges[j].a, b = edges[j].b, w = edges[j].w;
				if (dist[b] > dist[a] + w)
					dist[b] = dist[a] + w;
			}
		}
		
		if (dist[n] == 0x3f3f3f3f) return -1;
		return dist[n];
	}

例题:

853 有边数限制的最短路
//给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
//请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。
//注意:图中可能 存在负权回路。

#include<iostream>
#include<cstring>
using namespace std;

const int N = 510, M = 10010;
int dis[N], last[N];
int n, m, k;

struct Edge{
    int a, b, w;
}edge[M];

void bellman(){
    memset(dis, 0x3f, sizeof dis);
    dis[1] = 0;
    for(int i = 1; i <= k; i++){
        memcpy(last, dis, sizeof dis);
        for(int j = 1; j <= m; j++){
            auto e = edge[j];
            dis[e.b] = min(dis[e.b], last[e.a] + e.w);
        }
    }
}

int main(){
    scanf("%d%d%d", &n, &m, &k);
    for(int i = 1; i <= m; i++){
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edge[i].a = a, edge[i].b = b, edge[i].w = w;
    }
    
    bellman();
    if(dis[n] > 0x3f3f3f3f / 2) puts("impossible");
    else cout << dis[n] << endl;
    return 0;
}
4.spfa算法

1.Bellman_ford算法会遍历所有的边,但是有很多的边遍历了其实没有什么意义,我们只用遍历那些到源点距离变小的点所连接的边即可,只有当一个点的前驱结点更新了,该节点才会得到更新;因此考虑到这一点,我们将创建一个队列每一次加入距离被更新的结点。(抄的点赞数最多的题解)

2.spfa同样可以运用于大部分dijkstra算法的情况,但有的时候会被卡。

3.spfa不可以有负权回路。

模板:

spfa 算法(队列优化的Bellman-Ford算法)
	int n;		// 总点数
	int h[N], w[N], e[N], ne[N], idx;		// 邻接表存储所有边
	int dist[N];		// 存储每个点到1号点的最短距离
	bool st[N];		// 存储每个点是否在队列中

	// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
	int spfa()
	{
		memset(dist, 0x3f, sizeof dist);
		dist[1] = 0;
		
		queue<int> q;
		q.push(1);
		st[1] = true;
		
		while (q.size())
		{
			auto t = q.front();
			q.pop();
			
			st[t] = false;
			
			for (int i = h[t]; i != -1; i = ne[i])
			{
				int j = e[i];
				if (dist[j] > dist[t] + w[i])
				{
					dist[j] = dist[t] + w[i];
					if (!st[j])		// 如果队列中已存在j,则不需要将j重复插入
					{
						q.push(j);
						st[j] = true;
					}
				}
			}
		}
		
		if (dist[n] == 0x3f3f3f3f) return -1;
		return dist[n];
	}

例题:

851 spfa求最短路
//给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
//请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible。
//数据保证不存在负权回路。

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;

const int N = 1e5 + 10;
bool st[N];
int dis[N];
int e[N], ne[N], w[N], h[N], idx;
int n, m;

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

int spfa(){
    memset(dis, 0x3f, sizeof dis);
    dis[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(dis[j] > dis[t] + w[i]){
                dis[j] = dis[t] + w[i];
                if(!st[j]){
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return dis[n];
}

int main(){
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);
    for(int i = 0; i < m; i++){
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        add(a, b, w);
    }
    
    int t = spfa();
    if(t == 0x3f3f3f3f) puts("impossible");
    else cout << t << endl;
    return 0;
}
5.spfa判断是否存在负环

模板:

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

	// 如果存在负环,则返回true,否则返回false。
	bool spfa()
	{
		// 不需要初始化dist数组
		// 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。
	
		queue<int> q;
		for (int i = 1; i <= n; i ++ )// 不是从源点开始,是所有点都要加入队列去判断
		{
			q.push(i);
			st[i] = true;
		}
		
		while (q.size())
		{
			auto t = q.front();
			q.pop();
			
			st[t] = false;
			
			for (int i = h[t]; i != -1; i = ne[i])
			{
				int j = e[i];
				if (dist[j] > dist[t] + w[i])
				{
					dist[j] = dist[t] + w[i];
					cnt[j] = cnt[t] + 1;
					if (cnt[j] >= n) return true;		// 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
					if (!st[j])
					{
						q.push(j);
						st[j] = true;
					}
				}
			}
		}
		
		return false;
	}

例题:

852 spfa判断负环
//给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
//请你判断图中是否存在负权回路。

#include<iostream>
#include<queue>
#include<cstring>
using namespace std;

const int N = 2010;
const int M = 10010;
bool st[N];
int dis[N];
int cnt[N];
int e[M], ne[M], h[N], w[M], idx;
int n, m;

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

bool 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(dis[j] > dis[t] + w[i]){
                dis[j] = dis[t] + w[i];
                cnt[j] = cnt[t] + 1;
                
                if(cnt[j] >= n) return true;
                if(!st[j]){
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;
}

int main(){
    memset(h, -1, sizeof h);
    scanf("%d%d", &n, &m);
    for(int i = 0; i < m; i++){
        int a, b, c;
        scanf("%d%d%d", &a, &b ,&c);
        add(a, b, c);
    }
    
    if(spfa()) puts("Yes");
    else puts("No");
    return 0;
}
6.floyd算法

没太懂算法的思想是啥,但是挺好背的,忘了就看下floyd的模板就行,做题的时候注意例题里的两个注释。

模板:

floyd算法

	初始化:
		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;

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

例题:

854 Floyd求最短路
//给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。
//再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出 //impossible。

数据保证图中不存在负权回路。
#include<iostream>
using namespace std;

const int N = 210, M = 20010, INF = 1e9;
int dist[N][N];
int n, m, k;

void floyd(){
    for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]);
}

int main(){
    scanf("%d%d%d", &n, &m, &k);
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            if(i == j) dist[i][j] = 0;
            else dist[i][j] = INF;
    while(m--){
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        dist[x][y] = min(dist[x][y], z);// 有重边存在,所以要注意保留最小边
    }
    
    floyd();
    
    while(k--){
        int x, y;
        scanf("%d%d", &x, &y);
        // 因为有负边的存在,所以不能到的点的边权有可能会有减小
        if(dist[x][y] > INF / 2) cout << "impossible" << endl;
        else cout << dist[x][y] << endl;
    }
    return 0;
}

最小生成树与二分图

稠密图用朴素版Prim,稀疏图用Kruskal,堆优化基本上用不到。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6dIQzuQ0-1662635838720)(…/Study_images/image-20220905224044485.png)]

1.朴素版Prim算法

与朴素版dijkstra算法类似。

1.先找出距离集合最近的点。

2.将这个点加入集合。

3.用这个点去更新其它点到集合的距离。

摘自示例代码评论:

1.与dijkstra不同,prim需要迭代n次
2.最小生成树是针对无向图的,所以在读入边的时候,需要赋值两次
3.要先累加再更新,避免t有自环,影响答案的正确性。后更新不会影响后面的结果么?不会的,因为dist[i]为i到集合S的距离,当t放入集合后,其dist[t]就已经没有意义了,再更新也不会影响答案的正确性。
4.需要特判一下第一次迭代,在我们没有做特殊处理时,第一次迭代中所有点到集合S的距离必然为无穷大,而且不会进行更新(也没有必要),所以不需要将这条边(第一次迭代时,找到的距离集合S最短的边)累加到答案中,也不能认定为图不连通。
5.如果需要设置起点为i的话,在初始化dist数组之后,dist[i] = 0即可,这样也可以省去每轮迭代中的两个if判断。

模板:

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


	// 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
	int prim()
	{
		memset(dist, 0x3f, sizeof dist);
		
		int res = 0;
		for (int i = 0; i < n; i ++ )
		{
			int t = -1;
			for (int j = 1; j <= n; j ++ )
				if (!st[j] && (t == -1 || dist[t] > dist[j]))
					t = j;
			
			if (i && dist[t] == INF) return INF;
			
			if (i) res += dist[t];
			st[t] = true;
			
			for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
		}
		
		return res;
	}

例题:

858 Prim算法求最小生成树
//给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
//求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
//给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。
//由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

int n, m;
const int N = 510, INF = 0x3f3f3f3f;
bool st[N];
int g[N][N];
int dist[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];// 将权重加起来
        st[t] = true;// 将点加入集合
        
        for(int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);// 用新加入集合的点来更新其它点到集合的距离
    }
    return res;
}

int main(){
    memset(g, 0x3f, sizeof g);
    scanf("%d%d", &n, &m);
    while(m--){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = g[b][a] = min(g[a][b], c);// 注意是无向图,且有重边
    }
    
    int t = prim();
    if(t == INF) puts("impossible");
    else cout << t;
    
    return 0;
}
2.Kruskal算法

这个算法的实现用到了并查集。

先对整个边的集合进行排序,如果两个点不在同一个集合,则把他们连起来,否则就不要这条边。

第一次学到这的时候把并查集忘得差不多了,第二遍的时候好好看看,也不是太难。

模板:

Kruskal算法
	int n, m;		// n是点数,m是边数
	int p[N];		// 并查集的父节点数组

	struct Edge		// 存储边
	{
		int a, b, w;
		
		bool operator< (const Edge &W)const
		{
			return w < W.w;
		}
	}edges[M];

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

	int kruskal()
	{
		sort(edges, edges + m);
		
		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;
			
			a = find(a), b = find(b);
			if (a != b)		// 如果两个连通块不连通,则将这两个连通块合并
			{
				p[a] = b;
				res += w;
				cnt ++ ;
			}
		}
		
		if (cnt < n - 1) return INF;
		return res;
	}

例题:

859 Kruskal算法求最小生成树
//给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
//求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。
//给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。
//由 V 中的全部 n 个顶点和 E 中 n−1 条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。

#include<iostream>
#include<algorithm>
#include <cstring>
using namespace std;

const int N = 100010, M = 200010, INF = 0x3f3f3f3f;
int p[N];
int n, m;

struct Edge{
    int a, b, w;
    bool operator< (const Edge &W){// 这里的运算符重载也可以写一个cmp函数,加在sort的第三个参数
        return w < W.w;
    }
}edges[M];

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

int kruskal(){
    sort(edges, edges + m);
    
    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;
        
        a = find(a), b = find(b);
        if(a != b){
            p[a] = b;
            cnt++;
            res += w;
        }
    }
    
    if(cnt < n - 1) return INF;
    return res;
}

int main(){
    scanf("%d%d", &n, &m);
    
    for(int i = 0; i < m; i++){
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        edges[i].a = a;
        edges[i].b = b;
        edges[i].w = w;
    }
    
    int t = kruskal();
    if(t == INF) puts("impossible");
    else cout << t;
    
    return 0;
}
3.染色法

奇数环:环中边的数量为奇数。

二分图定义:一个顶点集合可以分为两个不相邻的子集,两个子集的内部没有边,并且图中每条边依附的两个顶点分别属于这两个子集。

二分图性质:当且仅当图中没有奇数环。

dfs判断一条边的两个顶点颜色是否相同,不相同是正确的,相同就直接判断为不是二分图。

模板:

染色法判别二分图
	int n;		// n表示点数
	int h[N], e[M], ne[M], idx;		// 邻接表存储图
	int color[N];		// 表示每个点的颜色,-1表示为染色,0表示白色,1表示黑色

	// 参数:u表示当前节点,father表示当前节点的父节点(防止向树根遍历),c表示当前点的颜色
	bool dfs(int u, int father, int c)
	{
		color[u] = c;
		for (int i = h[u]; i != -1; i = ne[i])
		{
			int j = e[i];
			if (color[j] == -1)
			{
				if (!dfs(j, u, !c)) return false;
			}
			else if (color[j] == c) return false;
		}
		
		return true;
	}

	bool check()
	{
		memset(color, -1, sizeof color);
		bool flag = true;
		for (int i = 1; i <= n; i ++ )
			if (color[i] == -1)
				if (!dfs(i, -1, 0))
				{
					flag = false;
					break;
				}
		return flag;
	}

例题:

860 染色法判定二分图
//给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环。
//请你判断这个图是否是二分图。

#include<iostream>
#include<cstring>
using namespace std;

const int N = 100010, M = 200010;// 无向图
int h[N], e[M], ne[M], idx;// 邻接表
int color[N];// 0代表没访问过,1和2分别代表一种颜色
int n, m;

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

bool dfs(int u, int c){
    color[u] = c;
    
    for(int i = h[u]; i != -1; i = ne[i]){
        
        int j = e[i];
        if(!color[j]){// 如果没染色过
            if(!dfs(j, 3 - c)) return false;// 染和相邻节点不一样的色
        }
        else if(color[j] == c) return false;// 染色过就判断这个点颜色是否和相邻点颜色相同,如果相同则代表有奇数环
    }
    return true;
}

int main(){
    memset(h, -1, sizeof h);
    scanf("%d%d", &n, &m);
    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]){
            if(!dfs(i, 1)){
                flag = false;
                break;
            }
        }
    }
    
    if(flag) puts("Yes");
    else puts("No");
    
    return 0;
}
4.匈牙利算法

规定图为一个二分图,然后拿左边的点集去匹配右边的点集,任意两条匹配的边没有公共顶点。

摘自点赞数最多的题解:

要了解匈牙利算法必须先理解下面的概念:

匹配:在图论中,一个「匹配」是一个边的集合,其中任意两条边都没有公共顶点。

最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。

模板:

匈牙利算法
	int n;		// n表示点数
	int h[N], e[M], ne[M], idx;		// 邻接表存储所有边
	int match[N];		// 存储每个点当前匹配的点
	bool st[N];		// 表示每个点是否已经被遍历过
	
	bool find(int x)
	{
		for (int i = h[x]; i != -1; i = ne[i])
		{
			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 res = 0;
    for (int i = 1; i <= n; i ++ )
	{
		memset(st, false, sizeof st);
		if (find(i)) res ++ ;
	}

例题:

861 二分图的最大匹配
//给定一个二分图,其中左半部包含 n1 个点(编号 1∼n1),右半部包含 n2 个点(编号 1∼n2),二分图共包含 m 条边。
//数据保证任意一条边的两个端点都不可能在同一部分中。
//请你求出二分图的最大匹配数。
//二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M //是一个匹配。
//二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
    
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;

const int N = 510, M = 100010;
bool st[N];// 表示当前这个男生的匹配轮次下,这个女生被男生预定了          预约数组
int match[N];// match[j]表示第j个女生的男朋友是谁,0的话代表还没有男朋友  匹配数组
int e[M], ne[M], h[N], idx;
int n1, n2, m;

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

bool find(int u){
    
    for(int i = h[u]; i != -1; i = ne[i]){
        int j = e[i];
        
        if(!st[j]){
            st[j] = true;// 女孩j被预约了
            // 当j这个女孩没有匹配对象或者匹配对象有备胎的时候,这时候匹配成功
            if(match[j] == 0 || find(match[j])){
                match[j] = u;
                return true;
            }
        }
    }
    return false;
}

int main(){
    memset(h, -1, sizeof h);
    scanf("%d%d%d", &n1, &n2, &m);
    while(m--){
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);// 虽然是无向图,但只需要保存男生指向女生的那部分就行了
    }
    
    int res = 0;// 保存匹配的边数
    for(int i = 1; i <= n1; i++){
        // 每一轮重置一次st数组,相当于每一次查询第i个男生的匹配对象的时候,女孩j被男孩i预约了,是否匹配还需要再看
        // 如果不重置的话,那这个女孩被预约就相当于匹配上了,就不存在找备胎的情况了
        // 也就是说达不到最大匹配数
        memset(st, false, sizeof st);
        if(find(i)) res ++;
    }
    
    cout << res;
    
    return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值