AcWing算法基础课笔记(持续更新中)

目录

1.基础算法篇

1.1快速排序

1.1.1快速排序求第K个数

1.2归并排序

1.2.1求逆序对的数量

1.4二分

1.4.1数的范围

1.4.2数的三次方根

1.5高精度算法

1.5.1高精度加法

1.5.2高精度减法

1.5.3高精度乘法

1.5.4高精度除法

1.5.5高精度阶乘

1.6前缀和与差分

1.6.1一维前缀和

1.6.2二维前缀和

1.6.3一维差分

1.6.4二维差分

1.7双指针算法

1.7.0输出单词

1.7.1最长不重复子序列

1.7.2数组元素的目标和

1.7.3判断子序列

1.8位运算

1.8.1二进制中1的个数

1.9离散化

 (1)离散化方法1--map哈希映射离散化后的值(考虑相对大小),查询离散化后值的时间复杂度O(1)

(2)离散化方法2--二分查找离散化的值(考虑相对大小),查询离散化后的值的时间复杂度O(logn)

(3) 离散化方法3--不需要保序的离散化--unordered_map

1.9.1区间和

1.10区间合并

2.数据结构篇

2.1单链表

2.2双链表

2.3栈

2.3.1模拟栈

2.3.2表达式求值

2.4队列

2.5单调栈

2.6单调队列

2.6.1滑动窗口

2.7KMP

2.8Trie

2.8.1Trie字符串统计

2.8.2最大异或对

2.9并查集

2.9.1合并集合

2.9.2连通块中点的数量

2.9.3食物链

2.9.4连通图

2.10堆

2.10.1堆排序

2.10.2模拟堆

2.11哈希表

2.11.1模拟散列表

2.11.2字符串哈希表

2.12 STL

2.12.1 sort

2.12.2 lower_bound/upper_bound(二分)

2.12.3 vector

2.12.4 pair

2.12.5 set

2.12.6 multiset

2.12.7 deque(双端队列)

2.12.8 list(双向链表)

2.12.9 map

2.12.10 生成排列组合

2.12.11 stable_sort稳定排序

2.12.12 multimap(多重映照容器)

2.12.13 stack(栈)

2.12.14 queue(队列)

2.12.15 priority_queue(优先队列/堆)

2.12.16 bitset(压位)

2.12.17 其他容器和函数

3.搜索与图论篇

3.1DFS

3.1.1排列数字

3.1.2n-皇后问题

3.2BFS

3.2.1走迷宫

3.2.2八数码

3.3.0树和图的一些预备知识

3.3树的深度优先遍历

3.3.1树的重心

3.4数的广度优先遍历

3.4.1图中点的层次

3.5拓扑排序

3.6.0最短路问题

3.6Dijkstra

3.6.1Dijkstra求最短路I

3.6.2Dijkstra求最短路II

3.7bellman-ford

3.7.1有边数限制的最短路

3.8spfa

3.8.1spfa求最短路

3.8.2spfa判断负环

3.9Floyd

3.9.1Floyd求最短路

3.10Prim

3.10.1Prim求最小生成树

3.11Kruskal

3.11.1Kruskal求最小生成树

3.12染色法判定二分图

3.13匈牙利算法

3.13.1二分图的最大匹配

4.数学知识篇

4.1质数

4.1.1试除法判定质数

4.1.2分解质因数

4.1.3筛质数

4.2约数

4.2.1 试除法求约数

4.2.2约数个数

4.2.3约数之和

4.2.4最大公约数

4.3欧拉函数

4.3.1 欧拉函数

4.3.2筛法求欧拉函数

4.4快速幂

4.4.1快速幂

4.4.2 快速幂求逆元

4.5扩展欧几里得算法

4.5.1扩展欧几里得算法

4.5.2线性同余方程

4.6中国剩余定理

4.6.1表达整数奇怪的方式

4.7高斯消元

4.7.1高斯消元解线性方程组

4.8求组合数

4.8.1求组合数 I

4.8.2 求组合数 II

4.8.3求组合数 III

4.8.4求组合数 IV

4.8.5满足条件的01序列

4.9容斥原理

4.9.1能被整除的数

4.10博弈论

4.10.1 Nim游戏

4.10.2台阶-Nim游戏

4.10.3集合-Nim游戏

4.10.4拆分-Nim游戏

5.动态规划篇

5.1背包问题

5.1.1 01背包问题

5.1.2 完全背包问题

5.1.3 多重背包问题I

5.1.4多重背包问题 II

5.1.5分组背包问题

5.2线性DP

5.2.1数字三角形

5.2.2最长上升子序列

5.2.3 最长上升子序列 II

5.2.4最长公共子序列

5.2.5最短编辑距离

5.2.6编辑距离

5.3区间DP

5.3.1石子合并

5.4计数类DP

5.4.1整数划分

5.5数位统计DP

5.5.1计数问题

5.6状态压缩DP

5.6.1蒙德里安的梦想

5.6.2 最短Hamilton路径

5.7树形DP

5.7.1 没有上司的舞会

5.8记忆化搜索

5.8.1记忆化搜索

6.贪心

6.1区间问题

6.1.1区间选点

6.1.2 最大不相交区间数量

6.1.3 区间分组

6.1.4 区间覆盖

6.2Huffman树

6.2.1合并果子

6.3排序不等式

6.3.1排队打水

6.4绝对值不等式

6.4.1货仓选址

6.5推公式

6.5.1耍杂技的牛


1.基础算法篇

1.1快速排序

给定你一个长度为 n 的整数数列。

请你使用快速排序对这个数列按照从小到大进行排序。

并将排好序的数列按顺序输出。

输入格式

输入共两行,第一行包含整数 n。

第二行包含 n 个整数(所有整数均在 1∼1e9),表示整个数列。

输出格式

输出共一行,包含 n 个整数,表示排好序的数列。

数据范围

1≤n≤100000

输入样例:

5

3 1 2 4 5

输出样例:

1 2 3 4 5

快速排序是基于分治的思想对区间进行处理。

思路

快排是基于分治的思想对区间进行处理,每次都会选择一个比较的值x,x可以是首元素、中间元素、尾元素,一般多为选择中间的元素作为排序的关键字元素,把两个指针分别置于左右两个端点,利用两个指针的暂停的条件,使得所有小于等于x的分在x的左边,大于等于x的在x的右边,当两个指针相遇的时候,这一趟结束,然后分别进行递归处理划分的子区间,直到结束。

时间复杂度

快排最坏的时间复杂度为O(nlongn),因为每次都是将区间一分为二,假设一共x次才将区间划分好,n/2^x=1,x=log2n,一共n趟,那么为n*logn.

快排的性质

快速排序是不稳定的一种排序方法,判断一个排序算法是否稳定,取决于两个相同的元素在排好序前后的相对位置是否一样,比如x1=x2,在排序之前的相对位置为x1,x2,在排好序之后相对位置依然不发生改变,那么这个排序算法就是稳定的,否则是不稳定的。

快速排序的做法是非常优美的做法,利用了两个指针的相向而行和规定的限制条件,就可以将序列排好序。

流程

  1. 确定分界点:x=q[l]或者q[r]或者q[l+r>>1]。
  2. 调整区间:

使得小于等于x在x左边;大于等于x在x右边。

  1. 递归处理左右端点。

模拟:

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int q[N];
void quick_sort(int q[],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(q,l,j);
    quick_sort(q,j+1,r);
}
int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++) cin>>q[i];
    quick_sort(q,0,n-1);
    for(int i=0;i<n;i++) cout<<q[i]<<" ";
    return 0;
}

1.1.1快速排序求第K个数

给定一个长度为 n 的整数数列,以及一个整数 k,请用快速选择算法求出数列从小到大排序后的第 k 个数。

输入格式

第一行包含两个整数 n和 k。

第二行包含 n个整数(所有整数均在 1∼1e9 范围内),表示整数数列。

输出格式

输出一个整数,表示数列的第 k小数。

数据范围

1≤n≤100000,
1≤k≤n

输入样例:

5 3
2 4 1 5 3

输出样例:

3

时间复杂度O(n)

思路

如何求出第k小的数?

#include<iostream>
using namespace std;
const int N=1e5+10;
int q[N],n,k;

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

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

1.2归并排序

给定你一个长度为 n 的整数数列。

请你使用归并排序对这个数列按照从小到大进行排序。

并将排好序的数列按顺序输出。

输入格式

输入共两行,第一行包含整数 nn。

第二行包含 n 个整数(所有整数均在 1∼109),表示整个数列。

输出格式

输出共一行,包含 n 个整数,表示排好序的数列。

数据范围

1≤n≤100000

输入样例:

5

3 1 2 4 5

输出样例:

1 2 3 4 5

归并排序主要是先将区间划分成两个区间,然后再递归两个区间,开一个辅助数组tmp[]对两个区间的数值进行处理,本质也是双指针算法。

思路:

  1. 确定分界点mid=l+r>>1;
  2. 递归排序l,r;
  3. 将两个有序的数组合二为一;

模拟:

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int q[N],tmp[N];
void merge_sort(int q[],int l,int r){
    if(l>=r) return ;
    int mid=l+r>>1;
    merge_sort(q,l,mid);
    merge_sort(q,mid+1,r);
    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];
}
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;i++) scanf("%d",&q[i]);
    merge_sort(q,0,n-1);
    for(int i=0;i<n;i++) cout<<q[i]<<" ";
    return 0;
}

1.2.1求逆序对的数量

给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。

逆序对的定义如下:对于数列的第 i个和第 j个元素,如果满足 i<j 且 a[i]>a[j],则其为一个逆序对;否则不是。

输入格式

第一行包含整数 n,表示数列的长度。

第二行包含 n 个整数,表示整个数列。

输出格式

输出一个整数,表示逆序对的个数。

数据范围

1≤n≤100000,

数列中的元素的取值范围 [1,109]。

输入样例:

6

2 3 4 5 6 1

输出样例:

5

假设归并排序在排好序的同时可以将区间中的逆序对求出来。

思路:

  1. 划分区间;[l,r]->[l,mid],[mid+1,r]
  2. 递归处理[l,mid]和[mid+1,r];
  3. 归并,将左右两个有序序列合并成为一个序列;

区间中逆序对的分布:

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e6+10;
int n;
int q[N],tmp[N];
LL merge_sort(int l,int r)
{
    if(l>=r) return 0;
    int mid=l+r>>1;
    LL res=merge_sort(l,mid)+merge_sort(mid+1,r);
    int k=0,i=l,j=mid+1;
    while(i<=mid&&j<=r)
    {
        if(q[i]<=q[j]) tmp[k++]=q[i++];
        else
        {
            tmp[k++]=q[j++];
            res+=mid-i+1;
        }
    }
    while(i<=mid) tmp[k++]=q[i++];
    while(j<=r) tmp[k++]=q[j++];
    for(int i=l,j=0;i<=r;i++,j++) q[i]=tmp[j];
    return res;
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++) cin>>q[i];
    cout<<merge_sort(0,n-1)<<endl;
    return 0;
}

1.4二分

1.4.1数的范围

给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。

对于每个查询,返回一个元素 k的起始位置和终止位置(位置从 0 开始计数)。

如果数组中不存在该元素,则返回 -1 -1。

输入格式

第一行包含整数 n和 q,表示数组长度和询问个数。

第二行包含 n 个整数(均在 1∼10000 范围内),表示完整数组。

接下来 q 行,每行包含一个整数 k,表示一个询问元素。

输出格式

共 q 行,每行包含两个整数,表示所求元素的起始位置和终止位置。

如果数组中不存在该元素,则返回 -1 -1。

数据范围

1≤n≤100000

1≤q≤10000

1≤k≤10000

输入样例:

6 3

1 2 2 3 3 4

3

4

5

输出样例:

3 4

5 5

-1 -1

整数二分模板

bool check(int x) {/* ... */} // 检查x是否满足某种性质

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

AC代码

#include<bits/stdc++.h>
using namespace std;
const int N=100100;
int q[N],x;

int bsearch_1(int l,int r)
{
    while(l<r)
    {
        int mid=l+r+1>>1;
        if(q[mid]<=x) l=mid;
        else r=mid-1;
    }
    
    return l;
}

int bsearch_2(int l,int r)
{
    while(l<r)
    {
        int mid=l+r>>1;
        if(q[mid]>=x) r=mid;
        else l=mid+1;
    }
    
    return l;
}

int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=0;i<n;i++) cin>>q[i];
    while(m--){
        cin>>x;
        if(q[bsearch_1(0,n-1)]==x&&q[bsearch_2(0,n-1)]==x)
        {
            cout<<bsearch_2(0,n-1)<<" "<<bsearch_1(0,n-1)<<endl;
        }
        else
            cout<<"-1 -1"<<endl;
    }
    return 0;
}

1.4.2数的三次方根

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

输入格式

共一行,包含一个浮点数 n。

输出格式

共一行,包含一个浮点数,表示问题的解。

注意,结果保留 6 位小数。

数据范围

−10000≤n≤10000

输入样例:

1000.00

输出样例:

10.000000

浮点数二分模板

bool check(double x) {/* ... */} // 检查x是否满足某种性质

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

AC代码

#include<bits/stdc++.h>
using namespace std;
int main()
{
    double n;
    cin>>n;
    double l=-1000,r=1000;
    while(r-l>1e-8){
        double mid=(l+r)/2;
        if(mid*mid*mid>=n) r=mid;
        else l=mid;
    }
    printf("%.6lf",r);
    return 0;
}

1.5高精度算法

性质:数组或者容器从低位往高位依次存储大整数,方便进位。

1.5.1高精度加法

给定两个正整数(不含前导 0),计算它们的和。

输入格式

共两行,每行包含一个整数。

输出格式

共一行,包含所求的和。

数据范围

1≤整数长度≤100000

输入样例:

12

23

输出样例:

35

思路:

模拟人工加法。

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

vector<int> sum(vector<int> &A,vector<int> &B)
{
    vector<int> C;
    int k=0;
    for(int i=0;i<max(A.size(),B.size());i++)
    {
        if(i<A.size()) k+=A[i];
        if(i<B.size()) k+=B[i];
        C.push_back(k%10);
        k/=10;
     }
     if(k) C.push_back(1);
     return C;
} 

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

1.5.2高精度减法

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

输入格式

共两行,每行包含一个整数。

输出格式

共一行,包含所求的差。

数据范围

1≤整数长度≤105

输入样例:

32

11

输出样例:

21

思路:

模拟人工减法。

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

vector<int> A,B;

bool cmp(vector<int> &A,vector<int> &B){
    if(A.size()!=B.size()) return A.size()>B.size();
    else{
        for(int i=A.size()-1;i>=0;i--){
            if(A[i]!=B[i]) return A[i]>B[i];
        }
    }
    return 1;
}

vector<int> sub(vector<int> &A,vector<int> &B){
    int k=0;//表示上一位在这一位借走的位数
    vector<int> C;
    for(int i=0;i<A.size();i++){
        int t=A[i]-k;
        if(i<B.size()) t-=B[i];
        if(t<0) t+=10,k=1;
        else k=0;
        C.push_back(t%10);
    }
    while(C.size()>1&&C.back()==0) C.pop_back();
    return C;
}

int main(){
    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');
    vector<int> C;
    if(cmp(A,B)) C=sub(A,B);  //当A>=B时,答案为0或正值
    else C=sub(B,A),cout<<"-";  //当A<B时,答案为负值
    for(int i=C.size()-1;i>=0;i--) cout<<C[i];
    return 0;
}

1.5.3高精度乘法

给定两个非负整数(不含前导 0)A 和 B,请你计算 A×B 的值。

输入格式

共两行,第一行包含整数 A,第二行包含整数 B。

输出格式

共一行,包含 A×B 的值。

数据范围

1≤A的长度≤100000,

0≤B≤10000

输入样例:

2

3

输出样例:

6

高精度x低精度

//高精度x低精度
#include<bits/stdc++.h>
#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();i++)
    {
        t+=A[i]*b;
        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');
    auto C=mul(A,b);
    for(int i=C.size()-1;i>=0;i--) cout<<C[i];
    return 0;
}

高精度x高精度

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 1e5+10;

int A[N],B[N],C[N];
int la,lb,lc;

void mul(int A[],int B[],int C[])
{
    for(int i=0;i<la;i++)
        for(int j=0;j<lb;j++)
        {
            C[i+j]+=A[i]*B[j];
            C[i+j+1]+=C[i+j]/10;
            C[i+j]%=10;
        }
    while(lc&&C[lc]==0) lc--;
}

int main()
{
    string a,b;
    cin>>a>>b;
    la=a.size();
    lb=b.size();
    lc=la+lb+10;
    for(int i=a.size()-1;i>=0;i--) A[la-i-1]=a[i]-'0';
    for(int i=b.size()-1;i>=0;i--) B[lb-i-1]=b[i]-'0';
    mul(A,B,C);
    for(int i=lc;i>=0;i--) cout<<C[i];
    return 0;
}

1.5.4高精度除法

给定两个非负整数(不含前导 0)A,B,请你计算 A/B的商和余数。

输入格式

共两行,第一行包含整数 A,第二行包含整数 B。

输出格式

共两行,第一行输出所求的商,第二行输出所求余数。

数据范围

1≤A的长度≤100000,

1≤B≤10000,

B 一定不为 00

输入样例:

7

2

输出样例:

3

1

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
vector<int> div(vector<int> &A,int B,int &r)
{
    vector<int> C;
    for(int i=0;i<A.size();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,r=0;
    cin>>a>>B;
    vector<int> A;
    for(int i=0;i<a.size();i++) A.push_back(a[i]-'0');
    auto C=div(A,B,r);
    for(int i=C.size()-1;i>=0;i--) cout<<C[i];
    cout<<endl<<r;// 输出余数
    return 0;
}

1.5.5高精度阶乘

问题描述

  输入一个正整数n,输出n!的值。

  其中n!=1*2*3*…*n

算法描述

  n!可能很大,而计算机能表示的整数范围有限,需要使用高精度计算的方法。使用一个数组A来表示一个大整数aA[0]表示a的个位,A[1]表示a的十位,依次类推。

  将a乘以一个整数k变为将数组A的每一个元素都乘以k,请注意处理相应的进位。

  首先将a设为1,然后乘2,乘3,当乘到n时,即得到了n!的值。

输入格式

  输入包含一个正整数nn<=1000。

输出格式

  输出n!的准确值。

样例输入

10

样例输出

3628800

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

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

int main()
{
    scanf("%d",&n);
    a[1]=1;
    int t=0;
    for(int i=2;i<=n;i++)
    {
        for(int j=1;j<=10000;j++)
        {
            int p=a[j]*i+t;
            a[j]=p%10;
            t=p/10;
        }
    }
    n=10000;
    while(a[n]==0) n--;
    for(int i=n;i>=1;i--) cout<<a[i];
    return 0;
}

1.6前缀和与差分

1.6.1一维前缀和

输入一个长度为 n 的整数序列。

接下来再输入 m个询问,每个询问输入一对 l,r。

对于每个询问,输出原序列中从第 l个数到第 r 个数的和。

输入格式

第一行包含两个整数 n 和 m。

第二行包含 n 个整数,表示整数数列。

接下来 m 行,每行包含两个整数 l 和 r,表示一个询问的区间范围。

输出格式

共 m 行,每行输出一个询问的结果。

数据范围

1≤l≤r≤n,

1≤n,m≤100000,

−1000≤数列中元素的值≤1000

输入样例:

5 3

2 1 3 6 4

1 2

1 3

2 4

输出样例:

3

6

10

思路:

如果s[i]是a[i]的前缀和数组,那么就有s[i]=s[i-1]+a[i];

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int a[N],s[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        s[i]=s[i-1]+a[i];
    }
    while(m--)
    {
        int l,r;
        cin>>l>>r;
        cout<<s[r]-s[l-1]<<endl;
    }
    return 0;
}

1.6.2二维前缀和

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

对于每个询问输出子矩阵中所有数的和。

输入格式

第一行包含三个整数 n,m,q。

接下来 n 行,每行包含 m 个整数,表示整数矩阵。

接下来 q 行,每行包含四个整数 x1,y1,x2,y2,表示一组询问。

输出格式

共 q 行,每行输出一个询问的结果。

数据范围

1≤n,m≤1000,

1≤q≤200000,

1≤x1≤x2≤n,

1≤y1≤y2≤m,

−1000≤矩阵内元素的值≤1000

输入样例:

3 4 3

1 7 2 4

3 6 2 8

2 1 2 3

1 1 2 2

2 1 3 4

1 3 3 4

输出样例:

17

27

21

思路:

如果s[i][j]是a[i][j]的前缀和数组,那么我们求(x1,y1)到(x2,y2)就有:

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

1.6.3一维差分

输入一个长度为 n 的整数序列。

接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r]之间的每个数加上 c。

请你输出进行完所有操作后的序列。

输入格式

第一行包含两个整数 n 和 m。

第二行包含 n 个整数,表示整数序列。

接下来 m 行,每行包含三个整数 l,r,c表示一个操作。

输出格式

共一行,包含 n个整数,表示最终序列。

数据范围

1≤n,m≤100000,

1≤l≤r≤n,

−1000≤c≤1000,

−1000≤整数序列中元素的值≤1000

输入样例:

6 3

1 2 2 1 2 1

1 3 1

3 5 1

1 6 1

输出样例:

3 4 5 3 4 2

思路:

我们要改变a[]数组中[l,r]区间上的数,那么我们就可以想到一种方法改变a[]数组,我们可以构造a[]数组的差分数组b[]数组。

首先,第一个问题,什么是差分数组?

差分数组的意思是某个数组是差分数组的前缀和数组,则本题中a[]数组就是b[]数组的前缀和数组。

第二,为什么差分数组能够改变a[]数组中任意[l,r]区间里面的数值?

因为a[]是b[]数组的前缀和如果b[l]+1,那么a[]数组中a[l]之后的所有的数都会+1,因为我们要改变的是a数组[l,r]区间的值,这时候r之后的值也会改变,只需要b[r+1]-1这样就可以只改变[l,r]区间的值.

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int a[N],b[N];
int n,m;
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        b[i]=a[i]-a[i-1];//构造差分数组
    }
    while(m--)
    {
        int l,r,c;
        scanf("%d%d%d",&l,&r,&c);
        b[l]+=c;
        b[r+1]-=c;
    }
    for(int i=1;i<=n;i++)
    {
        a[i]=a[i-1]+b[i];//构造b数组的前缀和维a数组
        cout<<a[i]<<" ";
    }
    return 0;
}

1.6.4二维差分

输入一个 n行 m列的整数矩阵,再输入 q个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1)和 (x2,y2)表示一个子矩阵的左上角坐标和右下角坐标。

每个操作都要将选中的子矩阵中的每个元素的值加上 cc。

请你将进行完所有操作后的矩阵输出。

输入格式

第一行包含整数 n,m,q。

接下来 n行,每行包含 m个整数,表示整数矩阵。

接下来 q行,每行包含 5个整数 x1,y1,x2,y2,c,表示一个操作。

输出格式

共 n行,每行 m个整数,表示所有操作进行完毕后的最终矩阵。

数据范围

1≤n,m≤1000,

1≤q≤100000,

1≤x1≤x2≤n,

1≤y1≤y2≤m,

−1000≤c≤1000,

−1000≤矩阵内元素的值≤1000

输入样例:

3 4 3

1 2 2 1

3 2 2 1

1 1 1 1

1 1 2 2 1

1 3 2 3 2

3 1 3 4 1

输出样例:

2 3 4 1

4 3 4 1

2 2 2 2

思路:

基于一维差分的思想,我们来考虑二维数组的差分。

我们如果让a[][]数组(x1,y1)和(x2,y2)区间的每个数的值都加上c,我们构造b[][]数组的前缀和是a[][]数组。

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int a[N][N],b[N][N];
int main()
{
    int n,m,q;
    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]=a[i][j]-a[i-1][j]-a[i][j-1]+a[i-1][j-1];//构造a数组的差数组
        }
    }
    
    while(q--)
    {
        int x1,y1,x2,y2,c;
        cin>>x1>>y1>>x2>>y2>>c;
        b[x1][y1]+=c;
        b[x1][y2+1]-=c;
        b[x2+1][y1]-=c;
        b[x2+1][y2+1]+=c;
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            a[i][j]=b[i][j]+a[i-1][j]+a[i][j-1]-a[i-1][j-1];//构造b数组的前缀和
            cout<<a[i][j]<<" ";
        }
        cout<<endl;
    }
    return 0;
}

1.7双指针算法

基本思路:找到某种性质,对循环进行优化。

1.7.0输出单词

给定一行句子,输出其中的单词

样例输入

i am student

样例输出

i

am

student  

#include<bits/stdc++.h>
#include<string>
using namespace std;
int main()
{
    char str[1000];
    gets(str);
    int n=strlen(str);
    for(int i=0;i<n;i++)
    {
        int j=i;
        while(j<n&&str[j]!=' ') j++; //指针j扫描到空格
        for(int k=i;k<j;k++) cout<<str[k];
        cout<<endl;
        i=j; //指针i等于空格后的第一个字符
    }
    return 0;
}

1.7.1最长不重复子序列

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

输入格式

第一行包含整数 n。

第二行包含 n 个整数(均在 0∼105 范围内),表示整数序列。

输出格式

共一行,包含一个整数,表示最长的不包含重复的数的连续区间的长度。

数据范围

1≤n≤1e5

输入样例:

5

1 2 2 3 5

输出样例:

3

思路:

我们用count(x)记录x是否重复出现过,我们先用i指针向后探测,j指针先不动,直到出现过重复的,再将j指针向后移动,count(x)--来移动j指针。

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
using namespace std;
const int N = 1e5 + 10;
int n;
int a[N], s[N];
int res;
int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    for(int i = 1, j = 1; 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;
}

1.7.2数组元素的目标和

给定两个升序排序的有序数组 A 和 B,以及一个目标值 x。

数组下标从 0开始。

请你求出满足 A[i]+B[j]=x 的数对 (i,j)。

数据保证有唯一解。

输入格式

第一行包含三个整数 n,m,x,分别表示 A 的长度,B 的长度以及目标值 x。

第二行包含 n个整数,表示数组 A。

第三行包含 m 个整数,表示数组 B。

输出格式

共一行,包含两个整数 i 和 j。

数据范围

数组长度不超过105。

同一数组内元素各不相同。

1≤数组元素≤109

输入样例:

4 5 6

1 2 4 7

3 4 6 8 9

输出样例:

1 1

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int a[N],b[N];
int main()
{
    int n,m,x;
    cin>>n>>m>>x;
    for(int i=0;i<n;i++) cin>>a[i];
    for(int i=0;i<m;i++) cin>>b[i];
    int l=0,r=m-1;
    while(l<n&&r>=0)
    {
        if(a[l]+b[r]<x) l++;
        else if(a[l]+b[r]>x) r--;
        else
        {
            cout<<l<<" "<<r;
            return 0;
        }
    }
    return 0;
}

1.7.3判断子序列

给定一个长度为 n 的整数序列 a1,a2,…,an 以及一个长度为 m 的整数序列 b1,b2,…,bm。

请你判断 a 序列是否为 b 序列的子序列。

子序列指序列的一部分项按原有次序排列而得的序列,例如序列 {a1,a3,a5}是序列 {a1,a2,a3,a4,a5} 的一个子序列。

输入格式

第一行包含两个整数 n,m。

第二行包含 n 个整数,表示 a1,a2,…,an。

第三行包含 m 个整数,表示 b1,b2,…,bm。

输出格式

如果 a 序列是 b 序列的子序列,输出一行 Yes。

否则,输出 No。

数据范围

1≤n≤m≤105,

−1e9≤ai,bi≤1e9

输入样例:

3 5

1 3 5

1 2 3 4 5

输出样例:

Yes

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int a[N],b[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=0;i<n;i++) cin>>a[i];
    for(int i=0;i<m;i++) cin>>b[i];
    int ans=0;
       int i=0,j=0;
       while(i<n&&j<m)
       {
           if(a[i]==b[j]) ans++,i++;
           j++;
    }
    if(ans==n) cout<<"Yes";
    else cout<<"No";
    return 0;
}

1.8位运算

&:按位与,1&0=0,0&1=0,0&0=0,1&1=1,只有都为1时才为1.

|:按位或,1|1=1,1|0=1,0|1=1,0|0=0,只有都为0时才为0.

^:按位异或,1^1=0,1^0=1,0^a=a,相同为0,不同为非0的那个数.

>>:右移,a>>x,表示a除以2^x;

<<:左移,a<<x,表示a乘2^x;

~:把0变成1,把1变成0;

-x=~x+1;

(1)lowbit(x)

将十进制数的二进制表示的最低位1取出来。

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

如x的二进制表示时100,-x在计算机中为~x+1,则~x=011,~x+1=111,那么就有

(100)&(111)=(100),这样就可以把最低位上面的1取出来。

(2)把n对应二进制表示中第k位取出来(注意有第0位)

int get(int n,int k)
{
    return n>>k&1;
}

(3)输出所有小于k的十进制

for(int i=0;i<1<<k;i++)
    cout<<i;

1.8.1二进制中1的个数

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

输入格式

第一行包含整数 n。

第二行包含 n 个整数,表示整个数列。

输出格式

共一行,包含 n 个整数,其中的第 i 个数表示数列中的第 i 个数的二进制表示中 1 的个数。

数据范围

1≤n≤100000,

0≤数列中元素的值≤1e9

输入样例:

5

1 2 3 4 5

输出样例:

1 1 2 1 2

#include<iostream>
using namespace std;
int f(int n)
{
    int ans=0;
    while(n>0)
    {
        if(n%2==1) ans++;
        n/=2;
    }
    return ans;
}
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        int n;
        cin>>n;
        cout<<f(n)<<" ";
    }
    return 0;
}

1.9离散化

概念:在一些问题中,我们只关心n个数字之间的相对大小关系,而不关心他们具体是什么,因此我们可以将这n个数映射成1~n的整数,从而降低规模,通常的实现方法是对所有的数字进行排序,然后再重新遍历一遍所有的数字,通过二分查找法来找到他们的"排名",然后用排名代替数字。

如我们将9999 1 100 1000进行离散化:

 (1)离散化方法1--map哈希映射离散化后的值(考虑相对大小),查询离散化后值的时间复杂度O(1)

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
#include<unordered_map>
using namespace std;
unordered_map<int,int> mp;
vector<int> v,h;
int n;

int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {
        int x;
        scanf("%d",&x);
        v.push_back(x);
        h.push_back(x);
    }

    sort(h.begin(),h.end());
    h.erase(unique(h.begin(),h.end()),h.end());

    for(int i=0;i<h.size();i++)
        mp[h[i]]=i+1;
    int x;
    scanf("%d",&x);
    printf("%d",mp[x]);
    return 0;
}

(2)离散化方法2--二分查找离散化的值(考虑相对大小),查询离散化后的值的时间复杂度O(logn)

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<vector>
#include<unordered_map>
using namespace std;
unordered_map<int,int> mp;
vector<int> v,h;
int n;

int find(int x)
{
    int l=0,r=h.size()-1;
    while(l<r)
    {
        int mid=l+r>>1;
        if(h[mid]>=x) r=mid;
        else l=mid+1;
    }
    return l+1;
}

int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
    {
        int x;
        scanf("%d",&x);
        v.push_back(x);
        h.push_back(x);
    }

    sort(h.begin(),h.end());
    h.erase(unique(h.begin(),h.end()),h.end());
    int x;
    scanf("%d",&x);
    printf("%d",find(x));
    return 0;
}

(3) 离散化方法3--不需要保序的离散化--unordered_map

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include<unordered_map>
using namespace std;
unordered_map<int,int> h;
int idx;
int x;

int get(int x)
{
    if(h.count(x)==0) h[x]=++idx;
    return h[x];
}

int main()
{
    scanf("%d",&x);
    printf("%d",get(x));
    return 0;
}

1.9.1区间和

假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。

现在,我们首先进行 n 次操作,每次操作将某一位置 x上的数加 c。

接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r]之间的所有数的和。

输入格式

第一行包含两个整数 n 和 m。

接下来 n 行,每行包含两个整数 x 和 c。

再接下来 m 行,每行包含两个整数 l和 r。

输出格式

共 m 行,每行输出一个询问中所求的区间内数字和。

数据范围

−109≤x≤109,

1≤n,m≤105,

−109≤l≤r≤109,

−10000≤c≤10000

输入样例:

3 3

1 2

3 6

7 5

1 3

4 6

7 8

输出样例:

8

0

5

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

using namespace std;

typedef pair<int, int> PII;

const int N = 300010;

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

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

int find(int x)
{
    int l = 0, r = alls.size() - 1;
    while (l < r)
    {
        int mid = l + r >> 1;
        if (alls[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return r + 1;
}

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

    return a.begin() + j;
}

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

        alls.push_back(x);
    }

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

        alls.push_back(l);
        alls.push_back(r);
    }

    // 去重
    sort(alls.begin(), alls.end());
    alls.erase(unique(alls), alls.end());

    // 处理插入
    for (auto item : add)
    {
        int x = find(item.first);
        a[x] += item.second;
    }

    // 预处理前缀和
    for (int i = 1; i <= alls.size(); i ++ ) s[i] = s[i - 1] + a[i];

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

    return 0;
}

1.10区间合并

给定 n 个区间 [li,ri],要求合并所有有交集的区间。

注意如果在端点处相交,也算有交集。

输出合并完成后的区间个数。

例如:[1,3] 和 [2,6] 可以合并为一个区间 [1,6]。

输入格式

第一行包含整数 nn。

接下来 nn 行,每行包含两个整数 l 和 r。

输出格式

共一行,包含一个整数,表示合并区间完成后的区间个数。

数据范围

1≤n≤100000,

−109≤li≤ri≤109

输入样例:

5

1 2

2 4

5 6

7 8

7 9

输出样例:

3

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

using namespace std;

typedef pair<int, int> PII;

int n;

void merge(vector<PII> &interval)
{
    vector<PII> ans;

    sort(interval.begin(), interval.end()); //! pair排序 优先左端点, 再以右端点排序

    int st = -1e9-10, ed = -1e9-10;  //! 只要比 -1e9 小就可以
    for(auto item:interval)
    {
        if(ed<item.first) //! 第一段区间一定是  ed< item.first
        {
            if(st!=-1e9-10) ans.push_back({st,ed}); //! 第一次在这里初始化
            st = item.first, ed = item.second;//! 第一段区间从这里开始
        }
        else ed = max(ed, item.second);
    }//todo 这个循环结束之后还会剩下一个区间
    if(st!=-1e9-10) ans.push_back({st,ed});  //! 如果不是空的  那我们就加上一段

    interval = ans;
}

int main(void)
{
    ios::sync_with_stdio(false);
    cin >> n;

    vector<PII> interval;
    while(n--)
    {
        int l, r;
        cin >> l >> r;

        interval.push_back({l, r});
    }

    merge(interval);

    cout << interval.size() << endl;

    return 0;
}

2.数据结构篇

用数组模拟链表。

2.1单链表

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

向链表头插入一个数;

删除第 k 个插入的数后面的数;

在第 k个插入的数后插入一个数。

现在要对该链表进行 M次操作,进行完所有操作后,从头到尾输出整个链表。

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

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

H x,表示向链表头插入一个数 x。

D k,表示删除第 k个插入的数后面的数(当 k为 0 时,表示删除头结点)。

I k x,表示在第 k 个插入的数后面插入一个数 x(此操作中 k 均大于 0)。

输出格式

共一行,将整个链表从头到尾输出。

数据范围

1≤M≤100000

所有操作保证合法。

输入样例:

10

H 9

I 1 1

D 1

D 0

H 6

I 3 6

I 4 5

I 4 5

I 3 4

D 6

输出样例:

6 4 6 5

我们用-1表示空指针。

实现一些基本的操作:

(1)初始化

头节点指向-1表示空节点,idx=0表示从0好节点进行编号。

void init()//链表的初始化
{
    head=-1;//头节点指向空节点
    idx=0;
}

(2)向头节点后面插入一个新节点

(3)向第k个插入的点后面添加一个点同(2)

void add(int k,int x)//向第k个插入的数后面插入一个数
{
    e[idx]=x,ne[idx]=ne[k],ne[k]=idx++;
}

因为是从0号节点进行编号的,所以第k个插入的点其实是第k-1个点add(k-1,x);

(4)删除头节点

void remove()//删除头节点
{
    head=ne[head];
}

(5)删除第k个插入的点

void de(int k)//删除第k个插入的数
{
    ne[k]=ne[ne[k]];
}

remove(k-1);

AC代码

#include<bits/stdc++.h>
#include<string>
using namespace std;
const int N=1e6+10;
int head,e[N],ne[N],idx;

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

void add_head(int x)//向头节点之后插入一个数
{
    e[idx]=x,ne[idx]=head,head=idx++;
}

void add(int k,int x)//向第k个插入的数后面插入一个数
{
    e[idx]=x,ne[idx]=ne[k],ne[k]=idx++;
}

void de(int k)//删除第k个插入的数
{
    ne[k]=ne[ne[k]];
}

void remove()//删除头节点
{
    head=ne[head];
}

int main()
{
    int t;
    scanf("%d",&t);
    init();
    while(t--){
        string op;
        int k,x;
        cin>>op;
        if(op=="H"){
            scanf("%d",&x);
            add_head(x);
        }
        else if(op=="D"){
            scanf("%d",&k);
            if(k==0) remove();
            de(k-1);
        }
        else{
            scanf("%d%d",&k,&x);
            add(k-1,x);
        }
    }
    for(int i=head;i!=-1;i=ne[i])
        cout<<e[i]<<" ";
    return 0;
}

2.2双链表

实现一个双链表,双链表初始为空,支持 55 种操作:

在最左侧插入一个数;

在最右侧插入一个数;

将第 k 个插入的数删除;

在第 k 个插入的数左侧插入一个数;

在第 k 个插入的数右侧插入一个数

现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。

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

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:

L x,表示在链表的最左端插入数 x。

R x,表示在链表的最右端插入数 x。

D k,表示将第 k 个插入的数删除。

IL k x,表示在第 k 个插入的数左侧插入一个数。

IR k x,表示在第 k 个插入的数右侧插入一个数。

输出格式

共一行,将整个链表从左到右输出。

数据范围

1≤M≤100000

所有操作保证合法。

输入样例:

10

R 7

D 1

L 3

IL 2 10

D 3

IL 2 7

L 8

R 9

IL 4 7

IR 2 2

输出样例:

8 7 7 3 2 9

双链表类似单链表的操作进行处理,只是每个节点都有两个指针l[],r[],分别指向前驱和后继。

模板:

// 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];
}
#include<bits/stdc++.h>
#include<string>
#include<algorithm>
using namespace std;
const int N=1e6+10;
int l[N],r[N],e[N],idx;
void init()
{
    r[0]=1;
    l[1]=0;
    idx=2;
}

void add(int k,int x)
{
    e[idx]=x;
    r[idx]=r[k];
    l[idx]=k;
    l[r[k]]=idx;
    r[k]=idx;
    idx++;
}

void remove(int k)
{
    r[l[k]]=r[k];
    l[r[k]]=l[k];
}
int main()
{
    init();
    int t;
    cin>>t;
    while(t--)
    {
        string op;
        cin>>op;
        int k,x;
        if(op=="R")
        {
            cin>>x;
            add(l[1],x);
        }
        else if(op=="L")
        {
            cin>>x;
            add(0,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;
}

2.3栈

2.3.1模拟栈

实现一个栈,栈初始为空,支持四种操作:

push x – 向栈顶插入一个数 x;

pop – 从栈顶弹出一个数;

empty – 判断栈是否为空;

query – 查询栈顶元素。

现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M 行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。

输出格式

对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示栈顶元素的值。

数据范围

1≤M≤100000,

1≤x≤1e9

所有操作保证合法。

输入样例:

10

push 5

query

push 6

pop

query

pop

empty

push 4

query

empty

输出样例:

5

5

YES

4

NO

栈:后进先出的数据结构。

// tt表示栈顶
int stk[N], tt = 0;

// 向栈顶插入一个数
stk[ ++ tt] = x;

// 从栈顶弹出一个数
tt -- ;

// 栈顶的值
stk[tt];

// 判断栈是否为空
if (tt > 0)
{

}

AC代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int stk[N],tt=0;
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        string op;
        cin>>op;
        if(op=="push")
        {
            int x;
            cin>>x;
            stk[++tt]=x;
        }
        else if(op=="pop")
        {
            tt--;
        }
        else if(op=="empty")
        {
            if(tt>0)
            cout<<"NO"<<endl;
            else
            cout<<"YES"<<endl;
        }
        else if(op=="query")
        {
            cout<<stk[tt]<<endl;
        }
    }
    return 0;
}

2.3.2表达式求值

给定一个表达式,其中运算符仅包含 +,-,*,/(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。

注意:

数据保证给定的表达式合法。

题目保证符号 - 只作为减号出现,不会作为负号出现,例如,-1+2,(2+2)*(-(1+1)+2) 之类表达式均不会出现。

题目保证表达式中所有数字均为正整数。

题目保证表达式在中间计算过程以及结果中,均不超过 2^31-1。

题目中的整除是指向 0取整,也就是说对于大于 0的结果向下取整,例如 5/3=1,对于小于 0的结果向上取整,例如 5/(1−4)=−1。

C++和Java中的整除默认是向零取整;Python中的整除//默认向下取整,因此Python的eval()函数中的整除也是向下取整,在本题中不能直接使用。

输入格式

共一行,为给定表达式。

输出格式

共一行,为表达式的结果。

数据范围

表达式的长度不超过 1e5。

输入样例:

(2+2)*(1+1)

输出样例:

8

我们按照优先级和入栈的顺序对表达式进行处理即可。

#include<bits/stdc++.h>
#include<iostream>
#include<string>
#include<stack>
#include<unordered_map>
using namespace std;

stack<int> num;
stack<char> op;

unordered_map<char,int> h{{'+',1},{'-',1},{'*',2},{'/',2}};//规定优先级

void eval()
{
    int a=num.top();
    num.pop();
    int b=num.top();
    num.pop();
    char p=op.top();
    op.pop();
    int r=0;
    if(p=='+') r=b+a;
    else if(p=='-') r=b-a;
    else if(p=='*') r=b*a;
    else if(p=='/') r=b/a;
    
    num.push(r);
}

int main()
{
    string s;
    cin>>s;
    for(int i=0;i<s.size();i++)
    {
        if(isdigit(s[i]))
        {
            int x=0,j=i;
            while(j<s.size()&&isdigit(s[j]))
            {
                x=x*10+s[j]-'0';
                j++;
            }
            num.push(x);
            i=j-1;
        }
        else if(s[i]=='(')  op.push(s[i]);
        else if(s[i]==')') 
        {
            while(op.top()!='(')    
                eval();
            op.pop();
        }
        else 
        {
            while(op.size()&&h[op.top()]>=h[s[i]])
                eval();
            op.push(s[i]);
        }
    }
    while(op.size()) eval();
    cout<<num.top()<<endl;
    return 0;
}  

2.4队列

实现一个队列,队列初始为空,支持四种操作:

push x – 向队尾插入一个数 x;

pop – 从队头弹出一个数;

empty – 判断队列是否为空;

query – 查询队头元素。

现在要对队列进行 M个操作,其中的每个操作 3和操作 4 都要输出相应的结果。

输入格式

第一行包含整数 M,表示操作次数。

接下来 M行,每行包含一个操作命令,操作命令为 push x,pop,empty,query 中的一种。

输出格式

对于每个 empty 和 query 操作都要输出一个查询结果,每个结果占一行。

其中,empty 操作的查询结果为 YES 或 NO,query 操作的查询结果为一个整数,表示队头元素的值。

数据范围

1≤M≤100000,

1≤x≤1e9,

所有操作保证合法。

输入样例:

10

push 6

empty

query

pop

empty

push 3

push 4

pop

query

push 6

输出样例:

NO

6

YES

4

队列:先进先出。

普通队列

// hh 表示队头,tt表示队尾
int q[N], hh = 0, tt = -1;

// 向队尾插入一个数
q[ ++ tt] = x;

// 从队头弹出一个数
hh ++ ;

// 队头的值
q[hh];

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

}

循环队列

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

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

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

// 队头的值
q[hh];

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

}

AC代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int q[N],hh=0,tt=-1;
int main()
{
    ios::sync_with_stdio(false);
    int T;
    cin>>T;
    while(T--){
        string op;
        cin>>op;
        if(op=="push"){
            int x;
            cin>>x;
            q[++tt]=x;
        }
        else if(op=="pop"){
            hh++;
        }
        else if(op=="empty"){
            if(hh<=tt) cout<<"NO"<<endl;
            else cout<<"YES"<<endl;
        }
        else cout<<q[hh]<<endl;
    }
    return 0;
}

2.5单调栈

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

输入格式

第一行包含整数 N,表示数列长度。

第二行包含 N个整数,表示整数数列。

输出格式

共一行,包含 N个整数,其中第 i 个数表示第 i个数的左边第一个比它小的数,如果不存在则输出 −1。

数据范围

1≤N≤1e5

1≤数列中元素≤1e9

输入样例:

5

3 4 2 7 5

输出样例:

-1 3 -1 2 2

常见模型:给定一个序列,求序列中每个数左边或者右边第一个比他小或者比他大的数。

暴力做法:

for(int i=1;i<=n;i++)
{
        int ans=0;
        for(int j=i;j>=1;j--)
        {
                if(a[i]>a[j]) 
                {
                        cout<<a[j]<<" ";
                        ans++;
                        break;
                }
        }
        if(ans==0) cout<<"-1"<<" ";
}

暴力做法的时间复杂度约为O(n2),我们可以考虑用单调栈去维护减低时间复杂度。

单调栈做法:

我们把a[i]左边的数用一个栈存储起来:

如果在栈中,有x<y并且a[x]>=a[y],那么a[x]一定不会作为答案输出,因为a[y]在a[x]的右边,且离a[i]最近,那么我们就可以把a[x]从栈中删除,最后栈中的元素一定是单调的。

如图所示,i=7时,栈中元素分布:

模板:

常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
    while (tt && check(stk[tt], i)) tt -- ;
    stk[ ++ tt] = i;
}

AC代码:

#include <iostream>
using namespace std;
const int N = 100010;
int stk[N], tt;

int main()
{
    int n;
    cin >> n;
    while (n -- )
    {
        int x;
        scanf("%d", &x);
        while (tt && stk[tt] >= x) tt -- ;//如果栈顶元素大于当前待入栈元素,则出栈
        if (!tt) printf("-1 ");//如果栈空,则没有比该元素小的值。
        else printf("%d ", stk[tt]);//栈顶元素就是左侧第一个比它小的元素。
        stk[ ++ tt] = x;
    }
    return 0;
}

2.6单调队列

2.6.1滑动窗口

给定一个大小为 n≤1e6的数组。

有一个大小为 k的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到 kk 个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。

你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式

输入包含两行。

第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。

第二行有 n个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式

输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。

第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:

8 3

1 3 -1 -3 5 3 6 7

输出样例:

-1 -3 -3 -3 3 3

3 3 5 5 6 7

性质:队列里面的元素值是单调的,递增或者递减。

思想:

例如:求滑动窗口的最大值。

用单调队列储存当前窗口内单调递减的元素的下标,并且队头是窗口内的最大值,队尾是窗口内的尾元素。也就是说,队列从队头到队尾对应窗口内从最大值到窗口的尾元素的子序列下标。

1.队头出队:当队头元素从滑动窗口划出时,队头元素出队,hh++。

2.队尾出队:当新的元素进入滑动窗口时,要把新元素从队尾插入,分两种情况:

(1).直接插入:如果新元素小于队尾元素,那么直接从队尾插入(q[++tt]=i),因为他可能在前面的最大值滑出窗口后成为最大值。

(2).先删后插:如果新元素大于等于队尾元素,那就先删除队尾元素(因为队尾不可能成为滑动窗口的最大值),删除队尾tt--,循环删除,直到队列为空或遇到一个大于新元素的值,再插入。

求最小值的思路相同。

AC代码

#include<iostream>
using namespace std;
const int N = 1e6+10;
int a[N],q[N];
int n,k;
int main()
{
    int n,k;
    cin>>n>>k;
    for(int i=1;i<=n;i++) cin>>a[i];

    //求滑动窗口里面的最小值。
    int hh=0,tt=-1;
    for(int i=1;i<=n;i++)
    {
        if(hh<=tt&&q[hh]<i-k+1) hh++; //如果队头元素值表示序列的下表不在滑动窗口的范围内,队头出队。
        while(hh<=tt&&a[i]<=a[q[tt]]) tt--; //如果插入的元素小于队尾元素,队尾出队,直到不小于为止。
        q[++tt]=i; //下表入队
        if(i>k-1) cout<<a[q[hh]]<<" "; //如果在滑动窗口的范围,输出最小值即可。
    }
    puts("");

    //求滑动窗口里面的最大值
    hh=0,tt=-1;
    for(int i=1;i<=n;i++)
    {
        if(hh<=tt&&q[hh]<i-k+1) hh++;
        while(hh<=tt&&a[i]>=a[q[tt]]) tt--;
        q[++tt]=i;
        if(i>k-1) cout<<a[q[hh]]<<" ";
    }
    return 0;
}

2.7KMP

给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。

模式串 P在字符串 S中多次作为子串出现。

求出模式串 P在字符串 S中所有出现的位置的起始下标。

输入格式

第一行输入整数 N,表示字符串 P 的长度。

第二行输入字符串 P。

第三行输入整数 M,表示字符串 S的长度。

第四行输入字符串 S。

输出格式

共一行,输出所有出现位置的起始下标(下标从 0开始计数),整数之间用空格隔开。

数据范围

1≤N≤1e5

1≤M≤1e6

输入样例:

3

aba

5

ababa

输出样例:

0 2

1.串的普通算法BF

BF算法图示过程(返回匹配成功的位置)

思想:

从主串的第pos个字符开始匹配和模式串中第一个字符串开始比较。

(1)如果相等:继续比后续字符,i++,j++;

(2)如果不相等,从主串的下一个字符和模式串 的第一个字符相比较。

任何求主串的下一个字符的位置?

方法一:设置一个变量k,在主串未开始时,领k=i+1(主串的下一个位置),每当匹配失败,另i=j,即可。

int bf(char s[],char t[],int pos)
{
    int i=pos,j=1;//从主串的第pos个字符,和模式串第一个字符比较
    while(i<=s.length&&j<=t.length)
    {
        int k=i+1; //让k等于i的下一个位置
        if(s[i]==t[j]) //匹配成功,继续比较下一个位置
        {
            ++i;
            ++j;
        }
        else //匹配失败
        {
            i=k;
            j=1;
        }
    }
    if(j>T.length) return i-T.length;//如果j大于模式串的长度,说明匹配成功
    else return 0; //匹配失败
}

方法二:找出每次失败i和j的关系。

则下一个位置是i-j+2.

int BF(char s[],char t[],int pos)
{
    int i=pos,j=1;
    while(i<=s.length&&j<t.length)
    {
        if(s[i]==s[j]) 
        {
            ++i;
            ++j;
        }
        else
        {
            i=i-j+2;
            j=1;
        }
    }
    if(j>t.length) return i-t.length;
    else return 0;
}

2.KMP算法

特点:在匹配过程中,不需要回溯主串的指针i,时间复杂度为O(m+n)

思路:

则我们可知next数组的含义,next[i]表示:以i结尾的后缀和从1开始模式串的前缀相等,且相等最大 。

假设我们已知next数组,则模式匹配如下:

思想

主串的第pos个字符和模式串的第一个字符串进行比较

(1).相等:继续比较后继字符 i++,j++。

(2).不相等:主串的位置不变和模式串的第next[j]字符比较,j=next[j]。

下面展示一个代码:

int KMP(char s[],char t[],int pos)
{
    int i=pos,j=1;
    while(i<=s.length&&j<=t.length)
    {
        if(j==0||s[i]==t[j]) //j==0表示当前比较的是模式串的首字符且不匹配,应从主串的后一个位置继续匹配;s[i]==t[j]表示匹配成功,继续匹配。
        {
            ++i;
            ++j;
        }
        else j=next[j];
    }
    if(j>t.length) return i-t.length;
    else return 0;
}

求KMP的next指针的值

(1)如果t[j]==t[next[j]],则next[j+1]=next[j]+1.

(2)如果t[j]!=t[next[j]],判断t[j]和t[next[...next[j]...]],重复 过程(1),直到相等,退到0时,表示不存在,next[j+1]=1.

换句话说,要求next[j],需要判断t[j-1]和t[next[j-1]].

void get_next(char t[],int next[])
{
    int j=1,k=0;
    next[1]=0;
    while(j<t.length)
    {
        if(k==0||t[j]==t[k])//k为0,或者找到时,next[j+1]=k。
        {
            ++j;
            ++k;
            next[j]=k;
        }
        else k=next[k];
    }
}

KMP的nextval值

思想:

当s[i]和t[j]比较后,发现两者不相等时,但t[j]和t[k]相等,那就意味着s[i]和t[k]不需要进行额外的比较,因此j的位置的nextval值修改为k位置的nextval值,当s[i]和t[j]比较后,发现两者不相等,发现t[j]和t[k]也不相等,因此j位置的nextval值仍是k,即nextval[j]=next[j].

已知next[j],应如下修改nextval值

k=next[j];

if(t[j]==t[k]) nextval[j]=next[k];

else nextval[j]=next[j];

例如:求aaaab的nextval值。

如果t[j]==t[next[j]],nextval[j]=nextval[next[j]]

否则nextval[j]=next[j].

void get_nextval(chat t[],int next[],int nextval[])
{
    int j=2,k=0;
    get_next(t,next);
    nextval[1]=0;
    while(j<=t.length())
    {
        k=next[j];
        if(t[j]==t[k]) nextval[j]=nextval[j];
        else nextval[j]=next[j];
    }
}

匹配过程和next的匹配过程类似。

AC代码

#include <iostream>

using namespace std;

const int N = 100100, M = 1000010;

int n, m;
int ne[N];
char s[M], p[N];

void get_next()
{
    for (int i = 2, j = 0; i <= n; i ++ )
    {
        while (j && p[i] != p[j + 1]) j = ne[j];//没有推到0并且不相等的话,j指针一直回退
        if (p[i] == p[j + 1]) j ++ ;
        ne[i] = j;
    }
}

void kmp()
{
    for (int i = 1, j = 0; i <= m; i ++ )
    {
        while (j && s[i] != p[j + 1]) j = ne[j];//直到找到相等的为止
        if (s[i] == p[j + 1]) j ++ ;
        if (j == n)
        {
            printf("%d ", i - n);
            j = ne[j];
        }
    }
}

int main()
{
    cin >> n >> p + 1 >> m >> s + 1;
    get_next();
    kmp();
    return 0;
}

2.8Trie

2.8.1Trie字符串统计

维护一个字符串集合,支持两种操作:

I x 向集合中插入一个字符串 x;

Q x 询问一个字符串在集合中出现了多少次。

共有 N个操作,所有输入的字符串总长度不超过 1e5,字符串仅包含小写英文字母。

输入格式

第一行包含整数 N,表示操作数。

接下来 N行,每行包含一个操作指令,指令为 I x 或 Q x 中的一种。

输出格式

对于每个询问指令 Q x,都要输出一个整数作为结果,表示 xx 在集合中出现的次数。

每个结果占一行。

数据范围

1≤N≤2∗1e4

输入样例:

5

I abc

Q abc

Q ab

I ab

Q ab

输出样例:

1

0

1

一.Trie树的原理

1.Trie树的作用

快速地查询某个字符串在集合中出现的次数,高效地存储和查找字符串,时间复杂度可以达到O(n)。

2.实现思路

类似于树的形式,将字符串存储起来,如果存在以某个字符结尾的字符串,我们就进行标记次数,方便查找字符串出现的次数。

我们把小写字母或者大写字母映射成0-25进行创建Trie树。

3.各个变量代表的意思

儿子数组son[p][j]:存储从节点p沿着j这条边走的子节点。边为26个小写的字母(a-z)对应的映射值0-25,每个节点最多可以有26个分支。

例如,son[0][2]=1,son[1][2]=0.

计数数组cnt[p]:存储以p结尾字符串出现的次数。

节点编号idx:来给节点进行编号。

二.建Trie树

1.过程

(1)空的Trie树只有一个节点,节点编号为0.

(2)从根开始进行插入,枚举字符串的每个字符,如果有儿子,p 指针走到儿子,如果没有儿子,先创建儿子,p指针再走向儿子。

(3).在单词的结尾记录插入的次数。

2.图解过程

3.代码展示

void insert(char str[])
{
    int p=0;//从根开始遍历
    for(int i=0;str[i];i++)//沿着字符串一直走
    {
        int j=str[i]-'a';//映射成分支
        if(!son[p][j]) son[p][j]=++idx;//如果没有这个节点,创建节点
        p=son[p][j];//令p走向该节点
    }
    cnt[p]++;//记录次字符串出现的次数
}

三.查询Trie

1.过程

(1).从根开始查询,对字符串进行扫描。

(2).有字符串str[i],则走到下一个节点,走到字符串尾,返回插入的次数。

(3).没有字符串str[i],返回0.

2.代码展示

int query(char str[])
{
    int p=0;//从根开始
    for(int i=0;str[i];i++)    
    {
        int j=str[i]-'a';
        if(!son[p][j]) return 0;//不存在节点,返回0
        p=son[p][j];
    }
    return cnt[p];//返回字符串的次数
}

AC代码

2.8.2最大异或对

在给定的 N个整数 A1,A2……AN中选出两个进行 xor(异或)运算,得到的结果最大是多少?

输入格式

第一行输入一个整数 N。

第二行输入 N个整数 A1~AN。

输出格式

输出一个整数表示答案。

数据范围

1≤N≤1e5,

0≤Ai<2^31,

输入样例:

3

1 2 3

输出样例:

3

1.思路

我们首先考虑遍历枚举的方法,然后通过发现某些性质去优化它。

int res=0;
for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
        res=max(res,a[i]^a[j]);
cout<<res<<endl;

显然暴力的方法为O(n2),会超时。

我们发现异或(^)的性质为二进制表示中,两个数某一位进行异或, 相同为0,不同为1,如果二进制100,我们首先考虑011,因为只有不同的位时,得到的值才能最大,我们可以用trie树从高位往低位存储,如果找某一个数的最大值时,我们应该首先考虑它对于二进制某一位不同的值是否存在,如果存在,我们沿着这个分支走到 下一个节点,如果不存在,只能走和他相同的分支。

说明

用Trie存储单词,由26个字母构成的Trie树,是一颗26叉树,26个字母构成分支,深度为最长单词的长度。

用Trie存储整数,由整数的十进制位构成的Trie,是一颗10叉树,0-9个数字构成分支,深度为10层。

用Trie存储整数,由整数的二进制位构成的Trie,是一颗二叉树,0和1构成分支,深度为31层。

2. 图解

int res=0;
for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
        res=max(res,a[i]^a[j]);
cout<<res<<endl;

AC代码

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e6+10,M=30000000;
int n,a[N],son[M][2],idx;
 
void insert(int x)//和字典树一样的思路
{
    int p=0;
    for(int i=30;i>=0;i--)//从二进制的最高位开始建树
    {
        int j=x>>i&1;//取出该位置的二进制表示的数
        if(!son[p][j]) son[p][j]=++idx;
        p=son[p][j];
    }
}
 
int query(int x)
{
    int res=0,p=0;
    for(int i=30;i>=0;i--)
    {
        int j=x>>i&1;
        if(son[p][!j])//如果存在某个节点和x该位置的二进制数不相同的话,说明异或结果为1,加上这一个二进制位对应十进制的数值,让p走到下一个节点
        {
            res+=1<<i;
            p=son[p][!j];
        }
        else p=son[p][j];//否则只能走相等的分支,说明异或结果为0,即res+=0<<i,因为0<<i的结果为0,所有可以省略,p走到下一个节点
    }
    return res;
}
 
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
        insert(a[i]);
    }
    int res=0;
    for(int i=0;i<n;i++)
        res=max(res,query(a[i]));
    cout<<res<<endl;
    return 0;
}

2.9并查集

(1)朴素并查集:

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

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

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

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


(2)维护size的并查集:

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

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

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

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


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

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

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

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

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

2.9.1合并集合

一共有 n个数,编号是 1∼n1∼n,最开始每个数各自在一个集合中。

现在要进行 mm 个操作,操作共有两种:

M a b,将编号为 aa 和 bb 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;

Q a b,询问编号为 aa 和 bb 的两个数是否在同一个集合中;

输入格式

第一行输入整数 nn 和 mm。

接下来 mm 行,每行包含一个操作指令,指令为 M a b 或 Q a b 中的一种。

输出格式

对于每个询问指令 Q a b,都要输出一个结果,如果 aa 和 bb 在同一集合内,则输出 Yes,否则输出 No。

每个结果占一行。

数据范围

1≤n,m≤1051≤n,m≤105

输入样例:

4 5

M 1 2

M 3 4

Q 1 2

Q 1 3

Q 3 4

输出样例:

Yes

No

Yes

一.基本原理

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

1.支持的操作

(1).将两个集合合并。

(2).询问两个集合是否在同一集合当中。

2.问题

(1).如何判断树根?if(p[x]==x)

(2).如何求x的集合编号?while(p[x]!=x) x=p[x];

(3).如何合并两个集合?px是x的集合编号,py是y的集合编号,px=y。

因为在查询的过程中,如果不进行压缩处理的话,需要的时间复杂度是很高的,所以我们可以进行压缩处理,我们在寻找祖宗的时候,我们可以让每个节点都指向祖宗节点,这样的话,我们就降低了树的高度,降低了时间复杂度。

合并其实也有一个优化,称为按秩合并,让小集合的根指向大集合的根 ,因为这样的话层数会缩小一些。

3.图解

(1).大集合接到小集合

此时树的高度为4

(2).小集合接到大集合

此时高度为3。

vector<int>siz(N,1);
 
void refind(int a,int b)
{
    a=find(a),b=find(b);//让a,b分别指向自己的祖宗节点
    if(a==b) return ;//如果a和b在同一集合中
    if(siz[a]>siz[b]) swap(a,b);
    p[a]=b;
    siz[b]+=siz[a];
}

find(x):返回x的祖宗节点;

p[x]:表示x的父亲;

AC代码

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
 
const int N=1e6+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()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) p[i]=i;//开始时,每个节点都是一个独立的集合,让自己的祖宗指向自己
    while(m--)
    {
        string op;
        int a,b;
        cin>>op>>a>>b;
        if(op=="M") 
        {
            if(find(a)!=find(b)) p[find(a)]=find(b);//如果a的集合编号不等于b的集合编号,合并两个集合
        }
        else
        {
            if(find(a)==find(b)) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

2.9.2连通块中点的数量

给定一个包含 nn 个点(编号为 1∼n1∼n)的无向图,初始时图中没有边。

现在要进行 mm 个操作,操作共有三种:

C a b,在点 aa 和点 bb 之间连一条边,aa 和 bb 可能相等;

Q1 a b,询问点 aa 和点 bb 是否在同一个连通块中,aa 和 bb 可能相等;

Q2 a,询问点 aa 所在连通块中点的数量;

输入格式

第一行输入整数 nn 和 mm。

接下来 mm 行,每行包含一个操作指令,指令为 C a b,Q1 a b 或 Q2 a 中的一种。

输出格式

对于每个询问指令 Q1 a b,如果 aa 和 bb 在同一个连通块中,则输出 Yes,否则输出 No。

对于每个询问指令 Q2 a,输出一个整数表示点 aa 所在连通块中点的数量

每个结果占一行。

数据范围

1≤n,m≤1051≤n,m≤105

输入样例:

5 5

C 1 2

Q1 1 2

Q2 1

C 2 5

Q2 5

输出样例:

Yes

2

3

思路:

维护一个size[]数组,size[x]:表示以x为根节点连通块的数量。我们只需要保证根节点的size有意义即可,如果x不是根节点,我们可以通过find(x),找到x的祖宗节点,也就是根节点;

如果合并两个节点,找到根节点a,b,让size[a]+=size[b]即可。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e6+10;
int p[N],s[N];
 
int n,m;
 
int find(int x)
{
    if(p[x]!=x) p[x]=find(p[x]);//如果x不是祖宗,让x父亲节点更新成祖宗节点
    return p[x];//返回x的祖宗节点
}
 
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        p[i]=i;//每个节点都是一个单独的集合,让自己的祖宗指向自己
        s[i]=1;//每个集合初始化为1
    }
    while(m--)
    {
        string op;
        int a,b;
        cin>>op;
        if(op=="C")
        {
            cin>>a>>b;
            if(find(a)==find(b)) continue;//如果a和b已经在一个集合中
            s[find(b)]+=s[find(a)];//把a集合中的连通块的数量加到b的祖宗上
            p[find(a)]=find(b);//合并两个集合
        }
        else if(op=="Q1")
        {
            cin>>a>>b;
            if(find(a)==find(b)) puts("Yes");
            else puts("No");
        }
        else
        {
            cin>>a;
            cout<<s[find(a)]<<endl;//输出a集合中连通块的数量
        }
    }
    return 0;
}

2.9.3食物链

动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。

A吃 B,B吃 C,C吃 A。

现有 N个动物,以 1∼N 编号。

每个动物都是 A,B,C 中的一种,但是我们并不知道它到底是哪一种。

有人用两种说法对这 N个动物所构成的食物链关系进行描述:

第一种说法是 1 X Y,表示 XX 和 YY 是同类。

第二种说法是 2 X Y,表示 XX 吃 YY。

此人对 N个动物,用上述两种说法,一句接一句地说出 K句话,这 K句话有的是真的,有的是假的。

当一句话满足下列三条之一时,这句话就是假话,否则就是真话。

当前的话与前面的某些真的话冲突,就是假话;

当前的话中 X或 Y比 N大,就是假话;

当前的话表示 X吃 X,就是假话。

你的任务是根据给定的 N和 K句话,输出假话的总数。

输入格式

第一行是两个整数 N和 K,以一个空格分隔。

以下 K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中 D表示说法的种类。

若 D=1,则表示 X和 Y是同类。

若 D=2,则表示 X吃 Y。

输出格式

只有一个整数,表示假话的数目。

数据范围

1≤N≤50000,

0≤K≤100000

输入样例:

100 7

1 101 1

2 1 2

2 2 3

2 3 3

1 1 3

2 3 1

1 5 5

输出样例:

3

思路: 不管两个集合是否属于同类,还是x吃y的捕食关系,我们都把他放进一个集合中,这样我们就可以知道题目两两之间的关系。

如何确定之间的关系?记录一下每个点和根节点之间的关系。

如果我们知道每个点和根节点之间的关系的话,我们就可以知道任意两个点之间的关系。

由于只有3类关系,我们用每个点到根节点的距离来表示和根节点的关系,如果某个点到根节点的距离是1的话,表示它可以吃根节点,如果某个点到根节点的距离是2的话,表示它可以被根节点吃,如果到根节点的距离是3的话,说明他和根节点是同类,然后3个一个循环。

d%3==1:吃根节点;

d%3==2:被根节点吃;

d%3==0:和根节点是同类;

我们维护某个点到根节点的距离,把距离分成上面3类即可。

find(x)维护d[x]的操作,d[x]:x到父节点的距离。

d[i]的正确理解,应是第 i 个节点到其父节点距离,而不是像有些同学所讲的,到根节点的距离!

//使得路径上的点直接指向根节点
int find(int x)
{
    if(p[x]!=x)//如果x不是树根
    {
        int root=find(p[x]);

        //该点到根节点的距离 = 该点到父节点的距离 + 父节点到根节点的距离
        d[x]+=d[p[x]];

        //使该点的父节点直接指向根节点
        p[x]=root;
    }

    return p[x];
}

AC代码

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=50010;
 
int n,m;
int p[N],d[N];
 
int find(int x)
{
    if(p[x]!=x)
    {
        int t=find(p[x]);//t先存储旧的祖宗节点
        d[x]+=d[p[x]];//更新节点长度即x到父节点之间的距离加上父节点到根节点之间的距离
        p[x]=t;//x的父亲指向祖宗节点
    }
    return p[x];
}
 
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) p[i]=i;
    int res=0;//存储假话的个数
    while(m--)
    {
        int t,x,y;
        scanf("%d%d%d",&t,&x,&y);
        if(x>n||y>n) res++;//如果不符合x,y的取值范围,是假话
        else
        {
            int px=find(x),py=find(y);
            if(t==1)
            {
                if(px==py&&(d[x]-d[y])%3!=0) res++;//如果x和y在同一集合中,但是发现他们之间不属于同类,则是假话
                else if(px!=py)//如果不属于一个集合,我们合并两个集合
                {
                    p[p[x]]=py;
                    d[px]=d[y]-d[x];//更新两个集合
                }
            }
            else
            {
                if(px==py&&(d[x]-d[y]-1)%3!=0) res++;
                else if(px!=py)
                {
                    p[px]=py;
                    d[px]=d[y]+1-d[x];
                }
            }
        }
    }
    printf("%d",res);
    return 0;
}

2.9.4连通图

题目描述

给你一个无向图,还有这个图上顶点与顶点之间的边,你能判断这个图连通吗?也就是能否从任意一个点开始可以直接或者间接访问到图上的任何一个顶点。

输入

首先输入一个整数t,表示有t组例子。

每组例子包括两部分;

第一部分(占一行): 一个整数n和m 表示图的顶点的个数和边的个数。

第二部分 有m行 ,每行两个整数s和t,表示顶点s和t之间有一条边(顶点的标号为1到n,其中1<=s,t<=n,1<=n<=100)。

输出

对应每组例子,如果此图为联通图,输出yes,否则输出no。每组结果占一行。

样例输入 Copy

2

4 4

1 2

1 3

1 4

2 4

4 3

1 2

2 4

1 4

样例输出 Copy

yes

no

维护每条边数的并查集。

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int p[N],size[N];
 
int find(int x)
{
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}
 
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        memset(p,0,sizeof(p));
        memset(size,0,sizeof(size));
        int n,m;
        cin>>n>>m;
        for(int i=1;i<=n;i++) p[i]=i,size[i]=1;
        while(m--)
        {
            int a,b;    
            cin>>a>>b;
            if(find(a)==find(b)) continue;
            size[find(b)]+=size[find(a)];
            p[find(a)]=find(b);
        }
        int ans=0;
        for(int i=1;i<=n;i++)
            if(size[find(i)]==n) ans++; //每个点可以到所有的边
        if(ans==n) cout<<"yes"<<endl; //所有的点都满足
        else cout<<"no"<<endl;
    }
    return 0;
}

2.10堆

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

2.10.1堆排序

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

输入格式

第一行包含整数 n和 m。

第二行包含 n个整数,表示整数数列。

输出格式

共一行,包含 m个整数,表示整数数列中前 m小的数。

数据范围

1≤m≤n≤1e5,

1≤数列中元素≤1e9

输入样例:

5 3

4 5 1 3 2

输出样例:

1 2 3

一、堆的基本概念

堆:是一个完全二叉树。

堆分成两类,小根堆和大根堆。

小根堆:父节点小于等于左右孩子节点;

大根堆:父节点大于等于左右孩子节点。

STL里面的堆又称为优先队列;

如何手写一个堆?

本篇文章以小根堆为例,实现堆的一些基本的操作。

我们用一维数组来维护一个堆,规定数组的下标从1开始,每个下标的左右儿子分别为2*x,2*x+1;

我们先讲述堆中两个最基本的操作down(x),up(x)两个操作。

down(x),如果我们修改堆某个节点或者删除某个节点 ,我们就需要用down和up来维护我们堆中的关系,我们以小根堆为例,如果父节点变大,那么他就要往下沉,因为我们小根堆满足父节点小于等于左右儿子,同理,up恰好相反,如果父节点变小,它就要和自己的父节点比较,直到满足小根堆的定义为止。

二、堆的基本操作

那么我们就可以用down和up操作完成堆中最基本的操作:

1.插入一个数

我们插入一个数一般是插入到堆中最后一个数的后面再进行up操作。

heap[++size]=x,up(size);

2.求集合当中的最小值

因为是小根堆,我们堆顶元素是最小值。

heap[1];

3.删除最小值

我们需要删除堆顶元素,都是如果直接删除堆顶元素的话,会很麻烦,我们可以用最后一个元素来覆盖堆顶元素,如何进行down(1)操作。

heap[1]=heap[size];size--;down(1);

4.删除任意一个值

我们类似于删除堆顶元素的操作,我们先用最后一个元素的值覆盖删除元素的值,因为我们不知道覆盖后的元素是变大还是变小了,所有我们需要判断是执行up还是down。

int t=heap[k];

heap[k]=heap[size];

size--;

if(heap[k]>t) down(k);

else up(k);

当然我们可以简化:

heap[k]=heap[size];

size--;

down(k);

up(k);

5.修改任意一个元素

heap[k]=x;

down(k);

up(k);

AC代码

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int h[N],siz;
int n,m;
 
void down(int u)
{
    int t=u;//t存储3个节点中的最小值,开始时假设最小值为父节点
    if(2*u<=siz&&h[2*u]<h[t]) t=2*u;//和左儿子比较
    if(2*u+1<=siz&&h[2*u+1]<h[t]) t=2*u+1;//和右儿子比较
    if(t!=u)
    {
        swap(h[t],h[u]);
        down(t);
    }
}
 
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>h[i];
    siz=n;
    for(int i=n/2;i;i--) down(i);
    while(m--)
    {
        cout<<h[1]<<" ";
        h[1]=h[siz];
        siz--;
        down(1);
    }
    return 0;
}

2.10.2模拟堆

维护一个集合,初始时集合为空,支持如下几种操作:

I x,插入一个数 x;

PM,输出当前集合中的最小值;

DM,删除当前集合中的最小值(数据保证此时的最小值唯一);

D k,删除第 k个插入的数;

C k x,修改第 k个插入的数,将其变为 x;

现在要进行 N次操作,对于所有第 2个操作,输出当前集合的最小值。

输入格式

第一行包含整数 N。

接下来 N行,每行包含一个操作指令,操作指令为 I x,PM,DM,D k 或 C k x 中的一种。

输出格式

对于每个输出指令 PM,输出一个结果,表示当前集合中的最小值。

每个结果占一行。

数据范围

1≤N≤1e5

−1e9≤x≤1e9

数据保证合法。

输入样例:

8

I -10

PM

I -10

D 1

C 2 8

I 6

PM

DM

输出样例:

-10

6

思路:

我们需要维护第i个插入的数,则需要再开两个数组维护信息;

AC代码

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e6+10;
int n,h[N],ph[N],hp[N],siz;
 
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(2*u<=siz&&h[2*u]<h[t]) t=2*u;
    if(2*u+1<=siz&&h[2*u+1]<h[t]) t=2*u+1;
    if(u!=t)
    {
        heap_swap(t,u);
        down(t);
    }
}
 
void up(int u)
{
    if(u/2&&h[u/2]>h[u])
    {
        heap_swap(u/2,u);
        up(u/2);
    }
}
 
int main()
{
    scanf("%d",&n);
    int m=0;
    while(n--)
    {
        string op;
        cin>>op;
        if(op=="I")
        {
            int x;
            scanf("%d",&x);
            m++;
            h[++siz]=x;
            ph[m]=siz;
            hp[siz]=m;
            up(siz);
        }
        else if(op=="PM") printf("%d\n",h[1]);
        else if(op=="DM")
        {
            heap_swap(1,siz);
            siz--;
            down(1);
        }
        else if(op=="D")
        {
            int k;
            scanf("%d",&k);
            k=ph[k];
            heap_swap(k,siz);
            siz--;
            down(k);
            up(k);
        }
        else
        {
            int k,x;
            scanf("%d%d",&k,&x);
            k=ph[k];
            h[k]=x;
            down(k);
            up(k);
        }
    }
    
    return 0;
}

2.11哈希表

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

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

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

        return false;
    }

(2) 开放寻址法
    int h[N];

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

1.什么是哈希表?

哈希表就是当范围很大时,我们可以通过哈希表将范围缩小,并快速找出一些数,如数组的下标范围是1~1000000000,但是其中的数很少,我们可以将其映射为1~100000,并快速找出,如原本数组下标是500000,我们可以映射成50,40....

2.哈希表产生的冲突

我们可以在映射的过程中,把两个数映射成为一个数,这个就是哈希表的冲突。

如何解决冲突?

有两种办法:开放寻址法和链地址法

(1)开放寻址法

我们可以先将h[]中每个位置上的值初始化成一个很大的数,如何通过除留余数法来找到每个数映射后的地址,如果该位置上有数,那么就继续向下一个位置探测,如果探测到最后一个位置,从第0个位置再进行探测。

查找一个数也是类似的,如果这个数待探测的位置上有数,那么就向下一个位置探测,如果最终探测的位置上面的数为很大的数,那么查找失败,哈希表中没有该数。

(2)拉链法

拉链法不同于开放地址法的是,把每个位置看成一个单链表,如果要某个数通过除留余数法算出来的数位置上有数,不用向后探测,只需要用头插法插入到该位置上的单链表上,查找也是如此。

2.11.1模拟散列表

维护一个集合,支持如下几种操作:

I x,插入一个数 x;

Q x,询问数 x是否在集合中出现过;

现在要进行 N次操作,对于每个询问操作输出对应的结果。

输入格式

第一行包含整数 N,表示操作数量。

接下来 N行,每行包含一个操作指令,操作指令为 I x,Q x 中的一种。

输出格式

对于每个询问指令 Q x,输出一个询问结果,如果 xx 在集合中出现过,则输出 Yes,否则输出 No。

每个结果占一行。

数据范围

1≤N≤1e5

−1e9≤x≤1e9

输入样例:

5

I 1

I 2

I 3

Q 2

Q 5

输出样例:

Yes

No

开放寻址法

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=2e5+3;
const int null=0x3f3f3f3f;
int h[N];
int n;

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

int main()
{
    cin>>n;
    memset(h,0x3f,sizeof h);
    while(n--)
    {
        string op;
        int x;
        cin>>op>>x;
        if(op=="I") h[find(x)]=x;
        else
        {
            if(h[find(x)]==null) puts("No");
            else puts("Yes");
        }
    }
    return 0;
}

链地址法

#include <cstring>
#include <iostream>

using namespace std;

const int N = 1e5 + 3;  // 取大于1e5的第一个质数,取质数冲突的概率最小 可以百度

//* 开一个槽 h
int h[N], e[N], ne[N], idx;  //邻接表

void insert(int x) {
    // c++中如果是负数 那他取模也是负的 所以 加N 再 %N 就一定是一个正数
    int k = (x % N + N) % N;
    e[idx] = x;
    ne[idx] = h[k];
    h[k] = idx++;
}

bool find(int x) {
    //用上面同样的 Hash函数 讲x映射到 从 0-1e5 之间的数
    int k = (x % N + N) % N;
    for (int i = h[k]; i != -1; i = ne[i]) {
        if (e[i] == x) {
            return true;
        }
    }
    return false;
}

int n;

int main() {
    cin >> n;

    memset(h, -1, sizeof h);  //将槽先清空 空指针一般用 -1 来表示

    while (n--) {
        string op;
        int x;
        cin >> op >> x;
        if (op == "I") {
            insert(x);
        } else {
            if (find(x)) {
                puts("Yes");
            } else {
                puts("No");
            }
        }
    }
    return 0;
}

2.11.2字符串哈希表

给定一个长度为 n的字符串,再给定 m个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1]和 [l2,r2]这两个区间所包含的字符串子串是否完全相同。

字符串中只包含大小写英文字母和数字。

输入格式

第一行包含整数 n和 m,表示字符串长度和询问次数。

第二行包含一个长度为 n的字符串,字符串中只包含大小写英文字母和数字。

接下来 m行,每行包含四个整数 l1,r1,l2,r2,表示一次询问所涉及的两个区间。

注意,字符串的位置从 1开始编号。

输出格式

对于每个询问输出一个结果,如果两个字符串子串完全相同则输出 Yes,否则输出 No。

每个结果占一行。

数据范围

1≤n,m≤1e5

输入样例:

8 3

aabbaabb

1 3 5 7

1 3 6 8

1 2 1 2

输出样例:

Yes

No

Yes

字符串前缀哈希法。

str="ABCADEFGKLM"

预处理出所有字符串的前缀的哈希

h[0]=0

h[1]="A"的哈希值

h[2]="AB"的哈希值

h[3]="ABC"的哈希值

1.如何定义某个前缀的哈希?

把字符串看成P进制的数。

如"ABCD"可以看成P进制的1234

转化成十进制的数就是(1*p^3+2*p^2+3*p^1+4*p^0)%Q;

由于结果很大,我们模上2^64次方,可以直接用unsigned long long 来存储,unsigned long long 相当于2^64,溢出的部分就相当于取模。

注:一般不能映射成0,比如A->0,则AA->00,这样就十分容易产生冲突。

前面的数字哈希会产生冲突,但是这里如果P取131或者13331的话,在99.99%的情况下不会产生冲突,则不需要进行处理冲突。

2.好处就是可以快速的求[l,r]子串的哈希值,判断两个子串是否相等。

前缀和公式 h[i+1]=h[i]×P+s[i] i∈[0,n−1] h为前缀和数组,s为字符串数组;

区间和公式 h[l,r]=h[r]−h[l−1]×P^(r−l+1);

#include <iostream>
#include <algorithm>

using namespace std;

typedef unsigned long long ULL;

const int N = 100010, P = 131;

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

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

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

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

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

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

    return 0;
}

2.12 STL

2.12.1 sort

1.头文件:<algorithm>

2.采用的是快速排序算法,可以保证很好的平均性能。

3.时间复杂度:O(nlogn)

4.对数字排序

(1)从小到大

for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+n+1);

(2)从大到小

bool cmp(int a,int b)
{
    return a>b;
}

for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+n+1,cmp);

5.对字母排序

(1)从小到大

char a[7]="abcedfg";
sort(a,a+7);//从小到大

(2)从大到小

char a[7]="abcedfg";
sort(a,a+7,greater<char>());//从大到小

2.12.2 lower_bound/upper_bound(二分)

  1. 原理:二分
  2. 数组:a[1~n];
  3. lower_bound(a+1,a+n+1,x):从数组1~n查找第一个大于等于x的数,返回该数的地址,不存在的话返回n+1,然后减去起始地址a,得到下标。
for(int i=1;i<=n;i++) cin>>a[i];
int x;
cin>>x;
cout<<lower_boumd(1,n,x)-a<<endl;

//如果想要查找降序数组
cout<<lower_bound(a+1,a+n+1,x,greater<int>())-a<<endl;
  1. upper_bound(a+1,a+n+1,x):从数组1~n查找第一个大于x的数,返回该数的地址,不存在的话返回n+1,然后减去起始地址a,得到下标。
for(int i=1;i<=n;i++) cin>>a[i];
int x;
cin>>x;
cout<<upper_boumd(1,n,x)-a<<endl;

//如果想要查找降序数组
cout<<upper_bound(a+1,a+n+1,x,greater<int>())-a<<endl;

2.12.3 vector

  1. 原理:变长数组倍增的思想。
  2. 一些基本操作
vector<int> v;
v.resverse(30);//调整空间大小

v.size();//返回大小

v.push_back(x);//尾部插入一个数x
v.pop_back();//尾部删除一个数

v.insert(v.begin(),x);//向v的头部插入一个数
v.insert(v.end(),x);//向v的尾部插入一个数
v.insert(v.begin()+2,x)//向第二个元素前插入一个数

v.erase(v,begin()+1);//删除第二个数
v.erase(v.begin(),begin+2);删除前3个元素

v.front()/v.end();//返回头/尾

v.empty();//判空

v.clear();//清空

reverse(v.begin(),v.end());//反转

2.12.4 pair

  1. 头文件:<utility>
  2. 作用:将一对值组合成一个元素,必须通过两个类型名,两个类型的属性可以不相同。
  3. 一些操作
pair<int,double> P;//包含两种属性

pair<int,double> P[10];//数组

P.first;//第一个元素类型的元素值
P.second;//第二个元素类型的元素值

pair<int,pair<int,int>> P;//存3种属性

//用make_pair来生成pair
pair<string,double> p=make_pair("zsy",100.0);

2.12.5 set

1.头文件<set>

2.不会插入重复元素

3.类似树形结构

4.时间复杂度:O(logn)

5.检索效率高于vector,deque,list等容器

6.迭代器 set<int>::iterator it;定义了一个set<int>类型的迭代器。

7.用法

set<int> s;
s.insert(x);//插入整数x
s.erase(s.begin());//删除首元元素
s.find(x);//查找x
s.count(x);//返回x在集合中的数量

//迭代器
for(set<int>::iterator it=s.begin();it!=s.end();it++)
    cout<<*it<<" ";

s.clear();//清空
s.empty();//判空

8.说明

使用insert()将元素插入set集合中,集合默认从小到大的顺序插入元素,可以直接编写从大到小。

//方法一
struct cmp{
    bool operator()(const int &a,const int &b)const
    {
        return a>b;
    }
};
set<int,cmp> s;

//方法二
set<int,greater<int>> s;

2.12.6 multiset

  1. 允许插入重复元素,用法和set类似
  2. 时间复杂度:O(logn)
  3. 头文件<set>

2.12.7 deque(双端队列)

  1. 头文件<set>
  2. 作用

是一种双向开口的连续线性空间,可以高效地在头尾两端插入和删除元素,时间复杂度为O(1),考虑容器的内存分配策略和操作性能时,deque比vector有优势。

3.操作

deque<string> d;
d.push_back("A");//尾部插入元素
d.push_front("B");//头部插入元素
d.pop_back();//删除尾部元素
d.pop_front();//删除头部元素
d.erase(d.begin()+x);//删除指定位置上的元素
d.erase();//清空
d.insert(d.end()-2,"0");//指定位置插入元素
reverse(d.begin(),d.end());//反转
swap(d[1],d[2]);//交换
deque<string>::iterator it;//迭代器
d.empty();//判空
d.size();//长度
d.front();//首元素
d.end();//尾元素

2.12.8 list(双向链表)

  1. 头文件:<list>
  2. 作用:对任意位置元素的查找、插入、删除都有高效的常数阶算法时间复杂度。

3.操作

list<int> l;

l.push_back(x);//尾部插入一个数x
l.push_front(x);//头部插入一个数x

list<int>::iterator it;//迭代器
l.insert(it,20);//当前位置插入新的元素
l.erase(it);//删除迭代器位置上的元素
l.remove(x);//删除所有值为x的元素
l.pop_front();//删除链表首元素
l.pop_back();//删除链表尾元素
it=find(l.begin(),l.end(),x);//查找值为x的元素
l.sort();//升序
l.unique();//删除连续重复的元素,只保留一个
for(auto it=l.begin();it!=l.end();it++)//正向遍历

2.12.9 map

  1. 头文件<map>
  2. map和set都是采用红黑树来实现的,插入的元素不能重复,重复的元素个数会增加,元素是默认按键值由小到大排序的,如果定义比较函数,比较函数也只能对元素的键值进行比较。
  3. map和set的区别:map是处理带有键值记录型元素数据的快速插入、删除、检索,而set对单一的数据进行处理。
  4. 操作
map<char,float> m;
m['x']=3.4;

//迭代器
map<string,int> h;
for(map<string,int>::iterator it=h.begin();it!=h.end();it++)
{
    it->first;
    it->second;
}

h.find(x);//查找
h.count(x);//条件x数量
h.erase(h.begin(),h.end());//删除全部元素
h.size();//返回大小
h.empty();//判空

2.12.10 生成排列组合

1.STL提供了两个用来分析排列组合的算法,分别是next_permutation()和prev_permutation().例如有3个字符的{'a','b','c'},生成的全排列abc,acb,bac,bca,cab,cba,使用两者可以很方便地生成排列组合。

2.next_permutation():按照字典序升序。

3.prev_permutation():按照字典序降序。

4.时间复杂度O(n!)

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

do
{
    for(int i=1;i<=n;i++) cout<<a[i]<<" ";
    puts("");
}while(next_permutation(a+1,a+n+1));

2.12.11 stable_sort稳定排序

stable_sort()和sort()的区别:前者排序后可以使原来相同的值在排好序之后相对位置不发生改变,但是后者不可以,用法和sort类似。

2.12.12 multimap(多重映照容器)

可以插入重复的元素,用法和sort类似。

2.12.13 stack(栈)

头文件<stack>

stack<int> s;
s.push(x);
s.top();//栈顶元素
s.pop();//从栈顶弹出一个元素
s.size();//返回栈的大小
s.empty();//判空

2.12.14 queue(队列)

头文件<queue>

queue<int> q;
q.push(x);//插入一个数x
q.pop();//弹出队头元素
q.front();//取队头
q.back();//取队尾
q.empty();//判空

2.12.15 priority_queue(优先队列/堆)

//大根堆
priority_queue<int> heap;

//小根堆
priority_queue<int,vector<int>,greater<int>> heap;

heap.push(x);//插入一个数x
heap.top();//返回堆顶元素
heap.pop();//弹出堆顶元素

2.12.16 bitset(压位)

用法:想开一个1024B的bool数组(1KB=1024B),如果存一个10000*10000B的bool数组(1e8->100MB),但是限制为64MB,我们用bitset存储,bitset<10000> s;

2.12.17 其他容器和函数

unordered_set,unordered_map,unordered_multiset,unordered_multimap,和上面类似,增删改查的时间复杂度为O(1),不支持lower_bound/upper_bound

count(a+1,a+n+1,x);//返回x在数组a中的个数
find(a+1,a+n+1,x);//查找x在数组a中是否出现过
any();//判断至少有一个1
none();//判断是否全为0
set();//把所有位置变为1
set(k,v);//把第k为变为v
reset();//把所有位置变为0
flip();//等价于~
flip(k);//把第k位取反

3.搜索与图论篇

3.1DFS

3.1.1排列数字

给定一个整数 n,将数字 1∼n排成一排,将会有很多种排列方法。

现在,请你按照字典序将所有的排列方法输出。

输入格式

共一行,包含一个整数 n。

输出格式

按字典序输出所有排列方案,每个方案占一行。

数据范围

1≤n≤7

输入样例:

3

输出样例:

1 2 3

1 3 2

2 1 3

2 3 1

3 1 2

3 2 1

思路:

dfs的基本应用,类似树的形式,一直向深处搜索,然后进行回溯。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
int n;
bool st[1000];
int path[1000];

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()
{
    cin >> n;
    dfs(0);
    return 0;
}

3.1.2n-皇后问题

n−皇后问题是指将 n个皇后放在 n×n的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。

现在给定整数 n,请你输出所有的满足条件的棋子摆法。

输入格式

共一行,包含整数 n。

输出格式

每个解决方案占 n行,每行输出一个长度为 n的字符串,用来表示完整的棋盘状态。

其中 . 表示某一个位置的方格状态为空,Q 表示某一个位置的方格上摆着皇后。

每个方案输出完成后,输出一个空行。

注意:行末不能有多余空格。

输出方案的顺序任意,只要不重复且没有遗漏即可。

数据范围

1≤n≤9

输入样例:

4

输出样例:

.Q..

...Q

Q...

..Q.

..Q.

Q...

...Q

.Q..

思路:dfs

先解决对角线问题

dfs的本质是递归+系统维护的栈。

有两种搜索方式:

第一种:按行搜索,直到放完n个皇后位置。

第二种:一个格子一个格子地进行摆放,一直枚举。

对应的斜率的判断(借鉴别人画的图):

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=20;

char g[N][N];

bool col[N],eg[N],neg[N];

int n;

void dfs(int u)
{
    if(u==n)
    {
        for(int i=0;i<n;i++)
            puts(g[i]);
        puts("");
        return ;
    }
    for(int i=0;i<n;i++)
        if(!col[i]&&!eg[i+u]&&!neg[n-u+i])
        {
            g[u][i]='Q';
            col[i]=eg[i+u]=neg[n-u+i]=true;
            dfs(u+1);
            col[i]=eg[i+u]=neg[n-u+i]=false;
            g[u][i]='.';
        }
}

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

//一个格子一个格子进行搜索
#include <iostream>
using namespace std;
const int N = 20;

int n;
char g[N][N];
bool row[N], col[N], dg[N], udg[N]; // 因为是一个个搜索,所以加了row

// s表示已经放上去的皇后个数
void dfs(int x, int y, int s)
{
    // 处理超出边界的情况
    if (y == n) y = 0, x ++ ;

    if (x == n) { // x==n说明已经枚举完n^2个位置了
        if (s == n) { // s==n说明成功放上去了n个皇后
            for (int i = 0; i < n; i ++ ) puts(g[i]);
            puts("");
        }
        return;
    }

    // 分支1:放皇后
    if (!row[x] && !col[y] && !dg[x + y] && !udg[x - y + n]) {
        g[x][y] = 'Q';
        row[x] = col[y] = dg[x + y] = udg[x - y + n] = true;
        dfs(x, y + 1, s + 1);
        row[x] = col[y] = dg[x + y] = udg[x - y + n] = false;
        g[x][y] = '.';
    }

    // 分支2:不放皇后
    dfs(x, y + 1, s);
}


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

    dfs(0, 0, 0);

    return 0;
}

3.2BFS

3.2.1走迷宫

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

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

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

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

输入格式

第一行包含两个整数 n和 m。

接下来 n行,每行包含 m个整数(0或 1),表示完整的二维数组迷宫。

输出格式

输出一个整数,表示从左上角移动至右下角的最少移动次数。

数据范围

1≤n,m≤100

输入样例:

5 5

0 1 0 0 0

0 1 0 1 0

0 0 0 0 0

0 1 1 1 0

0 0 0 1 0

输出样例:

8

bfs:一层一层地向外进行扩展,直到搜到终点位置,本质是队列,最先搜到的位置一定是最短路径,所以bfs有最短路径。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=110;
typedef pair<int,int> PII;
int d[N][N];
int g[N][N];
bool st[N][N];
int n,m;
PII q[N*N];
PII Pre[N][N];
int bfs()
{
    int hh=0,tt=-1;
    q[++tt]={1,1};//把起始位置放进来
    st[1][1]=true;//对起始位置进行标记
    d[1][1]=0;
    int dx[4]={-1,0,1,0},dy[4]={0,1,0,-1};//记下偏移量
    while(hh<=tt)
    {
        auto t=q[hh++];
        for(int i=0;i<4;i++)
        {
            int x=t.first+dx[i],y=t.second+dy[i];
            if(x>=1&&x<=n&&y>=1&&y<=m&&g[x][y]==0&&!st[x][y])//该点符合待更新的点
            {
                d[x][y]=d[t.first][t.second]+1;//路径从上一个路径更新过来
                Pre[x][y]=t;
                q[++tt]={x,y};//入队
                st[x][y]=true;//标记
            }
        }
    }
    /*输出路径
    int x=n,y=m;
    while(x||y)
    {
        cout<<x<<" "<<y<<endl;
        auto t=Pre[x][y];
        x=t.first,y=t.second;
    }
    */
    return d[n][m];
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>g[i][j];
    printf("%d\n",bfs());
    return 0;
}

3.2.2八数码

在一个 3×3 的网格中,1∼8 这 8个数字和一个 x 恰好不重不漏地分布在这 3×3 的网格中。

例如:

1 2 3

x 4 6

7 5 8

在游戏过程中,可以把 x 与其上、下、左、右四个方向之一的数字交换(如果存在)。

我们的目的是通过交换,使得网格变为如下排列(称为正确排列):

1 2 3

4 5 6

7 8 x

例如,示例中图形就可以通过让 x 先后与右、下、右三个方向的数字交换成功得到正确排列。

交换过程如下:

1 2 3 1 2 3 1 2 3 1 2 3

x 4 6 4 x 6 4 5 6 4 5 6

7 5 8 7 5 8 7 x 8 7 8 x

现在,给你一个初始网格,请你求出得到正确排列至少需要进行多少次交换。

输入格式

输入占一行,将 3×3的初始网格描绘出来。

例如,如果初始网格如下所示:

1 2 3

x 4 6

7 5 8

则输入为:1 2 3 x 4 6 7 5 8

输出格式

输出占一行,包含一个整数,表示最少交换次数。

如果不存在解决方案,则输出 −1。

输入样例:

2 3 4 1 5 x 7 6 8

输出样例

19

#include <iostream>
#include <algorithm>
#include <queue>
#include <unordered_map>

using namespace std;

int bfs(string start)
{
    //定义目标状态
    string end = "12345678x";
    //定义队列和dist数组
    queue<string> q;
    unordered_map<string, int> d;
    //初始化队列和dist数组
    q.push(start);
    d[start] = 0;
    //转移方式
    int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};

    while(q.size())
    {
        auto t = q.front();
        q.pop();
        //记录当前状态的距离,如果是最终状态则返回距离
        int distance = d[t];
        if(t == end) return distance;
        //查询x在字符串中的下标,然后转换为在矩阵中的坐标
        int k = t.find('x');
        int x = k / 3, y = k % 3;

        for(int i = 0; i < 4; i++)
        {
            //求转移后x的坐标
            int a = x + dx[i], b = y + dy[i];
            //当前坐标没有越界
            if(a >= 0 && a < 3 && b >= 0 && b < 3)
            {
                //转移x
                swap(t[k], t[a * 3 + b]);
                //如果当前状态是第一次遍历,记录距离,入队
                if(!d.count(t))//如果当前状态没有搜索过,入队更新
                {
                    d[t] = distance + 1;
                    q.push(t);
                }
                //还原状态,为下一种转换情况做准备
                swap(t[k], t[a * 3 + b]);
            }
        }
    }
    //无法转换到目标状态,返回-1
    return -1;
}

int main()
{
    string c, start;
    //输入起始状态
    for(int i = 0; i < 9; i++)
    {
        cin >> c;
        start += c;
    }

    cout << bfs(start) << endl;

    return 0;
}

3.3.0树和图的一些预备知识

树与图的存储

树是一种特殊的图,与图的存储方式相同。

对于无向图中的边ab,存储两条有向边a->b, b->a。

因此我们可以只考虑有向图的存储。

n:点数,m:边数

稀疏图:如果m和n是一个级别的,用邻接表。

稠密图:如果m和n^2是一个级别的,用邻接矩阵。

(1) 邻接矩阵:g[a][b] 存储边a->b,先初始化g位正无穷

memset(g,0x3f,sizeof g);
g[a][b]=c;

(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 ++ ;
}

// 初始化
idx = 0;
memset(h, -1, sizeof h);//初始化表头

(1) 深度优先遍历

时间复杂度 O(n+m) ,n表示点数,m表示边数.

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

(2) 宽度优先遍历

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 (!st[j])
        {
            st[j] = true; // 表示点j已经被遍历过
            q.push(j);
        }
    }
}

3.3树的深度优先遍历

树和图的深度优先遍历的模板:

// 需要标记数组st[N],  遍历节点的每个相邻的便
void dfs(int u) {
    st[u] = true; // 标记一下,记录为已经被搜索过了,下面进行搜索过程
    for (int i = h[u]; i != -1; i = ne[i]) {
        int j = e[i];
        if (!st[j]) {
            dfs(j);
        }
    }
}

3.3.1树的重心

给定一颗树,树中包含 n个结点(编号 1∼n)和 n−1 条无向边。

请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。

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

输入格式

第一行包含整数 n,表示树的结点数。

接下来 n−1行,每行包含两个整数 a和 b,表示点 a和点 b之间存在一条边。

输出格式

输出一个整数 m,表示将重心删除后,剩余各个连通块中点数的最大值。

数据范围

1≤n≤1e5

输入样例

9

1 2

1 7

1 4

2 8

2 5

4 3

3 9

4 6

输出样例:

4

每次算出他下面的size和n-size进行比较即可。

AC代码

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

using namespace std;

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

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

bool st[N]; //记录节点是否被访问过,访问过则标记为true

//a所对应的单链表中插入b  a作为根 
void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

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

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

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

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

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

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

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

    cout << ans << endl;

    return 0;
}

3.4数的广度优先遍历

3.4.1图中点的层次

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

所有边的长度都是 1,点的编号为 1∼n。

请你求出 1号点到 n号点的最短距离,如果从 1号点无法走到 n号点,输出 −1。

输入格式

第一行包含两个整数 n和 m。

接下来 m行,每行包含两个整数 a和 b,表示存在一条从 a走到 b的长度为 1 的边。

输出格式

输出一个整数,表示 1号点到 n号点的最短距离。

数据范围

1≤n,m≤1e5

输入样例:

4 5

1 2

2 3

3 4

1 3

1 4

输出样例:

1

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;

//邻接表表示方法
int h[N],e[N*2],ne[N*2],idx;
int n,m;
int d[N];//标记距离1号点的最短距离
bool st[N];//标记访问标志
int q[N];//定义一个队列

//从a->b连接一条边
void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

int bfs()
{
    memset(d,-1,sizeof d);
    int hh=0,tt=-1;
    d[1]=0;
    q[++tt]=1;//从1号点开始搜索
    st[1]=true;
    while(hh<=tt)
    {
        int t=q[hh++];
        for(int i=h[t];i!=-1;i=ne[i])//访问该点的邻接点
        {
            int j=e[i];
            if(!st[j])
            {
                d[j]=d[t]+1;
                q[++tt]=j;
                st[j]=true;
            }
        }
    }
    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);
    }
    printf("%d\n",bfs());
    return 0;
}

3.5拓扑排序

给定一个 n个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。

请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。

若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y之前,则称 A是该图的一个拓扑序列。

输入格式

第一行包含两个整数 n和 m。

接下来 m 行,每行包含两个整数 x 和 y,表示存在一条从点 x 到点 y 的有向边 (x,y)。

输出格式

共一行,如果存在拓扑序列,则输出任意一个合法的拓扑序列即可。

否则输出 −1。

数据范围

1≤n,m≤1e5

输入样例:

3 3

1 2

2 3

1 3

输出样例:

1 2 3

思路:

AC代码

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;

//邻接表表示方法
int h[N],e[N*2],ne[N*2],idx;

int d[N*2];
int q[N*2];//定义一个队列
int n,m;

void add(int a,int b)
{
    e[idx]=b,ne[idx]=h[a],h[a]=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)
    {
        auto t=q[hh++];
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            d[j]--;
            if(d[j]==0) q[++tt]=j;
        }
    }
    return tt==n-1;//如果队列里面有n个点,则存在拓扑序列,否则有环,不存在拓扑序列
}

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

3.6.0最短路问题

3.6Dijkstra

3.6.1Dijkstra求最短路I

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

请你求出 1号点到 n号点的最短距离,如果无法从 1号点走到 n号点,则输出 −1。

输入格式

第一行包含整数 n和 m。

接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。

输出格式

输出一个整数,表示 11 号点到 nn 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围

1≤n≤500,

1≤m≤1e5,

图中涉及边长均不超过10000。

输入样例:

3 3

1 2 2

2 3 1

1 3 4

输出样例:

3

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;
const int M=2*N;

int h[N],e[N],w[N],ne[N],idx;
int g[510][510];
int dist[N];
int n,m;
bool st[N];

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

//邻接矩阵法
int dijkstra1()
{
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    for(int i=1;i<=n;i++)
    {
        int t=-1;
        for(int j=1;j<=n;j++)
            if(!st[j]&&(t==-1||dist[j]<dist[t])) t=j;
        st[t]=true;
        for(int j=1;j<=n;j++)
            dist[j]=min(dist[j],dist[t]+g[t][j]);
    }
    if(dist[n]==0x3f3f3f3f) return -1;
    return dist[n];
}

//邻接表法
int dijkstra2()
{
    memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    for(int i=1;i<=n;i++)
    {
        int t=-1;
        for(int j=1;j<=n;j++)
            if(!st[j]&&(t==-1||dist[j]<dist[t])) t=j;
        st[t]=true;
        for(int j=h[t];j!=-1;j=ne[j])
        {
            int k=e[j];
            dist[k]=min(dist[k],dist[t]+w[j]);
        }
    }
    if(dist[n]==0x3f3f3f3f) return -1;
    return dist[n];
}
int main()
{
    memset(h,-1,sizeof h);
    memset(g,0x3f,sizeof g);
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        g[a][b]=min(g[a][b],c);
        add(a,b,c);
    }
    cout<<dijkstra2();
    return 0;
}

3.6.2Dijkstra求最短路II

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

请你求出 1号点到 n号点的最短距离,如果无法从 1号点走到 n号点,则输出 −1。

输入格式

第一行包含整数 n和 m。

接下来 m行每行包含三个整数x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式

输出一个整数,表示 1号点到 n号点的最短距离。

如果路径不存在,则输出−1。

数据范围

1≤n,m≤1.5×1e5,

图中涉及边长均不小于 0,且不超过10000。

数据保证:如果最短路存在,则最短路的长度不超过 1e9。

输入样例:

3 3

1 2 2

2 3 1

1 3 4

输出样例:

3

用小根堆即可,我们把每次求离起点最近进行了堆优化,可以缩短时间。

#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>//堆的头文件

using namespace std;

typedef pair<int, int> PII;//堆里存储距离和节点编号

const int N = 1e6 + 10;

int n, m;//节点数量和边数
int h[N], w[N], e[N], ne[N], idx;//邻接矩阵存储图
int dist[N];//存储距离
bool st[N];//存储状态

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

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

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

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

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

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

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

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

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

    cout << dijkstra() << endl;

    return 0;
}

3.7bellman-ford

3.7.1有边数限制的最短路

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

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

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

输入格式

第一行包含三个整数 n,m,k。

接下来 m行,每行包含三个整数x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。

点的编号为 1∼n。

输出格式

输出一个整数,表示从 1号点到 n号点的最多经过 k条边的最短距离。

如果不存在满足条件的路径,则输出 impossible。

数据范围

1≤n,k≤500,

1≤m≤10000,

1≤x,y≤n,

任意边长的绝对值不超过10000。

输入样例:

3 3 1

1 2 1

2 3 1

1 3 3

输出样例:

3

思路:

#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;
int dist[N];
int 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");//可能存在负权回路,可能在求最短路径的时候
    //存在负权边,把正无穷相对于原来的正无穷减少,所以要>0x3f3f3f3f/2
    else printf("%d\n", dist[n]);

    return 0;
}

3.8spfa

3.8.1spfa求最短路

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

请你求出 1号点到 n号点的最短距离,如果无法从 1号点走到 n号点,则输出 impossible。

数据保证不存在负权回路。

输入格式

第一行包含整数 n和 m。

接下来 m行每行包含三个整数x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。

输出格式

输出一个整数,表示 1号点到 n号点的最短距离。

如果路径不存在,则输出 impossible。

数据范围

1≤n,m≤1e5,

图中涉及边长绝对值均不超过10000。

输入样例:

3 3

1 2 5

2 3 -3

1 3 4

输出样例:

2

spfa用的最多

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

using namespace std;

const int N = 100010;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];

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

int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

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

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return dist[n];
}

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

    memset(h, -1, sizeof h);

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

    int t = spfa();

    if (t == 0x3f3f3f3f) puts("impossible");
    else printf("%d\n", t);

    return 0;
}

3.8.2spfa判断负环

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

请你判断图中是否存在负权回路。

输入格式

第一行包含整数 n和 m。

接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。

输出格式

如果图中存在负权回路,则输出 Yes,否则输出 No。

数据范围

1≤n≤2000,

1≤m≤10000,

图中涉及边长绝对值均不超过 10000。

输入样例:

3 3

1 2 -1

2 3 4

3 1 -4

输出样例:

Yes

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

using namespace std;

const int N = 2010, M = 10010;

int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N];

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

bool spfa()
{
    queue<int> q;

    for (int i = 1; i <= n; i ++ )
    {
        st[i] = true;
        q.push(i);
    }

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

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;

                if (cnt[j] >= n) return true;
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

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

    memset(h, -1, sizeof h);

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

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

    return 0;
}

3.9Floyd

3.9.1Floyd求最短路

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

再给定 k个询问,每个询问包含两个整数 x 和 y,表示查询从点 x到点 y的最短距离,如果路径不存在,则输出 impossible。

数据保证图中不存在负权回路。

输入格式

第一行包含三个整数 n,m,k。

接下来 m行,每行包含三个整数x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。

接下来 k 行,每行包含两个整数 x,y,表示询问点 x到点 y的最短距离。

输出格式

共 k行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible。

数据范围

1≤n≤200,

1≤k≤n2

1≤m≤20000,

图中涉及边长绝对值均不超过10000。

输入样例:

3 3 2

1 2 1

2 3 2

1 3 1

2 1

1 3

输出样例:

impossible

1

多源汇最短路

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

const int N = 210, INF = 1e9;

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

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

int main()
{
    scanf("%d%d%d", &n, &m, &Q);
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;
    while (m--)
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        d[a][b] = min(d[a][b], c);
    }
    floyd();
    while (Q--)
    {
        int a, b;
        scanf("%d%d", &a, &b);
        int t = d[a][b];
        if (t > INF / 2) puts("impossible");
        else printf("%d\n", t);
    }
    return 0;
}

3.10Prim

3.10.1Prim求最小生成树

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

求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。

给定一张边带权的无向图 G=(V,E),其中 VV 表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。

由 V中的全部 n个顶点和 E中 n−1 条边构成的无向连通子图被称为 G的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G的最小生成树。

输入格式

第一行包含两个整数 n和 m。

接下来 m行,每行包含三个整数u,v,w,表示点 u和点 v之间存在一条权值为 w的边。

输出格式

共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。

数据范围

1≤n≤500,

1≤m≤105,

图中涉及边的边权的绝对值均不超过10000。

输入样例:

4 5

1 2 1

1 3 2

1 4 3

2 3 2

3 4 4

输出样例:

6

思路:

模拟样例

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

using namespace std;

const int N = 510, INF = 0x3f3f3f3f;

int n, m;
int g[N][N];
int dist[N];
bool st[N];


int prim()
{
    memset(dist, 0x3f, sizeof dist);

    int res = 0;
    for (int i = 1; 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!=1 && dist[t] == INF) return INF;//如果不是第一个点,并且该点和集合不连通,则没有最小生成树

        if (i!=1) res += dist[t];//我们把1号点原本就看成集合内部的点,所以1号点到集合的距离是0,只需要加上其他点到集合内部的最短距离
        st[t] = true;

        for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);
    }

    return res;
}


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

    memset(g, 0x3f, sizeof g);

    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 printf("%d\n", t);

    return 0;
}

3.11Kruskal

3.11.1Kruskal求最小生成树

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

求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。

给定一张边带权的无向图 G=(V,E),其中 VV 表示图中点的集合,EE 表示图中边的集合,n=|V|,m=|E|。

由 V中的全部 n个顶点和 E中 n−1 条边构成的无向连通子图被称为 G的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G的最小生成树。

输入格式

第一行包含两个整数 n和 m。

接下来 m行,每行包含三个整数u,v,w,表示点 u和点 v之间存在一条权值为 w的边。

输出格式

共一行,若存在最小生成树,则输出一个整数,表示最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible。

数据范围

1≤n≤105,

1≤m≤2∗1e5,

图中涉及边的边权的绝对值均不超过 1000。

输入样例:

4 5

1 2 1

1 3 2

1 4 3

2 3 2

3 4 4

输出样例:

6

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 2e6+10;
int n,m;
int p[N];

struct Edg
{
    int a,b,w;
}edg[N];

bool cmp(Edg &a,Edg &b)
{
    return a.w<b.w;
}

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

int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int a,b,w;
        cin>>a>>b>>w;
        edg[i]={a,b,w};
    }
    sort(edg+1,edg+m+1,cmp);
    for (int i = 1; i <= n; i ++ ) p[i] = i;
    int res=0,cnt=0;
    for(int i=1;i<=m;i++)
    {
        int a=find(edg[i].a),b=find(edg[i].b),w=edg[i].w;
        if(a!=b)
        {
            p[a]=b;
            res+=w;
            cnt++;
        }
    }
    
    if(cnt<n-1) puts("impossible");
    else cout<<res<<endl;
    return 0;
}

3.12染色法判定二分图

给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环。

请你判断这个图是否是二分图。

输入格式

第一行包含两个整数 n 和 m。

接下来 m 行,每行包含两个整数 u 和 v,表示点 u 和点 v 之间存在一条边。

输出格式

如果给定图是二分图,则输出 Yes,否则输出 No。

数据范围

1≤n,m≤1e5

输入样例:

4 4

1 3

1 4

2 3

2 4

输出样例:

Yes

思路:

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

const int N=1e5+10,M=N*2;

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

int n,m;

int color[N];

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]&&!dfs(j,3-c)) return  false;
        else if(color[j]!=3-c) return false;
    }
    return true;
}

int main()
{
    memset(h,-1,sizeof h);
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int a,b;
        cin>>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;
        }
    }
    if(flag) puts("Yes");
    else puts("No");
    return 0;
}

3.13匈牙利算法

3.13.1二分图的最大匹配

给定一个二分图,其中左半部包含 n1 个点(编号1∼n1),右半部包含 n2 个点(编号 1∼n2),二分图共包含 m 条边。

数据保证任意一条边的两个端点都不可能在同一部分中。

请你求出二分图的最大匹配数。

二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。

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

输入格式

第一行包含三个整数 n1、 n2和 m。

接下来 m 行,每行包含两个整数 u 和 v,表示左半部点集中的点 u 和右半部点集中的点 v 之间存在一条边。

输出格式

输出一个整数,表示二分图的最大匹配数。

数据范围

1≤n1,n2≤500,

1≤u≤n1,

1≤v≤n2,

1≤m≤1e5

输入样例:

2 2 4

1 1

1 2

2 1

2 2

输出样例:

2

思路

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

using namespace std;

const int N = 510, M = 100010;

int n1, n2, m;
int h[N], e[M], ne[M], idx;
int match[N];//存每个妹子现在和哪个男生在一块
bool st[N];//判重,每次不要搜重复的点

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

bool find(int x)
{
    for (int i = h[x]; i != -1; i = ne[i])//枚举这个男生所有看上的妹子
    {
        int j = e[i];//当前看上的妹子
        if (!st[j])//如果这个妹子没有考虑过
        {
            st[j] = true;//已经考虑
            if (match[j] == 0 || find(match[j]))/*如果这个妹子还没有匹配男生或者她匹配的男生还有其他没匹配的姑娘*/
            {
                match[j] = x;//将当前男生和他匹配的姑娘进行配对,x代表男生的编号
                return true;//返回true
            }
        }
    }

    return false;//如果这个男生实在匹配不到姑娘,返回false
}

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

    memset(h, -1, sizeof h);

    while (m -- )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }

    int res = 0;//存匹配的数量
    for (int i = 1; i <= n1; i ++ )//依次分析每个男生该找哪个妹子
    {
        memset(st, false, sizeof st);//先把所有妹子清空,表示这些妹子在当前男生这里还未被考虑过
        if (find(i)) res ++ ;//如果这个男生成功找到一个妹子,总数++
    }

    printf("%d\n", res);

    return 0;
}

4.数学知识篇

4.1质数

4.1.1试除法判定质数

给定 n个正整数 ai,判定每个数是否是质数。

输入格式

第一行包含整数 n。

接下来 n行,每行包含一个正整数 ai。

输出格式

共 n行,其中第 i行输出第 i个正整数 ai是否为质数,是则输出 Yes,否则输出 No。

数据范围

1≤n≤100,

1≤ai≤2^31−1

输入样例:

2

2

6

输出样例:

Yes

No

用试除法判断一个数n是不是质数的时间复杂度为O(sqrt(n)).

#include<bits/stdc++.h>
#include<algorithm>
using namespace std;
bool isprime(int n)
{
    if(n<2) return false;
    for(int i=2;i<=n/i;i++)
    {
        if(n%i==0) return false;
    }
    return true;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        int n;
        scanf("%d",&n);
        if(isprime(n)) printf("Yes\n");
        else printf("No\n");
    }
    return 0;
}

4.1.2分解质因数

给定 n个正整数 ai,将每个数分解质因数,并按照质因数从小到大的顺序输出每个质因数的底数和指数。

输入格式

第一行包含整数 n。

接下来 n行,每行包含一个正整数 ai。

输出格式

对于每个正整数 ai,按照从小到大的顺序输出其分解质因数后,每个质因数的底数和指数,每个底数和指数占一行。

每个正整数的质因数全部输出完毕后,输出一个空行。

数据范围

1≤n≤100,

2≤ai≤2×1e9

输入样例:

2

6

8

输出样例:

2 1

3 1

2 3

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

4.1.3筛质数

给定一个正整数 n,请你求出 1∼n 中质数的个数。

输入格式

共一行,包含整数 n。

输出格式

共一行,包含一个整数,表示1∼n 中质数的个数。

数据范围

1≤n≤1e6

输入样例:

8

输出样例:

4

质数定理:1~n中有近似n/lnn个质数(粗略计算)

当n=1e6,线性筛法和埃氏筛法时间近乎一样

当n=1e7,线性筛法比埃氏筛法快一倍

st[]数组标记合数

(1)朴素筛法O(nlogn)

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;

int primes[N],cnt;
bool st[N];//标记是否被筛过

void get_primes(int n)
{
    for(int i=2;i<=n;i++)
    {
        if(!st[i])
        {
            primes[cnt++]=i;
        }
        for(int j=i+i;j<=n;j+=i) st[j]=true;//把质数的倍数筛掉,质数的倍数一定是合数
    }
}

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

    get_primes(n);

    cout<<cnt<<endl;

    return 0;
}

(2)埃氏筛法O(nloglogn)近乎O(n)

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;

int primes[N],cnt;
bool st[N];//标记是否被筛过

void get_primes(int n)
{
    for(int i=2;i<=n;i++)
    {
        if(!st[i])
        {
            primes[cnt++]=i;
            for(int j=i+i;j<=n;j+=i) st[j]=true;
        }
    }
}

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

    get_primes(n);

    cout<<cnt<<endl;

    return 0;
}

(3)线性筛法

线性筛法

思路:每个合数,只会被它的最小质因子筛掉.

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

      /*(1)当i%primes[j]!=0时,说明此时遍历到的primes[j]不是i的质因子,那么只可能是此时的primes[j]<i的          最小质因子,所以primes[j]*i的最小质因子就是primes[j];
        (2)当有i%primes[j]==0时,说明i的最小质因子是primes[j],因此primes[j]*i的最小质因子也就应该是
            prime[j],之后接着用st[primes[j+1]*i]=true去筛合数时,就不是用最小质因子去更新了,因为i有
            最小质因子primes[j]<primes[j+1],此时的primes[j+1]不是primes[j+1]*i的最小质因子,此就              应该退出循环,避免之后重复进行筛选。*/
            if(i%primes[j]==0) break;
        }
    }

}
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;

int primes[N],cnt;
bool st[N];//标记是否被筛过

void get_primes(int n)
{
    for(int i=2;i<=n;i++)
    {
        if(!st[i]) primes[cnt++]=i;
        for(int j=0;primes[j]<=n/i;j++)/*从小到大枚举所有的质数,primes[j]*i<=n保证了要筛的合数
在n的范围内*/
        {
            st[primes[j]*i]=true;//每次把当前质数和i的乘积筛掉,也就是筛掉一个合数
            if(i%primes[j]==0) break;//当这一语句执行,primes[j]一定是i的最小质因子
        }
    }
}

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

    get_primes(n);

    cout<<cnt<<endl;

    return 0;
}

4.2约数

4.2.1 试除法求约数

给定 n个正整数 ai,对于每个整数 ai,请你按照从小到大的顺序输出它的所有约数。

输入格式

第一行包含整数 n。

接下来 n 行,每行包含一个整数 ai。

输出格式

输出共 n 行,其中第 i 行输出第 i 个整数 ai 的所有约数。

数据范围

1≤n≤100,

2≤ai≤2×1e9

输入样例:

2

6

8

输出样例:

1 2 3 6

1 2 4 8

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

using namespace std;

int n;

void get_divisors(int n)
{
    vector<int> res;

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

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

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

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

4.2.2约数个数

给定 n 个正整数 ai,请你输出这些数的乘积的约数个数,答案对 1e9+7 取模。

输入格式

第一行包含整数n。

接下来 n 行,每行包含一个整数 ai。

输出格式

输出一个整数,表示所给正整数的乘积的约数个数,答案需对1e9+7 取模。

数据范围

1≤n≤100,

1≤ai≤2×1e9

输入样例:

3

2

6

8

输出样例:

12

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

4.2.3约数之和

给定 n 个正整数 ai,请你输出这些数的乘积的约数之和,答案对 1e9+7 取模。

输入格式

第一行包含整数 n。

接下来 n 行,每行包含一个整数 ai。

输出格式

输出一个整数,表示所给正整数的乘积的约数之和,答案需对 1e9+7 取模。

数据范围

1≤n≤100,

1≤ai≤2×1e9

输入样例:

3

2

6

8

输出样例:

252

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

using namespace std;

typedef long long LL;

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

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

    unordered_map<int, int> primes;

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

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

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

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

    cout << res << endl;

    return 0;
}

4.2.4最大公约数

给定 n 对正整数ai,bi,请你求出每对数的最大公约数。

输入格式

第一行包含整数 n。

接下来 n 行,每行包含一个整数对 ai,bi。

输出格式

输出共 n 行,每行输出一个整数对的最大公约数。

数据范围

1≤n≤1e5,

1≤ai,bi≤2×1e9

输入样例:

2

3 6

4 6

输出样例:

3

2

#include<bits/stdc++.h>
using namespace std;
int gcd(int a,int b)
{
    return b ? gcd(b,a%b) : a;
}
int main()
{
    int n;
    scanf("%d",&n);
    while(n--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        printf("%d\n",gcd(a,b));
    }
    return 0;
}
int gcd(int a,int b){
    if(b > 0) return gcd(b,a % b); //如果b大于0,那么继续除
    else return a; //否则直接返回a
}

4.3欧拉函数

4.3.1 欧拉函数

给定 n 个正整数 ai,请你求出每个数的欧拉函数。

欧拉函数的定义

输入格式

第一行包含整数 n。

接下来 n 行,每行包含一个正整数 ai。

输出格式

输出共 n 行,每行输出一个正整数 ai 的欧拉函数。

数据范围

1≤n≤100,

1≤ai≤2×1e9

输入样例:

3

3

6

8

输出样例:

2

2

4

#include <iostream>
using namespace std;

int n;

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

4.3.2筛法求欧拉函数

给定一个正整数 n,求1∼n 中每个数的欧拉函数之和。

输入格式

共一行,包含一个整数 n。

输出格式

共一行,包含一个整数,表示 1∼n 中每个数的欧拉函数之和。

数据范围

1≤n≤1e6

输入样例:

6

输出样例:

12

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;

const int N = 1000010;

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

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

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

    get_eulers(n);

    LL res = 0;
    for (int i = 1; i <= n; i++) res += phi[i];
    printf("%lld\n", res);

    return 0;
}

4.4快速幂

4.4.1快速幂

时间复杂度为O(log n)

#include <iostream>
#include <cstring>
#include <algorithm>
#include <cstdio>
using namespace std;
typedef long long LL;

int qmi(int a,int k,int p)
{
    int res=1;
    while(k)
    {
        if(k&1) res=(LL)res*a%p;
        k/=2;
        a=(LL)a*a%p;
    }
    return res;
}
int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        int a,k,p;
        cin>>a>>k>>p;
        cout<<qmi(a,k,p)<<endl;
    }
    return 0;
}

4.4.2 快速幂求逆元

a/b在模m的条件下可以写成a*x,x就是b在模m下的乘法逆元,x=b^(n-2),a/b=a*b^(m-2).

我们把上面的b换成题目中的a,其实就是求a^(p-2)%p.

#include<iostream>
using namespace std;
long long qmi(long long a,int b,int p)
{
    long long res=1;
    if(a%p==0) return -1;
    while(b)
    {
        if(b&1) res = res*a % p;
        b>>=1;
        a=a*a%p;
    }
    return res;
}
int main()
{
    int n;
    cin>>n;
    while(n--)
    {
        int a,p;
        cin>>a>>p;
        long long res = qmi(a,p-2,p);
        if(res==-1) cout<<"impossible"<<endl;
        else cout<<res<<endl;
    }
}

4.5扩展欧几里得算法

4.5.1扩展欧几里得算法

思路:

#include<iostream>
using namespace std;

void exgcd(int a,int b,int &x,int &y)
{
    if(!b)
    {
        x=1,y=0;
        return ;
    }
    else
    {
        int x1,y1;
        exgcd(b,a%b,x1,y1);
        x=y1;
        y=x1-a/b*y1;
    }
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        int a,b,x,y;
        scanf("%d%d",&a,&b);
        exgcd(a,b,x,y);
        cout<<x<<" "<<y<<endl;
    }
    return 0;
}

4.5.2线性同余方程

#include<iostream>
using namespace std;

int exgcd(int a,int b,int &x,int &y)
{
    if(b==0)
    {
        x=1,y=0;
        return a;
    }
    else
    {
        int x1,y1;
        int d=exgcd(b,a%b,x1,y1);
        x=y1;
        y=x1-a/b*y1;
        return d;
    }
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        int a,b,m,x,y;
        scanf("%d%d%d",&a,&b,&m);
        int d=exgcd(a,m,x,y);
        if((b%d)!=0) puts("impossible");
        else
        {
            printf("%d\n",(long long)x*(b/d)%m);
        }
    }
    return 0;
}

4.6中国剩余定理

4.6.1表达整数奇怪的方式

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

typedef long long LL;

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

int main()
{
    int n;
    cin>>n;
    bool has_answer=true;
    LL a1,m1;
    cin>>a1>>m1;
    for(int i=0;i<n-1;i++)
    {
        LL a2,m2;
        cin>>a2>>m2;
        LL k1,k2,d=exgcd(a1,a2,k1,k2);
        if((m2-m1)%d!=0)
        {
            has_answer=false;
            break;
        }
        k1*=(m2-m1)/d;
        LL t=a2/d;
        k1=(k1%t+t)%t;
        m1=a1*k1+m1;
        a1=abs(a1/d*a2);
    }
    if(has_answer) cout<<(m1%a1+a1)%a1<<endl;
    else puts("-1");
    return 0;
}

4.7高斯消元

4.7.1高斯消元解线性方程组

输入一个包含 n 个方程 n 个未知数的线性方程组。

方程组中的系数为实数。

求解这个方程组。

下图为一个包含 m 个方程 n 个未知数的线性方程组示例:

输入格式
第一行包含整数 n。

接下来 n 行,每行包含 n+1 个实数,表示一个方程的 n 个系数以及等号右侧的常数。

输出格式
如果给定线性方程组存在唯一解,则输出共 n 行,其中第 i 行输出第 i 个未知数的解,结果保留两位小数。

如果给定线性方程组存在无数解,则输出 Infinite group solutions。

如果给定线性方程组无解,则输出 No solution。

数据范围
1≤n≤100,
所有输入系数以及常数均保留两位小数,绝对值均不超过 100。

输入样例:
3
1.00 2.00 -1.00 -6.00
2.00 1.00 -3.00 -9.00
-1.00 -1.00 2.00 7.00
输出样例:
1.00
-2.00
3.00

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

using namespace std;

const int N = 110;
const double eps = 1e-8;

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

int gauss()  // 高斯消元,答案存于a[i][n]中,0 <= i < n
{
    int c, r;
    for (c = 0, r = 0; c < n; c ++ )
    {
        int t = r;
        for (int i = r; i < n; i ++ )  // 找绝对值最大的行
            if (fabs(a[i][c]) > fabs(a[t][c]))
                t = i;

        if (fabs(a[t][c]) < eps) continue;

        for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]);  // 将绝对值最大的行换到最顶端
        for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];  // 将当前行的首位变成1
        for (int i = r + 1; i < n; i ++ )  // 用当前行将下面所有的列消成0
            if (fabs(a[i][c]) > eps)
                for (int j = n; j >= c; j -- )
                    a[i][j] -= a[r][j] * a[i][c];

        r ++ ;
    }

    if (r < n)
    {
        for (int i = r; i < n; i ++ )
            if (fabs(a[i][n]) > eps)
                return 2; // 无解
        return 1; // 有无穷多组解
    }

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

    return 0; // 有唯一解
}

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

    int t = gauss();
    if (t == 2) puts("No solution");
    else if (t == 1) puts("Infinite group solutions");
    else
    {
        for (int i = 0; i < n; i ++ )
        {
            if (fabs(a[i][n]) < eps) a[i][n] = 0;  // 去掉输出 -0.00 的情况
            printf("%.2lf\n", a[i][n]);
        }
    }

    return 0;
}

4.8求组合数

4.8.1求组合数 I

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=2010,mod=1e9+7;
int c[N][N];
int n;
void init()
{
    for(int i=0;i<2010;i++)
        for(int j=0;j<=i;j++)
            if(!j) c[i][j]=1;
            else c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod;
}
int main()
{
    init();
    cin>>n;
    while(n--)
    {
        int a,b;
        cin>>a>>b;
        cout<<c[a][b]<<endl;
    }
    return 0;
}

4.8.2 求组合数 II

#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
const int N=100010,mod=1e9+7;
int fact[N],infact[N];

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

int main()
{
    fact[0]=infact[0]=1;
    for(int i=1;i<N;i++)
    {
        fact[i]=(LL)fact[i-1]*i%mod;
        infact[i]=(LL)infact[i-1]*qmi(i,mod-2,mod)%mod;
    }
    int n;
    cin>>n;
    while(n--)
    {
        int a,b;
        cin>>a>>b;
        printf("%d\n",(LL)fact[a]*infact[b]%mod*infact[a-b]%mod);
    }
    return 0;
}

4.8.3求组合数 III

4.8.4求组合数 IV

4.8.5满足条件的01序列

4.9容斥原理

4.9.1能被整除的数

4.10博弈论

4.10.1 Nim游戏

4.10.2台阶-Nim游戏

4.10.3集合-Nim游戏

4.10.4拆分-Nim游戏

5.动态规划篇

5.1背包问题

5.1.1 01背包问题

有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000

0<vi,wi≤1000

输入样例

4 5

1 2

2 4

3 4

4 5

输出样例:

8

思路:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
 
const int N=1005;
int v[N*N],w[N*N];
int f[N][N];
int 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=1;j<=m;j++)
        {
            f[i][j]=f[i-1][j];
            if(j>=v[i])
                f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

5.1.2 完全背包问题

有 N 种物品和一个容量是 V的背包,每种物品都有无限件可用。

第 i种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行两个整数vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000

0<vi,wi≤1000

输入样例

4 5

1 2

2 4

3 4

4 5

输出样例:

10

思路:

完全背包是求前缀的最大值,第一次求前1项的max,第二次求前2项的max,......

#include<iostream>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N],w[N];
int main()
{
    int n,m;
    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=1;j<=m;j++)
        {
            f[i][j]=f[i-1][j];
            if(j>=v[i])
                f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

5.1.3 多重背包问题I

有 N种物品和一个容量是 V 的背包。

第 i种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。

输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 ii 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤100

0<vi,wi,si≤100

输入样例

4 5

1 2 3

2 4 1

3 4 3

4 5 2

输出样例:

10

完全背包模型的基础上加了一个限制。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1010;
int v[N],w[N],s[N];
int f[N][N];
int n,m;
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=1;j<=m;j++)
        {
            f[i][j]=f[i-1][j];//一个也不选的状态
            for(int k=1;k<=s[i];k++)
                if(j>=k*v[i]) f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
        }
    cout<<f[n][m]<<endl;
    return 0;
}

按照01背包进行优化为一维:

#include<iostream>
#include<algorithm>

using namespace std;

const int N = 1100;

int n,m,v,w,s;

int f[N];

int main()
{
    scanf("%d%d", &n,&m);
    
    for(int i=1;i<=n;i++)
    {
        cin>>v>>w>>s;
        for(int j=m;j>=0;j--)
            for(int k=0;k<=s&&k*v<=j;k++)
                f[j]=max(f[j],f[j-k*v]+k*w);
    }
    cout<<f[m]<<endl;
    return 0;
}

5.1.4多重背包问题 II

有 N种物品和一个容量是 V 的背包。

第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。

输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N≤1000

0<V≤2000

0<vi,wi,si≤2000

提示:

本题考查多重背包的二进制优化方法。

输入样例

4 5

1 2 3

2 4 1

3 4 3

4 5 2

输出样例:

10

我们可以用二进制对其进行优化。

#include<iostream>
using namespace std;
 
const int N = 12010, M = 2010;
 
int n, m;
int v[N], w[N]; //逐一枚举最大是N*logS
int f[M]; // 体积<M
 
int main()
{
    cin >> n >> m;
    int cnt = 0; //分组的组别
    for(int i = 1;i <= n;i ++)
    {
        int a,b,s;
        cin >> a >> b >> s;
        int k = 1; // 组别里面的个数
        while(k<=s)
        {
            cnt ++ ; //组别先增加
            v[cnt] = a * k ; //整体体积
            w[cnt] = b * k; // 整体价值
            s -= k; // s要减小
            k *= 2; // 组别里的个数增加
        }
        //剩余的一组
        if(s>0)
        {
            cnt ++ ;
            v[cnt] = a*s; 
            w[cnt] = b*s;
        }
    }
 
    n = cnt ; //枚举次数正式由个数变成组别数
 
    //01背包一维优化
    for(int i = 1;i <= n ;i ++)
        for(int j = m ;j >= v[i];j --)
            f[j] = max(f[j],f[j-v[i]] + w[i]);
 
    cout << f[m] << endl;
    return 0;
}

5.1.5分组背包问题

有 N组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。

每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输出最大价值。

输入格式

第一行有两个整数 N,V,用空格隔开,分别表示物品组数和背包容量。

接下来有 N组数据:

每组数据第一行有一个整数 Si,表示第 i 个物品组的物品数量;

每组数据接下来有Si 行,每行有两个整数 vij,wij,用空格隔开,分别表示第 i个物品组的第 j个物品的体积和价值;

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤100

0<Si<100

0<vij,wij≤100

输入样例

3 5

2

1 2

2 4

1

3 4

1

4 5

输出样例:

8

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 110;
 
int w[N][N],v[N][N];
int f[N][N];
int s[N];
int n,m;
 
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) 
    {
        cin>>s[i];
        for(int k=1;k<=s[i];k++)
            cin>>v[i][k]>>w[i][k];
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        {
            f[i][j]=f[i-1][j];
            for(int k=1;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;
}

5.1.6二维背包的费用问题

有 N件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。

每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。

求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。

输出最大价值。

输入格式

第一行三个整数,N,V,M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。

接下来有 N行,每行三个整数vi,mi,wi,用空格隔开,分别表示第 i件物品的体积、重量和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N≤1000

0<V,M≤100

0<vi,mi≤100

0<wi≤1000

输入样例

4 5 6

1 2 3

2 4 4

3 4 5

4 5 6

输出样例:

8

思路:01背包的变形,多加了一个限制条件。

#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int M = 1100;

int N,V,W,x,y,z;

int f[M][M];

int main()
{
    cin>>N>>V>>W;
    for(int i=1;i<=N;i++)
    {
        cin>>x>>y>>z;
        for(int j=V;j>=x;j--)
            for(int k=W;k>=y;k--)
                f[j][k]=max(f[j][k],f[j-x][k-y]+z);
    }
    cout<<f[V][W]<<endl;
    return 0;
}

5.2线性DP

5.2.1数字三角形

给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

7

3 8

8 1 0

2 7 4 4

4 5 2 6 5

输入格式

第一行包含整数 n,表示数字三角形的层数。

接下来 n行,每行包含若干整数,其中第 i 行表示数字三角形第 i层包含的整数。

输出格式

输出一个整数,表示最大的路径数字和。

数据范围

1≤n≤500,

−10000≤三角形中的整数≤10000

输入样例:

5

7

3 8

8 1 0

2 7 4 4

4 5 2 6 5

输出样例:

30

思路:

数字三角形的模型

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510, INF = 1e9;

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

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

    for (int i = 0; i <= n; i ++ )
        for (int j = 0; j <= i + 1; j ++ )
            f[i][j] = -INF;

    f[1][1] = a[1][1];
    for (int i = 2; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ )
            f[i][j] = max(f[i - 1][j - 1], f[i - 1][j])+a[i][j];

    int res = -INF;
    for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);

    printf("%d\n", res);
    return 0;
}

5.2.2最长上升子序列

给定一个长度为 N的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数 N。

第二行包含 N个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤1000,

−1e9≤数列中的数≤1e9

输入样例:

7

3 1 2 1 8 5 6

输出样例:

4

思路:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 1100;
int a[N];
int f[N];
int n,ans;
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=n;i++)
    {
        f[i]=1;
        for(int j=1;j<i;j++)
            if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);
        ans=max(ans,f[i]);
    }
    cout<<ans;
    return 0;
}

5.2.3 最长上升子序列 II

给定一个长度为 N的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式

第一行包含整数 N。

第二行包含 N个整数,表示完整序列。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N≤100000,

−1e9≤数列中的数≤1e9

输入样例:

7

3 1 2 1 8 5 6

输出样例:

4

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
int a[N];
int q[N];//存储不同长度下,结尾的最小值

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

    int len = 0;
    for (int i = 0; i < n; i ++ )
    {
        int l = 0, r = len;
        while (l < r)
        {
            int mid = l + r + 1 >> 1;
            if (q[mid] < a[i]) l = mid;
            else r = mid - 1;
        }
        len = max(len, r + 1);//每次都是更大的范围
        q[r + 1] = a[i];//r是小于a[i]的最大的数
    }

    printf("%d\n", len);

    return 0;
}

5.2.4最长公共子序列

给定两个长度分别为 N 和 M 的字符串 A和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。

输入格式

第一行包含两个整数 N 和 M。

第二行包含一个长度为 N 的字符串,表示字符串 A。

第三行包含一个长度为 M的字符串,表示字符串 B。

字符串均由小写字母构成。

输出格式

输出一个整数,表示最大长度。

数据范围

1≤N,M≤1000

输入样例:

4 5

acbd

abedc

输出样例:

3

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1100;

char A[N],B[N];
int n,m;
int f[N][N];

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

5.2.5最短编辑距离

给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:

删除–将字符串 A 中的某个字符删除。

插入–在字符串 A 的某个位置插入某个字符。

替换–将字符串 A 中的某个字符替换为另一个字符。

现在请你求出,将 A 变为 B 至少需要进行多少次操作。

输入格式

第一行包含整数 n,表示字符串 A 的长度。

第二行包含一个长度为 n 的字符串 A。

第三行包含整数 m,表示字符串 B 的长度。

第四行包含一个长度为 m 的字符串 B。

字符串中均只包含大小写字母。

输出格式

输出一个整数,表示最少操作次数。

数据范围

1≤n,m≤1000

输入样例:

10

AGTCTGACGC

11

AGTAAGTAGGC

输出样例:

4

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

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

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

    /*先初始化边界,当a的前0个字母与b匹配时只能添加和b对应位置上相同的字母,次数为a的长度*/
    for (int i = 0; i <= m; i ++ ) f[0][i] = i;

    /*同理,当a与b的前0个字母匹配时,只能删除a中所有的字母,次数为a的长度*/
    for (int i = 0; i <= n; i ++ ) f[i][0] = i;

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

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

    return 0;
}

5.2.6编辑距离

给定 n 个长度不超过 10 的字符串以及 m 次询问,每次询问给出一个字符串和一个操作次数上限。

对于每次询问,请你求出给定的 n 个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。

每个对字符串进行的单个字符的插入、删除或替换算作一次操作。

输入格式

第一行包含两个整数 n 和 m。

接下来 n 行,每行包含一个字符串,表示给定的字符串。

再接下来 m 行,每行包含一个字符串和一个整数,表示一次询问。

字符串中只包含小写字母,且长度均不超过10。

输出格式

输出共 m 行,每行输出一个整数作为结果,表示一次询问中满足条件的字符串个数。

数据范围

1≤n,m≤1000,

输入样例:

3 2

abc

acd

bcd

ab 1

acbd 2

输出样例:

1

和上一题思路相同

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

using namespace std;

const int N = 15, M = 1010;

int n, m;
int f[N][N];
char str[M][N];

int edit_distance(char a[], char b[])
{
    int la = strlen(a + 1), lb = strlen(b + 1);

    for (int i = 0; i <= lb; i ++ ) f[0][i] = i;
    for (int i = 0; i <= la; i ++ ) f[i][0] = i;

    for (int i = 1; i <= la; i ++ )
        for (int j = 1; j <= lb; j ++ )
        {
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
        }

    return f[la][lb];
}

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

    while (m -- )
    {
        char s[N];
        int limit;
        scanf("%s%d", s + 1, &limit);

        int res = 0;
        for (int i = 0; i < n; i ++ )
            if (edit_distance(str[i], s) <= limit)
                res ++ ;

        printf("%d\n", res);
    }

    return 0;
}

5.3区间DP

5.3.1石子合并

设有 N 堆石子排成一排,其编号为 1,2,3,…,N。

每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1、2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;

如果第二步是先合并 2、3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

输入格式

第一行一个数 N 表示石子的堆数 N。

第二行 N 个数,表示每堆石子的质量(均不超过1000)。

输出格式

输出一个整数,表示最小代价。

数据范围

1≤N≤300

输入样例:

4

1 3 5 2

输出样例:

22

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 310;

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

int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        s[i]=s[i-1]+a[i];
    }
    for(int len=2;len<=n;len++)
    {
        for(int i=1;i+len-1<=n;i++)
        {
            int j=i+len-1;
            f[i][j]=1e8;
            for(int k=i;k<j;k++) f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
        }
    }
    cout<<f[1][n]<<endl;
    return 0;
}

5.4计数类DP

5.4.1整数划分

思路:完全背包求方案数的模型。

完全背包解法

状态表示:

f[i][j]表示只从1~i中选,且总和等于j的方案数

状态转移方程:

f[i][j] = f[i - 1][j] + f[i][j - i];

二维

#include <iostream>

using namespace std;

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

int f[N][N];

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

    for (int i = 0; i <= n; i ++) {
        f[i][0] = 1; // 容量为0时,前 i 个物品全不选也是一种方案
    }

    for (int i = 1; i <= n; i ++) {
        for (int j = 1; j <= n; j ++) {
            f[i][j] = f[i - 1][j] % mod; // 特殊 f[0][0] = 1
            if (j >= i) f[i][j] = (f[i - 1][j] + f[i][j - i]) % mod;
        }
    }

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

一维优化

#include <iostream>
#include <algorithm>

using namespace std;

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

int n;
int f[N];

int main()
{
    cin >> n;

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

    cout << f[n] << endl;

    return 0;
}

5.5数位统计DP

5.5.1计数问题

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

using namespace std;

const int N = 10;

/*

001~abc-1, 999

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

*/

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

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

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

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

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

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

    return res;
}

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

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

    return 0;
}

5.6状态压缩DP

5.6.1蒙德里安的梦想

5.6.2 最短Hamilton路径

5.7树形DP

5.7.1 没有上司的舞会

活动 - AcWing

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=6e3+10;
int n,w[N];
vector<int> g[N];
int f[N][3];
int fa[N];

void dfs(int u) {
	f[u][1]=w[u];
	for(auto x: g[u]) {
		dfs(x);
		f[u][0]+=max(f[x][0],f[x][1]);
		f[u][1]+=f[x][0];
	}
}

signed main() {
	cin>>n;
	for(int i=1;i<=n;i++) cin>>w[i];
	for(int i=1;i<n;i++) {
		int a,b;
		cin>>a>>b;
		g[b].push_back(a);
		fa[a]++;
	}
	int root=0;
	for(int i=1;i<=n;i++)
		if(!fa[i]) {
			root=i;
			break;
		} 
	dfs(root);
	cout<<max(f[root][0],f[root][1]);
	return 0;
}

 

5.8记忆化搜索

5.8.1记忆化搜索

活动 - AcWing

6.贪心

6.1区间问题

6.1.1区间选点

6.1.2 最大不相交区间数量

6.1.3 区间分组

6.1.4 区间覆盖

6.2Huffman树

6.2.1合并果子

哈夫曼树的应用,每次都选择两个最小的合并,把这两个进行删除,把合并的进入集合中,维护一个小根堆即可实现哈夫曼树。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
#include <queue>
using namespace std;
const int N = 11000;
priority_queue<int,vector<int>,greater<int>> heap;
int ans;
int main()
{
    int n;
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        int x;
        cin>>x;
        heap.push(x);
    }
    while(heap.size()!=1)
    {
        auto x=heap.top();
        heap.pop();
        auto y=heap.top();
        heap.pop();
        heap.push(x+y);
        ans+=x+y;
    }
    cout<<ans<<endl;
    return 0;
}

6.3排序不等式

6.3.1排队打水

6.4绝对值不等式

6.4.1货仓选址

6.5推公式

6.5.1耍杂技的牛

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值