ACwing算法基础课全程笔记(持续更新~)

※算法基础课

2021年1月31日更新:数论部分

2021年1月24日更新:

  • 笔记同步至当天
  • 补全了文本中缺失的图片

文章目录

  • ※算法基础课
    • 第一章:基础算法
      • 1-1-1快速排序
          • 应用:求第k个数
      • 1-1-2归并排序
          • 应用:求逆序对个数
      • 1-1-3 整数二分
      • 1-1-4 浮点数二分
      • 1-2 高精度
          • 1、高精度加法
      • 1-2-1 一维前缀和
      • 1-2-2 二维前缀和
      • 1-2-3一维差分
      • 1-2-4 二维差分
      • 1-3-1双指针算法
      • 1-3-2 位运算
          • 1-3-2-1、n的二进制表示中第k位是几
          • 1-3-2-2 lowbit(x):返回x的最后一位1
      • 1-3-3 离散化
      • 1-3-4 区间合并
    • 第二章:数据结构
      • 2-1-1 单链表
      • 2-2-2 双链表
      • 2-2-3 栈
      • 2-2-4 队列
      • 2-2-5 单调栈
      • 2-2-6 单调队列
      • 2-2-7 KMP算法
      • 2-2-8 Tire树
          • 应用:最大异或对
      • 2-2-9 并查集
      • 2-2-10 堆
      • 2-2-11 哈希表
    • 第三章:搜索与图论
      • 3-1-1 深度优先搜索
      • 3-1-2 宽度优先搜索
      • 3-1-3 树和图的遍历
        • 树和图的存储:
        • 拓扑排序:
      • 3-2 最短路
        • 一、朴素Dijkstra算法(解决稠密图)
        • 二、堆优化的dijkstra算法
        • 三、Bellman-Ford算法(处理有负权边的图)
        • 四、spfa算法(Bellman-Ford算法的队列优化算法)
          • spfa求负环
        • 五、Floyd算法(多源最短路)
      • 3-3-1 最小生成树
        • 一、朴素Prim算法
        • 二、Kruskal算法
      • 3-3-2 二分图
        • 一、染色法判断二分图
        • ~~二、匈牙利算法~~
    • 第四章:数学知识
        • 4-1-1 质数
          • 1、试除法判定素数
          • 2、分解质因数——试除法
          • 3、朴素筛法求素数
          • 4、线性筛
        • 4-1-2 约数
          • 1、试除法求约数
          • 2、约数个数
          • 3、约数之和
          • 4、欧几里得算法(辗转相除法)
        • 4-2-1 欧拉函数
        • 4-2-2 筛法求欧拉函数
          • 欧拉定理与费马定理:
        • 4-2-3 快速幂
        • 4-2-4 快速幂求逆元
          • 乘法逆元的定义
        • 4-2-5 扩展欧几里得算法
        • 4-2-6 线性同余方程(拓展欧几里得的应用)
        • 4-2-7 中国剩余定理
        • ~~4-3-1 高斯消元~~
          • 1、高斯消元解线性方程组
      • 4-4-1 容斥原理
    • 第五章:动态规划
      • 5-1 背包问题
          • 1、01背包(每件物品最多只用一次)
          • 2、完全背包(每件物品有无限个)
          • 3、多重背包(每件物品 s i s_i si个)
          • 4、分组背包( N N N组,每一组里有若干个)
      • 5-2-1 线性dp
          • 1、数字三角形
          • 2、最长上升子序列
            • ~~优化版本(见习题课)~~
          • 3、最长公共子序列
          • 4、最短编辑距离
          • 5、编辑距离
      • 5-2-2 区间dp
          • 1、石子合并
      • 5-2-3 计数类dp
          • 1、整数划分
      • 5-3-1 数位统计dp
    • 第六章:贪心
      • 6-1-1 区间问题
          • 1、区间选点
          • 2、最大不相交区间数量
          • 3、区间分组
          • 4、区间覆盖
      • 6-1-2 合并果子
      • 6-2-1 排序不等式
      • 6-2-2 绝对值不等式
      • 6-2-3 推公式

第一章:基础算法

1-1-1快速排序

#include
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]);
}
应用:求第k个数

快速选择算法,时间复杂度 O ( n ) O(n) O(n)

1-1-2归并排序

https://www.acwing.com/problem/content/789/

#include
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]);
}
应用:求逆序对个数
#include
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-1-3 整数二分

ACwing算法基础课全程笔记(持续更新~)_第1张图片

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

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

例题(数的范围):

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

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

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

输入格式

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

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

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

输出格式

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

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

数据范围

1≤n≤1000001≤n≤100000
1≤q≤100001≤q≤10000
1≤k≤100001≤k≤10000

输入样例:

6 3
1 2 2 3 3 4
3
4
5

输出样例:

3 4
5 5
-1 -1

代码:2021年1月21日修改

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

1-1-4 浮点数二分

例题:对n开方

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

课后题:

785 786 787 788 789 790

786 第K大数

https://www.acwing.com/problem/content/788/

将k值当做物理地址的值,比如第5个数其实就是数组4的位置,第2个数就是数组1的位置

每次只需要判断k在左区间还是右区间,一直递归查找k所在区间
最后只剩一个数时,只会有数组[k]一个数,返回数组[k]的值就是答案

#include
using namespace std;
const int N=100010;
int n,k,a[N];
int quick_sort(int l,int r){
     
	if(l>=r) return a[k];
	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) return quick_sort(l,j);
	else return quick_sort(j+1,r);
}
int main(){
     
	scanf("%d%d",&n,&k);
	k--;
	for(int i=0;i<n;i++) scanf("%d",&a[i]);
	cout<<quick_sort(0,n-1);
}

1-2 高精度

1、高精度加法
#include
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]);
}

2、高精度减法

1-2-1 一维前缀和

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

#include
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]);
	}
}

1-2-2 二维前缀和

求前缀和的方法:

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

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

#include
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]);
	}
}

1-2-3一维差分

#include
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]);
	}
}

1-2-4 二维差分

( 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
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-3-1双指针算法

for(i=0,j=0;i

核心思想:

for(int i=0;i

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

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

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

例题2:最长连续不重复子序列

朴素做法: 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
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;
}

1-3-2 位运算

1-3-2-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

1-3-2-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的个数

#include
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-3-3 离散化

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

例题:

  1. 区间和

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

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

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

输入格式

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

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

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

输出格式

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

#include
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-3-4 区间合并

ACwing算法基础课全程笔记(持续更新~)_第2张图片

#include
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-1 单链表

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

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

#include
using namespace std;
const int N=100010;
//head表示头结点的下标
//e[i]表示结点i的值
//ne[i]表示结点i的next指针是多少
//idx存储当前已经用到了哪个点
int head,e[N],ne[N],idx;
//初始化
void init(){
     
    head=-1;//指向空结点
    idx=0;
}
//头插法:将x插到头结点
void add_to_head(int x){
     
    e[idx]=x,ne[idx]=head,head=idx,idx++;
}
//将x插到下标是k的结点后面
void add(int k,int x){
     
    e[idx]=x;
    ne[idx]=ne[k];
    ne[k]=idx;
    idx++:
}
//将下标是k的点后面的点删掉
void remove(int k){
     
    ne[k]=ne[ne[k]];
}
int main(){
     
    
    
}

2-2-2 双链表

/*双链表*/
#include
using namespace std;
const int N=100010;
int m;
int e[N],l[N],r[N],idx;
//初始化
void init(){
     
    //0表示左端点,1表示右端点
    r[0]=1;l[1]=0;
    idx=2;
}
//在下标是k的点的右边插入x
//如果是在下标为k的点的左边插入X,直接调用add(k-1,x)
void add(int k,int x){
     
    e[idx]=x;
    r[idx]=r[k];
    l[idx]=k;
    l[r[k]]idx;
    r[k]=idx;
}
//删除第k个点
void remove(int k){
     
    r[l[k]]=r[k];
    l[r[k]]=l[k];
}
int main(){
     
    
    
}

2-2-3 栈

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

2-2-4 队列

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

2-2-5 单调栈

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

暴力做法:

for(int i=0;i<n;i++){
     
	for(int j=i-1;j>=0;j--){
     
        if(a[j]<a[i]){
     
            cout<<j<<endl;
            break;
        }
    }
}
#include
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-2-6 单调队列

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

例题:154.滑动窗口

#include
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-2-7 KMP算法

#include
using namespace std;
const int N=100010,M=1000010;
int n,m;//n是模式串,m是原串
char p[N],s[M];
int ne[N];
int main(){
     
	cin>>n>>p+1>>m>>s+1;
    //求Next的过程
    for(int i=2,j=0;i<=n;i++){
     
        while(j && p[i]!=p[j+1]) j=ne[j];
        if(p[i]==p[j+1]) j++;
        ne[i]=j;
    }
    //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);//不加1是因为下标要从0开始
            //j已经走到头了,为了下一次匹配j走到ne[j]
            j=ne[j];//为下一次匹配做准备
        }
    }
    return 0;
}

2-2-8 Tire树

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

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

存储方法:如图所示。

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

ACwing算法基础课全程笔记(持续更新~)_第3张图片

查询方法:

①在上图查询单词"abcf"

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

②在上图查询单词"abcd"

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

模板题:

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

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

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

#include
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;
}
应用:最大异或对

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

#include
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-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个数,编号是1~n,最开始每个数各自在一个集合中。

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

  1. “M a b”,将编号为a和b的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. “Q a b”,询问编号为a和b的两个数是否在同一个集合中;
#include
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;
}

并查集记录集合中元素个数

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

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

  1. “C a b”,在点a和点b之间连一条边,a和b可能相等;
  2. “Q1 a b”,询问点a和点b是否在同一个连通块中,a和b可能相等;
  3. “Q2 a”,询问点a所在连通块中点的数量;
#include
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";
		}
	}
}

2-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
#include
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;
}
  1. 模拟堆

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

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

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

#include
#include
#include
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-2-11 哈希表

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

字符串哈希方式

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

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

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

  1. 模拟散列表

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

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

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

拉链法:

#include
#include
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
#include
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");
		}
	}
}

第三章:搜索与图论

3-1-1 深度优先搜索

DFS和BFS的对比:

数据结构 空间
DFS Stack O ( h ) O(h) O(h) 不具最短性
BFS queue O ( 2 h ) O(2^h) O(2h) 最短路
  1. 排列数字

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

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

#include
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
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
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
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
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
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 
#include 
#include 

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
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
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一个级别 稀疏图

ACwing算法基础课全程笔记(持续更新~)_第4张图片

一、朴素Dijkstra算法(解决稠密图)

  1. Dijkstra求最短路

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

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

ACwing算法基础课全程笔记(持续更新~)_第5张图片

#include
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
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
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
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
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
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 最小生成树

本节导航:

ACwing算法基础课全程笔记(持续更新~)_第6张图片

**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 
#include 
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
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
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
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
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
#include
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
#include
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
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
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
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
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
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
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
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
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
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
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)

ACwing算法基础课全程笔记(持续更新~)_第7张图片

最后的 m 0 m_0 m0就是答案

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

#include
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背包(每件物品最多只用一次)

ACwing算法基础课全程笔记(持续更新~)_第8张图片

代码1:朴素写法

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

优化:
ACwing算法基础课全程笔记(持续更新~)_第9张图片

最终, 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
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
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
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

ACwing算法基础课全程笔记(持续更新~)_第10张图片

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

状态计算:ACwing算法基础课全程笔记(持续更新~)_第11张图片

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

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
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个数结尾的上升子序列

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

状态计算:

ACwing算法基础课全程笔记(持续更新~)_第12张图片

划分的依据是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

ACwing算法基础课全程笔记(持续更新~)_第13张图片

#include
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
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
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堆石子合并成一堆石子的合并方式

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

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

ACwing算法基础课全程笔记(持续更新~)_第14张图片

假如在第 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
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
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
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
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从小到大的顺序排,最大的危险系数一定是最小的

ACwing算法基础课全程笔记(持续更新~)_第15张图片

#include
using namespace std;
typedef 

你可能感兴趣的:(ACM/ICPC/蓝桥杯,笔记,算法,数据结构)