ACwing算法基础课全程笔记(2021年8月12日开始重写+优化)

更好的阅读体验

※基础模板

2021年8月12日开始对基础课笔记进行重写+优化

请大家支持AcWing正版,购买网课能让自己获得更好的学习体验哦~

链接:https://www.acwing.com/about/

比赛常用技巧及库函数

1、快速读\快速写

快读

template<typename T>void in(T &x) {
    char ch = getchar();bool flag = 0;x = 0;
    while(ch < '0' || ch > '9') flag |= (ch == '-'), ch = getchar();
    while(ch <= '9' && ch >= '0') x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
    if(flag) x = -x;return ;
}

快写

template <typename T>
inline void print(T x)
{
    if(x<0)
    {
        putchar('-');
        x=-x;
    }
    if(x>9)
        print(x/10);
    putchar(x%10+'0');
}

2、__int128_t

输入输出:

inline __int128 read()
{
   int X=0,w=0; char ch=0;
   while(!isdigit(ch)) {w|=ch=='-';ch=getchar();}
   while(isdigit(ch)) X=(X<<3)+(X<<1)+(ch^48),ch=getchar();
   return w?-X:X;
}
inline void print(__int128 x)
{    
   if(x<0){putchar('-');x=-x;}
   if(x>9) print(x/10);
   putchar(x%10+'0');
}

3、火车头

#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC optimize("Ofast")
#pragma GCC optimize("inline")

第一章:基础算法

1.1 快速排序

785. 快速排序

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

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

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

输入格式

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

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

输出格式

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

数据范围

1 ≤ n ≤ 100000 1≤n≤100000 1n100000

输入样例:

5
3 1 2 4 5

输出样例:

1 2 3 4 5
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
typedef long long ll;
ll q[N];int n;
void quick_sort(int l,int r){
    if(l>=r) return;
    ll x=q[(l+r)>>1];
    int i=l-1,j=r+1;
    while(i<j){
        do i++;while(q[i]<x);
        do j--;while(q[j]>x);
        if(i<j) swap(q[i],q[j]);
    }
    quick_sort(l,j);
    quick_sort(j+1,r);
}
int main(){
    scanf("%d",&n);
    for(int i=0;i<n;i++) scanf("%lld",&q[i]);
    quick_sort(0,n-1);
    for(int i=0;i<n;i++) printf("%lld ",q[i]);
}
786. 第k个数

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

输入格式

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

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

输出格式

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

数据范围

1 ≤ n ≤ 100000 1≤n≤100000 1n100000,
1 ≤ k ≤ n 1≤k≤n 1kn

输入样例:

5 3
2 4 1 5 3

输出样例:

3
#include<bits/stdc++.h>
using namespace std;
const int N=100005;
int a[N],n,k;
int quick_sort(int l,int r){
	if(l>=r) return a[l];
	int mid=(l+r)>>1;
	int x=a[mid];
	int i=l-1,j=r+1;
	while(i<j){
		do i++;while(a[i]<x);
		do j--;while(a[j]>x);
		if(i<j){
			swap(a[i],a[j]);
		}
	}
	if(k<=j) quick_sort(l,j);
	else quick_sort(j+1,r);
}
int main(){
	cin>>n>>k;
	for(int i=1;i<=n;i++) cin>>a[i];
	cout<<quick_sort(1,n);
	
}

1.2归并排序

787. 归并排序

题目描述同785.快速排序

#include<bits/stdc++.h>
using namespace std;
int a[100000],tmp[100000];
void merge_sort(int l,int r){
	if(l>=r) return;
	int mid=(l+r)>>1;
	merge_sort(l,mid);
	merge_sort(mid+1,r);
	int i=l,j=mid+1,k=0;
	while(i<=mid&&j<=r){
		if(a[i]<=a[j]) tmp[k++]=a[i++];
		if(a[i]>a[j]) tmp[k++]=a[j++];
	}
	while(i<=mid) tmp[k++]=a[i++];
	while(j<=r) tmp[k++]=a[j++]; 
	for(i=l,j=0;i<=r;i++,j++){
		a[i]=tmp[j];
	}
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i=0;i<n;i++) scanf("%d",&a[i]);
	merge_sort(0,n-1);
	printf("%d",a[0]);
	for(int i=1;i<n;i++) printf(" %d",a[i]);
}
788. 逆序对的数量

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

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

输入格式

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

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

输出格式

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

数据范围

1 ≤ n ≤ 100000 1≤n≤100000 1n100000

输入样例:

6
2 3 4 5 6 1

输出样例:

5
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a[100005],cnt,tmp[100005];
int n;
void merge_sort(int l,int r){
    if(l>=r) return;
    int mid=(l+r)>>1;
    merge_sort(l,mid);
    merge_sort(mid+1,r);
    int i=l,j=mid+1,k=0;
    while(i<=mid&&j<=r){
        if(a[i]<=a[j]) tmp[k++]=a[i++];
        else{
            cnt+=(mid-i+1);
            tmp[k++]=a[j++];
        }
    }
    while(i<=mid) tmp[k++]=a[i++];
    while(j<=r) tmp[k++]=a[j++];
    for(int i=l,j=0;i<=r;i++,j++){
        a[i]=tmp[j];
    }
}
int main(){
    scanf("%d",&n);
    for(int i=0;i<n;i++) scanf("%lld",&a[i]);
    merge_sort(0,n-1);
    cout<<cnt;
    return 0;
}

1.3 二分

整数二分:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nN1xvSca-1628778125326)(D:\ACM\ACwing学习\算法基础课.assets\image-20210121131346679.png)]

①要求红色区间的最右端点

if(check(mid)) l=mid;//区间变为[mid,r]
else r=mid-1;//区间变为[l,mid-1]
int bsearch_2(int l,int r){
	while(l<r){
		int mid=(l+r+1)>>1;
		if(check(mid)) l=mid;
		else r=mid-1;
	}
	return l;
}

②要求绿色区间的最左端点

if(check(mid)) r=mid;//区间变为[l,mid]
else l=mid+1;//区间变为[mid+1,r]
int bsearch_1(int l,int r){
	while(l<r){
		int mid=(l+r)>>1;
		if(check(mid)) r=mid;
		else l=mid+1;
	}
	return l;
}

注意:l=mid 要补上mid=(l+r+1)/2

789. 数的范围(整数二分)

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

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

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

输入格式

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

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

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

输出格式

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

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

数据范围

1 ≤ n ≤ 100000 1≤n≤100000 1n100000
1 ≤ q ≤ 10000 1≤q≤10000 1q10000
1 ≤ k ≤ 10000 1≤k≤10000 1k10000

输入样例:

6 3
1 2 2 3 3 4
3
4
5

输出样例:

3 4
5 5
-1 -1

代码:2021年1月21日修改

#include<bits/stdc++.h>
using namespace std;
const int N=100005;
int a[N];
int lower(int l,int r,int x){
	while(l<r){
		int mid=(l+r)>>1;
		if(a[mid]>=x) r=mid;
		else l=mid+1;
	}
	return r;
}
int upper(int l,int r,int x){
	while(l<r){
		int mid=(l+r+1)>>1;
		if(a[mid]<=x) l=mid;
		else r=mid-1;
	}
	return l;
}
int main(){
	int n,q;
	cin>>n>>q;
	for(int i=1;i<=n;i++) cin>>a[i];
	while(q--){
		int k;
		cin>>k;
		int l=lower(1,n,k);
		if(a[l]!=k) puts("-1 -1");
		else{
			int r=upper(1,n,k);
			printf("%d %d\n",l-1,r-1);
		}
	}	
}
790. 数的三次方根(浮点数二分)

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

输入格式

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

输出格式

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

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

数据范围

− 10000 ≤ n ≤ 10000 −10000≤n≤10000 10000n10000

输入样例:

1000.00

输出样例:

10.000000
#include<bits/stdc++.h>
using namespace std;
int main(){
	double x;
	cin>>x;
	double l=0,r=x;
	for(int i=0;i<100;i++){
		double mid=(l+r)/2;
		if(mid*mid<=x) l=mid;
		else r=mid;
	}
	printf("%lf",l);
}

1.4 高精度

791. 高精度加法
#include<bits/stdc++.h>
using namespace std;
vector<int> add(vector<int>& a,vector<int> &b){
	int t=0;
	vector<int >c;
	for(int i=0;i<a.size()||i<b.size();i++){
		if(i<a.size()) t+=a[i];
		if(i<b.size()) t+=b[i];
		c.push_back(t%10);
		t/=10;
	}
	if(t) c.push_back(1);
	return c;
}
int main(){
	string a,b;
	vector<int> va,vb;
	cin>>a>>b;
	for(int i=a.length()-1;i>=0;i--) va.push_back(a[i]-'0');
	for(int i=b.length()-1;i>=0;i--) vb.push_back(b[i]-'0');
	auto vc=add(va,vb);
	for(int i=vc.size()-1;i>=0;i--) printf("%d",vc[i]);
}

1.5 前缀和与差分

795. 前缀和(一维前缀和)

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

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

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

输入格式

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

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

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

输出格式

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

数据范围

1 ≤ l ≤ r ≤ n 1≤l≤r≤n 1lrn,
1 ≤ n , m ≤ 100000 1≤n,m≤100000 1n,m100000,
− 1000 ≤ 数 列 中 元 素 的 值 ≤ 1000 −1000≤数列中元素的值≤1000 10001000

输入样例:

5 3
2 1 3 6 4
1 2
1 3
2 4

输出样例:

3
6
10
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n,m;
int a[N],s[N];
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
	while(m--){
		int l,r;
		scanf("%d%d",&l,&r);
		printf("%d\n",s[r]-s[l-1]);
	}
}
796. 子矩阵的和(二维前缀和)

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

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

输入格式

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

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

接下来 q q q 行,每行包含四个整数 x 1 , y 1 , x 2 , y 2 x_1,y_1,x_2,y_2 x1,y1,x2,y2,表示一组询问。

输出格式

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

数据范围

1 ≤ n , m ≤ 1000 1≤n,m≤1000 1n,m1000,
1 ≤ q ≤ 200000 1≤q≤200000 1q200000,
1 ≤ x 1 ≤ x 2 ≤ n 1≤x_1≤x_2≤n 1x1x2n,
1 ≤ y 1 ≤ y 2 ≤ m 1≤y_1≤y_2≤m 1y1y2m,
− 1000 ≤ 矩 阵 内 元 素 的 值 ≤ 1000 −1000≤矩阵内元素的值≤1000 10001000

输入样例:

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 = S i − 1 , j + S i , j − 1 − S i − 1 , j − 1 + a i , j S_{i,j}=S_{i-1,j}+S_{i,j-1}-S_{i-1,j-1}+a_{i,j} Si,j=Si1,j+Si,j1Si1,j1+ai,j

( x 1 , y 1 ) (x1,y1) (x1,y1) ( x 2 , y 2 ) (x2,y2) (x2,y2)之间的元素和公式: S = S x 2 , y 2 − S x 2 , y 1 − 1 − S x 1 − 1 , y 2 + S x 1 − 1 , y 1 − 1 S=S_{x2,y2}-S_{x2,y1-1}-S_{x1-1,y2}+S_{x1-1,y1-1} S=Sx2,y2Sx2,y11Sx11,y2+Sx11,y11

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1010;
int n,m,q;
ll a[N][N],s[N][N];
int main(){
	scanf("%d%d%d",&n,&m,&q);
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			scanf("%lld",&a[i][j]);
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1]+a[i][j];
		}
	}
	while(q--){
		int x1,y1,x2,y2;
		scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
		printf("%lld\n",s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1]);
	}
}
797. 差分(一维差分)

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

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

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

输入格式

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

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

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

输出格式

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

数据范围

1 ≤ n , m ≤ 100000 1≤n,m≤100000 1n,m100000,
1 ≤ l ≤ r ≤ n 1≤l≤r≤n 1lrn,
− 1000 ≤ c ≤ 1000 −1000≤c≤1000 1000c1000,
− 1000 ≤ 整 数 序 列 中 元 素 的 值 ≤ 1000 −1000≤整数序列中元素的值≤1000 10001000

输入样例:

6 3
1 2 2 1 2 1
1 3 1
3 5 1
1 6 1

输出样例:

3 4 5 3 4 2
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int a[N],b[N];
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	for(int i=1;i<=n;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];
		printf("%d ",a[i]);
	}
}
798. 差分矩阵(二维差分)

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

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

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

输入格式

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

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

接下来 q q q 行,每行包含 5 5 5 个整数 x 1 , y 1 , x 2 , y 2 , c x_1,y_1,x_2,y_2,c x1,y1,x2,y2,c,表示一个操作。

输出格式

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

数据范围

1 ≤ n , m ≤ 1000 1≤n,m≤1000 1n,m1000,
1 ≤ q ≤ 100000 1≤q≤100000 1q100000,
1 ≤ x 1 ≤ x 2 ≤ n 1≤x1≤x2≤n 1x1x2n,
1 ≤ y 1 ≤ y 2 ≤ m 1≤y_1≤y_2≤m 1y1y2m,
− 1000 ≤ c ≤ 1000 −1000≤c≤1000 1000c1000,
− 1000 ≤ 矩 阵 内 元 素 的 值 ≤ 1000 −1000≤矩阵内元素的值≤1000 10001000

输入样例:

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

分析:

( x 1 , y 1 ) (x1,y1) (x1,y1) ( x 2 , y 2 ) (x2,y2) (x2,y2)之间的所有元素 + = c +=c +=c

b [ x 1 ] [ y 1 ] + = c , b [ x 2 + 1 ] [ y 1 ] − = c , b [ x 1 ] [ y 2 + 1 ] − = c , b [ x 2 + 1 ] [ y 2 + 1 ] + = c b[x1][y1]+=c,b[x2+1][y1]-=c,b[x1][y2+1]-=c,b[x2+1][y2+1]+=c b[x1][y1]+=c,b[x2+1][y1]=c,b[x1][y2+1]=c,b[x2+1][y2+1]+=c

模板题:http://oj.hzjingma.com/p/41?view=classic

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1010;
ll a[N][N],b[N][N];
void Insert(int x1,int y1,int x2,int y2,ll e){
	b[x1][y1]+=e;
	b[x2+1][y1]-=e;
	b[x1][y2+1]-=e;
	b[x2+1][y2+1]+=e;
}
int main(){
	int n,m,q;
	scanf("%d%d%d",&n,&m,&q);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			scanf("%lld",&a[i][j]);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			Insert(i,j,i,j,a[i][j]);
	while(q--){
		int x1,y1,x2,y2;
		ll c; 
		scanf("%d%d%d%d%lld",&x1,&y1,&x2,&y2,&c);
		Insert(x1,y1,x2,y2,c);//起初将a数组看成空的,执行插入操作
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=m;j++)
			b[i][j]+=b[i-1][j]+b[i][j-1]-b[i-1][j-1];//原数组即差分数组的前缀和
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++)
			printf("%lld ",b[i][j]);
		cout<<endl;
	}
} 

1.6 双指针算法

for(i=0,j=0;i<n;i++){
	while(j<i&&check(i,j)) j++;
	
	
	//每道题目的具体逻辑
}

核心思想:

for(int i=0;i<n;i++){
	for(int j=0;j<n;j++){
		//O(n^2)
	}
}

双指针算法就是要将上面的朴素算法优化到 O ( n ) O(n) O(n)

例题1:给出一行带有空格的字符串,求出里面的单词

#include<bits/stdc++.h>
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++;
		for(int k=i;k<j;k++) cout<<str[k];
		cout<<endl;
		i=j;
	}
	return 0;
}
799. 最长连续不重复子序列

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

输入格式

第一行包含整数 n n n

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

输出格式

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

数据范围

1 ≤ n ≤ 1 0 5 1≤n≤10^5 1n105

输入样例:

5
1 2 2 3 5

输出样例:

3

分析:

朴素做法: O ( n 2 ) O(n^2) O(n2)

for(int i=0;i<n;i++)
	for(int j=0;j<=i;j++)
		if(check(j,i))
			res=max(res,i-j+1);

双指针算法:

核心思路:

遍历数组a中的每一个元素a[i], 对于每一个i,找到j使得双指针[j, i]维护的是以a[i]结尾的最长连续不重复子序列,长度为i - j + 1, 将这一长度与r的较大者更新给r
对于每一个i,如何确定j的位置:由于[j, i - 1]是前一步得到的最长连续不重复子序列,所以如果[j, i]中有重复元素,一定是a[i],因此右移j直到a[i]不重复为止(由于[j, i - 1]已经是前一步的最优解,此时j只可能右移以剔除重复元素a[i],不可能左移增加元素,因此,j具有“单调性”、本题可用双指针降低复杂度)。
用数组s记录子序列a[j ~ i]中各元素出现次数,遍历过程中对于每一个i有四步操作:cin元素a[i] -> 将a[i]出现次数s[a[i]]加1 -> 若a[i]重复则右移js[a[j]]要减1) -> 确定j及更新当前长度i - j + 1r

for(int i=0,j=0;i<n;i++){
	while(j<=i&&check(j,i)) j++;
	res=max(res,i-j+1);
}
#include<bits/stdc++.h>
using namespace std;
const int N =100010;
int n;
int a[N],s[N];
int main(){
	cin>>n;
	for(int i=0;i<n;i++) cin>>a[i];
	int res=0;
	for(int i=0,j=0;i<n;i++){
		s[a[i]]++;
		while(s[a[i]]>1){
			s[a[j]]--;
			j++;
		}
		res=max(res,i-j+1);
	}
	cout<<res<<endl;
	return 0;
}
800. 数组元素的目标和

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

数组下标从 0 0 0 开始。

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

数据保证有唯一解。

输入格式

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

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

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

输出格式

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

数据范围

数组长度不超过 1 0 5 10^5 105
同一数组内元素各不相同。
1 ≤ 数 组 元 素 ≤ 1 0 9 1≤数组元素≤10^9 1109

输入样例

4 5 6
1 2 4 7
3 4 6 8 9

输出样例:

1 1

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+5;
ll a[N],b[N];
int main(){
    int n,m,x;
    cin>>n>>m>>x;
    for(int i=1;i<=n;i++) cin>>a[i];
    for(int i=1;i<=m;i++) cin>>b[i];
    sort(a,a+n);sort(b,b+m);
    int ansi,ansj;
    for(int i=1,j=m;i<=n && j>=1;i++){
        while(a[i]+b[j]>x) j--;
        if(a[i]+b[j]==x){
            ansi=i,ansj=j;
            break;
        }
    }
    cout<<ansi-1<<" "<<ansj-1;
}
2816. 判断子序列

给定一个长度为 n n n 的整数序列 a 1 , a 2 , … , a n a_1,a_2,…,a_n a1,a2,,an 以及一个长度为 m m m 的整数序列 b 1 , b 2 , … , b m b_1,b_2,…,b_m b1,b2,,bm

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

子序列指序列的一部分项按原有次序排列而得的序列,例如序列 a 1 , a 3 , a 5 {a1,a3,a5} a1,a3,a5 是序列 a 1 , a 2 , a 3 , a 4 , a 5 {a_1,a_2,a_3,a_4,a_5} a1,a2,a3,a4,a5 的一个子序列。

输入格式

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

第二行包含 n n n 个整数,表示 a 1 , a 2 , … , a n a_1,a_2,…,a_n a1,a2,,an

第三行包含 m m m 个整数,表示 b 1 , b 2 , … , b m b_1,b_2,…,b_m b1,b2,,bm

输出格式

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

否则,输出 No

数据范围

1 ≤ n ≤ m ≤ 1 0 5 1≤n≤m≤10^5 1nm105,
− 1 0 9 ≤ a i , b i ≤ 1 0 9 −10^9≤a_i,b_i≤10^9 109ai,bi109

输入样例:

3 5
1 3 5
1 2 3 4 5

输出样例:

Yes

1.7 位运算

知识点:

1、n的二进制表示中第k位是几

方法:①先把第k位移到最后一位 n > > = k n>>=k n>>=k

②看个位是几 n & 1 n\&1 n&1

最终结果为 n > > k & 1 n>>k\&1 n>>k&1

2、lowbit(x):返回x的最后一位1

x=1010 lowbit(x)=10

x=101000 lowbit(x)=1000

操作: x & ( − x ) = x & (   x + 1 ) x\&(-x)=x\&(~x+1) x&(x)=x&( x+1)

801. 二进制中1的个数

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

输入格式

第一行包含整数 n n n

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

输出格式

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

数据范围

1 ≤ n ≤ 100000 1≤n≤100000 1n100000,
0 ≤ 数 列 中 元 素 的 值 ≤ 1 0 9 0≤数列中元素的值≤10^9 0109

输入样例:

5
1 2 3 4 5

输出样例:

1 1 2 1 2
#include<bits/stdc++.h>
using namespace std;
int lowbit(int x){
	return x&(-x);
}
int main(){
	int n;
	scanf("%d",&n);
	while(n--){
		int x;
		scanf("%d",&x);
		int res=0;
		while(x) x-=lowbit(x),res++;
		cout<<res<<" ";
	}
	
}

1.8 离散化

①a[i]中有重复元素要注意去重

②用二分法算出离散化的值 x x x

模板:

vector<int> alls;//存储所有待离散化的值
sort(alls.begin(),all.end());//排序
alls.erase(unique(alls.begin(),alls.end()),alls.end());//去重
//二分求出x对应离散化的值
int find(int x){//找到第一个大于等于x的位置
    int l=0,r=alls.size()-1;
    while(l<r){
        int mid=l+r>>1;
        if(alls[mid]>=x) r=mid;
        else l=mid+1;
    }
    return r+1;//映射到1,2,....n需要加1
}
802. 区间和

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

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

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

输入格式

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

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

再接下里 m m m​ 行,每行包含两个整数 l l l r r r

输出格式

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

#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> pii;
vector<pii> p;
vector<pii> query;
vector<int> alls;
int find(int x){
    int l=0,r=alls.size()-1;
    while(l<r){
        int mid=(l+r)>>1;
        if(alls[mid]>=x) r=mid;
        else l=mid+1;
    }
    return r+1;
}
int a[300005],sum[300005],n,m;
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        int x,c;
        cin>>x>>c;
        alls.push_back(x);
        p.push_back({x,c});
    }
    for(int i=1;i<=m;i++){
        int l,r;
        cin>>l>>r;
        alls.push_back(l);
        alls.push_back(r);
        query.push_back({l,r});
    }
    sort(alls.begin(),alls.end());
    alls.erase(unique(alls.begin(),alls.end()),alls.end());
    for(auto item:p){
        a[find(item.first)]+=item.second;
    }
    for(int i=1;i<=alls.size();i++) sum[i]=sum[i-1]+a[i];
    for(auto item:query){
        int l=find(item.first),r=find(item.second);
        cout<<sum[r]-sum[l-1]<<endl;
    }
}

1.9 区间合并

803. 区间合并

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

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

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

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

输入格式

第一行包含整数 n n n

接下来 n n n 行,每行包含两个整数 l l l r r r

输出格式

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

数据范围

1 ≤ n ≤ 100000 1≤n≤100000 1n100000,
− 1 0 9 ≤ l i ≤ r i ≤ 1 0 9 −10^9≤l_i≤r_i≤10^9 109liri109

输入样例:

5
1 2
2 4
5 6
7 8
7 9

输出样例:

3
#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int N=100010;
int n;
vector<PII> segs;
void merge(vector<PII> &segs){
    vector<PII> res;
    sort(segs.begin(),segs.end());
    int st=-2e9,ed=-2e9;
    for(auto seg:segs){
        if(ed<seg.first){
            if(st!=-2e9) res.push_back({st,ed});
            st=seg.first,ed=seg.second;
        }else ed=max(ed,seg.second);
    }
    if(st!=-2e9) res.push_back({st,ed});
    segs=res;
}
int main(){
    cin>>n;
    for(int i=0;i<n;i++){
        int l,r;
        cin>>l>>r;
        segs.push_back({l,r});
    }
    merge(segs);
    cout<<segs.size()<<endl;
}

第二章:数据结构

2.1 单链表

826. 单链表

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

  1. 向链表头插入一个数;
  2. 删除第 k k k 个插入的数后面的数;
  3. 在第 k k k 个插入的数后插入一个数。

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

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

输入格式

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

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

  1. H x,表示向链表头插入一个数 x x x
  2. D k,表示删除第 k k k 个插入的数后面的数(当 k k k 0 0 0 时,表示删除头结点)。
  3. I k x,表示在第 k k k 个插入的数后面插入一个数 x x x(此操作中 k k k 均大于 0 0 0)。

输出格式

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

数据范围

1 ≤ M ≤ 100000 1≤M≤100000 1M100000
所有操作保证合法。

输入样例:

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

(因为传统链式存储需要大量使用New函数,会带来大量时间上的开销,所以我们用数组模拟)

(通常情况下,数据规模1e6)

#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int e[N],ne[N],idx,head;
void init(){
    head=-1;
    idx=0;
}
void add_to_head(int x){
    e[idx]=x;
    ne[idx]=head;
    head=idx;
    idx++;
}
void del(int k){
    ne[k]=ne[ne[k]];
}
void add(int k,int x){
    e[idx]=x;
    ne[idx]=ne[k];
    ne[k]=idx;
    idx++;
}
int main(){
    init();
    int m;
    scanf("%d",&m);
    while(m--){
        char op;int x;
        cin>>op;
        if(op=='H'){
            cin>>x;
            add_to_head(x);
        }else if(op=='D'){
            cin>>x;
            if(!x) head=ne[head];//注意特判头结点
            else del(x-1);
        }else if(op=='I'){
            int k;
            cin>>k>>x;
            add(k-1,x);
        }
    }
    for(int i=head;i!=-1;i=ne[i]){
        cout<<e[i]<<" ";
    }
}

2.2 双链表

827. 双链表

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

  1. 在最左侧插入一个数;
  2. 在最右侧插入一个数;
  3. 将第 k k k 个插入的数删除;
  4. 在第 k k k 个插入的数左侧插入一个数;
  5. 在第 k k k 个插入的数右侧插入一个数

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

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

输入格式

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

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

  1. L x,表示在链表的最左端插入数 x x x
  2. R x,表示在链表的最右端插入数 x x x
  3. D k,表示将第 k k k 个插入的数删除。
  4. IL k x,表示在第 k k k 个插入的数左侧插入一个数。
  5. IR k x,表示在第 k k k 个插入的数右侧插入一个数。

输出格式

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

数据范围

1 ≤ M ≤ 100000 1≤M≤100000 1M100000
所有操作保证合法。

输入样例:

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
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int e[N],l[N],r[N],idx;
int m;
void init(){
    r[0]=1,l[1]=0;
    idx=2;
}
void addl(int x){
    l[idx]=0;//新节点->pre=head
    r[idx]=r[0];//新节点->next=head->next
    r[0]=idx;//head->next=新节点
    l[r[idx]]=idx;//新节点->next->pre=新节点
    e[idx++]=x;//add x
}
void addr(int x){
    r[idx]=1;//新节点->next=tail
    l[idx]=l[1];//新节点->pre=tail->pre
    r[l[idx]]=idx;//新节点->pre->next=新节点
    l[1]=idx;//tail->pre=新节点
    e[idx++]=x;//add x
}
void remove(int k){
    r[l[k]]=r[k];
    l[r[k]]=l[k];
}
void insertkl(int k,int x){
    l[idx]=l[k];//新节点->pre=k->pre
    r[idx]=k;//新节点->next=k
    r[l[idx]]=idx;//新节点->pre->next=新节点
    l[k]=idx;//k->pre=新节点
    e[idx++]=x;//add x
}
void insertkr(int k,int x){
    r[idx]=r[k];//新节点->next=k->next
    l[idx]=k;//新节点->pre=k
    l[r[idx]]=idx;//新节点->next->pre=新节点
    r[k]=idx;//k->next=新节点
    e[idx++]=x;//add x
}
int main(){
    init();
    cin>>m;
    while(m--){
        string op;
        int k,x;
        cin>>op;
        if(op=="L"){
            cin>>x;
            addl(x);
        }else if(op=="R"){
            cin>>x;
            addr(x);
        }else if(op=="D"){
            cin>>k;
            remove(k+1);
        }else if(op=="IL"){
            cin>>k>>x;
            insertkl(k+1,x);
        }else if(op=="IR"){
            cin>>k>>x;
            insertkr(k+1,x);
        }
    }
    for(int i=r[0];i!=1;i=r[i]) cout<<e[i]<<" ";

}

2.3 栈

const int N=100010;
//定义栈
int stk[N],tt;
//插入
stk[++tt] = x;
//弹出
tt--;
//判断栈是否为空
if (tt >0)  not empty
else empty
//取栈顶元素
stk[tt];
828. 模拟栈

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

  1. push x – 向栈顶插入一个数 xx;
  2. pop – 从栈顶弹出一个数;
  3. empty – 判断栈是否为空;
  4. query – 查询栈顶元素。

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

输入格式

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

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

输出格式

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

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

数据范围

1 ≤ M ≤ 100000 1≤M≤100000 1M100000,
1 ≤ x ≤ 1 0 9 1≤x≤10^9 1x109
所有操作保证合法。

输入样例:

10
push 5
query
push 6
pop
query
pop
empty
push 4
query
empty

输出样例:

5
5
YES
4
NO
#include<bits/stdc++.h>
using namespace std;
int stk[100005],hh=-1;
int main(){
	int m;
	scanf("%d",&m);
	while(m--){
		string op;int x;
		cin>>op;
		if(op=="push"){
			cin>>x;
			stk[++hh]=x;
		}else if(op=="pop"){
			hh--;
		}else if(op=="empty"){
			if(hh==-1) cout<<"YES"<<endl;
			else cout<<"NO"<<endl;
		}else if(op=="query"){
			cout<<stk[hh]<<endl;
		}
	}
}
3302. 表达式求值(模板)

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

注意:

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

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

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

  • 题目保证表达式在中间计算过程以及结果中,均不超过 2 31 − 1 2^{31}−1 2311

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

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

    #include<bits/stdc++.h>
    using namespace std;
    stack<int> num;
    stack<char> op;
    void eval(){
    	auto b=num.top();num.pop();
    	auto a=num.top();num.pop();
    	auto c=op.top();op.pop();
    	int x;
    	if(c=='+') x=a+b;
    	else if(c=='-') x=a-b;
    	else if(c=='*') x=a*b;
    	else x=a/b;
    	num.push(x);
    }
    int main(){
    	//定义运算符优先级顺序
    	unordered_map<char,int> pr{{'+',1},{'-',1},{'*',2},{'/',2}};//这里可以拓展 
    	string str;
    	cin>>str;
    	for(int i=0;i<str.size();i++){
    		auto c=str[i];
    		if(isdigit(c)){
    			int x=0,j=i;
    			while(j<str.size() && isdigit(str[j])){
    				x=x*10+str[j++]-'0';
    			}
    			i=j-1;
    			num.push(x);
    		}else if(c=='(') op.push(c);
    		else if(c==')'){
    			while(op.top()!='(') eval();
    			op.pop();
    		}else{
    			while(op.size() && op.top()!='(' && pr[op.top()]>=pr[c]) eval();
    			op.push(c);
    		}
    	}
    	while(op.size()) eval();
    	cout<<num.top()<<endl;
    	return 0;
    }
    

2.4 队列

//定义队列
int q[N],hh,tt=-1;
//在队尾插入元素,在队头弹出元素
//插入
q[++tt]=x;
//弹出
hh++;
//判断队列是否为空
if(hh<=tt) not empty
else empty
//取出队头元素
q[hh]
829. 模拟队列

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

  1. push x – 向队尾插入一个数 x x x
  2. pop – 从队头弹出一个数;
  3. empty – 判断队列是否为空;
  4. query – 查询队头元素。

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

输入格式

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

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

输出格式

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

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

数据范围

1 ≤ M ≤ 100000 1≤M≤100000 1M100000,
1 ≤ x ≤ 1 0 9 1≤x≤10^9 1x109,
所有操作保证合法。

输入样例:

10
push 6
empty
query
pop
empty
push 3
push 4
pop
query
push 6

输出样例:

NO
6
YES
4
#include<bits/stdc++.h>
using namespace std;
int qu[100005],hh,tt=-1;
int main(){
	int m;
	scanf("%d",&m);
	while(m--){
		string op;int x;
		cin>>op;
		if(op=="push"){
			cin>>x;
			qu[++tt]=x;
		}else if(op=="pop"){
			hh++;
		}else if(op=="empty"){
			if(hh<=tt) cout<<"NO"<<endl;
			else cout<<"YES"<<endl;
		}else if(op=="query"){
			cout<<qu[hh]<<endl;
		}
	}
}

2.5 单调栈

常见题型:给出一个序列,找出每个数左边(或右边)满足某个性质最近的数

830. 单调栈

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

输入格式

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

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

输出格式

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

数据范围

1 ≤ N ≤ 1 0 5 1≤N≤10^5 1N105
1 ≤ 数 列 中 元 素 ≤ 1 0 9 1≤数列中元素≤10^9 1109

输入样例:

5
3 4 2 7 5

输出样例:

-1 3 -1 2 2
#include<iostream>
using namespace std;
const int N=100010;
int n;
int stk[N],tt;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
	cin>>n;
    for(int i=0;i<n;i++){
        int x;
        cin>>x;
        while(tt && stk[tt]>=x) tt--;//一直出栈,直到找到第一个比栈顶小的元素
        if(tt) cout<<stk[tt]<<" ";//栈不空,直接输出栈顶
        else cout<<-1<<' ';//不存在
        stk[++tt]=x;//该数入栈
    }
    return 0;
}

时间复杂度 O ( n ) O(n) O(n)

2.6 单调队列

求滑动窗口的最大值最小值

154. 滑动窗口

给定一个大小为 n ≤ 1 0 6 n≤10^6 n106 的数组。

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

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

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

以下是一个例子:

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

窗口位置最小值最大值
[1 3 -1] -3 5 3 6 7-13
1 [3 -1 -3] 5 3 6 7-33
1 3 [-1 -3 5] 3 6 7-35
1 3 -1 [-3 5 3] 6 7-35
1 3 -1 -3 [5 3 6] 736
1 3 -1 -3 5 [3 6 7]37

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

输入格式

输入包含两行。

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

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

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

输出格式

输出包含两个。

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

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

输入样例:

8 3
1 3 -1 -3 5 3 6 7

输出样例:

-1 -3 -3 -3 3 3
3 3 5 5 6 7
#include<bits/stdc++.h>
using namespace std;
const int N=1000010;
int n,k;
int a[N],q[N];
int main(){
	scanf("%d%d",&n,&k);
	for(int i=0;i<n;i++) scanf("%d",&a[i]);
	int hh=0,tt=-1;
	for(int i=0;i<n;i++){
		//判断队头是否已经滑出窗口
		if(hh<=tt&&i-k+1>q[hh]) hh++;
		while(hh<=tt && a[q[tt]]>=a[i]) tt--;//把队列里所有比a[i]大的数都踢掉,它们将永无出头之日
		q[++tt]=i;
		if(i>=k-1) printf("%d ",a[q[hh]]); 
	}
	puts("");
	hh=0,tt=-1;
	for(int i=0;i<n;i++){
		//判断队头是否已经滑出窗口
		if(hh<=tt && i-k+1>q[hh]) hh++;
		while(hh<=tt && a[q[tt]]<=a[i]) tt--;
		q[++tt]=i;
		if(i>=k-1) printf("%d ",a[q[hh]]);
	}
	puts("");
	return 0;
}

2.7 KMP算法

831. KMP字符串

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

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

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

输入格式

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

第二行输入字符串 P P P

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

第四行输入字符串 S S S

输出格式

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

数据范围

1 ≤ N ≤ 1 0 5 1≤N≤10^5 1N105
1 ≤ M ≤ 1 0 6 1≤M≤10^6 1M106

输入样例:

3
aba
5
ababa

输出样例:

0 2

分析:

kmp算法中的next[]数组,若next[i] = j,含义就是:以i为结尾的,长度是j的串和从1开始,长度为j的串相等,即s[1, j] = s[i - j + 1, i]

时间复杂度 O ( n ) O(n) O(n)

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

const int N = 1e6 + 5;
int n, m;
char p[N], s[N];
int ne[N];
int main()
{
    cin >> n >> p + 1 >> m >> s + 1;
    for(int i = 2, j = 0; i <= m; i ++ )
    {
        while(j && p[i] != p[j + 1]) j = ne[j];
        if(p[i] == p[j + 1]) j ++;
        ne[i] = j;
    }
    
    for(int i = 1, j = 0; i <= 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];
        }
    }
}

hdu写法:

#include<bits/stdc++.h>
using namespace std;
const int N=100005;
char a[N],b[N];
int p[N];
int n,m;
void pre(){
	p[1]=0;
	int j=0;
	for(int i=1;i<m;i++){
		while(j>0 && b[j+1]!=b[i+1]) j=p[j];
		if(b[i+1]==b[j+1]) j++;
		p[i+1]=j;
	}
}
void kmp(){
	int ans=0,j=0;
	for(int i=0;i<n;i++){
		while(j>0 && b[j+1]!=a[i+1]) j=p[j];
		if(b[j+1]==a[i+1]) j++;
		if(j==m){
			printf("%d\n",i-m);
			j=p[j];
			//如果不允许重叠,j=0
			ans++;//统计匹配的数量 
		}
	}
}
int main(){
	cin>>a+1>>b+1;
	n=strlen(a+1),m=strlen(b+1);
		
}

2.8 Tire树

用来高效的存储和查找字符串集合的数据结构

题目一定会限制字符的种类

存储方法:如图所示。

最后需要在每一个单词的结尾加上一个标记

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p3pN18Wp-1628778125329)(D:\ACM\ACwing学习\ACwing学习笔记.assets\image-20201031192726489.png)]

查询方法:

①在上图查询单词"abcf"

由根节点a–>b—>c,发现下面没有f,故不存在单词"abcf"

②在上图查询单词"abcd"

由根节点a—>b—>c—>d,虽然存在,但是d字母上没有标记,所以"abcd"也不存在

835. Trie字符串统计

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

  1. I x 向集合中插入一个字符串 x x x
  2. Q x 询问一个字符串在集合中出现了多少次。

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

输入格式

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

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

输出格式

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

每个结果占一行。

数据范围

1 ≤ N ≤ 2 ∗ 1 0 4 1≤N≤2∗10^4 1N2104

输入样例:

5
I abc
Q abc
Q ab
I ab
Q ab

输出样例:

1
0
1
#include<iostream>
using namespace std;
const int N=100010;
int son[N][26],cnt[N],idx;//son存储每个点的子结点,cnt存储当前点为结尾的单词个数,idx指示当前用到的下标
char str[N];
//下标是0的点,既是根结点,又是空结点
void insert(char str[]){
    int p=0;//从根节点开始
    for(int i=0;str[i];i++){
        int u=str[i]-'a';
        if(!son[p][u]) son[p][u]=++idx;//如果p点不存在u这个儿子,创建它
        p=son[p][u];
    }
    //最后p点为该单词的结尾,对其加上标记,次数+1
    cnt[p]++;
}
int query(char str[]){
    int p=0;//从根结点开始
    for(int i=0;str[i];i++){
        int u=str[i]-'a';
        if(!son[p][u]) return 0;
        p=son[p][u];
    }
    return cnt[p];
}
int main(){
    int n;
    scanf("%d",&n);
    while(n--){
        char op[2];
        scanf("%s%s",op,str);
        if(op[0]=='I') insert(str);
        else printf("%d\n",query(str));
    }
    return 0;
}
143. 最大异或对

在给定的 N N N 个整数 A 1 , A 2 … … A N A_1,A_2……A_N A1A2AN 中选出两个进行 x o r xor xor(异或)运算,得到的结果最大是多少?

输入格式

第一行输入一个整数 N N N

第二行输入 N N N 个整数 A 1 A_1 A1 A N A_N AN

输出格式

输出一个整数表示答案。

数据范围

1 ≤ N ≤ 1 0 5 1≤N≤10^5 1N105,

0 ≤ A i < 2 31 0≤A_i<2^{31} 0Ai<231

输入样例:

3
1 2 3

输出样例:

3
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=32*N+5;
int son[N][2],idx;
int a[N];
void insert(int x){
	int p=0;
	for(int i=30;i>=0;i--){
		int u=x>>i &1;//取出整数的第i位 
		if(!son[p][u]) son[p][u]=++idx;
		p=son[p][u];
	}
}
int query(int x){
	int p=0;
	int t=0;
	for(int i=30;i>=0;i--){
		int u=x>>i &1;
		if(son[p][!u]){
			p=son[p][!u];
			t=t*2+!u;
		}else{
			p=son[p][u];
			t=t*2+u;
		}
	}
	return t;
}
int main(){
	int n;
	cin>>n;
	for(int i=0;i<n;i++) cin>>a[i];
	int res=0;
	for(int i=0;i<n;i++){
		insert(a[i]);
		int t=query(a[i]);
		res=max(res,a[i]^t);
	}
	cout<<res<<endl;
	return 0;
}

2.9 并查集

快速地处理以下问题:

1、将两个集合合并

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

对于暴力解法:

belong[x]=a;//x元素属于集合a
//查询是否在同一个集合操作
if(belong[x]==belong[y]) //O(1)
//合并两个集合
//需要一个一个修改每个元素的编号,时间复杂度太高

并查集可以在近乎O(1)的复杂度内解决这些问题

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

问题1:如何判断树根:if(p[x]==x

问题2:如何求x的集合编号:

while(p[x]!=x) x=p[x];

问题3:如何合并两个集合

px是x的集合编号,py是y的集合编号,p[x]=y

836. 合并集合

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

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

  1. M a b,将编号为 a a a b b b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a a a b b b 的两个数是否在同一个集合中;

输入格式

第一行输入整数 n n n m m m

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

输出格式

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

每个结果占一行。

数据范围

1 ≤ n , m ≤ 1 0 5 1≤n,m≤10^5 1n,m105

输入样例:

4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4

输出样例:

Yes
No
Yes
#include<iostream>
using namespace std;
const int N=100010;
int n,m;
int p[N];
int find(int x){//返回x的祖宗结点+路径压缩
	if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) p[i]=i;
    while(m--){
        char op[2];
        int a,b;
        scanf("%s%d%d",op,&a,&b);
        if(op[0]=='M') p[find(a)]=find(b);
        else{
            if(find(a)==find(b)){
                puts("Yes");
            }else puts("No");
        }
    }
    return 0;
}
837. 连通块中点的数量

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

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

  1. C a b,在点 a a a 和点 b b b 之间连一条边, a a a b b b 可能相等;
  2. Q1 a b,询问点 a a a 和点 b b b 是否在同一个连通块中, a a a b b b 可能相等;
  3. Q2 a,询问点 a a a 所在连通块中点的数量;

输入格式

第一行输入整数 n n n m m m

接下来 m m m行,每行包含一个操作指令,指令为 C a bQ1 a bQ2 a 中的一种。

输出格式

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

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

每个结果占一行。

数据范围

1 ≤ n , m ≤ 1 0 5 1≤n,m≤10^5 1n,m105

输入样例:

5 5
C 1 2
Q1 1 2
Q2 1
C 2 5
Q2 5

输出样例:

Yes
2
3
#include<iostream>
using namespace std;
const int N=1e5+5;
int fa[N],Size[N];
void init(){
	for(int i=0;i<N;i++) {
		fa[i]=i;
		Size[i]=1;
	}
}
int find(int x){
	return fa[x]==x?x:fa[x]=find(fa[x]);
}
void un(int a,int b){
	int aa=find(a);
	int bb=find(b);
	if(aa!=bb){
		fa[aa]=bb;
		Size[bb]+=Size[aa];
	}
}
int main(){
	init();
	int n,m;
	scanf("%d%d",&n,&m);
	while(m--){
		char op[5];
		int a,b;
		scanf("%s",op);
		if(op[0]=='C'){
			scanf("%d%d",&a,&b);
			un(a,b);
		}else if(op[1]=='1'){
			scanf("%d%d",&a,&b);
			if(find(a)==find(b)){
				cout<<"Yes\n";
			}else{
				cout<<"No\n";
			}
		}else if(op[1]=='2'){
			scanf("%d",&a);
			cout<<Size[find(a)]<<"\n";
		}
	}
}
240. 食物链

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

A A A B B B B B B C C C C C C A A A

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

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

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

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

第二种说法是 2 X Y,表示 X X X Y Y Y

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

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

  1. 当前的话与前面的某些真的话冲突,就是假话;
  2. 当前的话中 X X X Y Y Y N N N 大,就是假话;
  3. 当前的话表示 X X X X X X,就是假话。

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

输入格式

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

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

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

D = 2 D=2 D=2,则表示 X X X Y Y Y

输出格式

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

数据范围

1 ≤ N ≤ 50000 1≤N≤50000 1N50000,
0 ≤ K ≤ 100000 0≤K≤100000 0K100000

输入样例:

100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5

输出样例:

3

分析:

如何写find函数:

int d[N],p[N];
int find(int x){
    if(p[x]!=x){
		int u=find(p[x]);//找到根节点(最顶头的那个)
        d[x]+=d[p[x]];//d[x]原来是到父节点的距离,find完之后,d[p[x]]存的是p[x]到根节点的距离,所以加上d[p[x]]
        p[x]=u;//x指向根节点
    }
    return p[x];
}

题目分析:

x 和 y 同类的情况
判断 x 和 y 是否是同类

1、首先判断 xy 是否都在同一个 root 节点上
1.1 如果 x 和 y 的父节点(p1, p2)在同一个 root 节点上(说明 p1 和 p2 已经处理过关系了),判断距离是否相等 (d[x] % M == d[y] % M)
1.2 如果p1 != p2,说明 x 和 y 还没有关系,可以进行合并

x和y异类的情况

1、首先判断xy是否都在同一个root节点上

1.1 如果 p1,p2在同一个root节点上,判断距离是否相差1 (d[x]-d[y]-1)%3!=0,如果不等于0,说明是假话

1.2 如果p1!=p2,说明x和y还没有关系,可以进行合并

#include<bits/stdc++.h>
using namespace std;
const int N=50005;
int p[N],d[N];
void init(){
	for(int i=1;i<=N;i++) {
		p[i]=i;
		d[i]=0;
	}
}
int find(int x){
	if(p[x]!=x){
		int u=find(p[x]);
		d[x]+=d[p[x]];
		p[x]=u;
	}
	return p[x];
}
int main(){
	init();
	int n,k;
	scanf("%d%d",&n,&k);
	int res=0;
	while(k--){
		int t,x,y;
		scanf("%d%d%d",&t,&x,&y);
		if(x>n || y>n) res++;
		else{
			int px=find(x),py=find(y);
			if(t==1){
				if(px==py &&(d[x]-d[y])%3!=0) res++;
				else if(px != py){
					p[px]=py;
					d[px]=d[y]-d[x];
					//d[x]+d[px]=d[y]
				}
			}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];
				}
			}
		}
	}
	cout<<res;
}

拓展域写法:

#include<bits/stdc++.h>
using namespace std;
const int N=50005;
int p[N*3];
int find(int x){
	if(p[x]!=x) p[x]=find(p[x]);
	return p[x];
}
void un(int a,int b){
	int aa=find(a);
	int bb=find(b);
	p[aa]=bb;
}
int main(){
	int n,k;
	scanf("%d%d",&n,&k);
	for(int i=1;i<=3*n;i++) p[i]=i;
	int res=0;
	while(k--){
		int d,x,y;
		scanf("%d%d%d",&d,&x,&y);
		if(x>n || y>n || d==2&&x==y) {
			res++;
			continue;
		}
		if(d==1){
			if(find(x)==find(y+n)||find(x)==find(y+2*n)){
				res++;
				continue;
			}
			un(x,y);
			un(x+n,y+n);
			un(x+2*n,y+2*n);
		}else if(d==2){
			if(find(x)==find(y)||find(x+n)==find(y+n)||find(x+2*n)==find(y+2*n)||find(x)==find(y+2*n)){
				res++;
				continue;
			}
			un(x,y+n);
			un(x+n,y+2*n);
			un(x+2*n,y);
		}
	}
	cout<<res;
	return 0;
}

2.10 堆

如何手写一个堆?

手写堆主要解决以下问题:

1、插入一个数

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

2、求集合当中的最小值

heap[1];

3、删除最小值

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

4、删除任意一个元素

heap[k]=heap[size];size--;down(k);up(k);//down和up只会执行一次

5、修改任意一个元素

heap[k]=x;down(k);up(k);

注:下标从1开始比较方便

核心函数:

down操作

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

up操作:

void up(int u){
    while(u/2 &&h[u/2]>h[u]){
        swap(h[u/2],h[u]);
        u/=2;
    }
}
838. 堆排序

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

#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010;
int n,m;
int h[N],Size;
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){
        swap(h[u],h[t]);
        down(t);
    }
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++) scanf("%d",&h[i]);
    Size=n;
    for(int i=n/2;i;i--) down(i);
    while(m--){
        printf("%d ",h[1]);
        h[1]=h[Size];
        Size--;
        down(1);
    }
    return 0;
}
839. 模拟堆

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

  1. I x,插入一个数 x x x
  2. PM,输出当前集合中的最小值;
  3. DM,删除当前集合中的最小值(数据保证此时的最小值唯一);
  4. D k,删除第 k k k 个插入的数;
  5. C k x,修改第 k k k 个插入的数,将其变为 x x x

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

输入格式

第一行包含整数 N N N

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

输出格式

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

每个结果占一行。

数据范围

1 ≤ N ≤ 1 0 5 1≤N≤10^5 1N105
− 1 0 9 ≤ x ≤ 1 0 9 −10^9≤x≤10^9 109x109
数据保证合法。

输入样例:

8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM

输出样例:

-10
6
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int N=100010;
int n;
int h[N],ph[N],hp[N],Size;
/*ph[i]表示第i个插入的数的下标   hp[i]表示下标为i的数是第几个插入的*/
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/2]>h[u]){
      	heap_swap(u/2,u);
        u/=2;
    }
}
int main(){
   	scanf("%d",&n);
    int m=0;
    while(n--){
        char op[10];
        int k,x;
        scanf("%s",op);
        if(!strcmp(op,"I")){
            scanf("%d",&x);
            Size++;
            m++;
            ph[m]=Size;
            hp[Size]=m;
            h[Size]=x;
            up(Size);
        }else if(!strcmp(op,"PM")){
			printf("%d\n",h[1]);
        }else if(!strcmp(op,"DM")){
			heap_swap(1,Size);
            Size--;
            down(1);
        }else if(!strcmp(op,"D")){
            scanf("%d",&k);
            k=ph[k];
            heap_swap(k,Size);
            Size--;
            down(k);up(k);
        }else{
            scanf("%d%d",&k,&x);
            k=ph[k];
            h[k]=x;
            down(k),up(k);
        }
    }
    return 0;
}

2.11 哈希表

哈希表存储结构:①开放寻址法 ②拉链法

字符串哈希方式

例如有哈希函数 h(x),将区间[-1e9,1e9]的数字映射到[0,1e5]中

方法:直接将 x mod 1e5,但是这样会存在哈希冲突**(取模的数尽可能是质数)**

解决哈希冲突的方法:①开放寻址法 ②拉链法

840. 模拟散列表

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

  1. “I x”,插入一个数x;
  2. “Q x”,询问数x是否在集合中出现过;

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

拉链法:

#include<iostream>
#include<cstring>
using namespace std;
const int N=100003;//尽可能取成素数
int h[N],e[N],ne[N],idx;
void insert(int x){
	int t=(x%N+N)%N;//防止出现负数
	e[idx]=x;ne[idx]=h[t];h[t]=idx++;
}
bool find(int x){
	int t=(x%N+N)%N;
	for(int i=h[t];i!=-1;i=ne[i]){
		int u=e[i];
		if(u==x) return 1;
	}
	return 0;
}
int main(){
	int n;
	scanf("%d",&n);
	memset(h,-1,sizeof h);
	while(n--){
		char op[2];
		int x;
		scanf("%s%d",op,&x);
		if(op[0]=='I'){
			insert(x);
		}else{
			if(find(x)){
				puts("Yes");
			}else{
				puts("No");
			}
		}
	}

}

开放寻址法:

#include<iostream>
#include<cstring>
using namespace std;
const int N=100003,null=0x3f3f3f3f;
int h[3*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(){
	int n;
	scanf("%d",&n);
	memset(h,0x3f,sizeof h);
	while(n--){
		char op[2];
		int x;
		scanf("%s%d",op,&x);
		int k=find(x);
		if(op[0]=='I'){
			h[k]=x;
		}else{
			if(h[k]==null) puts("No");
			else puts("Yes");
		}
	}
}

2.12 字符串哈希

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

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

数据范围

1 ≤ n , m ≤ 1 0 5 1≤n,m≤10^5 1n,m105

分析:

字符串前缀哈希法,将字符串变成一个p进制数字,实现不同的字符串映射到不同的数字

对字符串 X 1 X 2 X 3 . . . X n − 1 X n X_1X_2X_3...X_{n-1}X_{n} X1X2X3...Xn1Xn,采用字符的ascii码乘上P的次方来计算哈希值

映射公式: ( X 1 ∗ P n − 1 + X 2 ∗ P n − 2 + . . . + X n − 1 ∗ P 1 + X n ∗ P 0 )   m o d   Q (X_1*P^{n-1}+X_2*{P^{n-2}}+...+X_{n-1}*P^1+X_n*P^0)\ mod \ Q (X1Pn1+X2Pn2+...+Xn1P1+XnP0) mod Q

**注意:**1、任何字符不可映射为0

2、冲突问题,通过巧妙设置P为131或13331,Q= 2 64 2^{64} 264,一般不会产生冲突

比较不同区间的子串是否相同,就转为对应的哈希值是否相同

l-r一段的哈希值公式 h [ r ] − h [ l − 1 ] ∗ P r − l + 1 h[r]-h[l-1]*P^{r-l+1} h[r]h[l1]Prl+1

#include<iostream>
using namespace std;
typedef unsigned long long ull;//ull溢出相当于对2^64取模 
const int N=100010, P=131;//P一般取131或13331
int n,m;
char str[N];
ull h[N],p[N];
ull get(int l,int r){
	//求l~r一段的哈希值公式 
	return h[r]-h[l-1]*p[r-l+1];
} 
int main(){
	scanf("%d%d%s",&n,&m,str+1);
	p[0]=1;
	for(int i=1;i<=n;i++){
		p[i]=p[i-1]*P;//求p的次幂 
		h[i]=h[i-1]*P+str[i];//求前缀哈希值 
	}
	while(m--){
		int l1,r1,l2,r2;
		scanf("%d%d%d%d",&l1,&r1,&l2,&r2);
		if(get(l1,r1)==get(l2,r2)) puts("Yes");
		else puts("No");
	}
	return 0;
}

第三章:搜索与图论

3-1-1 深度优先搜索

DFS和BFS的对比:

数据结构空间
DFSStack O ( h ) O(h) O(h)不具最短性
BFSqueue O ( 2 h ) O(2^h) O(2h)最短路
  1. 排列数字

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

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

#include<bits/stdc++.h>
using namespace std;
const int N=10;
int n;
int path[N];
bool st[N];
void dfs(int u){
    if(u==n){
        for(int i=0;i<n;i++) printf("%d ",path[i]);
        printf("\n");
    	return;
    }
    for(int i=0;i<n;i++){
        if(!st[i]){
            path[u]=i;
            st[i]=1;
            dfs(u+1);
            st[i]=0;
        }
    }
}
int main(){
	cin>>n;
    dfs(0);
}
  1. n-皇后问题

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

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

解法1:全排列思想

#include<bits/stdc++.h>
using namespace std;
const int N=20;
int n;
char mp[N][N];
bool col[N],dg[N],udg[N];
void dfs(int u){
	if(u==n){
		for(int i=0;i<n;i++) puts(mp[i]);
		puts("");
		return;
	}
	for(int i=0;i<n;i++){
		if(!col[i]&&!dg[u+i]&&!udg[n-u+i]){
			mp[u][i]='Q';
			col[i]=dg[u+i]=udg[n-u+i]=1;
            /*u+i  n-u+i可以分别看作x+y=b,y-x=b的截距
            	一条对角线对应一个截距,可以将这个截距映射到数组下标中
            */
			dfs(u+1);
			col[i]=dg[u+i]=udg[n-u+i]=0;
			mp[u][i]='.';
		}
	}
}
int main(){
	scanf("%d",&n);
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			mp[i][j]='.';
		}
	}
	dfs(0);
	return 0;
}

解法2:枚举每一个点,放or不放(更加原始的搜索方式,速度较慢)

#include<bits/stdc++.h>
using namespace std;
const int N=20;
int n;
char mp[N][N];
bool row[N],col[N],dg[N],udg[N];
void dfs(int x,int y,int s){
	if(y==n) y=0,x++;
	if(x==n){
		if(s==n){
			for(int i=0;i<n;i++) puts(mp[i]);
			puts("");
		}
		return;
	}
	//²»·Å»Êºó
	dfs(x,y+1,s);
	//·Å»Êºó
	if(!row[x]&&!col[y]&&!dg[x+y]&&!udg[x-y+n]) {
		mp[x][y]='Q';
		row[x]=col[y]=dg[x+y]=udg[x-y+n]=1;
		dfs(x,y+1,s+1);
		row[x]=col[y]=dg[x+y]=udg[x-y+n]=0;
		mp[x][y]='.';
	}
}
int main(){
	scanf("%d",&n);
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			mp[i][j]='.';
		}
	}
	dfs(0,0,0);
	return 0;
}

3-1-2 宽度优先搜索

当每条边的权重相同时,由于是一圈一圈向外拓展的,所以可以搜到最短路

初始状态入队
while queue不空:
​	t<----队头
​	扩展队头

844.走迷宫

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

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

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

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

#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int N=110;
int n,m;
int g[N][N];//地图
int d[N][N];//距离  
PII q[N*N];//模拟队列
int bfs(){
	int hh=0,tt=0;//hh指向队头元素,tt指向队尾元素 
	q[0]={0,0};
	memset(d,-1,sizeof(d));
	d[0][0]=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>=0&&x<n&&y>=0&&y<m&&g[x][y]==0&&d[x][y]==-1){
				/*注意d[x][y]==-1代表该点没有更新过距离
					也就是没有走过 
				*/
				d[x][y]=d[t.first][t.second]+1;
				q[++tt]={x,y};
			}
		}
	}
	return d[n-1][m-1];
} 
int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++)
		for(int j=0;j<m;j++)
			cin>>g[i][j];
			
	cout<<bfs()<<endl;
	return 0;
}

**拓展:**如果想求出每个点的路径呢?

方法:开一个prev数组,存放每一个点的上一个点

完整代码如下:

#include<bits/stdc++.h>
using namespace std;
typedef pair<int,int> PII;
const int N=110;
int n,m;
int g[N][N];//地图
int d[N][N];//距离  
PII q[N*N];//模拟队列
PII Prev[N][N];
int bfs(){
	int hh=0,tt=0;//hh指向队头元素,tt指向队尾元素 
	q[0]={0,0};
	memset(d,-1,sizeof(d));
	d[0][0]=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>=0&&x<n&&y>=0&&y<m&&g[x][y]==0&&d[x][y]==-1){
				/*注意d[x][y]==-1代表该点没有更新过距离
					也就是没有走过 
				*/
				d[x][y]=d[t.first][t.second]+1;
                //下面是重点!!!
                Prev[x][y]=t;
				q[++tt]={x,y};
			}
		}
	}
    //求路径的过程
    int x=n-1,y=m-1;
    while(x||y){//只要x,y不同时为0,就继续向前转移
		cout<<x<<" "<<y<<endl;
        auto t=Prev[x][y];
        x=t.first,y=t.second;
    }
	return d[n-1][m-1];
} 
int main(){
	cin>>n>>m;
	for(int i=0;i<n;i++)
		for(int j=0;j<m;j++)
			cin>>g[i][j];
			
	cout<<bfs()<<endl;
	return 0;
}

3-1-3 树和图的遍历

树和图的存储:

有向图:1、邻接矩阵 :适合稠密图 2、邻接表

邻接表:

#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=N*2;
int h[N],e[M],ne[M],idx;
//h为每一个结点,e为该条边的权值,ne代表该条边指向的结点的下一条边
/*插入一条由a指向b的边*/
void add(int a,int b){
    //头插法
    e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}
int main(){
    
    
}

树和图的遍历:

bool st[N];
/*
进入dfs函数后,先标记u点已经走过了,然后遍历u的邻接点
*/
void dfs(int u){
    st[u]=1;
    for(int i=h[u];i!=-1;i=ne[i]){
        int j=e[i];
        if(!st[j]) dfs(j);
    }
}

注意不需要回溯,每个点只能被遍历一次,打上标记的点表示已经被遍历过,防止以后重复遍历。

和一般的dfs搜索有所不同

例题:846.树的重心

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

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

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

本题的本质是树的dfs, 每次dfs可以确定以u为重心的最大连通块的节点数,并且更新一下ans。

也就是说,dfs并不直接返回答案,而是在每次更新中迭代一次答案。

这样的套路会经常用到,在 树的dfs 题目中‘

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

例题:847. 图中点的层次

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

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

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

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=N*2;
int n,m;
int h[N],e[M],ne[M],idx;
int dis[N];
void add(int a,int b){
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
int bfs(){
	memset(dis,-1,sizeof(dis));
	int q[N],hh=0,tt=0;
	q[hh]=1;
	dis[1]=0;
	while(hh<=tt){
		int u=q[hh++];
		for(int i=h[u];i!=-1;i=ne[i]){
			int v=e[i];
			if(dis[v]==-1){
				dis[v]=dis[u]+1;
				q[++tt]=v;
			}
		}
	}
	return dis[n];
}
int main(){
	cin>>n>>m;
	memset(h,-1,sizeof(h));
	while(m--){
		int a,b;
		cin>>a>>b;
		add(a,b);
	}
	cout<<bfs();
}

易错点:①head数组未初始化为-1 ②dis数组没有实现两用,初始化为-1,如果为-1代表没有访问过

拓扑排序:

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

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

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

例题:848.有向图的拓扑序列

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

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

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

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int h[N],e[N],ne[N],idx;
int q[N],hh,tt=-1,n,m;
int deg[N];
void add(int a,int b){
	e[idx]=b;
	ne[idx]=h[a];
	h[a]=idx++;
}
bool bfs(){
	for(int i=1;i<=n;i++){
		if(!deg[i]){
			q[++tt]=i;
		}
	}
	while(hh<=tt){
		int t=q[hh++];
		for(int i=h[t];i!=-1;i=ne[i]){
			int j=e[i];
			deg[j]--;
			if(deg[j]==0) q[++tt]=j;
		}
	}
	return tt==n-1;
}
int main(){
	scanf("%d%d",&n,&m);
	memset(h,-1,sizeof(h));
	while(m--){
		int x,y;
		cin>>x>>y;
		deg[y]++;
		add(x,y);
	}
	if(bfs()){
		for(int i=0;i<n;i++){
			cout<<q[i]<<" ";
		}
	}else{
		cout<<-1;
	}
	return 0;
}

3-2 最短路

最短路:单源最短路 :1、所有边权都是正数 ①朴素Dijkstra算法 O(n^2)②堆优化版的Dijkstra算法 O(mlogn)

​ 2、存在负权边 ①Bellman-ford算法 O(nm) ②SPFA 一般O(m),最坏O(nm)

​ 多源汇最短路 Floyd算法 O(n^3)

m和n^2一个级别 稠密图

m和n一个级别 稀疏图

在这里插入图片描述

一、朴素Dijkstra算法(解决稠密图)
  1. Dijkstra求最短路

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

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

98504cdcddd6d683a91c09357395d12.png

#include<bits/stdc++.h>
using namespace std;
const int N=510;
int n,m;
int g[N][N];//采用邻接矩阵存储
int dist[N];
bool st[N];
int dijkstra(){
	memset(dist,0x3f,sizeof dist);
    dist[1]=0;
    for(int i=0;i<n;i++){//n次迭代
        int t=-1;
        for(int j=1;j<=n;j++){
            if(!st[j]&&(t==-1||dist[j]<dist[t])){
                t=j;
            }
        }
        st[t]=1;
        for(int j=1;j<=n;j++){
            dist[j]=min(dist[j],dist[t]+g[t][j]);
        }
    }
    if(dist[n]==0x3f3f3f3f) return -1;
    else return dist[n];
}
int main(){
    scanf("%d%d",&n,&m);
    memset(g,0x3f,sizeof g);
    while(m--){
        int x,y,z;
        scanf("%d%d%d",&x,&y,&z);
        g[x][y]=min(g[x][y],z);
    }
    int t=dijkstra();
    printf("%d\n",t);
}
二、堆优化的dijkstra算法

例题:850.Dijkstra求最短路Ⅱ

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

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

针对稀疏图,我们采用邻接表法

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
typedef pair<int,int> PII;
int h[N],e[2*N],ne[2*N],w[N],idx;
int n,m;
int dist[N];
bool st[N];
void add(int a,int b,int c){
	e[idx]=b;ne[idx]=h[a];w[idx]=c;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;
		if(st[ver]) continue;
		st[ver]=1;
		for(int i=h[ver];i!=-1;i=ne[i]){
			int j=e[i];
			if(dist[j]>distance+w[i]){
				dist[j]=distance+w[i];
				heap.push({dist[j],j});
			}
		}
	}
	if(dist[n]==0x3f3f3f3f) return -1;
	return dist[n];
}
int main(){
	scanf("%d%d",&n,&m);
	memset(h,-1,sizeof h);
	while(m--){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	cout<<dijkstra();
}
三、Bellman-Ford算法(处理有负权边的图)

1、什么是bellman - ford算法?
Bellman - ford算法是求含负权图的单源最短路径的一种算法,效率较低,代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在n-1次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
(通俗的来讲就是:假设1号点到n号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环n-1次操作,若图中不存在负环,则1号点一定会到达n号点,若图中存在负环,则在n-1次松弛后一定还会更新)

2、bellman - ford算法的具体步骤
for n次
for 所有边 a,b,w (松弛操作)
dist[b] = min(dist[b],back[a] + w)

注意:back[]数组是上一次迭代后dist[]数组的备份,由于是每个点同时向外出发,因此需要对dist[]数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点

3、在下面代码中,是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断,而并非是if(dist[n] == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与INF相同数量级的数即可
4、bellman - ford算法擅长解决有边数限制的最短路问题
时间复杂度 O ( n m ) O(nm) O(nm)
其中n为点数,m为边数

例题:853. 有边数限制的最短路

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

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

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

#include<bits/stdc++.h>
using namespace std;
const int N=505,M=10010;
struct Node{
	int a,b,w;
}edge[M];
int dis[N],backup[N];
/*注意:back[]数组是上一次迭代后dist[]数组的备份,由于是每个点同时向外出发,因此需要对dist[]数组进行备份,若不进行备份会因此发生串联效应,影响到下一个点*/
int main(){
	memset(dis,0x3f,sizeof(dis));//初始化
	dis[1]=0;
	int n,m,k;
	scanf("%d%d%d",&n,&m,&k);
	for(int i=0;i<m;i++){
		scanf("%d%d%d",&edge[i].a,&edge[i].b,&edge[i].w);
	}
	for(int i=1;i<=k;i++){
		memcpy(backup,dis,sizeof(dis));//备份上一次迭代的结果
		for(int j=0;j<m;j++){
			int b=edge[j].b;
			int a=edge[j].a;
			int w=edge[j].w;
			dis[b]=min(dis[b],backup[a]+w);
		}
	}
	if(dis[n]>0x3f3f3f3f/2) printf("impossible");
    /*是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断,而并非是if(dist[n] == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与INF相同数量级的数即可*/
	else printf("%d",dis[n]);
}
四、spfa算法(Bellman-Ford算法的队列优化算法)

算法分析

1、什么是spfa算法?

​ SPFA 算法是 Bellman-Ford算法 的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环。SPFA一般情况复杂度是O(m)O(m) 最坏情况下复杂度和朴素 Bellman-Ford 相同,为O(nm)O(nm)。

bellman-ford算法操作如下:

for n次
	for 所有边 a,b,w (松弛操作)
		dist[b] = min(dist[b],back[a] + w)

spfa算法对第二行中所有边进行松弛操作进行了优化,原因是在bellman—ford算法中,即使该点的最短距离尚未更新过,但还是需要用尚未更新过的值去更新其他点,由此可知,该操作是不必要的,我们只需要找到更新过的值去更新其他点即可。

2、spfa算法步骤
queue <– 1
while queue 不为空
(1) t <– 队头
queue.pop()
(2)用 t 更新所有出边 t –> b,权值为w
queue <– b (若该点被更新过,则拿该点更新其他点)

时间复杂度 一般: O ( m ) O(m) O(m) 最坏: O ( n m ) O(nm) O(nm)
n为点数,m为边数

3、spfa也能解决权值为正的图的最短距离问题,且一般情况下比Dijkstra算法还好

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,dis[N];
bool st[N];//代表点是否在队列中
int h[N],e[N],ne[N],w[N],idx;
void add(int a,int b,int c){
	e[idx]=b;ne[idx]=h[a];w[idx]=c;h[a]=idx++;
}
int spfa(){
	queue<int> q;
	memset(dis,0x3f,sizeof dis);
	q.push(1);
	dis[1]=0;
	st[1]=1;
	while(!q.empty()){
		int u=q.front();
		q.pop();
		st[u]=0;
		for(int i=h[u];i!=-1;i=ne[i]){
			int v=e[i];
			if(dis[v]>dis[u]+w[i]){
				dis[v]=dis[u]+w[i];
				if(!st[v]){
					q.push(v);
					st[v]=1;
				}
			}
		}
	}
	if(dis[n]==0x3f3f3f3f) return -1;
	else return dis[n];
}
int main(){
	memset(h,-1,sizeof h);
	scanf("%d%d",&n,&m);
	while(m--){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	int t=spfa();
	if(t==-1) puts("impossible");
	else printf("%d",t);
}
spfa求负环

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

请你判断图中是否存在负权回路。

#include<bits/stdc++.h>
using namespace std;
const int N=2005,M=10005;
int h[N],e[M],ne[M],w[M],idx,n,m;
int dis[N],cnt[N];//需要额外一个cnt数组,记录当前点最短路上的边数
bool st[N];
void add(int a,int b,int c){
	e[idx]=b;w[idx]=c;ne[idx]=h[a];h[a]=idx++;
}
bool spfa(){
    /*首先距离不需要初始化*/
	queue<int> q;
    /*将所有点入队,可以防止有的点不能走到负环*/
	for(int i=1;i<=n;i++){
		q.push(i);
		st[i]=1;
	}
	while(!q.empty()){
		int u=q.front();
		q.pop();
		st[u]=0;
		for(int i=h[u];i!=-1;i=ne[i]){
			int v=e[i];
			if(dis[v]>dis[u]+w[i]){
				dis[v]=dis[u]+w[i];
				cnt[v]=cnt[u]+1;
				if(cnt[v]>=n) return 1;
                /*如果超过n,根据抽屉原理,中间经过的点数一定大于n,*/
				if(!st[v]){
					q.push(v);
					st[v]=1;
				}
			}
		}
	}
	return 0;
}
int main(){
	scanf("%d%d",&n,&m);
	memset(h,-1,sizeof h);
	for(int i=0;i<m;i++){
		int x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		add(x,y,z);
	}
	if(spfa()) puts("Yes");
	else puts("No");
}
五、Floyd算法(多源最短路)

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

再给定k个询问,每个询问包含两个整数x和y,表示查询从点x到点y的最短距离,如果路径不存在,则输出“impossible”。

数据保证图中不存在负权回路。

#include<bits/stdc++.h>
using namespace std;
const int N=210;
const int inf=0x3f3f3f3f;
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 x,y,z;
		scanf("%d%d%d",&x,&y,&z);
		d[x][y]=min(d[x][y],z);//注意重边
	}
	floyd();
	while(q--){
		int x,y;
		scanf("%d%d",&x,&y);
		if(d[x][y]>inf/2) puts("impossible");//题目可能会出现负权边,所以还要应用之前的套路
		else printf("%d\n",d[x][y]);
	}
	return 0;
}

3-3-1 最小生成树

本节导航:

在这里插入图片描述

**Prim算法:**①朴素Prim算法(稠密图常用) O ( n 2 ) O(n^2) O(n2)②堆优化版Prim(不常用) O ( m l o g n ) O(mlogn) O(mlogn)

**Kruskal算法:**适用于稀疏图, O ( m l o g m ) O(mlogm) O(mlogm)

一、朴素Prim算法

算法思想:

dist[i]<----inf
for(i=0;i<n;i++){
	t<----找到集合外距集合最近的点
	用t更新其他点到集合的距离
	st[t]=true
}

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

求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。

给定一张边带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。

由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树。

/*
S:当前已经在联通块中的所有点的集合
1. dist[i] = inf
2. for n 次
    t<-S外离S最近的点
    利用t更新S外点到S的距离
    st[t] = true
n次迭代之后所有点都已加入到S中
联系:Dijkstra算法是更新到起始点的距离,Prim是更新到集合S的距离
*/
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510, INF = 0x3f3f3f3f;

int n, m;
int g[N][N], dist[N];
//邻接矩阵存储所有边
//dist存储其他点到S的距离
bool st[N];

int prim() {
    //如果图不连通返回INF, 否则返回res
    memset(dist, INF, sizeof dist);
    int res = 0;

    for(int i = 0; i < n; i++) {
        int t = -1;
        for(int j = 1; j <= n; j++) 
            if(!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        //寻找离集合S最近的点        
        if(i && dist[t] == INF) return INF;
        //判断是否连通,有无最小生成树

        if(i) res += dist[t];
        //cout << i << ' ' << res << endl;
        st[t] = true;
        //更新最新S的权值和

        for(int j = 1; j <= n; j++) dist[j] = min(dist[j], g[t][j]);
    }

    return res;
}

int main() {
    cin >> n >> m;
    int u, v, w;

    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
            if(i ==j) g[i][j] = 0;
            else g[i][j] = INF;

    while(m--) {
        cin >> u >> v >> w;
        g[u][v] = g[v][u] = min(g[u][v], w);
    }
    int t = prim();
    //临时存储防止执行两次函数导致最后仅返回0
    if(t == INF) puts("impossible");
    else cout << t << endl;
}

二、Kruskal算法
算法思路:①将所有边按权重从小到大排序

②枚举每条边 a,b,权重是c

if a,b不连通 

​	将这条边加入集合

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

求最小生成树的树边权重之和,如果最小生成树不存在则输出impossible。

给定一张边带权的无向图G=(V, E),其中V表示图中点的集合,E表示图中边的集合,n=|V|,m=|E|。

由V中的全部n个顶点和E中n-1条边构成的无向连通子图被称为G的一棵生成树,其中边的权值之和最小的生成树被称为无向图G的最小生成树。

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m;
struct Edge{
	int u,v,w;
	bool operator<(const Edge &a) const{
		return w<a.w;
	}
}edge[N];
int p[N];
int find(int x){
	return p[x]==x?x:p[x]=find(p[x]);
}
int main(){
	int n,m;
	scanf("%d%d",&n,&m);
	for(int i=0;i<m;i++){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		edge[i]={u,v,w};
	}
	sort(edge,edge+m);
	for(int i=1;i<=n;i++) p[i]=i;
	int cnt=0,sum=0;
	for(int i=0;i<m;i++){
		int a=edge[i].u,b=edge[i].v,w=edge[i].w;
		a=find(a);
		b=find(b);
		if(a!=b){
			cnt++;
			sum+=w;
			p[a]=b;
		}
	}
	if(cnt<n-1) puts("impossible");
	else printf("%d",sum);
}

3-3-2 二分图

重要结论:一个图是二分图,当且仅当图中不含有奇数环

一、染色法判断二分图
  1. 染色法判定二分图

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

请你判断这个图是否是二分图。

#include<bits/stdc++.h>
using namespace std;
const int N=100010,M=200010;
int n,m;
int h[N],e[M],ne[M],idx;
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]){
            if(!dfs(j,3-c))  return 0;
        }else if(color[j]==c) return 0;
    }
    return 1;
}
int main(){
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof h);
    while(m--){
        int a,b;
        scanf("%d%d",&a,&b);
        add(a,b);
        add(b,a);
    }
    bool flag=1;
    for(int i=1;i<=n;i++){
        if(!color[i]){
            if(!dfs(i,1)){
                flag=0;
                break;
            }
        }
    }
    if(flag) puts("Yes");
    else puts("No");
}

二、匈牙利算法

第四章:数学知识

4-1-1 质数
1、试除法判定素数

给定n个正整数ai,判定每个数是否是质数。

分析:如果 d d d n n n的因子,那么 n / d n/d n/d也是 n n n的因子,故从 1 1 1 n n n的枚举可以缩减到1到1到 n \sqrt{n} n

d < = n d d<=\frac{n}{d} d<=dn所以 d < = n d<=\sqrt{n} d<=n

【注解】不推荐 i ∗ i < = n i*i<=n ii<=n i < = s q r t ( n ) i<=sqrt(n) i<=sqrt(n)的写法

时间复杂度 n \sqrt{n} n

#include<bits/stdc++.h>
using namespace std;
bool is_prime(int n){
	if(n<2) return false;
	for(int i=2;i<=n/i;i++){
		if(n%i==0)
			return false;
	}
	return true;
}
int main(){
	int n;
	scanf("%d",&n);
	while(n--){
		int x;
		scanf("%d",&x);
		if(is_prime(x)) puts("Yes");
		else puts("No");
	}
}
2、分解质因数——试除法

给定n个正整数 a i a_i ai,将每个数分解质因数,并按照质因数从小到大的顺序输出每个质因数的底数和指数。

另外有性质,n中最多只包含一个大于sqrt(n)的质因子,所以我们只需要枚举 [ 2 , n ] [2,\sqrt{n}] [2,n ]的质因子,然后特判一下n最后是否大于1就可以了

时间复杂度 [ log ⁡ n , n ] [\log{n},\sqrt{n}] [logn,n ]

#include<bits/stdc++.h>
using namespace std;
void divide(int n){
	for(int i=2;i<=n/i;i++){
		if(n%i==0){//i一定是质数,因为此时2到i-1的质因子已经被除干净了 
			int s=0;//计算次数 
			while(n%i==0){
				n/=i;
				s++;
			}
			printf("%d %d\n",i,s);
		}
	}
	if(n>1) printf("%d %d\n",n,1);
	cout<<endl;
	
}
int main(){
	int n;
	scanf("%d",&n);
	while(n--){
		int x;
		scanf("%d",&x);
		divide(x);
	}
}
3、朴素筛法求素数
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1000010;
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]=1;
		}
	}
}
int main(){
	int n;
	scanf("%d",&n);
	get_primes(n);
	cout<<cnt;
}
4、线性筛
#include<iostream>
#include<algorithm>
using namespace std;
const int N=1000010;
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++){
			st[primes[j]*i]=1;
			if(i%primes[j]==0) break;
		}
	}
}
int main(){
	int n;
	scanf("%d",&n);
	get_primes(n);
	cout<<cnt;
}
4-1-2 约数
1、试除法求约数

给定n个正整数 a i a_i ai,对于每个整数 a i a_i ai,请你按照从小到大的顺序输出它的所有约数。

#include<bits/stdc++.h>
using namespace std;
vector<int> get_divisors(int n){
	vector<int> res;
	for(int i=1;i<=n/i;i++){
		if(n%i==0){
			res.push_back(i);
			if(i!=n/i) res.push_back(n/i);
		}
	}
	sort(res.begin(),res.end());
	return res;
} 
int main(){
	int n;
	cin>>n;
	while(n--){
		int x;
		cin>>x;
		auto res=get_divisors(x);
		for(auto i:res){
			cout<<i<<" ";
		}
		cout<<endl;
	}
}
2、约数个数

由算术基本定理可知,任何一个大于1的自然数 N N N,如果 N N N不为质数,那么 N N N可以唯一分解成有限个质数的乘积 N = p 1 α 1 p 2 α 2 . . . p k α k N=p_1^{\alpha _1}p_2^{\alpha _2}...p_k^{\alpha _k} N=p1α1p2α2...pkαk,N的约数个数为 ( α 1 + 1 ) ( α 2 + 1 ) . . . ( α k + 1 ) (\alpha _1+1)(\alpha _2+1)...(\alpha _k+1) (α1+1)(α2+1)...(αk+1)

证明:任何一个约数 d d d可以表示成$p_1^{\beta _1}p_2^{\beta _2}…p_k^{\beta _k},0<=\beta_i<=\alpha _i $,

每一项的 β i \beta_i βi如果不同,那么约数 d d d就不同(根据算术基本定理可知,每个数的因式分解是唯一的)

所以 n n n的约数和 β i \beta_i βi的选法是一一对应的

β 1 \beta_1 β1 [ 0 , α 1 ] [0,\alpha_1] [0,α1]种选法

β 2 \beta_2 β2 [ 0 , α 2 ] [0,\alpha_2] [0,α2]种选法

β k \beta_k βk [ 0 , α k ] [0,\alpha_k] [0,αk]种选法

根据乘法原理,最终总的约数个数为$p_1^{\beta _1}p_2^{\beta _2}…p_k^{\beta _k},0<=\beta_i<=\alpha _i $

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
typedef long long ll;

int main(){
	int n;
	scanf("%d",&n);
	ll ans=1;
	unordered_map<int,int> hash;
	while(n--){
		int x;
		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;
}
3、约数之和

公式: ( p 1 0 + p 1 1 + . . . + p 1 α 1 ) . . . ( p k 0 + p k 1 + . . . + p k α k ) (p_1^{0}+p1^{1}+...+p_1^{\alpha_1})...(p_k^{0}+pk^{1}+...+p_k^{\alpha_k}) (p10+p11+...+p1α1)...(pk0+pk1+...+pkαk)

其中每一个小的多项式 ( p k 0 + p k 1 + . . . + p k α k ) (p_k^{0}+pk^{1}+...+p_k^{\alpha_k}) (pk0+pk1+...+pkαk)我们用秦九韶算法

例题:

给定n个正整数 a i a_i ai,请你输出这些数的乘积的约数之和,答案对 1 0 9 + 7 10^9+7 109+7取模。

#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
typedef long long ll;
int main(){
	int n;
	scanf("%d",&n);
	ll ans=1;
	unordered_map<int,int> hash;//记录素数及其个数
	while(n--){
		int x;
		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) {
		ll p=i.first,t=i.second;
		ll tmp=1;
		while(t--) tmp=(tmp*p+1)%mod;
		ans=ans*tmp%mod;
 	}
	cout<<ans%mod;
}
4、欧几里得算法(辗转相除法)

( a , b ) = ( b , a % b ) (a,b)=(b,a\%b) (a,b)=(b,a%b)

证明:

a % b = a − k ∗ b a\%b=a-k*b a%b=akb,其中 k = ⌊ a / b ⌋ k=\left\lfloor a/b\right\rfloor k=a/b

d d d ( a , b ) (a,b) (a,b)的公约数,则 d ∣ a d|a da d ∣ b d|b db,则易知 d ∣ ( a − k ∗ b ) d|(a-k*b) d(akb),故 d d d也是 ( b , a % b ) (b,a\%b) (b,a%b)的公约数

d d d ( b , a % b ) (b,a\%b) (b,a%b)的公约数,则 d ∣ b d|b db d ∣ ( a − k ∗ b ) d|(a-k*b) d(akb),则 d ∣ ( a − k ∗ b + k ∗ b ) = d ∣ a d|(a-k*b+k*b)=d|a d(akb+kb)=da,故 d d d也是 ( a , b ) (a,b) (a,b)的公约数。

因此 ( a , b ) (a,b) (a,b)的公约数集合和 ( b , a % b ) (b,a\%b) (b,a%b)的公约数集合相同,所以它们的最大公约数也一定相同

证毕#

由上述证明可知,我们只需要一步步递归下去知道b==0即可,0和任何数的最大公约数都等于这个数本身

#include<bits/stdc++.h>
using namespace std;
int Gcd(int a,int b){
	return b?Gcd(b,a%b):a;
}
int main(){
	int n;
	cin>>n;
	while(n--){
		int a,b;
		cin>>a>>b;
		cout<<Gcd(a,b)<<endl;
	} 
}
4-2-1 欧拉函数

给定n个正整数 a i a_i ai,请你求出每个数的欧拉函数。

欧拉函数的定义:

1-N中与N互质的数的个数称为欧拉函数,记为 ϕ ( N ) \phi(N) ϕ(N)

若在算术基本定理中, N = p 1 a 1 p 2 a 2 . . . p m a m N=p_1^{a_1}p_2^{a_2}...p_m^{a_m} N=p1a1p2a2...pmam

ϕ ( N ) = N ∗ p 1 − 1 p 1 ∗ p 2 − 1 p 2 ∗ . . . ∗ p m − 1 p m \phi(N)=N*\frac{p_1-1}{p_1}*\frac{p_2-1}{p_2}*...*\frac{p_m-1}{p_m} ϕ(N)=Np1p11p2p21...pmpm1

#include<bits/stdc++.h>
using namespace std;
int solve(int 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);//先除后乘
	return res;
}
int main(){
	int n;
	scanf("%d",&n);
	while(n--){
		int x;
		cin>>x;
		cout<<solve(x)<<"\n";
	}
}
4-2-2 筛法求欧拉函数

O ( N ) O(N) O(N)的复杂度计算从1到n每个数的欧拉函数

在线性筛法中顺便求欧拉函数

特殊规定phi[1]=1

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+10;
int primes[N],cnt;
bool st[N];
int phi[N];
ll get_euler(int n){
	phi[1]=1;
	for(int i=2;i<=n;i++){
		if(!st[i]){
			primes[cnt++]=i;
			phi[i]=i-1;//从1到n中和素数互质的除了自身都是
		}
		for(int j=0;primes[j]<=n/i;j++){
			int t=primes[j]*i;
			st[t]=1;
			if(i%primes[j]==0){
				phi[t]=phi[i]*primes[j];//primes[j]是i的一个质因子 
				break;
			}
			phi[t]=phi[i]*(primes[j]-1);//primes[j]不是i的质因子时 
		} 
	}
	ll res=0;
	for(int i=1;i<=n;i++) res+=phi[i];
	return res;
}
int main(){
	int n;
	cin>>n;
	cout<<get_euler(n)<<endl;
	return 0;
}
欧拉定理与费马定理:

a a a n n n互质,则 a ϕ ( n ) ≡ 1 ( m o d   n ) a^{\phi{(n)}}\equiv1 (mod\ n) aϕ(n)1(mod n)

当n取质数时, ϕ ( n ) = n − 1 \phi(n)=n-1 ϕ(n)=n1

故上式可变为 a n − 1 ≡ 1 ( m o d   n ) a^{n-1}\equiv1(mod\ n) an11(mod n),被称为费马定理

4-2-3 快速幂

l o g k logk logk的时间复杂度内求 a k m o d   p a^k mod\ p akmod p

方法:反复平方法

例如:求 4 5 4^5 45

我们先预处理出来 4 2 0 4^{2^0} 420 4 2 1 4^{2^1} 421 4 2 2 4^{2^2} 422的值

4 5 4^5 45= 4 ( 101 ) 2 = 4 2 0 + 2 2 = 4 2 0 ∗ 4 2 2 4^{(101)_2}=4^{2^0+2^2}=4^{2^0}*4^{2^2} 4(101)2=420+22=420422

查表,可以算出最终结果

#include<bits/stdc++.h>
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的末位是不是1,是1则乘a
		k>>=1;//k右移
		a=(ll)a*a%p;//把a变成下一个,平方一下
	}
	return res;
}
int main(){
	int n;
	scanf("%d",&n);
	while(n--){
		int a,k,p;
		scanf("%d%d%d",&a,&k,&p);
		printf("%d\n",qmi(a,k,p));
	}
	return 0;
}
4-2-4 快速幂求逆元
乘法逆元的定义

若整数 b b b m m m互质,并且对于任意的整数 a a a,如果满足 b ∣ a b|a ba,则存在一个整数 x x x,使得 a / b ≡ a ∗ x ( m o d m ) a/b≡a∗x(mod m) a/bax(modm),则称 x x x b b b的模 m m m乘法逆元,记为 b − 1 ( m o d   m ) b^{−1}(mod\ m) b1(mod m)
b b b存在乘法逆元的充要条件是** b b b与模数 m m m互质**。当模数 m m m为质数时, b m − 2 b^{m−2} bm2即为 b b b的乘法逆元。

当n为质数时,可以用快速幂求逆元:
a / b ≡ a * x (mod n)
两边同乘b可得 a ≡ a * b * x (mod n)
即 1 ≡ b * x (mod n)
同 b * x ≡ 1 (mod n)
由费马小定理可知,当n为质数时
b ^ (n - 1) ≡ 1 (mod n)
拆一个b出来可得 b * b ^ (n - 2) ≡ 1 (mod n)
故当n为质数时,b的乘法逆元 x = b ^ (n - 2)

注意充要条件: b b b与模数 m m m互质。当模数 m m m为质数时!

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll qmi(int a,int k,int p){
	ll res=1;
	while(k){
		if(k&1) res=(ll)res*a%p;
		k>>=1;
		a=(ll)a*a%p;
	}
	return res;
}
int main(){
	int n;
	scanf("%d",&n);
	while(n--){
		int a,p;
		scanf("%d%d",&a,&p);
		int t=qmi(a,p-2,p);
		if(a%p) cout<<t<<endl;//这里必须a和p是互质的,否则a%p等于0,不可能等于1
		else puts("impossible");
	}
}
4-2-5 扩展欧几里得算法

裴蜀定理:对任意正整数a,b,一定存在两个整数x,y,使得ax+by==gcd(a,b)

扩展欧几里得算法用于解决上面的问题

#include<bits/stdc++.h>
using namespace std;
int exgcd(int a,int b,int &x,int &y){//拓展欧几里得 
	if(!b){
		x=1,y=0;
		return a;
	}
	int d=exgcd(b,a%b,y,x);//注意x和y交换一下位置
	y=y-a/b*x;
	return d;//返回最大公约数 
}
int main(){
	int n;
	cin>>n;
	while(n--){
		int a,b,x,y;
		cin>>a>>b;
		exgcd(a,b,x,y);//用x,y的引用带回
		printf("%d %d\n",x,y); 
	}
}

注意:求得的解不唯一

4-2-6 线性同余方程(拓展欧几里得的应用)

给定 n n n组数据 a i , b i , m i a_i,b_i,m_i ai,bi,mi,对于每组数求出一个 x i x_i xi,使其满足 a i ∗ x i ≡ b i ( m o d   m i ) a_i∗x_i≡b_i(mod\ m_i) aixibi(mod mi),如果无解则输出impossible。

分析:

原式可化为存在整数 y y y,使得 a x = m y + b ax=my+b ax=my+b

移项得 a x − m y = b ax-my=b axmy=b

y ′ = − y y'=-y y=y,则 a x + m y ′ = b ax+my'=b ax+my=b,我们可以先按照拓展欧几里得算法求出 a x + m y ′ = d ax+my'=d ax+my=d,其中 d = ( a , b ) d=(a,b) d=(a,b)的解

如果 b b b d d d倍数,方程两边同时乘以 b / d b/d b/d得到的就是解,否则无解

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int exgcd(int a,int b,int &x,int &y){//拓展欧几里得 
	if(!b){
		x=1,y=0;
		return a;
	}
	int d=exgcd(b,a%b,y,x);//注意x和y交换一下位置
	
	y=y-a/b*x;
	return d;//返回最大公约数 
}
int main(){
	int n;
	cin>>n;
	while(n--){
		int a,b,m;
		cin>>a>>b>>m;
		int x,y;
		int d=exgcd(a,m,x,y);
		if(b%d==0){
			printf("%lld\n",(ll)x*(b/d)%m);//最后答案要对m取模,因为(a*x) % m = (a * (x % m)) % m
		}else{
			puts("impossible");
		}
	}
}
4-2-7 中国剩余定理

给定 2 n 2n 2n个整数 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an m 1 , m 2 , . . . , m n m_1,m_2,...,m_n m1,m2,...,mn,求一个最小的非负整数 x x x,满足对任意 i i i属于 [ 1 , n ] [1,n] [1,n] x ≡ m i ( m o d   a i ) x\equiv m_i(mod\ a_i) xmi(mod ai)

在这里插入图片描述

最后的 m 0 m_0 m0就是答案

我们在每次循环中不断更新 a 1 a_1 a1 m 1 m_1 m1的值,每来一个式子就进行一遍上面的操作,相当于合并

#include<bits/stdc++.h>
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 d=exgcd(b,a%b,y,x);
	y=y-a/b*x;
	return d;
}
int main(){
	int n;
	cin>>n;
	ll a1,m1;
	cin>>a1>>m1;
	bool flag=1;
	for(int i=0;i<n-1;i++){
		ll a2,m2,k1,k2;
		cin>>a2>>m2;
		ll d=exgcd(a1,-a2,k1,k2);
		if((m2-m1)%d){
			flag=0;
			break;
		}
		k1=(k1*(m2-m1)/d)%abs(a2/d);
		m1=k1*a1+m1;
		a1=abs(a1/d*a2);//a1*a2/d是最小公倍数
	}	
	if(flag) cout<<(m1 % a1 +a1) %a1;//取余的技巧,为了得到一个正数
	else cout<<-1;
}
4-3-1 高斯消元
1、高斯消元解线性方程组

4-4-1 容斥原理

第五章:动态规划

dp问题时间复杂度计算:状态数量*计算每一个状态需要的时间

5-1 背包问题

1、01背包(每件物品最多只用一次)

在这里插入图片描述

代码1:朴素写法

#include<iostream>
#include<algorithm>
using namespace std;
const int N=1010;
int v[N],w[N],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=0;j<=m;j++){
			f[i][j]=f[i-1][j];
			if(j>=v[i]) f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);
		}
	}
	cout<<f[n][m];
	return 0;
}

代码2:优化版

  1. f[i] 仅用到了f[i-1]层,
  2. j与j-v[i] 均不大于j
  3. 若用到上一层的状态时,从大到小枚举, 反之从小到大哦
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int v[N],w[N],f[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=m;j>=v[i];j--){
			f[j]=max(f[j],f[j-v[i]]+w[i]);
		}
	}
	cout<<f[m];
	return 0;
}
2、完全背包(每件物品有无限个)

状态表示: f ( i , j ) f(i,j) f(i,j)集合表示:只考虑前 i i i个物品,且总体积不大于 j j j的所有选法

​ 属性:最大值

状态计算:集合的划分

f ( i , j ) f(i,j) f(i,j)可以划分成:第 i i i个物品选0个,选1个,…,选 k k k

f ( i , j ) = M a x ( f i − 1 , j − v [ i ] ∗ k + k ∗ w i ) f(i,j)=Max(f_{i-1,j-v[i]*k}+k*w_i) f(i,j)=Max(fi1,jv[i]k+kwi)

(曲线救国的思想,第i个物品的不好求,先将第 i i i个物品除去,再加上第 i i i个物品的价值)

代码1:朴素写法

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

优化:
在这里插入图片描述

最终, f ( i , j ) = M a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − v ] + w ) f(i,j)=Max(f[i-1][j],f[i][j-v]+w) f(i,j)=Max(f[i1][j],f[i][jv]+w)

代码2:优化版

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int f[N],v[N],w[N],n,m;
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
	for(int i=1;i<=n;i++){
		for(int j=v[i];j<=m;j++){
			f[j]=max(f[j],f[j-v[i]]+w[i]);
		}
	}
	cout<<f[m];
}
3、多重背包(每件物品 s i s_i si个)

状态表示: f ( i , j ) f(i,j) f(i,j)集合表示:只考虑前 i i i个物品,且总体积不大于 j j j的所有选法

​ 属性:最大值

状态计算:集合的划分

f ( i , j ) f(i,j) f(i,j)可以划分成:第 i i i个物品选0个,选1个,…,选 k k k

f ( i , j ) = M a x ( f i − 1 , j − v [ i ] ∗ k + k ∗ w i ) f(i,j)=Max(f_{i-1,j-v[i]*k}+k*w_i) f(i,j)=Max(fi1,jv[i]k+kwi)

朴素写法:

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

下面讨论优化问题,这里给出一种常见优化方法:二进制优化

#include<bits/stdc++.h>
using namespace std;
const int N=20050,M=2010;
int n,m;
int f[N];
int v[N],w[N];
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;
			k*=2;
		}
		if(s>0){
			cnt++;
			v[cnt]=a*s;
			w[cnt]=b*s;
		}
	}
	n=cnt;
	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];
}
4、分组背包( N N N组,每一组里有若干个)

状态表示:集合:只从前 i i i个物品中选,且总体积不大于 j j j的所有宣发

​ 属性: M a x Max Max

在这里插入图片描述

f [ i , j ] = M a x ( f [ i − 1 , j ] , f [ i − 1 , j − v [ i , k ] ] + w [ i , k ] ) f[i,j]=Max(f[i-1,j],f[i-1,j-v[i,k]]+w[i,k]) f[i,j]=Max(f[i1,j],f[i1,jv[i,k]]+w[i,k])

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

5-2-1 线性dp

1、数字三角形

给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5

分析:

状态表示: f [ i , j ] f[i,j] f[i,j]:集合:所有从起点,走到 ( i , j ) (i,j) (i,j)的路径

​ 属性: M a x Max Max

状态计算:在这里插入图片描述

集合可以划分成:来自左上+来自右上,二者取最大值即为答案

import java.util.*;
public class Acwing898 {
	static int N=510;
	static int[][] a=new int[N][N];
	static int[][] f=new int[N][N];
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner in=new Scanner(System.in);
		int n=in.nextInt();
		for(int i=1;i<=n;i++) {
			for(int j=1;j<=i;j++) {
				a[i][j]=in.nextInt();
			}
		}
		for(int i=1;i<=n;i++) {
			for(int j=0;j<=i+1;j++) {//每行要多初始化两个
				f[i][j]=Integer.MIN_VALUE;
			}
		}
		f[1][1]=a[1][1];
		for(int i=2;i<=n;i++) {
			for(int j=1;j<=i;j++) {
				f[i][j]=Math.max(f[i-1][j-1], f[i-1][j])+a[i][j];
			}
		}
		int res=Integer.MIN_VALUE;
		for(int i=1;i<=n;i++) {
			res=Math.max(res, f[n][i]);
		}
		System.out.println(res);
	}

}

代码中要注意初始化问题,每行的第一个元素和最后一个元素的 f [ i , j ] f[i,j] f[i,j]都用到了空区域,所以要特别关注代码中的初始化问题

另解:从下往上递推

#include<bits/stdc++.h>
using namespace std;
const int N=505;
int a[N][N],f[N][N];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=i;j++){
			cin>>a[i][j];
		}
	}
	for(int i=1;i<=n;i++){
		f[n][i]=a[n][i];
	}
	for(int i=n-1;i>=1;i--){
		for(int j=1;j<=i;j++){
			f[i][j]=max(f[i+1][j],f[i+1][j+1])+a[i][j];
		}
	}
	cout<<f[1][1];
}
2、最长上升子序列

给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。

状态表示: f [ i ] f[i] f[i],集合:所有以第 i i i个数结尾的上升子序列

​ 属性:这些上升子序列长度的最大值

状态计算:

在这里插入图片描述

划分的依据是a[i]前一个元素选谁

时间复杂度:n*n O ( n 2 ) O(n^2) O(n2)

import java.util.*;
public class ACwing895 {
	static int N=1000;
	static int[] a=new int[N];
	static int[] f=new int[N];
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner in=new Scanner(System.in);
		int n=in.nextInt();
		for(int i=1;i<=n;i++) a[i]=in.nextInt();
		for(int i=1;i<=n;i++) {
			f[i]=1;
			for(int j=1;j<i;j++) {
				if(a[j]<a[i]) f[i]=Math.max(f[i], f[j]+1);
			}
		}
		int res=Integer.MIN_VALUE;
		for(int i=1;i<=n;i++) res=Math.max(res, f[i]);
		System.out.println(res);
	}

}

拓展:如何将序列输出出来

开一个数组g,存放每一个元素是由谁转移过来的,g数组中的值一定是在最大长度子序列的前提下保存的前驱元素的值

完整代码如下:

import java.util.*;
public class ACwing895 {
	static int N=1000;
	static int[] a=new int[N];
	static int[] f=new int[N];
	static int[] g=new int[N];
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner in=new Scanner(System.in);
		int n=in.nextInt();
		for(int i=1;i<=n;i++) a[i]=in.nextInt();
		for(int i=1;i<=n;i++) {
			f[i]=1;
			for(int j=1;j<i;j++) {
				if(a[j]<a[i]) {
					if(f[j]+1>f[i]) {
						f[i]=f[j]+1;
						g[i]=j;//标记i这个位置是由j位置转移过来的
					}
				}
			}
		}
		int res=Integer.MIN_VALUE;
		int k=0;//用k记录以答案是以k为结尾的最长子序列
		for(int i=1;i<=n;i++) {
			if(f[i]>res) {
				res=f[i];
				k=i;
			}
		}
		System.out.println(res);
		int len=f[k];
		for(int i=0;i<len;i++) {
			System.out.print(a[k]+" ");
			k=g[k];
		}
		//but,这样输出的次序是逆序的
	}

}

优化版本(见习题课)
3、最长公共子序列

给定两个长度分别为N和M的字符串A和B,求既是A的子序列又是B的子序列的字符串长度最长是多少。

分析:

状态表示 f [ i , j ] f[i,j] f[i,j]:集合:所有在第一个序列的前i个字母中出现,且在第二个序列的前j个字母中出现的子序列

​ 属性:公共子序列长度最大值

状态计算: f [ i , j ] f[i,j] f[i,j]可以分为四种情况:00 01 10 11(0代表不选,1代表选)

00: f [ i − 1 , j − 1 ] f[i-1,j-1] f[i1,j1]

01:不完全等价于 f [ i − 1 , j ] f[i-1,j] f[i1,j],但是 f [ i − 1 , j ] f[i-1,j] f[i1,j]中包含了01这种情况

10:不完全等价于 f [ i , j − 1 ] f[i,j-1] f[i,j1],但是它包含了10这种情况

11:当 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]时, f [ i − 1 , j − 1 ] + 1 f[i-1,j-1]+1 f[i1,j1]+1

在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m,f[N][N];
char a[N],b[N];
int main(){
	cin>>n>>m;
	cin>>a+1>>b+1;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			f[i][j]=max(f[i-1][j],f[i][j-1]);
			if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
		}
	}
	cout<<f[n][m];
}
4、最短编辑距离

给定两个字符串A和B,现在要将A经过若干操作变为B,可进行的操作有:

  1. 删除–将字符串A中的某个字符删除。
  2. 插入–在字符串A的某个位置插入某个字符。
  3. 替换–将字符串A中的某个字符替换为另一个字符。

现在请你求出,将A变为B至少需要进行多少次操作。

分析:

状态表示 f [ i , j ] f[i,j] f[i,j]:集合:将 a [ 1 , i ] a[1,i] a[1,i]变为 b [ 1 , j ] b[1,j] b[1,j]的所有方案

​ 属性:方案的最小操作次数

状态计算:

分三种情况:(1)删除。将 a [ 1 , i ] a[1,i] a[1,i]的第 i i i位删除,使得 a [ 1 , i − 1 ] a[1,i-1] a[1,i1] b [ 1 , j ] b[1,j] b[1,j]匹配

状态转移方程为 f [ i , j ] = f [ i − 1 , j ] + 1 f[i,j]=f[i-1,j]+1 f[i,j]=f[i1,j]+1

(2)插入。在第 i i i位的后面插入一个字符,使得 a [ 1 , i + 1 ] a[1,i+1] a[1,i+1] b [ 1 , j ] b[1,j] b[1,j]匹配,算 a [ 1 , i + 1 ] a[1,i+1] a[1,i+1] b [ 1 , j ] b[1,j] b[1,j]的方案数不好算,我们将其退一步,求 a [ 1 , i ] a[1,i] a[1,i] b [ 1 , j − 1 ] b[1,j-1] b[1,j1]匹配的最小方案数。

状态转移方程为 f [ i , j ] = f [ i , j − 1 ] + 1 f[i,j]=f[i,j-1]+1 f[i,j]=f[i,j1]+1

(3)修改。将第 a i a_i ai修改为 b j b_j bj,使得 a [ 1 , i ] a[1,i] a[1,i] b [ 1 , j ] b[1,j] b[1,j]匹配,退一步来说,就是 a [ 1 , i − 1 ] a[1,i-1] a[1,i1] b [ 1 , j − 1 ] b[1,j-1] b[1,j1]匹配。

但是要判断一下 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]

状态转移方程为,当 a [ i ] ! = b [ j ] a[i]!=b[j] a[i]!=b[j]时, f [ i , j ] = f [ i − 1 , j − 1 ] + 1 f[i,j]=f[i-1,j-1]+1 f[i,j]=f[i1,j1]+1

Code:

#include<bits/stdc++.h>
using namespace std;
const int N=1005;
char a[N],b[N];
int f[N][N];
int main(){
	int n,m;
	cin>>n>>a+1>>m>>b+1;
	//初始化 f[0][0...m]
	for(int i=0;i<=m;i++){
		f[0][i]=i;//a[0]变成b[0...i]只能执行插入操作 
	}
	//初始化 f[0...n][0]
	for(int i=0;i<=n;i++){
		f[i][0]=i;//a[0...i]变成b[0]只能执行删除操作 
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			f[i][j]=min(f[i-1][j],f[i][j-1])+1;
			if(a[i]!=b[j]) f[i][j]=min(f[i][j],f[i-1][j-1]+1);
			else f[i][j]=min(f[i][j],f[i-1][j-1]);
		}
	}
	cout<<f[n][m];	 
}
5、编辑距离

给定n个长度不超过10的字符串以及m次询问,每次询问给出一个字符串和一个操作次数上限。

对于每次询问,请你求出给定的n个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。

每个对字符串进行的单个字符的插入、删除或替换算作一次操作。

方法:执行多次最短编辑距离

#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int f[N][N];
char s[N][N];
int edit_distance(char a[],char b[]){
	int n=strlen(a+1);
	int m=strlen(b+1);
	for(int i=0;i<=n;i++) f[i][0]=i;
	for(int i=0;i<=m;i++) f[0][i]=i;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			f[i][j]=min(f[i-1][j],f[i][j-1])+1;
			f[i][j]=min(f[i][j],f[i-1][j-1]+(a[i]!=b[j]));
		}
	}
	return f[n][m];
}
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=0;i<n;i++) cin>>s[i]+1;
	while(m--){
		int limit;
		char str[N];
		cin>>str+1>>limit;
		int ans=0;
		for(int i=0;i<n;i++){
			if(edit_distance(s[i],str)<=limit) ans++;
		}
		printf("%d\n",ans);
	}
}

5-2-2 区间dp

1、石子合并

设有N堆石子排成一排,其编号为1,2,3,…,N。

每堆石子有一定的质量,可以用一个整数来描述,现在要将这N堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

分析:

状态表示 f [ i , j ] f[i,j] f[i,j]:集合:所有将第 i i i堆石子到第 j j j堆石子合并成一堆石子的合并方式

​ 属性:这些合并方式消耗的体力最小值

状态计算:以最后一次在哪个位置合并作为划分依据

在这里插入图片描述

假如在第 k k k个位置作为划分, f [ i , j ] = M i n { f [ i , k ] + f [ k + 1 , j ] + s u m { i . . . . j } } f[i,j]=Min\{f[i,k]+f[k+1,j]+sum\{i....j\}\} f[i,j]=Min{f[i,k]+f[k+1,j]+sum{i....j}}

其中 s u m { i . . . . j } sum\{i....j\} sum{i....j}用前缀和来求

时间复杂度 O ( n 3 ) O(n^3) O(n3),不会超时

#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int s[N];
int f[N][N];
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&s[i]);
	for(int i=1;i<=n;i++) s[i]=s[i-1]+s[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]=1e9;
			//枚举k,k从i到j-1
			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]);
			}
		}
	}
	printf("%d",f[1][n]);
}

5-2-3 计数类dp

1、整数划分

一个正整数 n n n可以表示成若干个正整数之和,形如: n = n 1 + n 2 + … + n k n=n_1+n_2+…+n_k n=n1+n2++nk,其中 n 1 ≥ n 2 ≥ … ≥ n k , k ≥ 1 n_1≥n_2≥…≥n_k,k≥1 n1n2nk,k1

我们将这样的一种表示称为正整数 n n n的一种划分。

现在给定一个正整数 n n n,请你求出 n n n共有多少种不同的划分方法。

分析:

把1,2,3…n分别看作n个物体的体积,这n个物体均可以使用无限次,问恰好能装满总体积是n的背包的总方案(完全背包问题变形)

初值问题:

求最大值,当一个也不选时,价值为0

但是求方案数,当都不选时,方案数为1(即前i个物品都不选的情况也是一种方案),故需要初始化为1,即f[i][0]=1

等价变形后,f[0]=1

状态表示:

f[i][j]表示从前i个物品中选,总体积恰好为j的方案

属性:方案数

状态计算f[i][j]表示前i个整数恰好拼成j的方案数

求方案数的方法:把集合中选0个i,1个i,2个i,…全部加起来(完全背包是求最大值)

每一个集合都有不多于j+1种划分,把这所有的划分加起来就是方案数

因此 f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i − 1 ] [ j − i ] + f [ i − 1 ] [ j − 2 i ] + . . . f[i][j]=f[i-1][j]+f[i-1][j-i]+f[i-1][j-2i]+... f[i][j]=f[i1][j]+f[i1][ji]+f[i1][j2i]+...

又因为 f [ i ] [ j − 1 ] = f [ i − 1 ] [ j − i ] + f [ i − 1 ] [ j − 2 i ] + . . . f[i][j-1]=f[i-1][j-i]+f[i-1][j-2i]+... f[i][j1]=f[i1][ji]+f[i1][j2i]+...

故原式可以化简为 f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i ] [ j − i ] f[i][j]=f[i-1][j]+f[i][j-i] f[i][j]=f[i1][j]+f[i][ji]

等价变形: f [ j ] = f [ j ] + f [ j − i ] f[j]=f[j]+f[j-i] f[j]=f[j]+f[ji]

#include<bits/stdc++.h>
using namespace std;
const int N=1005,mod=1e9+7;
int f[N];
int main(){
	int n;
	cin>>n;
	f[0]=1;
	for(int i=1;i<=n;i++){
		for(int j=i;j<=n;j++){
			f[j]=(f[j]%mod+f[j-i]%mod)%mod;
		}
	}
	cout<<f[n];
}

5-3-1 数位统计dp

第六章:贪心

6-1-1 区间问题

1、区间选点

给定N个闭区间 [ a i , b i ] [a_i,b_i] [ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。

输出选择的点的最小数量。

位于区间端点上的点也算作区间内。

分析:

1、将每个区间按右端点从小到大排序

2、从前往后依次枚举每个区间,如果当前区间已经包含点,则直接pass,否则,选择当前区间的右端点

证明:

1、ans<=cntcnt是一种可行方案,ans是可行方案中的最优解,也就是最小值

2、ans>=cntcnt可行方案是一个区间集合,区间从小到大排序,两两之间不相交,所以覆盖每一个区间至少需要cnt个点

java代码如下:

import java.util.*;
class Range implements Comparable<Range>{
	int l,r;
	Range(int l,int r){
		this.l=l;
		this.r=r;
	}
	public int compareTo(Range w) {
		return r-w.r;
	}
}
public class Main {
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner in=new Scanner(System.in);
		int n=in.nextInt();
		Range[] range=new Range[n];
		for(int i=0;i<n;i++) {
			range[i]=new Range(in.nextInt(),in.nextInt());
		}
		Arrays.sort(range,0,n);
		int res=0,ed=(int)-2e9;
		for(int i=0;i<n;i++) {
			if(ed<range[i].l) {
				res++;
				ed=range[i].r;
			}
		}
		System.out.println(res);
	}
	

}

拓展习题:112、雷达设备

2、最大不相交区间数量

给定N个闭区间 [ a i , b i ] [a_i,b_i] [ai,bi],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。

输出可选取区间的最大数量。

import java.util.*;
class Range implements Comparable<Range>{
	int l,r;
	Range(int l,int r){
		this.l=l;
		this.r=r;
	}
	public int compareTo(Range w) {
		return r-w.r;
	}
}
public class Main {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner in=new Scanner(System.in);
		int n;
		n=in.nextInt();
		Range[] range=new Range[n];
		for(int i=0;i<n;i++) {
			range[i]=new Range(in.nextInt(),in.nextInt());
		}
		Arrays.sort(range,0,n);
		int res=0,ed=(int)-2e9;
		for(int i=0;i<n;i++) {
			if(ed<range[i].l) {
				res++;
				ed=range[i].r;
			}
		}
		System.out.println(res);
	}

}

3、区间分组

给定N个闭区间 [ a i , b I i ] [a_i,bIi] [ai,bIi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。输出最小组数。

分析:

1、将所有区间按左端点从小到大排序

2、从前往后处理每个区间

​ 判断能否将其放入某个现有的组中 l[i]>max_r

​ (1)、如果不存在这样的组,则开新组,然后再将其放进去

​ (2)、如果存在这样的组,将其放进去,并更新当前的max_r

import java.util.*;
class Range implements Comparable<Range>{
	int l,r;
	Range(int l,int r){
		this.l=l;
		this.r=r;
	}
	public int compareTo(Range w) {
		return l-w.l;
	}
}
public class Main {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner in=new Scanner(System.in);
		int n=in.nextInt();
		Range[] range=new Range[n];
		for(int i=0;i<n;i++) {
			range[i]=new Range(in.nextInt(),in.nextInt());
		}
		Arrays.sort(range,0,n);
		PriorityQueue<Integer> qu=new PriorityQueue<Integer>(new Comparator<Integer>() {
			public int compare(Integer a,Integer b){
				return a-b;
			}
		});
		int res=0;
		for(int i=0;i<n;i++) {
			if(qu.isEmpty()||qu.peek()>=range[i].l) {
				res++;
				qu.add(range[i].r);
			}else {
				qu.remove();
				qu.add(range[i].r);
			}
		}
		System.out.println(qu.size());
	}	
}

拓展习题:111、畜栏预定

4、区间覆盖

给定N个闭区间 [ a i , b i ] [a_i,b_i] [ai,bi]以及一个线段区间 [ s , t ] [s,t] [s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。

输出最少区间数,如果无法完全覆盖则输出-1。

分析:

1、将所有区间按左端点从小到大排序

2、从前往后依次枚举每个区间,在所有能覆盖start的区间中,选择右端点最大的区间,然后将start更新为右端点的最大值

java代码:

import java.util.*;
class Range implements Comparable<Range>{
	int l,r;
	Range(int l,int r){
		this.l=l;
		this.r=r;
	}
	public int compareTo(Range w) {
		return l-w.l;
	}
}
public class Main {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner in=new Scanner(System.in);
		
		int st,ed;
		st=in.nextInt();
		ed=in.nextInt();
		int n=in.nextInt();
		Range[] range=new Range[n];
		for(int i=0;i<n;i++) {
			range[i]=new Range(in.nextInt(),in.nextInt());
		}
		Arrays.sort(range,0,n);
		int res=0;
		boolean flag=false;
		for(int i=0;i<n;i++) {
			int j=i,r=(int)-2e9;
			/*
			 * 双指针算法,在所有左端点小于等于st的区间中,找一个右端点最大的
			 * */
			while(j<n && range[j].l<=st) {
				r=Math.max(r, range[j].r);
				j++;
			}
			/*
			 * 如果找出来的右端点,比st都小,必然是不可行解
			 * */
			if(r<st) {
				res=-1;
				break;
			}
			res++;//更新答案
			if(r>=ed) {//找出来的区间右端点超过了ed,查找完毕
				flag=true;
				break;
			}
			/*
			 * 为下一次循环做准备,更新i,更新新的st
			 * */
			st=r;
			i=j-1;
		}
		if(!flag) res=-1;
		System.out.println(res);
	}

}

6-1-2 合并果子

哈夫曼树问题

java代码

import java.util.*;
public class Main {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Scanner in=new Scanner(System.in);
		int n;
		n=in.nextInt();
		PriorityQueue<Long> qu=new PriorityQueue<Long>(new Comparator<Long>() {
			public int compare(Long a,Long b) {
				return (int) (a-b);
			}
		});
		while(n-->0) {
			long t=in.nextInt();
			qu.add(t);
		}
		Long res=(long) 0;
		while(qu.size()>=2) {
			Long a=qu.peek();qu.remove();
			Long b=qu.peek();qu.remove();
			res+=(a+b);
			qu.add(a+b);
		}
		System.out.println(res);
	}

}

6-2-1 排序不等式

有 $n $个人排队到 1 个水龙头处打水,第 i i i 个人装满水桶所需的时间是 t i t_i ti,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?

思路:将时间从小到大排序,每个人对总时间的贡献均为 t [ i ] ∗ ( n − i ) t[i]*(n-i) t[i](ni),求出综合即可

证明:可以用反证法,任意设两个值

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
ll t[N];
int main(){
	int n;
	cin>>n;
	for(int i=1;i<=n;i++) cin>>t[i];
	sort(t+1,t+n+1);
	ll sum=0;
	for(int i=1;i<=n;i++) sum+=t[i]*(n-i);
	cout<<sum; 	
}

6-2-2 绝对值不等式

货仓选址

在一条数轴上有 N N N家商店,它们的坐标分别为 A 1 到 A N A_1到A_N A1AN。现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。

分析:

绝对值不等式问题

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=100005;
int a[N];
int main(){
	int n;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	sort(a+1,a+1+n);
	int ans=0;
	for(int i=1;i<=n;i++) ans+=abs(a[i]-a[n/2+1]);
	//当下标从0开始时,n/2
	//当下标从1开始时,n/2+1 
	cout<<ans;
}

6-2-3 推公式

农民约翰的N头奶牛(编号为1…N)计划逃跑并加入马戏团,为此它们决定练习表演杂技。

奶牛们不是非常有创意,只提出了一个杂技表演:

叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。

奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。

这N头奶牛中的每一头都有着自己的重量Wi以及自己的强壮程度Si。

一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。

您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。

分析:

按照wi+si从小到大的顺序排,最大的危险系数一定是最小的

在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=50005;
struct Node{
	ll w,s;
	bool operator<(const Node &a) const{
		return w+s<a.w+a.s;
	}
}node[N];
int main(){
	int n;
	cin>>n;
	for(int i=0;i<n;i++) {
		cin>>node[i].w>>node[i].s;
	}
	sort(node,node+n);
	ll res=-2e9,sum=0;
	for(int i=0;i<n;i++){
		res=max(res,sum-node[i].s);
		sum+=node[i].w;
	}
	cout<<res;
}
### 回答1: acwing算法基础课是一门针对算法学习的在线课程,在这门课程中,学生可以系统地学习和掌握算法基础知识,提高编程水平。为了方便学生学习,acwing提供了网盘服务。 acwing算法基础课网盘是一个用于存储课程资源的平台。通过这个网盘,学生可以下载课程讲义、代码模板以及补充材料等。这些资源都经过精心整理,供学生们参考和学习。 网盘中的资源是按照课程章节进行分类的,学生可以根据自己的学习需要,选择性地下载所需的资料。同时,网盘还提供了搜索功能,方便学生快速定位和获取所需资料。 acwing算法基础课网盘的使用对于学生们的学习非常有帮助。通过下载和学习这些资源,学生们可以更好地理解课程内容,加深对算法的理解。此外,学生们还可以通过研究代码模板,学习优秀的编程思想和技巧,提高自己的编程能力。 总之,acwing算法基础课网盘是一项非常便利和实用的服务,为学生们提供了更加全面和深入的学习资源,帮助他们更好地掌握和运用算法知识。 ### 回答2: acwing算法基础课是一门优质的算法学习资源,其中的课程内容丰富多样,涵盖了算法基础知识、数据结构、动态规划、图论等等。很多学习者都认为这门课程对他们的算法学习有很大的帮助。 网盘是指以网络为媒介,提供文件存储和下载服务的云存储平台。acwing算法基础课也提供了网盘服务,方便学习者下载课程资料并进行学习。 通过acwing算法基础课网盘,学习者可以方便地获取到课程的各种学习资料,包括讲义、习题集、代码示例等。这些资料可以帮助学习者更好地理解和掌握课程的内容。此外,网盘还提供了上传和分享功能,学习者可以将自己的学习心得、代码等资料分享给其他学习者,促进学习者之间的互相学习和交流。 acwing算法基础课网盘的优点不仅仅是方便快捷的下载和分享功能,还包括安全可靠的存储环境。学习者可以放心地将自己的学习资料上传到网盘进行备份,减少数据丢失的风险。同时,网盘还提供了多种存储空间容量的选择,满足学习者不同的需求。 总的来说,acwing算法基础课网盘为学习者提供了方便、安全和多样化的学习资源下载和分享服务,为学习者的算法学习和进步提供了有力的支持。如果你对算法感兴趣,我推荐你去尝试一下这门精彩的课程!
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值