前言
我认为算法编程是考核计算机功底的最重要、最方便的定量工具了。
- 从课程的角度来说,大一上5学分的程序设计,大一下5学分的数据结构与算法,大一小学期4学分的软件能力与实训 和 毕业要求170的ccf csp,大三上3学分的算法设计与分析,以及各种c++/python的专业选修课。 算法的性价比可以说是相当高了。
- 学习算法前,至少应该有c++的语法基础(也不一定很明白类,以我看过的算法题来看,几乎没怎么用过类),和各种数据结构的概念(知道线性表,hash表,树,图是什么东西就可);
- 如果你是大一小学期:
- 没有算法基础的话,千万千万千万别傻乎乎地跟着学院写cg题了,那个东西既不成系统,而且单纯写题也没有什么提高,不会还是不会。
- 或者你害怕因为不认真写cg题而课程分低:这并不是核心课,而且其实主要还是看ccfcsp的分数(笔者ccfcsp 240分;按照170及格,300分满分的方式计算,ccfcsp的成绩换算成课程分:(100-60)/(300-170)=0.30769,我应该是60+(240-170)*0.30769=81,实际分数84)
- 我站在大三下往回看,大一下这个时候是最适合练算法的时候;
- 不过你如果有算法的基础的话,可以用cg题来练习一下;你并不是本篇博客的目标读者。
- 没有算法基础的话,千万千万千万别傻乎乎地跟着学院写cg题了,那个东西既不成系统,而且单纯写题也没有什么提高,不会还是不会。
- 从机试的角度来说,保研夏令营大多数高校都有机试这一环节,这方面能拿高分,是相当有优势的。 笔者没太了解过考研和就业,但想来考研复试、就业考核应当也是不会缺少机试的。
- 从竞赛的角度来说:除了ccfcsp外,还有蓝桥杯等算法竞赛,拿了奖可以当综测加分,拿了比较好的奖还可以写在简历里。 也可以就拿竞赛练机试,相较于数模什么的,这个不需要组队,i人可以专攻这个,
算法基础课-acwing:首先声明这不是广告奥。
- 前面说院内的cg题没有系统性,就是对照着这个算法基础课说的;
- 而且对于没有算法基础的同学来说,正像这个课程视频里的yxc大佬说的“这是一个学习的过程,而不是创造的过程”:
- 没有基础,直接去刷题,不学就会的天才还是少数;如果在网上搜罗搜罗答案写上去,测试的时候背一下,那不会还是不会;
- 只有先学了比较经典的问题、解法,才明白套路,至少打个暴力骗骗基础分也是进步
- 而且里面不少技巧在解算法题上很方便:
- 比如链表的表示,并不用指针表示,而是几个数组h[],e[],ne[],除了指针的创建开销外,更重要的是能更方便的用代码表示这些数据结构;
- 像图这类数据结构,在学习院内的课程的时候,就一直糊涂;碰到类似树/图的题目,根本不知道怎么表示这种数据结构,就更不用说解题了;
- 免责声明:
- 这是站在大三的视角去回忆大一下学期,并与现在学习算法基础课的过程作对比;可能有了一些大一小学期没有的基础 而不自知,所以觉得算法基础课更好、通俗易懂;
- 但是,我仍然觉得你如果去单纯地自己刷题,不如看算法基础课。另外,我听说这个课程的来源,是在绿群(如果你不知道绿群,这里和平星-院校小计(道听途说版)-21级-计科有一些说明)里,有人询问提高机试能力的方法,回答除了算法基础课之外,还有刷力扣、洛谷等。 但笔者目前亲测的就只有算法基础课。
- 另外,算法基础课是付费的,个人以为一二百块的性价比还是很高的,不过你也有可能找到网络资源。
- 目前我的学习方法是:
- 先把全部的视频看了一遍(有些没有基础的,难搞懂的也看了几遍),这第一遍是最耗时间的;
- 然后对着ac的代码,抄一两遍,对关键的部分尽量有印象;
- 然后就试着默写,有些细节可能自己标注/备注一下,免得下次再忘。
- 默写一次两次记的总不牢靠,后面再背再写就比较熟练了。 对于像(最短路径Dijkstra,bellman-ford,spfa,floyd,最小生成树prim,kruskal,并查集),比较相似的框架和代码,可以一块对比着记。
- 时间不够的话,有些题目比较困难,而且感觉不是很会考,就暂且没有记。
- 算法提高课也还在计划中。
参考:
本文代码多来自于Acwing平台《算法基础课》相应题目下的高赞题解;
零、基础知识
1.数据类型范围
数据类型 | 说明 | 范围 |
int | 有符号 | -21,4748,3648~21,4748,3647 |
unsigned int | 无符号 | 0~42,9496,7295 |
long | 有符号 | -21,4748,3648~21,4748,3647 |
unsigned long | 无符号 | 0~42,9496,7295 |
long long | 有符号 | -922,3372,0368,5477,5808~922,3372,0368,5477,5807 |
unsigned long long | 无符号 | 0~1844,6744,0737,0955,1615 |
一、基础算法
1.快速排序
(1)快排
给定你一个长度为 n 的整数数列。
请你使用快速排序对这个数列按照从小到大进行排序。并将排好序的数列按顺序输出。
// 注:quick_sort(a,l,j)第二个参数是j,而非i;
#include <iostream>
using namespace std;
const int N=1e5+10;
int a[N];
int n;
void quick_sort(int a[],int l,int r){
if(l>=r) return;
int i=l-1,j=r+1,x=a[l+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]);
}
quick_sort(a,l,j);
quick_sort(a,j+1,r);
}
int main(){
cin>>n;
for(int i=0;i<n;i++)
cin>>a[i];
quick_sort(a,0,n-1);
for(int i=0;i<n;i++)
cout<<a[i]<<" ";
}
(2)第k个数
给定一个长度为 n 的整数数列,以及一个整数 k;
请用快速选择算法求出数列从小到大排序后的第 k 个数。
// 最后的判断是k<=sl,而非<
#include <iostream>
using namespace std;
const int N=1e5+10;
int a[N];
int n,k;
int kth(int a[],int l,int r,int k){
if(l==r) return a[l];
int i=l-1,j=r+1,x=a[l+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]);
}
int sl=j-l+1;
if(k<=sl) return kth(a,l,j,k);
return kth(a,j+1,r,k-sl);
}
int main(){
scanf("%d%d",&n,&k);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
printf("%d",kth(a,0,n-1,k));
}
2.归并排序
(1)归并排序
给定你一个长度为 n 的整数数列。
请你使用归并排序对这个数列按照从小到大进行排序。并将排好序的数列按顺序输出。
#include <iostream>
using namespace std;
const int N=1e5+10;
int a[N],temp[N];
int n;
void merge_sort(int a[],int l,int r){
if(l>=r) return;
int mid=l+r >> 1;
merge_sort(a,l,mid);
merge_sort(a,mid+1,r);
int i=l,j=mid+1,k=0;
while(i<=mid && j<=r){
if(a[i]<a[j]) temp[k++]=a[i++];
else temp[k++]=a[j++];
}
while(i<=mid) temp[k++]=a[i++];
while(j<=r) temp[k++]=a[j++];
for(i=l,k=0;i<=r;i++,k++) a[i]=temp[k];
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
merge_sort(a,0,n-1);
for(int i=0;i<n;i++) printf("%d ",a[i]);
}
(2)逆序对的数量
给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。
逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 i<j 且 a[i]>a[j],则为一个逆序对。
// 注第一个while中的if判断条件是<=,而非<;
#include <iostream>
using namespace std;
typedef long long ll;
const int N=1e5+10;
int a[N],temp[N];
int n;
ll merge_sort(int a[],int l,int r){
if(l>=r) return 0;
int mid=l+r >> 1;
ll res=merge_sort(a,l,mid)+merge_sort(a,mid+1,r);
int i=l,j=mid+1,k=0;
while(i<=mid && j<=r){
if(a[i]<=a[j]) temp[k++]=a[i++];
else{
temp[k++]=a[j++];
res+=mid-i+1;
}
}
while(i<=mid) temp[k++]=a[i++];
while(j<=r) temp[k++]=a[j++];
for(i=l,k=0;i<=r;i++,k++) a[i]=temp[k];
return res;
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
printf("%lld",merge_sort(a,0,n-1));
}
3.二分
(1)数的范围
给定一个按照升序排列的长度为 n 的整数数组,以及 q 个查询。
对于每个查询,返回一个元素 k 的起始位置和终止位置(位置从 00 开始计数)。
如果数组中不存在该元素,则返回 -1 -1
。
#include <iostream>
using namespace std;
const int N=1e5+10;
int a[N];
int n,q,m;
int main(){
scanf("%d%d",&n,&q);
for(int i=0;i<n;i++) scanf("%d",&a[i]);
while(q--){
scanf("%d",&m);
int l=0,r=n-1;
while(l<r){
int mid=(l+r)/2;
if(a[mid]>=m) r=mid;
else l=mid+1;
}
if(a[l]!=m) cout<<"-1 -1"<<endl;
else{
cout<<l<<" ";
l=0,r=n-1;
while(l<r){
int mid=(l+r+1)/2;
if(a[mid]<=m) l=mid;
else r=mid-1;
}
cout<<l<<endl;
}
}
}
(2)数的三次方根
给定一个浮点数 n,求它的三次方根。结果保留 6 位小数。
#include <iostream>
using namespace std;
double n;
int main(){
scanf("%lf",&n);
int negative=0;
if(n<0){
negative=1;
n=-n;
}
double l=-1e4,r=1e4,mid;
while((r-l)>1e-8){
mid=(l+r)/2;
if((mid*mid*mid)>n) r=mid;
else l=mid;
}
if(negative) mid=-mid;
printf("%.6lf",mid);
}
4.高精度
(1)高精度加法
给定两个正整数(不含前导 00),计算它们的和。
#include <iostream>
#include <vector>
using namespace std;
vector<int> add(vector<int> a,vector<int> b){
if(a.size()<b.size()) return add(b,a);
vector<int> C;
int t=0;
for(int i=0;i<a.size();i++){
t+=a[i];
if(i<b.size()) t+=b[i];
C.push_back(t%10);
t/=10;
}
if(t) C.push_back(t);
return C;
}
int main(){
string a,b;
cin>>a>>b;
vector<int> A,B;
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0');
auto C=add(A,B);
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
return 0;
}
(2)高精度减法
给定两个正整数(不含前导 00),计算它们的差,计算结果可能为负数。
#include <iostream>
#include <vector>
using namespace std;
vector<int> sub(vector<int> A,vector<int> B){
vector<int> C;
int t=0;
for(int i=0;i<A.size();i++){
if(i<B.size()) t+=A[i]-B[i];
else t+=A[i];
//printf("%d\n",t);
if(t<0){
C.push_back(10+t);
t=-1;
}
else{
C.push_back(t);
t=0;
}
}
while(C.size()>1 && C.back()==0) C.pop_back();
return C;
}
int main(){
string a,b;
cin>>a>>b;
vector<int> A,B;
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
for(int i=b.size()-1;i>=0;i--) B.push_back(b[i]-'0');
if(A.size()<B.size()){
auto C=sub(B,A);
printf("-");
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
return 0;
}
if(A.size()==B.size()){
for(int i=A.size()-1;i>=0;i--){
// printf("%d,%d\n",A[i],B[i]);
if(A[i]>B[i]){
auto C=sub(A,B);
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
return 0;
}
else if(A[i]<B[i]){
auto C=sub(B,A);
printf("-");
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
return 0;
}
}
}
auto C=sub(A,B);
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
return 0;
}
(3)高精度乘法
给定两个非负整数(不含前导 0) A 和 B,请你计算 A×B的值
(数据范围:1≤A的长度≤100000, 0≤B≤10000)
#include <iostream>
#include <vector>
using namespace std;
vector<int> mul(vector<int> a,int b){
vector<int> C;
int t=0;
for(int i=0;i<a.size();i++){
t+=a[i]*b;
C.push_back(t%10);
t/=10;
}
while(t){
C.push_back(t%10);
t/=10;
}
while(C.size()>1 && C.back()==0) C.pop_back();
return C;
}
int main(){
string a;
int b;
cin>>a>>b;
vector<int> A;
for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0');
auto C=mul(A,b);
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
return 0;
}
(4)高精度除法
给定两个非负整数(不含前导 00) A,B,请你计算 A/B 的商和余数。
数据范围:1≤A的长度≤100000,1≤B≤10000,B 一定不为 0
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
vector<int> div(vector<int> a,int b,int &r){
vector<int> C;
for(int i=0;i<a.size();i++){
r=r*10+a[i];
C.push_back(r/b);
r%=b;
}
reverse(C.begin(),C.end());
while(C.size()>1 && C.back()==0) C.pop_back();
return C;
}
int main(){
string a;
int b;
cin>>a>>b;
vector<int> A;
for(int i=0;i<a.size();i++) A.push_back(a[i]-'0');
int r=0;
auto C=div(A,b,r);
for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]);
printf("\n%d",r);
return 0;
}
5.前缀和、差分
(1)前缀和
输入一个长度为 n 的整数序列。
接下来再输入 m 个询问,每个询问输入一对 l,r。
对于每个询问,输出原序列中从第 l 个数到第 r 个数的和
#include <iostream>
using namespace std;
const int N=1e5+10;
int a[N],s[N];
int n,m;
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) {
cin>>a[i];
s[i]=s[i-1]+a[i];
}
while(m--){
int l,r;
cin>>l>>r;
cout<<s[r]-s[l-1]<<endl;
}
return 0;
}
(2)子矩阵的和
输入一个 n行 m 列的整数矩阵,再输入 q 个询问,
每个询问包含四个整数 x1,y1,x2,y2,表示一个子矩阵的左上角坐标和右下角坐标。
对于每个询问输出子矩阵中所有数的和。
#include <iostream>
using namespace std;
const int N=1010;
int a[N][N],s[N][N];
int n,m,q;
int main(){
cin>>n>>m>>q;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
s[i][j]=s[i-1][j]+s[i][j-1]+a[i][j]-s[i-1][j-1];
}
}
int x1,x2,y1,y2;
while(q--){
cin>>x1>>y1>>x2>>y2;
cout<<s[x2][y2]+s[x1-1][y1-1]-s[x1-1][y2]-s[x2][y1-1]<<endl;
}
return 0;
}
(3)差分
输入一个长度为 n 的整数序列。
接下来输入 m 个操作,每个操作包含三个整数 l,r,c,表示将序列中 [l,r] 之间的每个数加上 c。
请你输出进行完所有操作后的序列
//差分 时间复杂度 o(m)
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
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]);
b[i] = a[i] - a[i - 1]; //构建差分数组
}
int l, r, c;
while (m--)
{
scanf("%d%d%d", &l, &r, &c);
b[l] += c; //将序列中[l, r]之间的每个数都加上c
b[r + 1] -= c;
}
for (int i = 1; i <= n; i++)
{
a[i] = b[i] + a[i - 1]; //前缀和运算
printf("%d ", a[i]);
}
return 0;
}
(4)差分矩阵
输入一个 n 行 m 列的整数矩阵,再输入 q 个操作,每个操作包含五个整数 x1,y1,x2,y2,c,其中 (x1,y1)和 (x2,y2) 表示一个子矩阵的左上角坐标和右下角坐标。
每个操作都要将选中的子矩阵中的每个元素的值加上 c。
请你将进行完所有操作后的矩阵输出。
#include <iostream>
using namespace std;
const int N=1010;
int a[N][N],s[N][N];
int n,m,q;
int main(){
cin>>n>>m>>q;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
cin>>s[i][j];
a[i][j]=s[i][j]-s[i-1][j]-s[i][j-1]+s[i-1][j-1];
}
int x1,x2,y1,y2,c;
while(q--){
cin>>x1>>y1>>x2>>y2>>c;
a[x1][y1]+=c;
a[x2+1][y1]-=c;
a[x1][y2+1]-=c;
a[x2+1][y2+1]+=c;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
s[i][j]=a[i][j]+s[i-1][j]+s[i][j-1]-s[i-1][j-1];
cout<<s[i][j]<<' ';
}
cout<<endl;
}
}
6.双指针算法
(1)最长连续不重复子序列
给定一个长度为 n 的整数序列,请找出最长的不包含重复的数的连续区间,输出它的长度;
n 个整数(均在 0∼10^5 范围内)
// count数组类似于一个map,存储元素i出现的次数
#include <iostream>
using namespace std;
const int N=1e5+10;
int a[N],count[N];
int n,ans;
int main(){
cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
for(int i=0,j=0;j<n;j++){
count[a[j]]++;
while(count[a[j]]>1){
count[a[i]]--;
i++;
}
ans=max(ans,j-i+1);
}
cout<<ans<<endl;
}
(2)数组元素的目标和
给定两个升序排序的有序数组 A 和 B,以及一个目标值 x。
数组下标从 0 开始。
请你求出满足 A[i]+B[j]=x 的数对 (i,j)。
数据保证有唯一解。
#include <iostream>
using namespace std;
const int N=1e5+10;
int a[N],b[N];
int n,m,x;
int main(){
cin>>n>>m>>x;
for(int i=0;i<n;i++) cin>>a[i];
for(int i=0;i<m;i++) cin>>b[i];
for(int i=0,j=m-1;i<n;i++){
while(j>=0 && a[i]+b[j]>x) j--;
if(j>=0 && a[i]+b[j]==x) cout<<i<<" "<<j<<endl;
}
}
(3)判断子序列
给定一个长度为 n 的整数序列 a1,a2,…,an以及一个长度为 m 的整数序列 b1,b2,…,bm。
请你判断 a 序列是否为 b 序列的子序列。
子序列指序列的一部分项按原有次序排列而得的序列,例如序列 {a1,a3,a5}是序列 {a1,a2,a3,a4,a5}的一个子序列
#include <iostream>
using namespace std;
const int N=1e5+10;
int a[N],b[N];
int n,m;
int main(){
cin>>n>>m;
for(int i=0;i<n;i++) cin>>a[i];
for(int i=0;i<m;i++) cin>>b[i];
int i,j;
for(i=0,j=0;j<m;j++){
if(i<n && a[i]==b[j]) i++;
}
if(i==n) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
7.位运算-二进制中1的个数
给定一个长度为 n 的数列,请你求出数列中每个数的二进制表示中 1 的个数。
#include <iostream>
using namespace std;
int lowbit(int x){ //获取一个二进制数中最右边的1所对应的数值。
return x & (-x);
}
int main(){
int n;
cin>>n;
int x;
while(n--){
cin>>x;
int res=0;
while(x){
x-=lowbit(x);
res++;
}
cout<<res<<" ";
}
}
8.离散化-区间和
假定有一个无限长的数轴,数轴上每个坐标上的数都是 0。
现在,我们首先进行 n 次操作,每次操作将某一位置 x 上的数加 c。
接下来,进行 m 次询问,每个询问包含两个整数 l 和 r,你需要求出在区间 [l,r]之间的所有数的和。
数据范围
−10^9≤x≤10^9;1≤n,m≤10^5;−10^9≤l≤r≤10^9;−10000≤c≤10000
// alls.erase(unique(alls.begin(),alls.end()),alls.end());
find函数:在数组中,查找值为x的数是第几个;
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 300010; //n次插入和m次查询相关数据量的上界
int n, m;
int a[N];//存储坐标插入的值
int s[N];//存储数组a的前缀和
vector<int> alls; //存储(所有与插入和查询有关的)坐标
vector<pair<int, int>> add, query; //存储插入和询问操作的数据
int find(int x) { //返回的是输入的坐标的离散化下标
int l = 0, r = alls.size() - 1;
while (l < r) {
int mid = l + r >> 1;
if (alls[mid] >= x) r = mid;
else l = mid + 1;
}
return r + 1;
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
int x, c;
scanf("%d%d", &x, &c);
add.push_back({x, c});
alls.push_back(x);
}
for (int i = 1; i <= m; i++) {
int l , r;
scanf("%d%d", &l, &r);
query.push_back({l, r});
alls.push_back(l);
alls.push_back(r);
}
//排序,去重
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
//执行前n次插入操作
for (auto item : add) {
int x = find(item.first);
a[x] += item.second;
}
//前缀和
for (int i = 1; i <= alls.size(); i++) s[i] = s[i-1] + a[i];
//处理后m次询问操作
for (auto item : query) {
int l = find(item.first);
int r = find(item.second);
printf("%d\n", s[r] - s[l-1]);
}
return 0;
}
9.区间合并
给定 n 个区间 [li,ri],要求合并所有有交集的区间。
注意如果在端点处相交,也算有交集。
输出合并完成后的区间个数。
例如:[1,3][1,3] 和 [2,6][2,6] 可以合并为一个区间 [1,6][1,6]。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
vector<pair<int,int>> nums,res;
int n,l,r;
int main(){
cin>>n;
while(n--){
cin>>l>>r;
nums.push_back({l,r});
}
sort(nums.begin(),nums.end());
int st=-2e9,ed=-2e9;
for(auto i:nums){
if(ed<i.first){
st=i.first,ed=i.second;
res.push_back({st,ed});
}
else if(ed<i.second){
ed=i.second;
}
}
cout<<res.size()<<endl;
}
二、数据结构
1.单链表
实现一个单链表,链表初始为空,支持三种操作:
- 向链表头插入一个数;
- 删除第 k个插入的数后面的一个数;
- 在第 k 个插入的数后插入一个数。
现在要对该链表进行 M 次操作,进行完所有操作后,从头到尾输出整个链表。
- 注意:
- 题目中第 k个插入的数并不是指当前链表的第 k 个数。
- 例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
-
输入格式
-
第一行包含整数 M,表示操作次数。
-
接下来 M 行,每行包含一个操作命令,操作命令可能为以下几种:
H x
,表示向链表头插入一个数 xD k
,表示删除第 k 个插入的数后面的数(当 k 为 0 时,表示删除头结点)。I k x
,表示在第 k个插入的数后面插入一个数 x(此操作中 k 均大于 0)。
-
-
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int h[N], e[N], ne[N], head, idx;
//对链表进行初始化
void init(){
head = -1;
idx = 0;
}
//将x插入到头节点上
void int_to_head(int x){//和链表中间插入的区别就在于它有head头节点
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(){
cin >> n;
init();//初始化
for (int i = 0; i < n; i ++ ) {
char s;
cin >> s;
if (s == 'H') {
int x;
cin >> x;
int_to_head(x);
}
if (s == 'D'){
int k;
cin >> k;
if (k == 0) head = ne[head];//删除头节点
else remove(k - 1);//注意删除第k个输入后面的数,那函数里放的是下标,k要减去1
}
if (s == 'I'){
int k, x;
cin >> k >> x;
add(k - 1, x);//同样的,第k个数,和下标不同,所以要减1
}
}
for (int i = head; i != -1; i = ne[i]) cout << e[i] << ' ' ;
cout << endl;
return 0;
}
2.双链表
实现一个双链表,双链表初始为空,支持 5 种操作:
- 在最左侧插入一个数;
- 在最右侧插入一个数;
- 将第 k 个插入的数删除;
- 在第 k 个插入的数左侧插入一个数;
- 在第 k 个插入的数右侧插入一个数
现在要对该链表进行 M 次操作,进行完所有操作后,从左到右输出整个链表。
- 注意:
- 题目中第 k 个插入的数并不是指当前链表的第 k 个数。
- 例如操作过程中一共插入了 n 个数,则按照插入的时间顺序,这 n 个数依次为:第 1 个插入的数,第 2 个插入的数,…第 n 个插入的数。
- 输入格式
- 第一行包含整数 M,表示操作次数。
- 接下来 M行,每行包含一个操作命令,操作命令可能为以下几种:
L x
,表示在链表的最左端插入数 x。R x
,表示在链表的最右端插入数 x。D k
,表示将第 k 个插入的数删除。IL k x
,表示在第 k 个插入的数左侧插入一个数。IR k x
,表示在第 k 个插入的数右侧插入一个数。
#include<iostream>
using namespace std;
const int N = 1e5 + 10;
int m;
int e[N], l[N], r[N];
int idx;
//! 初始化
void init()
{
l[1] = 0, r[0] = 1;//* 初始化 第0点的右边是 1 第1的左边是 0
idx = 2;//! idx 此时已经用掉两个点了
}
//* 在第 K 个点右边插入一个 X
void add(int k, int x)
{
e[idx] = x;
l[idx] = k;
r[idx] = r[k]; //todo 这边的 k 不加 1 , 输入的时候 k+1 就好
l[r[k]] = idx;
r[k] = idx;
idx++;
}//! 当然在 K 的左边插入一个数 可以再写一个 , 也可以直接调用我们这个函数,在 k 的左边插入一个 数 等价于在 l[k] 的右边插入一个数 add(l[k],x)
//*删除第 k个 点
void remove(int k)
{
r[l[k]] = r[k];
l[r[k]] = l[k];
}
int main(void)
{
ios::sync_with_stdio(false);
cin >> m;
init();
while(m--)
{
string op;
cin >> op;
int k, x;
if(op=="R")
{
cin >> x;
add(l[1], x); //! 0和 1 只是代表 头和尾,所以 最右边插入 只要在指向1的 那个点的右边插入就可以了
}
else if(op=="L")//! 同理 最左边插入就是 在指向 0的数的左边插入就可以了 也就是可以直接在 0的 有右边插入
{
cin >> x;
add(0, x);
}
else if(op=="D")
{
cin >> k;
remove(k + 1);
}
else if(op=="IL")
{
cin >> k >> x;
add(l[k + 1], x);
}
else
{
cin >> k >> x;
add(k + 1, x);
}
}
for(int i = r[0]; i != 1; i = r[i]) cout << e[i] << ' ';
return 0;
}
3.栈
(1)模拟栈
实现一个栈,栈初始为空,支持四种操作:
push x
– 向栈顶插入一个数 x;pop
– 从栈顶弹出一个数;empty
– 判断栈是否为空;query
– 查询栈顶元素。
现在要对栈进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
#include <iostream>
using namespace std;
const int N=1e5+10;
int st[N],top;
int m,x;
int main(){
top=-1;
cin>>m;
while(m--){
string s;
cin>>s;
if(s=="push"){
cin>>x;
st[++top]=x;
}
else if(s=="pop"){
top--;
}
else if(s=="empty"){
cout<<(top==-1? "YES" : "NO")<<endl;
}
else{
cout<<st[top]<<endl;
}
}
}
{
int a;
cin >> a;
st[++top] = a;
}
//往前移动一格
if(s == "pop")
{
top --;
}
//返回栈顶元素
if(s == "query")
{
cout << st[top] << endl;
}
//大于等于 0 栈非空,小于 0 栈空
if(s == "empty")
{
cout << (top == -1 ? "YES" : "NO") << endl;
}
}
}
(2)表达式求值
给定一个表达式,其中运算符仅包含 +,-,*,/
(加 减 乘 整除),可能包含括号,请你求出表达式的最终值。
注意:
- 数据保证给定的表达式合法。
- 题目保证符号
-
只作为减号出现,不会作为负号出现,例如,-1+2
,(2+2)*(-(1+1)+2)
之类表达式均不会出现。 - 题目保证表达式中所有数字均为正整数。
- 题目保证表达式在中间计算过程以及结果中,均不超过 2^31−1。
- 题目中的整除是指向 0 取整,也就是说对于大于 0 的结果向下取整,例如 5/3=1,对于小于 0 的结果向上取整,例如 5/(1−4)=−1。
- C++和Java中的整除默认是向零取整;Python中的整除
//
默认向下取整,因此Python的eval()
函数中的整除也是向下取整,在本题中不能直接使用。
#include <iostream>
#include <stack>
#include <string>
#include <unordered_map>
using namespace std;
stack<int> num;
stack<char> op;
//优先级表
unordered_map<char, int> h{ {'+', 1}, {'-', 1}, {'*',2}, {'/', 2} };
void eval()//求值
{
int a = num.top();//第二个操作数
num.pop();
int b = num.top();//第一个操作数
num.pop();
char p = op.top();//运算符
op.pop();
int r = 0;//结果
//计算结果
if (p == '+') r = b + a;
if (p == '-') r = b - a;
if (p == '*') r = b * a;
if (p == '/') r = b / a;
num.push(r);//结果入栈
}
int main()
{
string s;//读入表达式
cin >> s;
for (int i = 0; i < s.size(); i++)
{
if (isdigit(s[i]))//数字入栈
{
int x = 0, j = i;//计算数字
while (j < s.size() && isdigit(s[j]))
{
x = x * 10 + s[j] - '0';
j++;
}
num.push(x);//数字入栈
i = j - 1;
}
//左括号无优先级,直接入栈
else if (s[i] == '(')//左括号入栈
{
op.push(s[i]);
}
//括号特殊,遇到左括号直接入栈,遇到右括号计算括号里面的
else if (s[i] == ')')//右括号
{
while(op.top() != '(')//一直计算到左括号
eval();
op.pop();//左括号出栈
}
else
{
while (op.size() && h[op.top()] >= h[s[i]])//待入栈运算符优先级低,则先计算
eval();
op.push(s[i]);//操作符入栈
}
}
while (op.size()) eval();//剩余的进行计算
cout << num.top() << endl;//输出结果
return 0;
}
4.队列
实现一个队列,队列初始为空,支持四种操作:
push x
– 向队尾插入一个数 x;pop
– 从队头弹出一个数;empty
– 判断队列是否为空;query
– 查询队头元素。
现在要对队列进行 M 个操作,其中的每个操作 3 和操作 4 都要输出相应的结果。
#include <iostream>
using namespace std;
const int N=1e5+10;
int q[N];
int hh=0,tt=-1;
int main(){
int m,x;
cin>>m;
while(m--){
string s;
cin>>s;
if(s=="push"){
cin>>x;
q[++tt]=x;
}
else if(s=="pop"){
hh++;
}
else if(s=="empty"){
if(tt>=hh) cout<<"NO"<<endl;
else cout<<"YES"<<endl;
}
else{
cout<<q[hh]<<endl;
}
}
}
5.单调栈
给定一个长度为 N 的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出 −1。
#include <iostream>
using namespace std;
const int N=1e5+10;
int st[N],top;
int main(){
int n,x;
cin>>n;
while(n--){
cin>>x;
while(top && st[top]>=x) top--;
if(!top) cout<<"-1 ";
else cout<<st[top]<<" ";
st[++top]=x;
}
}
6.单调队列
给定一个大小为 n≤10^6 的数组。
有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7]
,k 为 3。
窗口位置 | 最小值 | 最大值 |
---|---|---|
[1 3 -1] -3 5 3 6 7 | -1 | 3 |
1 [3 -1 -3] 5 3 6 7 | -3 | 3 |
1 3 [-1 -3 5] 3 6 7 | -3 | 5 |
1 3 -1 [-3 5 3] 6 7 | -3 | 5 |
1 3 -1 -3 [5 3 6] 7 | 3 | 6 |
1 3 -1 -3 5 [3 6 7] | 3 | 7 |
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <deque>
using namespace std;
const int N = 1000010;
int a[N];
int main()
{
int n, k;
cin >> n >> k;
for (int i = 1; i <= n; i ++ ) cin >> a[i];//读入数据
deque<int> q;
for(int i = 1; i <= n; i++)
{
while(q.size() && q.back() > a[i]) //新进入窗口的值小于队尾元素,则队尾出队列
q.pop_back();
q.push_back(a[i]);//将新进入的元素入队
if(i - k >= 1 && q.front() == a[i - k])//若队头是否滑出了窗口,队头出队
q.pop_front();
if(i >= k)//当窗口形成,输出队头对应的值
cout << q.front() <<" ";
}
q.clear();
cout << endl;
//最大值亦然
for(int i = 1; i <= n; i++)
{
while(q.size() && q.back() < a[i]) q.pop_back();
q.push_back(a[i]);
if(i - k >= 1 && a[i - k] == q.front()) q.pop_front();
if(i >= k) cout << q.front() << " ";
}
}
7.KMP
给定一个字符串 S,以及一个模式串 P,所有字符串中只包含大小写英文字母以及阿拉伯数字。
模式串 P 在字符串 S 中多次作为子串出现。
求出模式串 P 在字符串 S 中所有出现的位置的起始下标。
#include <iostream>
using namespace std;
const int N=1e6+10;
char p[N],s[N];
int ne[N];
int n,m;
int main(){
cin>>n>>p+1>>m>>s+1;
for(int i=2,j=0;i<=n;i++){
while(j && p[i]!=p[j+1]) j=ne[j];
if(p[i]==p[j+1]) j++;
ne[i]=j;
}
for(int i=1,j=0;i<=m;i++){
while(j && s[i]!=p[j+1]) j=ne[j];
if(s[i]==p[j+1]) j++;
if(j==n){
cout<<(i-j)<<" ";
j=ne[j];
}
}
}
8.Trie
(1)Trie字符串统计
维护一个字符串集合,支持两种操作:
I x
向集合中插入一个字符串 x;Q x
询问一个字符串在集合中出现了多少次。
共有 N 个操作,所有输入的字符串总长度不超过 10^5,字符串仅包含小写英文字母。
// insert最后是cnt[p]++,query最后也是返回的cnt[p],索引都是p
#include<iostream>
using namespace std;
const int N = 100010;
int idx; // 各个节点的编号,根节点编号为0
int son[N][26];//Trie 树本身
//cnt[x] 表示:以 编号为 x 为结尾的字符串的个数
int cnt[N];
int n;
void insert(string s){
int p = 0;//指向根节点
for(int i = 0; i < s.size(); i++){
//将当前字符转换成数字(a->0, b->1,...)
int u = s[i] - 'a';
//如果数中不能走到当前字符
//为当前字符创建新的节点,保存该字符
if(!son[p][u])
// 新节点编号为 idx + 1
son[p][u] = ++idx;
p = son[p][u];
}
//这个时候,p 等于字符串 s 的尾字符所对应的 idx
//cnt[p] 保存的是字符串 s 出现的次数
//故 cnt[p] ++
cnt[p] ++;
}
int query(string s){
int p = 0;//指向根节点
for(int i = 0; i < s.size(); i++){
//将当前字符转换成数字(a->0, b->1,...)
int u = s[i] - 'a';
//如果走不通了,即树中没有保存当前字符
//则说明树中不存在该字符串
if(!son[p][u])
return 0;
//指向下一个节点
p = son[p][u];
}
//循环结束的时候,p 等于字符串 s 的尾字符所对应的 idx
// cnt[p] 就是字符串 s 出现的次数
return cnt[p];
}
int main(){
cin >> n;
string s;
char q;
while(n--){
cin >> q >> s;
if(q == 'I'){
//插入操作
insert(s);
}
else{
//查询操作
cout << query(s) << endl;
}
}
}
(2)最大异或对
在给定的 N 个整数 A1,A2……AN 中选出两个进行 xor(异或)运算,得到的结果最大是多少?
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 100010;
//保存 Trie 树
int son[N * 31][2];
int n, idx;
void insert(int x)
{
int p = 0;//初始化指向根节点
//从最高位开始,依次取出每一位
for (int i = 31; i >= 0; i--)
{ // 取出当前位
int u = x >> i & 1;
//如果树中不能走到当前数字
//为当前数字创建新的节点,保存该数字
if (!son[p][u])
// 新节点编号为 idx + 1
son[p][u] = ++idx;
p = son[p][u];
}
}
int query(int x)
{
//指向根节点
int p = 0;
// 保存与 x 异或结果最大的那个数
int ret = 0;
//从最高位开始,依次取出 x 的每一位
for (int i = 31; i >= 0; i--)
{
// 取出 x 的当前位
int u = x >> i & 1;
//如果树中能走到 !u,就走到!u
if (son[p][!u]){
//走到!u
p = son[p][!u];
//更新 x 异或的对象
ret = ret * 2 + !u;
}
//没有!u,就只能走到u了
else{
p = son[p][u];
//更新 x 异或的对象
ret = ret * 2 + u;
}
}
//计算异或结果
ret = ret ^ x;
return ret;
}
int main()
{
cin >> n;
int maxXorNum = 0;
int x;
for (int i = 0; i < n; i++)
{
cin >> x;
insert(x);
maxXorNum = max(maxXorNum, query(x));
}
cout << maxXorNum << endl;
return 0;
}
9.并查集
(1)合并集合
一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。
现在要进行 m 个操作,操作共有两种:
M a b
,将编号为 a 和 b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;Q a b
,询问编号为 a 和 b 的两个数是否在同一个集合中;
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int p[N];
int find(int x){ //返回x的祖先节点 + 路径压缩
//祖先节点的父节点是自己本身
if(p[x] != x){
//将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; //初始化,让数x的父节点指向自己
while(m --){
char op[2];
int a, b;
scanf("%s%d%d", op, &a, &b);
if(op[0] == 'M') p[find(a)] = find(b); //将a的祖先点的父节点置为b的祖先节点
else{
if(find(a) == find(b)) puts("Yes");
else puts("No");
}
}
return 0;
}
(2)连通块中点的数量
给定一个包含 n 个点(编号为 1∼n)的无向图,初始时图中没有边。
现在要进行 m个操作,操作共有三种:
C a b
,在点 a 和点 b 之间连一条边,a 和 b 可能相等;Q1 a b
,询问点 a 和点 b 是否在同一个连通块中,a 和 b 可能相等;Q2 a
,询问点 a 所在连通块中点的数量;
#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int p[N], s[N];
int find(int x){
if(p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i ++){
p[i] = i;
s[i] = 1;
}
while(m --)
{
char op[5];
int a, b;
scanf("%s", op);
if(op[0] == 'C'){ //合并
scanf("%d%d", &a, &b);
if(find(a) == find(b)) continue;
s[find(b)] += s[find(a)];
p[find(a)] = find(b);
}
else if(op[1] == '1'){ //Q1
scanf("%d%d", &a, &b);
if(find(a) == find(b)) puts("Yes");
else puts("No");
}
else{
scanf("%d", &a);
printf("%d\n", s[find(a)]);
}
}
return 0;
}
(3)食物链
动物王国中有三类动物 A,B,C,这三类动物的食物链构成了有趣的环形。
A 吃 B,B 吃 C,C 吃 A。
现有 N 个动物,以 1∼N编号。每个动物都是 A,B,C中的一种,但是并不知道它到底是哪一种。
有人用两种说法对这 N 个动物所构成的食物链关系进行描述:
第一种说法是 1 X Y
,表示 X 和 Y 是同类。
第二种说法是 2 X Y
,表示 X 吃 Y。
此人对 N 个动物,用上述两种说法,一句接一句地说出 K 句话,这 K 句话有的是真的,有的是假的。
当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
- 当前的话与前面的某些真的话冲突,就是假话;
- 当前的话中 X 或 Y 比 N 大,就是假话;
- 当前的话表示 X 吃 X,就是假话。
你的任务是根据给定的 N 和 K 句话,输出假话的总数。
#include<iostream>
using namespace std;
const int N = 50010;
int n, k, res;
int p[N]; //父节点
int d[N]; //到父节点的距离
int find(int x)
{
if(p[x] != x){
int t = find(p[x]); //先把父节点及以上压缩到树根
d[x] += d[p[x]]; //更新边权
p[x] = t; //x节点也压缩到树根
}
return p[x];
}
int main()
{
cin >> n >> k;
for(int i = 1; i <= n; i++) p[i] = i;
while(k--){
int v, x, y;
cin >> v >> x >> y;
if(x > n || y > n) res++;
else{
int rx = find(x), ry = find(y);
if(v == 1){
//假话
if(rx == ry && (d[y] - d[x]) % 3) res++;
//真话
else if(rx != ry){ //当前不在同一集合中,无法判定为假。故为真,应加入同一集合表示存在同类关系
p[rx] = ry;
d[rx] = d[y] - d[x]; //(d[x]+d[rx]-d[y])%3 = 0,由于判断时都针对mod 3,故3可省略
}
}
else{ //x吃y
if(x == y) res++;
else if(rx == ry && (d[x] - d[y] - 1) % 3) res++; //C++中负数取模得非正数,需要注意别写错
else if(rx != ry){
p[rx] = ry;
d[rx] = d[y] + 1 - d[x];
}
}
}
}
cout << res << endl;
}
10.堆
(1)堆排序
输入一个长度为 n 的整数数列,从小到大输出前 m 小的数。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int a[N];//保存数组
int n, m;//n个点,求前m小
int r ;//堆得右边界
void down(int u)//调整函数
{
//t记录最小点的编号
int t = u;
//有左儿子,并且左儿子比t节点的值小,更新t
if(2 * u <= r && a[2 * u] < a[u]) t = 2 * u;
//有右儿子,并且右儿子比t节点的值小,更新t
if(2 * u + 1 <= r && a[2 * u + 1] < a[t]) t = 2 * u + 1;
//如果待调整点不是最小的
if(u != t)
{
//和最小的交换
swap(a[u], a[t]);
//递归处理
down(t);
}
}
int main()
{
cin >> n >> m;
r = n;//开始时,右边界是数组边界
//读入数据
for (int i = 1; i <= n; i ++ )
{
cin >> a[i];
}
//从第一个非叶节点开始,从右到左,从下到上处理每个节点
for(int i = n /2 ; i >= 1; i--)
{
down(i);
}
//输出m个最小值
while (m -- )
{
//堆顶保存的最小值,输出堆顶
cout << a[1] << " ";
//将堆顶和右边界交换
swap(a[1], a[r]);
//右边界左移
r--;
//从新处理堆顶
down(1);
}
}
(2)模拟堆
维护一个集合,初始时集合为空,支持如下几种操作:
I x
,插入一个数 x;PM
,输出当前集合中的最小值;DM
,删除当前集合中的最小值(数据保证此时的最小值唯一);D k
,删除第 k 个插入的数;C k x
,修改第 k 个插入的数,将其变为 x;
现在要进行 N 次操作,对于所有第 2 个操作,输出当前集合的最小值。
#include<iostream>
using namespace std;
int const maxn =1e5+10;
int h[maxn],kp[maxn],pk[maxn],idx,len;
void h_swap(int a,int b)
{
swap(h[a],h[b]); //交换数值
swap(pk[a],pk[b]); //交换pk映射
swap(kp[pk[a]],kp[pk[b]]); //交换kp映射
}
void down(int u) //需要down的根节点
{
int t=u; //t用来存储u的最小儿子
if(2*u<=len && h[u*2]<h[t]) t=u*2;
if(2*u+1<=len && h[u*2+1]<h[t]) t=u*2+1;
if(t!=u) //如果t不相等意味着u还不满足小堆的条件于最小儿子交换后继续down
{
h_swap(t,u); //当有映射关系时需要用这个自定义函数
down(t); //递归
}
}
void up(int u)
{
int t=u; //up中的t保存的是父结点
if(u/2>0 && h[u/2]>h[t]) t=u/2; //up操作中只需要判断up儿子与根的大小就可
if(t!=u) //递归操作
{
h_swap(t,u);
up(t);
}
}
int main()
{
int n; cin>>n;
while(n--)
{
string aim; cin>>aim;
if(aim=="I")
{
int x; cin>>x;
//insert(int x)操作
h[++len]=x;
pk[len]=++idx;
kp[idx]=len;
up(len);
}
else if(aim=="PM")
{
cout<<h[1]<<endl;
}
else if(aim=="DM")
{
h_swap(1,len--);
down(1);
}
else if(aim=="D")
{
int k; cin>>k;
int u=kp[k]; //一定要注意这个地方
/*
我之前是这么写的
{
h_swap(kp[k],len--)
up(kp[k]);
down(kp[k]);
}
这么写会在h_swap之后 kp[k]其实是len,所以要提前保存在u中
*/
h_swap(kp[k],len--);
up(u);
down(u);
}
else if(aim=="C")
{
int k,x; cin>>k>>x;
h[kp[k]]=x;
up(kp[k]);
down(kp[k]);
}
//for(int i=1;i<=len;i++) cout<<h[i]<<" ";
// cout<<"---------"<<endl;
}
}
11.哈希表
(1)模拟散列表
维护一个集合,支持如下几种操作:
I x
,插入一个整数 x;Q x
,询问整数 x 是否在集合中出现过;
现在要进行 N 次操作,对于每个询问操作输出对应的结果。
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e5 + 3; // 取大于1e5的第一个质数,取质数冲突的概率最小 可以百度
//* 开一个槽 h
int h[N], e[N], ne[N], idx; //邻接表
void insert(int x) {
// c++中如果是负数 那他取模也是负的 所以 加N 再 %N 就一定是一个正数
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
bool find(int x) {
//用上面同样的 Hash函数 讲x映射到 从 0-1e5 之间的数
int k = (x % N + N) % N;
for (int i = h[k]; i != -1; i = ne[i]) {
if (e[i] == x) {
return true;
}
}
return false;
}
int n;
int main() {
cin >> n;
memset(h, -1, sizeof h); //将槽先清空 空指针一般用 -1 来表示
while (n--) {
string op;
int x;
cin >> op >> x;
if (op == "I") {
insert(x);
} else {
if (find(x)) {
puts("Yes");
} else {
puts("No");
}
}
}
return 0;
}
(2)字符串哈希
给定一个长度为 n 的字符串,再给定 m 个询问,每个询问包含四个整数 l1,r1,l2,r2,请你判断 [l1,r1]和 [l2,r2]这两个区间所包含的字符串子串是否完全相同。
字符串中只包含大小写英文字母和数字。
#include<iostream>
#include<cstdio>
#include<string>
using namespace std;
typedef unsigned long long ULL;
const int N = 1e5+5,P = 131;//131 13331
ULL h[N],p[N];
// h[i]前i个字符的hash值
// 字符串变成一个p进制数字,体现了字符+顺序,需要确保不同的字符串对应不同的数字
// P = 131 或 13331 Q=2^64,在99%的情况下不会出现冲突
// 使用场景: 两个字符串的子串是否相同
ULL query(int l,int r){
return h[r] - h[l-1]*p[r-l+1];
}
int main(){
int n,m;
cin>>n>>m;
string x;
cin>>x;
//字符串从1开始编号,h[1]为前一个字符的哈希值
p[0] = 1;
h[0] = 0;
for(int i=0;i<n;i++){
p[i+1] = p[i]*P;
h[i+1] = h[i]*P +x[i]; //前缀和求整个字符串的哈希值
}
while(m--){
int l1,r1,l2,r2;
cin>>l1>>r1>>l2>>r2;
if(query(l1,r1) == query(l2,r2)) printf("Yes\n");
else printf("No\n");
}
return 0;
}
三、搜索与图论
1.DFS
(1)排列数字
给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
#include<iostream>
using namespace std;
const int N = 10;
int path[N];//保存序列
int state[N];//数字是否被用过
int n;
void dfs(int u)
{
if(u > n)//数字填完了,输出
{
for(int i = 1; i <= n; i++)//输出方案
cout << path[i] << " ";
cout << endl;
}
for(int i = 1; i <= n; i++)//空位上可以选择的数字为:1 ~ n
{
if(!state[i])//如果数字 i 没有被用过
{
path[u] = i;//放入空位
state[i] = 1;//数字被用,修改状态
dfs(u + 1);//填下一个位
state[i] = 0;//回溯,取出 i
}
}
}
int main()
{
cin >> n;
dfs(1);
}
(2)n-皇后问题
将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
给定整数 n,请你输出所有的满足条件的棋子摆法。
#include <iostream>
using namespace std;
const int N=20;
int n;
char g[N][N];
int col[N],dg[N],udg[N];
void dfs(int u){
if(u == n){
for(int i=0;i<n;i++) puts(g[i]);
puts("");
sum++;
return;
}
for(int i=0;i<n;i++){
if(!col[i] && !dg[u+i] && !udg[n-u+i]){
col[i]=dg[u+i]=udg[n-u+i]=1;
g[u][i]='Q';
dfs(u+1);
g[u][i]='.';
col[i]=dg[u+i]=udg[n-u+i]=0;
}
}
}
int main(){
scanf("%d",&n);
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
g[i][j]='.';
}
}
dfs(0);
}
2.BFS
(1)走迷宫
给定一个 n×m 的二维整数数组,用来表示一个迷宫,数组中只包含 0 或 1,其中 0 表示可以走的路,1 表示不可通过的墙壁。
最初,有一个人位于左上角 (1,1) 处,已知该人每次可以向上、下、左、右任意一个方向移动一个位置。
请问,该人从左上角移动至右下角 (n,m)处,至少需要移动多少次。
数据保证 (1,1)处和 (n,m)处的数字为 0,且一定至少存在一条通路。
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 110;
int g[N][N];//存储地图
int f[N][N];//存储距离
int n, m;
void bfs(int a, int b)//广度优先遍历
{
queue<PII> q;
q.push({a, b});
//初始点的距离为 0.
//可以不要这一句,因为f初始化的时候,各个点为0
f[0][0] = 0;
while(!q.empty())
{
PII start = q.front();
q.pop();
//这一句可以不要,因为入队的时候就置为了1
g[start.first][start.second] = 1;
int dx[4] = {0, 1, 0, -1}, dy[4] = {-1, 0, 1, 0};
for(int i = 0; i < 4; i++)//往四个方向走
{
//当前点能走到的点
int x = start.first + dx[i], y = start.second + dy[i];
//如果还没有走过
if(g[x][y] == 0)
{
//走到这个点,并计算距离
g[x][y] = 1;
f[x][y] = f[start.first][start.second] + 1;//从当前点走过去,则距离等于当前点的距离+1.
//这个点放入队列,用来走到和它相邻的点。
q.push({x, y});
}
}
}
cout << f[n][m];
}
int main()
{
memset(g, 1, sizeof(g));
cin >> n >>m;
for(int i = 1; i <= n; i++)
{
for(int j = 1; j <= m; j++)
{
cin >> g[i][j];
}
}
bfs(1,1);
}
(2)八数码问题
输入格式
输入占一行,将 3×3 的初始网格描绘出来。例如,如果初始网格如下所示:
1 2 3
x 4 6
7 5 8
则输入为:1 2 3 x 4 6 7 5 8
输出格式
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 x
输出占一行,包含一个整数,表示最少交换次数。
如果不存在解决方案,则输出 −1。
#include <iostream>
#include <algorithm>
#include <queue>
#include <unordered_map>
using namespace std;
int bfs(string start)
{
//定义目标状态
string end = "12345678x";
//定义队列和dist数组
queue<string> q;
unordered_map<string, int> d;
//初始化队列和dist数组
q.push(start);
d[start] = 0;
//转移方式
int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};
while(q.size())
{
auto t = q.front();
q.pop();
//记录当前状态的距离,如果是最终状态则返回距离
int distance = d[t];
if(t == end) return distance;
//查询x在字符串中的下标,然后转换为在矩阵中的坐标
int k = t.find('x');
int x = k / 3, y = k % 3;
for(int i = 0; i < 4; i++)
{
//求转移后x的坐标
int a = x + dx[i], b = y + dy[i];
//当前坐标没有越界
if(a >= 0 && a < 3 && b >= 0 && b < 3)
{
//转移x
swap(t[k], t[a * 3 + b]);
//如果当前状态是第一次遍历,记录距离,入队
if(!d.count(t))
{
d[t] = distance + 1;
q.push(t);
}
//还原状态,为下一种转换情况做准备
swap(t[k], t[a * 3 + b]);
}
}
}
//无法转换到目标状态,返回-1
return -1;
}
int main()
{
string c, start;
//输入起始状态
for(int i = 0; i < 9; i++)
{
cin >> c;
start += c;
}
cout << bfs(start) << endl;
return 0;
}
3.树与图的dfs-树的重心
给定一颗树,树中包含 n 个结点(编号 1∼n)和 n−1 条无向边。
请你找到树的重心,并输出将重心删除后,剩余各个连通块中点数的最大值。
重心定义:重心是指树中的一个结点,如果将这个点删除后,剩余各个连通块中点数的最大值最小,那么这个节点被称为树的重心。
#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;
}
4.树与图的bfs-图中点的层次
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环。
所有边的长度都是 1,点的编号为 1∼n。
请你求出 1 号点到 n 号点的最短距离,如果从 1 号点无法走到 n 号点,输出 −1。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int h[N],ne[N], e[N], idx;//邻接表数据结构
int dist[N];//存储距离
int st[N];//标记点是否走到过
int n, m;
void add(int a, int b)//邻接表存储图
{
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void bfs()
{
memset(dist, 0x3f, sizeof(dist));//初始都没有走到过,距离无穷大
dist[1] = 0;//从1号节点开始,距离为0
queue<int> q;//队列
q.push(1);//1号节点入队列
st[1] = 1;//1到1的距离为0,已经求出
while(q.size())//对列非空,就一直往后搜索
{
int t = q.front();//队头出队,找该点能到的点
q.pop();
for(int i = h[t]; i != -1; i = ne[i])//遍历所有t节点能到的点,i为节点索引
{
int j = e[i];//通过索引i得到t能到的节点编号
if(!st[j])//如果没有遍历过
{
dist[j] = dist[t] + 1;//距离为t号节点的距离+1
q.push(j);//节点入队
st[j] = 1;//入队后标记,已经遍历过了
}
}
}
}
int main()
{
cin >> n >>m;
memset(h, -1, sizeof h);//初始化,所有节点没有后继,后继都是-1
for(int i = 0; i < m; i++)//读入所有边
{
int a, b;
cin >> a >> b;
add(a, b);//加入邻接表
}
bfs();//广度优先遍历
cout << (dist[n] == 0x3f3f3f3f ? -1 : dist[n]);//如果到n号节点的距离不是无穷大,输出距离,如果是无穷大,输出-1.
return 0;
}
5.拓扑排序
给定一个 n 个点 m 条边的有向图,点的编号是 1 到 n,图中可能存在重边和自环。
请输出任意一个该有向图的拓扑序列,如果拓扑序列不存在,则输出 −1。
若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int e[N], ne[N], idx;//邻接表存储图
int h[N];
int q[N], hh = 0, tt = -1;//队列保存入度为0的点,也就是能够输出的点,
int n, m;//保存图的点数和边数
int d[N];保存各个点的入度
void add(int a, int b){
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
void topsort(){
for(int i = 1; i <= n; i++){//遍历一遍顶点的入度。
if(d[i] == 0)//如果入度为 0, 则可以入队列
q[++tt] = i;
}
while(tt >= hh){//循环处理队列中点的
int a = q[hh++];
for(int i = h[a]; i != -1; i = ne[i]){//循环删除 a 发出的边
int b = e[i];//a 有一条边指向b
d[b]--;//删除边后,b的入度减1
if(d[b] == 0)//如果b的入度减为 0,则 b 可以输出,入队列
q[++tt] = b;
}
}
if(tt == n - 1){//如果队列中的点的个数与图中点的个数相同,则可以进行拓扑排序
for(int i = 0; i < n; i++){//队列中保存了所有入度为0的点,依次输出
cout << q[i] << " ";
}
}
else//如果队列中的点的个数与图中点的个数不相同,则可以进行拓扑排序
cout << -1;//输出-1,代表错误
}
int main(){
cin >> n >> m;//保存点的个数和边的个数
memset(h, -1, sizeof h);//初始化邻接矩阵
while (m -- ){//依次读入边
int a, b;
cin >> a >> b;
d[b]++;//顶点b的入度+1
add(a, b);//添加到邻接矩阵
}
topsort();//进行拓扑排序
return 0;
}
6.最短路径
(1)Dijkstra
849. Dijkstra求最短路 I - AcWing题库
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1
#include<iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010;
int h[N], e[M], ne[M], w[M], idx;//邻接表存储图
int state[N];//state 记录是否找到了源点到该节点的最短距离
int dist[N];//dist 数组保存源点到其余各个节点的距离
int n, m;//图的节点个数和边数
void add(int a, int b, int c)//插入边
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
void Dijkstra()
{
memset(dist, 0x3f, sizeof(dist));//dist 数组的各个元素为无穷大
dist[1] = 0;//源点到源点的距离为置为 0
for (int i = 0; i < n; i++)
{
int t = -1;
for (int j = 1; j <= n; j++)//遍历 dist 数组,找到没有确定最短路径的节点中距离源点最近的点t
{
if (!state[j] && (t == -1 || dist[j] < dist[t]))
t = j;
}
state[t] = 1;//state[i] 置为 1。
for (int j = h[t]; j != -1; j = ne[j])//遍历 t 所有可以到达的节点 i
{
int i = e[j];
dist[i] = min(dist[i], dist[t] + w[j]);//更新 dist[j]
}
}
}
int main()
{
memset(h, -1, sizeof(h));//邻接表初始化
cin >> n >> m;
while (m--)//读入 m 条边
{
int a, b, w;
cin >> a >> b >> w;
add(a, b, w);
}
Dijkstra();
if (dist[n] != 0x3f3f3f3f)//如果dist[n]被更新了,则存在路径
cout << dist[n];
else
cout << "-1";
}
堆:(优化)
// 注意while中的if(!st[ver]);
此外,最后heap.push({d[j],j})前,可以加一个if(!st[j]),但不加也可以过;
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N=510,M=1e5+10;
//const int N=15e4+10,M=15e4+10;
int h[N],e[M],ne[M],w[M],idx;
int st[N],d[N];
int n,m;
typedef pair<int,int> PII;
void add(int a,int b,int c){
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
void Dijkstra(){
memset(d,0x3f,sizeof d);
d[1]=0;
priority_queue<PII,vector<PII>,greater<PII>> heap;
heap.push({0,1});
while(!heap.empty()){
auto t=heap.top();
heap.pop();
int ver=t.second,dist=t.first;
if(!st[ver]){
st[ver]=1;
for(int i=h[ver];i!=-1;i=ne[i]){
int j=e[i];
if(d[j] > d[ver]+w[i]){
d[j]=d[ver]+w[i];
heap.push({d[j],j});
}
}
}
}
}
int main(){
memset(h,-1,sizeof h);
cin>>n>>m;
while(m--){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
Dijkstra();
if(d[n]==0x3f3f3f3f) cout<<-1;
else cout<<d[n];
}
(2)Bellman-ford
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible
。
注意:图中可能 存在负权回路 。
// 结构体struct Edge,数组d[N],back[N];
#include<iostream>
#include<cstring>
using namespace std;
const int N = 510, M = 10010;
struct Edge { //使用结构体存储边,不用定义一大堆数组去加边
int a;
int b;
int w;
} e[M];//把每个边保存下来即可
int dist[N];
int back[N];//备份数组防止串联
int n, m, k;//k代表最短路径最多包涵k条边
void bellman_ford() {
memset(dist, 0x3f, sizeof dist); //dist初始化位无穷,八字节3f3f3f3f大于1e9
dist[1] = 0;
for (int i = 0; i < k; i++) {//k次循环
memcpy(back, dist, sizeof dist);//
for (int j = 0; j < m; j++) {//遍历所有边,而dijkstra是遍历所有顶点n*n
int a = e[j].a, b = e[j].b, w = e[j].w;
dist[b] = min(dist[b], back[a] + w);
//使用backup:避免给a更新后立马更新b, 这样b一次性最短路径就多了两条边出来
}
}
}
int main() {
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < m; i++) {
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
e[i] = {a, b, w};
}
bellman_ford();
if(dist[n]>0x3f3f3f3f/2) puts("impossible");
else printf("%d",dist[n]);
return 0;
}
(3)spfa
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible
。
数据保证不存在负权回路。
#include <iostream>
#include <queue>
#include <cstring>
using namespace std;
const int N=1e5+10;
int h[N],e[N],ne[N],w[N],idx;
int st[N],d[N];
int n,m;
void add(int a,int b,int c){
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
void spfa(){
memset(d,0x3f,sizeof d);
d[1]=0;
queue<int> q;
q.push(1);
st[1]=1;
while(!q.empty()){
int t=q.front();
q.pop();
st[t]=0;
for(int i=h[t];i!=-1;i=ne[i]){
int b=e[i],c=w[i];
if(d[b] > d[t]+c){
d[b]=d[t]+c;
if(!st[b]){
st[b]=1;
q.push(b);
}
}
}
}
}
int main(){
memset(h,-1,sizeof h);
cin>>n>>m;
while(m--){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
spfa();
if(d[n] > 0x3f3f3f3f/2) cout<<"impossible";
else cout<<d[n];
}
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N = 1e5 + 10;
int h[N], e[N], w[N], ne[N], idx;
int n, m;
queue<int> q;
int st[N], dist[N], cnt[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
int spfa() {
memset(dist, 0x3f, sizeof dist);
for (int i = 1; i <= n; i++) {
q.push(i);
st[i] = true;
}
st[1] = true;
while (q.size()) {
int t = q.front();
q.pop();
st[t] = false;
for (int i = h[t]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[t] + w[i]) {
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
// 如果超过了n-1
// 根据抽屉原理,说明经过某个节点两次,则说明有环
if (cnt[j] >= n) return true;
if (!st[j]) {
st[j] = true;
q.push(j);
}
}
}
}
return false;
}
int main() {
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 0; i < m; i++) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
if (spfa()) puts("Yes");
else puts("No");
}
(4)floyd
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出 impossible
。
数据保证图中不存在负权回路。
#include<iostream>
#include<cstring>
using namespace std;
const int N = 210;
int dist[N][N];
int n,m,q;
void floyd(){
for(int k = 1; k <= n; k ++){
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= n; j ++){
dist[i][j] = min(dist[i][j],dist[i][k] + dist[k][j]);
}
}
}
}
int main(){
cin >> n >> m >> q;
memset(dist,0x3f,sizeof dist);
for(int i = 1; i <= n; i ++) dist[i][i] = 0;
while(m --){
int a,b,c;
cin >> a >> b >> c;
dist[a][b] = min(dist[a][b],c);
}
floyd();
while(q --){
int x,y;
cin >> x >> y;
if(dist[x][y] >= 0x3f3f3f3f/2) printf("impossible\n"); //为什么是>=03xf3f3f3f/2?因为存在负权边 所以0x3f3f3f3f这个值可能会因为负值被更新 但值依然远大于0x3f3f3f3f/2
else printf("%d\n",dist[x][y]);
}
return 0;
}
7.最小生成树
(1)Prim
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible
。
给定一张边带权的无向图 G=(V,E),其中 V 表示图中点的集合,E 表示图中边的集合,n=|V|,m=|E|。
由 V 中的全部 n 个顶点和 E 中 n−1条边构成的无向连通子图被称为 G 的一棵生成树,其中边的权值之和最小的生成树被称为无向图 G 的最小生成树。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int g[N][N];//存储图
int dt[N];//存储各个节点到生成树的距离
int st[N];//节点是否被加入到生成树中
int pre[N];//节点的前去节点
int n, m;//n 个节点,m 条边
void prim()
{
memset(dt,0x3f, sizeof(dt));//初始化距离数组为一个很大的数(10亿左右)
int res= 0;
dt[1] = 0;//从 1 号节点开始生成
for(int i = 0; i < n; i++)//每次循环选出一个点加入到生成树
{
int t = -1;
for(int j = 1; j <= n; j++)//每个节点一次判断
{
if(!st[j] && (t == -1 || dt[j] < dt[t]))//如果没有在树中,且到树的距离最短,则选择该点
t = j;
}
//2022.6.1 发现测试用例加强后,需要判断孤立点了
//如果孤立点,直返输出不能,然后退出
if(dt[t] == 0x3f3f3f3f) {
cout << "impossible";
return;
}
st[t] = 1;// 选择该点
res += dt[t];
for(int i = 1; i <= n; i++)//更新生成树外的点到生成树的距离
{
if(dt[i] > g[t][i] && !st[i])//从 t 到节点 i 的距离小于原来距离,则更新。
{
dt[i] = g[t][i];//更新距离
pre[i] = t;//从 t 到 i 的距离更短,i 的前驱变为 t.
}
}
}
cout << res;
}
void getPath()//输出各个边
{
for(int i = n; i > 1; i--)//n 个节点,所以有 n-1 条边。
{
cout << i <<" " << pre[i] << " "<< endl;// i 是节点编号,pre[i] 是 i 节点的前驱节点。他们构成一条边。
}
}
int main()
{
memset(g, 0x3f, sizeof(g));//各个点之间的距离初始化成很大的数
cin >> n >> m;//输入节点数和边数
while(m --)
{
int a, b, w;
cin >> a >> b >> w;//输出边的两个顶点和权重
g[a][b] = g[b][a] = min(g[a][b],w);//存储权重
}
prim();//求最下生成树
//getPath();//输出路径
return 0;
}
(2)Kruskal
859. Kruskal算法求最小生成树 - AcWing题库
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环,边权可能为负数。
求最小生成树的树边权重之和,如果最小生成树不存在则输出 impossible
。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int p[N];//保存并查集
struct E{
int a;
int b;
int w;
bool operator < (const E& rhs){//通过边长进行排序
return this->w < rhs.w;
}
}edg[N * 2];
int res = 0;
int n, m;
int cnt = 0;
int find(int a){//并查集找祖宗
if(p[a] != a) p[a] = find(p[a]);
return p[a];
}
void klskr(){
for(int i = 1; i <= m; i++)//依次尝试加入每条边
{
int pa = find(edg[i].a);// a 点所在的集合
int pb = find(edg[i].b);// b 点所在的集合
if(pa != pb){//如果 a b 不在一个集合中
res += edg[i].w;//a b 之间这条边要
p[pa] = pb;// 合并a b
cnt ++; // 保留的边数量+1
}
}
}
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i++) p[i] = i;//初始化并查集
for(int i = 1; i <= m; i++){//读入每条边
int a, b , c;
cin >> a >> b >>c;
edg[i] = {a, b, c};
}
sort(edg + 1, edg + m + 1);//按边长排序
klskr();
//如果保留的边小于点数-1,则不能连通
if(cnt < n - 1) {
cout<< "impossible";
return 0;
}
cout << res;
return 0;
}
8.染色法判定二分图
给定一个 n 个点 m 条边的无向图,图中可能存在重边和自环。
请你判断这个图是否是二分图。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010 * 2;
int e[N], ne[N], idx;//邻接表存储图
int h[N];
int color[N];//保存各个点的颜色,0 未染色,1 是红色,2 是黑色
int n, m;//点和边
void add(int a, int b)//邻接表插入点和边
{
e[idx] = b, ne[idx]= h[a], h[a] = idx++;
}
bool dfs(int u, int c)//深度优先遍历
{
color[u] = c;//u的点成 c 染色
//遍历和 u 相邻的点
for(int i = h[u]; i!= -1; i = ne[i])
{
int b = e[i];
if(!color[b])//相邻的点没有颜色,则递归处理这个相邻点
{
if(!dfs(b, 3 - c)) return false;//(3 - 1 = 2, 如果 u 的颜色是2,则和 u 相邻的染成 1)
//(3 - 2 = 1, 如果 u 的颜色是1,则和 u 相邻的染成 2)
}
else if(color[b] && color[b] != 3 - c)//如果已经染色,判断颜色是否为 3 - c
{
return false;//如果不是,说明冲突,返回
}
}
return true;
}
int main()
{
memset(h, -1, sizeof h);//初始化邻接表
cin >> n >> m;
for(int i = 1; i <= m; i++)//读入边
{
int a, b;
cin >> a >> b;
add(a, b), add(b, a);
}
for(int i = 1; i <= n; i++)//遍历点
{
if(!color[i])//如果没染色
{
if(!dfs(i, 1))//染色该点,并递归处理和它相邻的点
{
cout << "No" << endl;//出现矛盾,输出NO
return 0;
}
}
}
cout << "Yes" << endl;//全部染色完成,没有矛盾,输出YES
return 0;
}
9.匈牙利算法
给定一个二分图,其中左半部包含 n1 个点(编号 1∼n1),右半部包含 n2 个点(编号 1∼n2),二分图共包含 m 条边。
数据保证任意一条边的两个端点都不可能在同一部分中。
请你求出二分图的最大匹配数。
二分图的匹配:给定一个二分图 G,在 G 的一个子图 M 中,M 的边集 {E} 中的任意两条边都不依附于同一个顶点,则称 M 是一个匹配。
二分图的最大匹配:所有匹配中包含边数最多的一组匹配被称为二分图的最大匹配,其边数即为最大匹配数。
#include<iostream>
#include <cstring>
#include<algorithm>
using namespace std;
// 邻接表存储图
int n1, n2, m;
int h[500], e[100010],ne[100010], idx = 0;
//st 标记是否递归找过, match[x]:和 x 编号的男生的编号
int st[510], match[510];
//存图函数
void add(int a, int b){
e[idx] = b, ne[idx] = h[a]; h[a] = idx++;
}
//递归找可以匹配的点
bool find(int x){
// 和各个点尝试能否匹配
for(int i = h[x]; i != -1; i = ne[i]){
int b = e[i];
if(!st[b]){//打标记
st[b] = 1;
// 当前尝试点没有被匹配或者和当前尝试点匹配的那个点可以换另一个匹配
if(match[b] == 0 || find(match[b])){
// 和当前尝试点匹配在一起
match[b] = x;
return true;
}
}
}
return false;
}
int main(){
memset(h, -1, sizeof h);
cin >> n1 >> n2 >> m;
// 保存图,因为只从一遍找另一边,所以该无向图只需要存储一个方向
for(int i = 0; i < m; i++){
int a, b;
cin >> a >> b;
add(a, b);
}
int res = 0;
//为各个点找匹配
for(int i = 1; i <= n1; i++){
memset(st, 0, sizeof st);
//找到匹配
if(find(i)) res++;
}
cout << res;
return 0;
}
四、数学知识
1.质数
(1)试除法判定质数
给定 n 个正整数 ai,判定每个数是否是质数。
#include <iostream>
#include <algorithm>
using namespace std;
bool is_prime(int x)
{
if (x < 2) return false;
for (int i = 2; i <= x / i; i ++ )//不要用开方函数或者i*i小于x。开方函数慢,i*i可能越界
if (x % i == 0)
return false;
return true;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
cin >> x;
if (is_prime(x)) cout << "Yes" << endl;
else cout << "No" << endl;;
}
return 0;
}
(2)分解质因数
给定 n 个正整数 ai,将每个数分解质因数,并按照质因数从小到大的顺序输出每个质因数的底数和指数。
#include <iostream>
#include <algorithm>
using namespace std;
void divide(int x)
{
for (int i = 2; i <= x / i; i ++ )//i <= x / i:防止越界,速度大于 i < sqrt(x)
if (x % i == 0)//i为底数
{
int s = 0;//s为指数
while (x % i == 0) x /= i, s ++ ;
cout << i << ' ' << s << endl;//输出
}
if (x > 1) cout << x << ' ' << 1 << endl;//如果x还有剩余,单独处理
cout << endl;
}
int main()
{
int n;
cin >> n;
while (n -- )
{
int x;
cin >> x;
divide(x);
}
return 0;
}
(3)筛质数
给定一个正整数 n,请你求出 1∼n 中质数的个数。
#include <iostream>
using namespace std;
const int N=1e6+10;
int primes[N],cnt;
int st[N];
// 最普通的筛法 O(nlogn)
void get_primes_nlogn(int n){
for(int i=2;i<=n;i++){
if(!st[i]) primes[cnt++]=i; //把素数存起来
for(int j=i;j<=n;j+=i) //不管是合数还是质数,都用来筛掉后面它的倍数
st[j]=1;
}
}
// 埃氏筛 O(nloglogn)
void get_primes_nloglogn(int n){
for(int i=2;i<=n;i++){
if(!st[i]){
primes[cnt++]=i;
for(int j=i;i<=n;j+=i){ //可以用质数就把所有的合数都筛掉;
st[j]=1;
}
}
}
}
// 线性筛 O(n)
void get_primes_n(int n){
//外层从2~n迭代,因为这毕竟算的是1~n中质数的个数,而不是某个数是不是质数的判定
for(int i=2;i<=n;i++){
if(!st[i]) primes[cnt++]=i;
for(int j=0;primes[j]<=n/i;j++){//primes[j]<=n/i:变形一下得到——primes[j]*i<=n,把大于n的合数都筛了就
//没啥意义了
st[primes[j]*i]=true;//用最小质因子去筛合数
//1)当i%primes[j]!=0时,说明此时遍历到的primes[j]不是i的质因子,那么只可能是此时的primes[j]<i的
//最小质因子,所以primes[j]*i的最小质因子就是primes[j];
//2)当有i%primes[j]==0时,说明i的最小质因子是primes[j],因此primes[j]*i的最小质因子也就应该是
//prime[j],之后接着用st[primes[j+1]*i]=true去筛合数时,就不是用最小质因子去更新了,因为i有最小
//质因子primes[j]<primes[j+1],此时的primes[j+1]不是primes[j+1]*i的最小质因子,此时就应该
//退出循环,避免之后重复进行筛选。
if(i%primes[j]==0) break;
}
}
}
int main(){
int n;
cin>>n;
get_primes_n(n);
cout<<cnt<<endl;
}
2.约数
(1)试除法求约数
给定 n 个正整数 ai,对于每个整数 ai,请你按照从小到大的顺序输出它的所有约数。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
int T;
cin >> T;
while(T--)
{
int n;
cin >> n;
vector<int> res;
//因为约数成对出现,所以只需要循环到根号x
// 不要是用 i *i <= n,因为可能溢出
for(int i = 1; i <= n /i; i++)
{
if(n % i == 0)
{
res.push_back(i);
//如果i * i = x,添加i即可,不用添加 x / i
if(n / i != i)
res.push_back(n / i);
}
}
sort(res.begin(), res.end());
for(auto x : res) cout << x << " ";
cout << endl;
}
}
(2)约束个数
给定 n 个正整数 ai,请你输出这些数的乘积的约数个数,答案对 10^9+7 取模
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;
const int mod = 1e9+7 ;
int main()
{
int T;
cin >> T;
unordered_map<int, int> h;
while(T--)
{
int n; cin >> n;
//依次求出指数
for(int i = 2; i <= n / i; i++)
{
while(n % i == 0)
{
//指数+1
h[i] ++;
n = n / i;
}
}
//如果有剩余,也是一个质因子
if(n > 1) h[n]++;
}
long long res = 1;
for(auto iter = h.begin(); iter != h.end(); iter++)
{
//res = (x1+1)(x2+1)(x3+1)…(xk+1)
res = res * (iter->second + 1) % mod ;
}
cout << res;
}
(3)约数之和
给定 n 个正整数 ai,请你输出这些数的乘积的约数之和,答案对 10^9+7 取模。
#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <vector>
using namespace std;
typedef long long LL;
const int N = 110, mod = 1e9 + 7;
int main()
{
int n;
cin >> n;
unordered_map<int, int> primes;
while (n -- )
{
int x;
cin >> x;
for (int i = 2; i <= x / i; i ++ )
while (x % i == 0)
{
x /= i;
primes[i] ++ ;
}
if (x > 1) primes[x] ++ ;
}
LL res = 1;
for (auto p : primes)
{
LL a = p.first, b = p.second;
LL t = 1;
while (b -- ) t = (t * a + 1) % mod;
res = res * t % mod;
}
cout << res << endl;
return 0;
}
(4)最大公约数
给定 n 对正整数 ai,bi,请你求出每对数的最大公约数。
【用(a,b)表示a和b的最大公因数:有结论(a,b)=(a,ka+b),其中a、b、k都为自然数】
#include <iostream>
using namespace std;
/*
求两个正整数 a 和 b 的 最大公约数 d
则有 gcd(a,b) = gcd(b,a%b)
证明:
设a%b = a - k*b 其中k = a/b(向下取整)
若d是(a,b)的公约数 则知 d|a 且 d|b 则易知 d|a-k*b 故d也是(b,a%b) 的公约数
若d是(b,a%b)的公约数 则知 d|b 且 d|a-k*b 则 d|a-k*b+k*b = d|a 故而d同时整除a和b 所以d也是(a,b)的公约数
因此(a,b)的公约数集合和(b,a%b)的公约数集合相同 所以他们的最大公约数也相同 证毕#
*/
int gcd(int a, int b){
return b ? gcd(b,a%b):a;
}
int main(){
int n,a,b;
cin>>n;
while(n--) cin>>a>>b,cout<<gcd(a,b)<<endl;
return 0;
}
3.欧拉函数
(1)欧拉函数
给定 n 个正整数 ai,请你求出每个数的欧拉函数。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int main()
{
ll k;cin>>k;
while(k--)
{
ll x;
cin>>x;
ll res=x;
for(ll i=2;i<=x/i;i++)
if(x%i==0)
{
while(x%i==0)x/=i;
res=res/i*(i-1); //注意先除再乘 N/pk*(pk-1);
}
if(x>1)res=res/x*(x-1);
cout<<res<<endl;
}
}
(2)筛法求欧拉函数
给定一个正整数 n,求 1∼n 中每个数的欧拉函数之和。
#include <iostream>
using namespace std;
const int N = 1000010;
int n;
int p[N], st[N], cnt;// 筛选质数 p[]存放已经找到的质数,st[]标记某个数是不是质数,cnt记录已经找到的质数数量
int phi[N]; // 欧拉函数 Φ(x)
long long res;
void ola_primes(int x)
{
for (int i = 2; i <= x; i ++)
{
if(!st[i])
{
p[cnt ++] = i;
//从1到质数i中与i互质的数个数为i-1
phi[i] = i - 1;
}
for (int j = 0; p[j] <= x / i; j ++)
{
st[p[j] * i] = 1;
if (i % p[j] == 0)
{
/*
当p[j]是i的一个约数时,i的质因数与p[j]*i的质因数完全相同。
i = (p1^a1)*(p2^a2)*(p3^a3)*...(p[j]^ap[j])...(pk^ak)
i*p[j] = (p1^a1)*(p2^a2)*(p3^a3)*...(p[j]^(ap[j]+1))...(pk^ak)
由欧拉函数的定义可知:
Φ(i) = i * (1-1/p1)*(1-1/p2)*(1-1/p3)*...(1-1/p[j])*...(1-1/pk)
Φ(i*p[j]) = i*p[j]*(1-1/p1)*(1-1/p2)*(1-1/p3)*...(1-1/p[j])*...(1-1/pk)
*/
phi[i * p[j]] = p[j] * phi[i];
break;
}
/*
当i%p[j]!=0时,p[j]不是i的约数,i与i*p[j]的质因子相差一个p[j]
i = (p1^a1)*(p2^a2)*(p3^a3)*...(pk^ak)
i*p[j] = (p1^a1)*(p2^a2)*(p3^a3)*...(pk^ak)*(p[j]^ap[j])
所以由欧拉函数的定义可知:
Φ(i) = i *(1-1/p1)*(1-1/p2)*(1-1/p3)*...(1-1/pk)
Φ(i*p[j]) = i*p[j]*(1-1/p1)*(1-1/p2)*(1-1/p3)*...(1-1/pk)(1-1/p[j])
*/
phi[i * p[j]] = (p[j] - 1) * phi[i]; //p[j]*(p[j]-1)/p[j]*phi[i] p[j]约了
}
}
}
int main()
{
cin >> n;
phi[1] = 1;
ola_primes(n);
for (int i = 1; i <= n; i ++) res += phi[i];
cout << res;
return 0;
}
4.快速幂
(1)快速幂
给定 n 组 ai,bi,pi,对于每组数据,求出 ai^bi mod pi 的值。
#include<iostream>
using namespace std;
long long qmi(long long a,int b,int p)
{
long long res=1;
while(b)//对b进行二进制化,从低位到高位
{
//如果b的二进制表示的第0位为1,则乘上当前的a
if(b&1) res = res *a %p;
//b右移一位
b>>=1;
//更新a,a依次为a^{2^0},a^{2^1},a^{2^2},....,a^{2^logb}
a=a*a%p;
}
return res;
}
int main()
{
int n;
cin>>n;
while(n--)
{
cin.tie(0);
ios::sync_with_stdio(false);
int a,b,p;
long long res=1;
cin>>a>>b>>p;
res = qmi(a,b,p);
cout<<res<<endl;
}
return 0;
}
(2)快速幂求逆元
给定 n 组 ai,pi,其中 pi 是质数,求 ai 模 pi 的乘法逆元,若逆元不存在则输出 impossible
。
注意:请返回在 0∼p−1 之间的逆元。
#include <iostream>
using namespace std;
typedef long long LL;
LL qmi(int a, int b, int p)
{
LL res = 1;
while(b){
if(b & 1) res = res * a % p;
a = (LL)a * a % p;
b >>= 1;
}
return res;
}
int main()
{
int n; cin >> n;
while(n --){
int a, p;
cin >> a >> p;
if(a % p == 0) puts("impossible");
else cout << qmi(a, p - 2, p) << endl;
}
return 0;
}
5.扩展欧几里得算法
(1)扩展欧几里得算法
给定 n 对正整数 ai,bi,对于每对数,求出一组 xi,yi,使其满足 ai×xi+bi×yi=gcd(ai,bi)。
#include<bits/stdc++.h>
using namespace std;
int exgcd(int a, int b, int &x, int &y){//返回gcd(a,b) 并求出解(引用带回)
if(b==0){
x = 1, y = 0;
return a;
}
int x1,y1,gcd;
gcd = exgcd(b, a%b, x1, y1);
x = y1, y = x1 - a/b*y1;
return gcd;
}
int main(){
int n,a,b,x,y;
cin>>n;
while(n--){
cin>>a>>b;
exgcd(a,b,x,y);
cout<<x<<" "<<y<<endl;
}
return 0;
}
(2)线性同余方程
给定 n 组数据 ai,bi,mi,对于每组数求出一个 xi,使其满足 ai×xi≡bi(mod mi),如果无解则输出 impossible
。
#include <iostream>
using namespace std;
int exgcd(int a,int b,int &x,int &y){
if(b==0){
x=1,y=0;
return a;
}
int x1,y1,gcd;
gcd=exgcd(b,a%b,x1,y1);
x=y1;
y=x1-a/b*y1;
return gcd;
}
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){
x = (long long)x * b / d % m;
cout << x << endl;
}
else cout<<"impossible"<<endl;
}
}
6.中国剩余定理-表达整数的奇怪方式
给定 2n 个整数 a1,a2,…,an 和 m1,m2,…,mn,求一个最小的非负整数 x,满足 ∀i∈[1,n],x≡mi(mod ai)。
#include<iostream>
using namespace std;
typedef long long LL;//数据范围比较大,所以用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-=a/b*x;
return d;
}
int main()
{
int n;
LL a1,m1;
cin>>n>>a1>>m1;
LL x=0;
for(int i=1;i<n;i++)
{
LL a2,m2;
cin>>a2>>m2;
LL k1,k2;
LL d=exgcd(a1,a2,k1,k2);
if((m2-m1)%d)
{
x=-1;
break;
}
k1*=(m2-m1)/d;
//因为此时k1是k1*a1+k2*a2=d的解,所以要乘上(m2-m1)/d的倍数大小
LL t=abs(a2/d);
k1=(k1%t+t)%t;
//数据比较极端,所以只求k的最小正整数解
m1=k1*a1+m1;
//m1在被赋值之后的值为当前"x"的值,此时赋值是为了方便下一轮的继续使用
a1=abs(a1*a2/d);
//循环结束时a1的值为当前所有的a1,a2,……an中的最小公倍数
}
if(x!=-1)
x=(m1%a1+a1)%a1;
//当循环结束时,此时的值应该与最小公倍数取模,以求得最小正整数解
printf("%lld\n",x);
return 0;
}
7.高斯消元
(1)高斯消元解线性方程组
输入一个包含 n 个方程 n 个未知数的线性方程组。
方程组中的系数为实数。
求解这个方程组。
下图为一个包含 m 个方程 n 个未知数的线性方程组示例:
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 110;
const double eps = 1e-6;
int n;
double a[N][N];
int gauss()
{
int c, r;// c 代表 列 col , r 代表 行 row
for (c = 0, r = 0; c < n; c ++ )
{
int t = r;// 先找到当前这一列,绝对值最大的一个数字所在的行号
for (int i = r; i < n; i ++ )
if (fabs(a[i][c]) > fabs(a[t][c]))
t = i;
if (fabs(a[t][c]) < eps) continue;// 如果当前这一列的最大数都是 0 ,那么所有数都是 0,就没必要去算了,因为它的约束方程,可能在上面几行
for (int i = c; i < n + 1; i ++ ) swap(a[t][i], a[r][i]); 把当前这一行,换到最上面(不是第一行,是第 r 行)去
for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];// 把当前这一行的第一个数,变成 1, 方程两边同时除以 第一个数,必须要到着算,不然第一个数直接变1,系数就被篡改,后面的数字没法算
for (int i = r + 1; i < n; i ++ )// 把当前列下面的所有数,全部消成 0
if (fabs(a[i][c]) > eps)// 如果非0 再操作,已经是 0就没必要操作了
for (int j = n; j >= c; j -- )// 从后往前,当前行的每个数字,都减去对应列 * 行首非0的数字,这样就能保证第一个数字是 a[i][0] -= 1*a[i][0];
a[i][j] -= a[r][j] * a[i][c];
r ++ ;// 这一行的工作做完,换下一行
}
if (r < n)// 说明剩下方程的个数是小于 n 的,说明不是唯一解,判断是无解还是无穷多解
{// 因为已经是阶梯型,所以 r ~ n-1 的值应该都为 0
for (int i = r; i < n; i ++ )//
if (fabs(a[i][n]) > eps)// a[i][n] 代表 b_i ,即 左边=0,右边=b_i,0 != b_i, 所以无解。
return 2;
return 1;// 否则, 0 = 0,就是r ~ n-1的方程都是多余方程
}
// 唯一解 ↓,从下往上回代,得到方程的解
for (int i = n - 1; i >= 0; i -- )
for (int j = i + 1; j < n; j ++ )
a[i][n] -= a[j][n] * a[i][j];//因为只要得到解,所以只用对 b_i 进行操作,中间的值,可以不用操作,因为不用输出
return 0;
}
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ )
for (int j = 0; j < n + 1; j ++ )
cin >> a[i][j];
int t = gauss();
if (t == 0)
{
for (int i = 0; i < n; i ++ ) printf("%.2lf\n", a[i][n]);
}
else if (t == 1) puts("Infinite group solutions");
else puts("No solution");
return 0;
}
(2)高斯消元解异或线性方程组
输入一个包含 n 个方程 n 个未知数的异或线性方程组。
方程组中的系数和常数为 0 或 1,每个未知数的取值也为 0 或 1。
求解这个方程组。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 110;
int n;
int a[N][N];
int gauss()
{
int c,r;
for(c=0,r=0;c<n;c++)
{
// 找主元
int t=-1;
for(int i=r;i<n;i++)
if(a[i][c])
{
t=i;
break;
}
if(t==-1) continue;
// 交换主元行
for(int j=c;j<=n;j++) swap(a[r][j],a[t][j]);
// 左下角消
for(int i=r+1;i<n;i++)
if(a[i][c])//漏了
for(int j=n;j>=c;j--)//漏了
a[i][j] ^= a[r][j];
r++;
}
// 判断
if(r<n)
{
for(int i=r;i<n;i++)//i=r
if(a[i][n])
return 2;
return 1;
}
// 消右上角
for(int i=n-1;i>=0;i--)
for(int j=i+1;j<n;j++)
//如果是0 就不用下面的a[j][j] 来^a[i][j]了
//如果不是0 才需要用第j行第j列a[j][j]来^第i行第j列a[i][j]
//进而进行整行row[i]^row[j] 间接导致 a[i][n]^a[j][n]
if(a[i][j])
a[i][n]^=a[j][n];
return 0;
}
int main()
{
cin >> n;
for(int i=0;i<n;i++)
for(int j=0;j<=n;j++)
cin >> a[i][j];
int t = gauss();
if(t==0)
{
for(int i=0;i<n;i++) cout << a[i][n] << endl;
}
else if(t==1) puts("Multiple sets of solutions");
else puts("No solution");
return 0;
}
8.求组合数
(1)求组合数I
数据范围:1≤n≤10000;1≤b≤a≤2000
给定 n 组询问,每组询问给定两个整数 a,b,请你输出 的值。
#include<iostream>
using namespace std;
const int mod = 1e9+7;
long long f[2010][2010];
int main()
{
//预处理
for(int i=0;i<=2000;i++)
{
for(int j=0;j<=i;j++)
{
if(!j) f[i][j]=1;
else f[i][j]=(f[i-1][j-1]+f[i-1][j])%mod;
}
}
int n;
cin>>n;
while(n--)
{
int a,b;
cin>>a>>b;
printf("%ld\n",f[a][b]);
}
}
(2)求组合数II
数据范围:1≤n≤10000;1≤b≤a≤10^5
给定 n 组询问,每组询问给定两个整数 a,b,请你输出 的值。
#include<iostream>
using namespace std;
const int mod=1e9+7,N=1e5+10;
typedef long long LL;
long long fac[N],infac[N];
int quick_pow(int a, int k, int p)
{
int res = 1;
while (k)
{
if (k & 1) res = (LL)res * a % p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
int main()
{
int n;
fac[0]=infac[0]=1;
for(int i=1;i<=1e5;i++)
{
fac[i]=fac[i-1]*i%mod;
infac[i]=(LL)infac[i - 1] * quick_pow(i,mod-2,mod)%mod;
}
cin>>n;
while(n--)
{
int a,b;
cin>>a>>b;
cout<<(LL)fac[a] * infac[b] % mod * infac[a - b] % mod<<endl;
}
}
(3)求组合数III
数据范围:1≤n≤20;1≤b≤a≤10^18;1≤p≤10^5
给定 n 组询问,每组询问给定三个整数 a,b,p,其中 p 是质数,请你输出 的值。
#include<iostream>
#include<algorithm>
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;
a = (LL)a*a%p;
k>>=1;
}
return res;
}
int C(int a,int b,int p)//自变量类型int
{
if(b>a)return 0;//漏了边界条件
int res = 1;
// a!/(b!(a-b)!) = (a-b+1)*...*a / b! 分子有b项
for(int i=1,j=a;i<=b;i++,j--)//i<=b而不是<
{
res = (LL)res*j%p;
res = (LL)res*qmi(i,p-2,p)%p;
}
return res;
}
//对公式敲
int lucas(LL a,LL b,int p)
{
if(a<p && b<p)return C(a,b,p);//lucas递归终点是C_{bk}^{ak}
return (LL)C(a%p,b%p,p)*lucas(a/p,b/p,p)%p;//a%p后肯定是<p的,所以可以用C(),但a/p后不一定<p 所以用lucas继续递归
}
int main()
{
int n;
cin >> n;
while(n--)
{
LL a,b;
int p;
cin >> a >> b >> p;
cout << lucas(a,b,p) << endl;
}
return 0;
}
(4)求组合数IV
数据范围:1≤b≤a≤5000
给定两个整数 a,b,请你输出 的值。注意结果可能很大,需要使用高精度计算。
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N=5010;
int primes[N],cnt;
int sum[N];
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]*i<=n;j++)
{
st[primes[j]*i]=true;
if(i%primes[j]==0)break;//==0每次漏
}
}
}
// 对p的各个<=a的次数算整除下取整倍数
int get(int n,int p)
{
int res =0;
while(n)
{
res+=n/p;
n/=p;
}
return res;
}
//高精度乘
vector<int> mul(vector<int> a, int b)
{
vector<int> c;
int t = 0;
for (int i = 0; i < a.size(); i ++ )
{
t += a[i] * b;
c.push_back(t % 10);
t /= 10;
}
while (t)
{
c.push_back(t % 10);
t /= 10;
}
// while(C.size()>1 && C.back()==0) C.pop_back();//考虑b==0时才有pop多余的0 b!=0不需要这行
return c;
}
int main()
{
int a,b;
cin >> a >> b;
get_primes(a);
for(int i=0;i<cnt;i++)
{
int p = primes[i];
sum[i] = get(a,p)-get(a-b,p)-get(b,p);//是a-b不是b-a
}
vector<int> res;
res.push_back(1);
for (int i = 0; i < cnt; i ++ )
for (int j = 0; j < sum[i]; j ++ )//primes[i]的次数
res = mul(res, primes[i]);
for (int i = res.size() - 1; i >= 0; i -- ) printf("%d", res[i]);
puts("");
return 0;
}
(5)满足条件的01序列
给定 n 个 0 和 n 个 1,它们将按照某种顺序排成长度为 2n 的序列,求它们能排列成的所有序列中,能够满足任意前缀序列中 0 的个数都不少于 1 的个数的序列有多少个。
输出的答案对 10^9+7 取模。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 200010, mod = 1e9 + 7;
int n;
int fact[N], infact[N];
int ksm(int a, int k) {
int res = 1;
while (k) {
if (k & 1) res = (LL)res * a % mod;
a = (LL)a * a % mod;
k >>= 1;
}
return res;
}
void init() {
fact[0] = infact[0] = 1;
for (int i = 1; i < N; i++) {
fact[i] = (LL)fact[i - 1] * i % mod;
infact[i] = (LL)infact[i - 1] * ksm(i, mod - 2) % mod;
}
}
int main() {
init();
cin >> n;
int res = (LL)fact[2 * n] * infact[n] % mod * infact[n] % mod * ksm(n + 1, mod - 2) % mod;
cout << res << endl;
return 0;
}
9.容斥原理-能被整除的数
给定一个整数 n 和 m 个不同的质数 p1,p2,…,pm。
请你求出 1∼n 中能被 p1,p2,…,pm 中的至少一个数整除的整数有多少个.
#include<iostream>
using namespace std;
typedef long long LL;
const int N = 20;
int p[N], n, m;
int main() {
cin >> n >> m;
for(int i = 0; i < m; i++) cin >> p[i];
int res = 0;
//枚举从1 到 1111...(m个1)的每一个集合状态, (至少选中一个集合)
for(int i = 1; i < 1 << m; i++) {
int t = 1; //选中集合对应质数的乘积
int s = 0; //选中的集合数量
//枚举当前状态的每一位
for(int j = 0; j < m; j++){
//选中一个集合
if(i >> j & 1){
//乘积大于n, 则n/t = 0, 跳出这轮循环
if((LL)t * p[j] > n){
t = -1;
break;
}
s++; //有一个1,集合数量+1
t *= p[j];
}
}
if(t == -1) continue;
if(s & 1) res += n / t; //选中奇数个集合, 则系数应该是1, n/t为当前这种状态的集合数量
else res -= n / t; //反之则为 -1
}
cout << res << endl;
return 0;
}
10.博弈论
(1)Nim游戏
给定 n 堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜
#include <iostream>
#include <cstdio>
using namespace std;
/*
先手必胜状态:先手操作完,可以走到某一个必败状态
先手必败状态:先手操作完,走不到任何一个必败状态
先手必败状态:a1 ^ a2 ^ a3 ^ ... ^an = 0
先手必胜状态:a1 ^ a2 ^ a3 ^ ... ^an ≠ 0
*/
int main(){
int n;
scanf("%d", &n);
int res = 0;
for(int i = 0; i < n; i++) {
int x;
scanf("%d", &x);
res ^= x;
}
if(res == 0) puts("No");
else puts("Yes");
}
(2)台阶-Nim游戏
892. 台阶-Nim游戏 - AcWing题库
现在,有一个 n 级台阶的楼梯,每级台阶上都有若干个石子,其中第 i 级台阶上有 ai 个石子(i≥1)。
两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。
已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
【如果先手时奇数台阶上的值的异或值为0,则先手必败,反之必胜】
#include <iostream>
using namespace std;
int main()
{
int res = 0;
int n;
cin >> n;
for(int i = 1 ; i <= n ; i++)
{
int x;
cin >> x;
if(i % 2) res ^= x;
}
if(res) puts("Yes");
else puts("No");
return 0;
}
(3)集合-Nim游戏
给定 n 堆石子以及一个由 k 个不同正整数构成的数字集合 S。
现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 S,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
#include<iostream>
#include<cstring>
#include<algorithm>
#include<set>
using namespace std;
const int N=110,M=10010;
int n,m;
int f[M],s[N];//s存储的是可供选择的集合,f存储的是所有可能出现过的情况的sg值
int sg(int x)
{
if(f[x]!=-1) return f[x];
//因为取石子数目的集合是已经确定了的,所以每个数的sg值也都是确定的,如果存储过了,直接返回即可
set<int> S;
//set代表的是有序集合(注:因为在函数内部定义,所以下一次递归中的S不与本次相同)
for(int i=0;i<m;i++)
{
int sum=s[i];
if(x>=sum) S.insert(sg(x-sum));
//先延伸到终点的sg值后,再从后往前排查出所有数的sg值
}
for(int i=0;;i++)
//循环完之后可以进行选出最小的没有出现的自然数的操作
if(!S.count(i))
return f[x]=i;
}
int main()
{
cin>>m;
for(int i=0;i<m;i++)
cin>>s[i];
cin>>n;
memset(f,-1,sizeof(f));//初始化f均为-1,方便在sg函数中查看x是否被记录过
int res=0;
for(int i=0;i<n;i++)
{
int x;
cin>>x;
res^=sg(x);
//观察异或值的变化,基本原理与Nim游戏相同
}
if(res) printf("Yes");
else printf("No");
return 0;
}
(4)拆分-Nim游戏
给定 n 堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为 00,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 110;
int n;
int f[N];
unordered_set<int> S;
int sg(int x)
{
if(f[x] != -1) return f[x];
for(int i = 0 ; i < x ; i++)
for(int j = 0 ; j <= i ; j++)//规定j不大于i,避免重复
S.insert(sg(i) ^ sg(j));//相当于一个局面拆分成了两个局面,由SG函数理论,多个独立局面的SG值,
//等于这些局面SG值的异或和
for(int i = 0 ; ; i++)
if(!S.count(i))
return f[x] = i;
}
int main()
{
memset(f , -1 , sizeof f);
cin >> n;
int res = 0;
while(n--)
{
int x;
cin >> x;
res ^= sg(x);
}
if(res) puts("Yes");
else puts("No");
return 0;
}
五、动态规划
1.背包问题
(1)01背包
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int f[MAXN][MAXN]; // f[i][j], j体积下前i个物品的最大价值
// void erwei(){
// for(int i=1;i<=n;i++){
// for(int j=1;j<=m;j++){
// if(j<v[i]) f[i][j]=f[i-1][j];
// else f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);
// }
// }
// }
// void yiwei(){
// for(int i=1;i<=n;i++){
// for(int j=m;j>=v[i];j--){
// g[j]=max(g[j],g[j-v[i]]+w[i]);
// }
// }
// }
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> v[i] >> w[i];
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
{
// 当前背包容量装不进第i个物品,则价值等于前i-1个物品
if(j < v[i])
f[i][j] = f[i - 1][j];
// 能装,需进行决策是否选择第i个物品
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
(2)完全背包问题
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值
#include <iostream>
using namespace std;
const int N=1010;
int v[N],w[N],f[N][N],g[N];
int n,m;
void erwei(){
for(int i=1;i<=n;i++){
for(int j=1;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]]+w[i]*k);
}
}
}
}
/* 01背包
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
void yiwei(){
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
g[j]=max(g[j],g[j-v[i]]+w[i]);
}
}
}
*/
/* 完全背包问题
f[i][j] = max(f[i][j],f[i][j-v[i]]+w[i]);
*/
void yiwei(){
for(int i=1;i<=n;i++){
for(int j=v[i];j<=m;j++){
g[j]=max(g[j],g[j-v[i]]+w[i]);
}
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>v[i]>>w[i];
// erwei();
// cout<<f[n][m]<<endl;
yiwei();
cout<<g[m]<<endl;
}
(3)多重背包问题
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
#include <iostream>
using namespace std;
const int N=1010;
int f[N];
int n,m;
/*
当 si=1 时,相当于01背包中的一件物品
当 si>1 时,相当于01背包中的多个一件物品
*/
int main(){
cin>>n>>m;
while(n--){
int v,w,s;
cin>>v>>w>>s;
while(s--){
for(int j=m;j>=v;j--)
f[j]=max(f[j],f[j-v]+w);
}
}
cout<<f[m]<<endl;
}
(4)多重背包问题II
有 N 种物品和一个容量是 V 的背包。
第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
二进制优化:
#include<iostream>
using namespace std;
const int N = 12010, M = 2010;
int n, m;
int v[N], w[N]; //逐一枚举最大是N*logS
int f[M]; // 体积<M
int main()
{
cin >> n >> m;
int cnt = 0; //分组的组别
for(int i = 1;i <= n;i ++)
{
int a,b,s;
cin >> a >> b >> s;
int k = 1; // 组别里面的个数
while(k<=s)
{
cnt ++ ; //组别先增加
v[cnt] = a * k ; //整体体积
w[cnt] = b * k; // 整体价值
s -= k; // s要减小
k *= 2; // 组别里的个数增加
}
//剩余的一组
if(s>0)
{
cnt ++ ;
v[cnt] = a*s;
w[cnt] = b*s;
}
}
n = cnt ; //枚举次数正式由个数变成组别数
//01背包一维优化
for(int i = 1;i <= n ;i ++)
for(int j = m ;j >= v[i];j --)
f[j] = max(f[j],f[j-v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
(5)分组背包问题
有 N 组物品和一个容量是 V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 vij,价值是 wij,其中 i 是组号,j是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
#include <iostream>
using namespace std;
const int N=110;
int v[N][N],w[N][N],s[N]; //v为体积,w为价值,s代表第i组物品的个数
int f[N][N]; //只从前i组物品中选,当前体积小于等于j的最大值
int g[N];
int n,m;
void erwei(){
for(int i=1;i<=n;i++){
for(int j=0;j<=m;j++){
f[i][j]=f[i-1][j];
for(int k=0;k<s[i];k++){
if(j>=v[i][k]) f[i][j]=max(f[i][j],f[i-1][j-v[i][k]]+w[i][k]);
}
}
}
}
// 只用到了第i-1列,所以可以仿照01背包的套路逆向枚举体积
void yiwei(){
for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){
for(int k=0;k<s[i];k++){
if(j>=v[i][k]) g[j]=max(g[j],g[j-v[i][k]]+w[i][k]);
}
}
}
}
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];
}
yiwei();
cout<<g[m];
}
2.线性DP
(1)数字三角形
给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
#include <iostream>
using namespace std;
const int N = 510;
int n;
int a[N][N];
int f[N][N];
int main () {
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] << endl;
return 0;
}
(2)最长公共上升子序列-O(n^2)
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
#include <iostream>
using namespace std;
const int N = 1010;
int n;
int w[N], f[N];
int main() {
cin >> n;
for (int i = 0; i < n; i++) cin >> w[i];
int mx = 1; // 找出所计算的f[i]之中的最大值,边算边找
for (int i = 0; i < n; i++) {
f[i] = 1; // 设f[i]默认为1,找不到前面数字小于自己的时候就为1
for (int j = 0; j < i; j++) {
if (w[i] > w[j]) f[i] = max(f[i], f[j] + 1); // 前一个小于自己的数结尾的最大上升子序列加上自己,即+1
}
mx = max(mx, f[i]);
}
cout << mx << endl;
return 0;
}
(3)最长公共上升子序列II-O(nlogn)
给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。
#include <iostream>
using namespace std;
const int N = 100010;
int n, cnt;
int w[N], f[N];
int main() {
cin >> n;
for (int i = 0 ; i < n; i++) cin >> w[i];
f[cnt++] = w[0];
for (int i = 1; i < n; i++) {
if (w[i] > f[cnt-1]) f[cnt++] = w[i];
else {
int l = 0, r = cnt - 1;
while (l < r) {
int mid = (l + r) >> 1;
if (f[mid] >= w[i]) r = mid;
else l = mid + 1;
}
f[r] = w[i];
}
}
cout << cnt << endl;
return 0;
}
(4)最长公共子序列
给定两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。
#include <iostream>
using namespace std;
const int N=1010;
char a[N],b[N];
int n,m;
int dp[N][N];
int main(){
cin>>n>>m>>a+1>>b+1;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i]==b[j]) dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
cout<<dp[n][m];
}
(5)最短编辑距离
给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:
- 删除–将字符串 A 中的某个字符删除。
- 插入–在字符串 A 的某个位置插入某个字符。
- 替换–将字符串 A 中的某个字符替换为另一个字符。
现在请你求出,将 A 变为 B 至少需要进行多少次操作。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1010;
int m, n;
char a[N],b[N];
int dp[N][N];
int main()
{
scanf("%d%s", &m, a + 1); // a + 1 这里的小技巧
scanf("%d%s", &n, b + 1);
for(int i = 0; i <= m; i++) dp[i][0] = i; //全删除
for(int i = 0; i <= n; i++) dp[0][i] = i; //全插入
for(int i = 1; i <=m; i++)
for(int j = 1; j <=n; j++)
{
dp[i][j]=min(dp[i][j-1],dp[i-1][j])+1; // 添加,删除
dp[i][j]=min(dp[i][j],dp[i-1][j-1]+(a[i]!=b[j])); //无操作,替换
}
printf("%d\n",dp[m][n]);
return 0;
}
(6)编辑距离
给定 n 个长度不超过 10 的字符串以及 m 次询问,每次询问给出一个字符串和一个操作次数上限。
对于每次询问,请你求出给定的 n 个字符串中有多少个字符串可以在上限操作次数内经过操作变成询问给出的字符串。
每个对字符串进行的单个字符的插入、删除或替换算作一次操作。
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e1 + 5, M = 1e3 + 10;
int n, m;
char str[M][N];
int dp[N][N];
int edit_distance(char a[], char b[])
{
int la = strlen(a + 1), lb = strlen(b + 1);
for (int i = 0; i <= lb; i++) {
dp[0][i] = i;
}
for (int i = 0; i <= la; i++) {
dp[i][0] = i;
}
for (int i = 1; i <= la; i++) {
for (int j = 1; j <= lb; j++) {
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
dp[i][j] = min(dp[i][j], dp[i - 1][j - 1] + (a[i] != b[j]));
}
}
return dp[la][lb];
}
int main()
{
cin >> n >> m;
for (int i = 0; i < n; i++) {
cin >> (str[i] + 1);
}
while (m--) {
int res = 0;
char s[N];
int limit;
cin >> (s + 1) >> limit;
for (int i = 0; i < n; i++) {
if (edit_distance(str[i], s) <= limit) {
res++;
}
}
cout << res << endl;
}
return 0;
}
3.区间DP-石子合并(矩阵链乘)
设有 N 堆石子排成一排,其编号为 1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 4 堆石子分别为 1 3 5 2
, 我们可以先合并 1、2堆,代价为 4,得到 4 5 2
, 又合并 1、2 堆,代价为 9,得到 9 2
,再合并得到 11,总代价为 4+9+11=24;
如果第二步是先合并 2、3 堆,则代价为 7,得到 4 7
,最后一次合并代价为 11,总代价为 4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价
#include <iostream>
#include <cstring>
using namespace std;
const int N = 307;
int a[N], s[N];
int f[N][N];
int main() {
int n;
cin >> n;
for (int i = 1; i <= n; i ++) {
cin >> a[i];
s[i] += s[i - 1] + a[i];
}
memset(f, 0x3f, sizeof f);
// 区间 DP 枚举套路:长度+左端点
for (int len = 1; len <= n; len ++) { // len表示[i, j]的元素个数
for (int i = 1; i + len - 1 <= n; i ++) {
int j = i + len - 1; // 自动得到右端点
if (len == 1) {
f[i][j] = 0; // 边界初始化
continue;
}
for (int k = i; k <= j - 1; k ++) { // 必须满足k + 1 <= j
f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
}
}
}
cout << f[1][n] << endl;
return 0;
}
4.计数类DP-整数划分
一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1。
我们将这样的一种表示称为正整数 n 的一种划分。
现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。
#include <iostream>
using namespace std;
const int N=1010,mod=1e9+7;
int dp[N][N];
int g[N];
int n;
void erwei(){
for(int i=0;i<=n;i++) dp[i][0]=1;// 容量为0时,前 i 个物品全不选也是一种方案
for(int i=1;i<=n;i++)
for(int j=0;j<=n;j++){
dp[i][j]=dp[i-1][j]%mod;
if(j>=i) dp[i][j]=(dp[i-1][j]+dp[i][j-i])%mod;
}
}
void yiwei(){
g[0]=1;
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++)
g[j]=(g[j]+g[j-i])%mod;
}
}
int main(){
cin>>n;
// erwei();
// cout<<dp[n][n]<<endl;
yiwei();
cout<<g[n]<<endl;
}
5.数位统计DP-计数问题
给定两个整数 a 和 b,求 a 和 b 之间的所有数字中 0∼9 的出现次数。
例如,a=1024,b=1032,则 a 和 b 之间共有 9 个数如下:
1024 1025 1026 1027 1028 1029 1030 1031 1032
其中 0
出现 10 次,1
出现 10 次,2
出现 7 次,3
出现 3 次等等…
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
int get(int n) { // 求数n的位数
int res = 0;
while (n) res ++, n /= 10;
return res;
}
int count(int n, int i) { // 求从1到数n中数i出现的次数
int res = 0, dgt = get(n);
for (int j = 1; j <= dgt; ++ j) {
/* p为当前遍历位次(第j位)的数大小 <10^(右边的数的位数)>, Ps:从左往右(从高位到低位)
l为第j位的左边的数,r为右边的数,dj为第j位上的数 */
int p = pow(10, dgt - j), l = n / p / 10, r = n % p, dj = n / p % 10;
// ps:下文的xxx、yyy均只为使读者眼熟,并不严格只是三位数啊~ 然后后续的...就代表省略的位数啦~
/* 求要选的数在i的左边的数小于l的情况:
(即视频中的xxx1yyy中的xxx的选法) --->
1)、当i不为0时 xxx : 0...0 ~ l - 1, 即 l * (右边的数的位数) == l * p 种选法
2)、当i为0时 由于不能有前导零 故xxx: 0....1 ~ l - 1,
即 (l-1) * (右边的数的位数) == (l-1) * p 种选法 */
if (i) res += l * p;
else res += (l - 1) * p;
/* 求要选的数在i的左边的数等于l的情况:(即视频中的xxx == l 时)
(即视频中的xxx1yyy中的yyy的选法)--->
1)、i > dj时 0种选法
2)、i == dj时 yyy : 0...0 ~ r 即 r + 1 种选法
3)、i < dj时 yyy : 0...0 ~ 9...9 即 10^(右边的数的位数) == p 种选法 */
if (i == dj) res += r + 1;
if (i < dj) res += p;
}
return res; // 返回结果
}
int main() {
int a, b;
while (cin >> a >> b, a) { // 输入处理,直到输入为0停止
if (a > b) swap(a, b); // 预处理-->让a为较小值,b为较大值
for (int i = 0; i <= 9; ++ i) cout << count(b, i) - count(a - 1, i) << ' ';
// 输出每一位数字(0 ~ 9)分别在[a,b]中出现的次数<利用前缀和思想:[l,r]的和=s[r] - s[l - 1]>
cout << endl; //换行
}
return 0; // 惯例:结束快乐~
}
6.状态压缩DP
(1)蒙德里安的梦想
求把 N×M 的棋盘分割成若干个 1×2 的长方形,有多少种方案。
例如当 N=2,M=4 时,共有 5 种方案。当 N=2,M=3 时,共有 3 种方案。
如下图所示:
/*
下文对 if ((j & k ) == 0 && st[ j | k] ) 有清晰的解释!!!
*/
#include <bits/stdc++.h>
using namespace std;
const int N = 12, M = 1<< N;
long long f[N][M] ;// 第一维表示列, 第二维表示所有可能的状态
bool st[M]; //存储每种状态是否有奇数个连续的0,如果奇数个0是无效状态,如果是偶数个零置为true。
//vector<int > state[M]; //二维数组记录合法的状态
vector<vector<int>> state(M); //两种写法等价:二维数组
int m, n;
int main() {
while (cin >> n >> m, n || m) { //读入n和m,并且不是两个0即合法输入就继续读入
//第一部分:预处理1
//对于每种状态,先预处理每列不能有奇数个连续的0
for(int i = 0; i < (1 << n); i ++) {
int cnt = 0 ;//记录连续的0的个数
bool isValid = true; // 某种状态没有奇数个连续的0则标记为true
for(int j = 0; j < n; j ++) { //遍历这一列,从上到下
if ( (i >> j) & 1) {
//i >> j位运算,表示i(i在此处是一种状态)的二进制数的第j位;
// &1为判断该位是否为1,如果为1进入if
if (cnt & 1) {
//这一位为1,看前面连续的0的个数,如果是奇数(cnt &1为真)则该状态不合法
isValid =false; break;
}
cnt = 0; // 既然该位是1,并且前面不是奇数个0(经过上面的if判断),计数器清零。
//其实清不清零没有影响
}
else cnt ++; //否则的话该位还是0,则统计连续0的计数器++。
}
if (cnt & 1) isValid = false; //最下面的那一段判断一下连续的0的个数
st[i] = isValid; //状态i是否有奇数个连续的0的情况,输入到数组st中
}
//第二部分:预处理2
// 经过上面每种状态 连续0的判断,已经筛掉一些状态。
//下面来看进一步的判断:看第i-2列伸出来的和第i-1列伸出去的是否冲突
for (int j = 0; j < (1 << n); j ++) { //对于第i列的所有状态
state[j].clear(); //清空上次操作遗留的状态,防止影响本次状态。
for (int k = 0; k < (1 << n); k ++) { //对于第i-1列所有状态
if ((j & k ) == 0 && st[ j | k])
// 第i-2列伸出来的 和第i-1列伸出来的不冲突(不在同一行)
//解释一下st[j | k]
//已经知道st[]数组表示的是这一列没有连续奇数个0的情况,
//我们要考虑的是第i-1列(第i-1列是这里的主体)中从第i-2列横插过来的,
//还要考虑自己这一列(i-1列)横插到第i列的
//比如 第i-2列插过来的是k=10101,第i-1列插出去到第i列的是 j =01000,
//那么合在第i-1列,到底有多少个1呢?
//自然想到的就是这两个操作共同的结果:两个状态或。 j | k = 01000 | 10101 = 11101
//这个 j|k 就是当前 第i-1列的到底有几个1,即哪几行是横着放格子的
state[j].push_back(k);
//二维数组state[j]表示第j行,
//j表示 第i列“真正”可行的状态,
//如果第i-1列的状态k和j不冲突则压入state数组中的第j行。
//“真正”可行是指:既没有前后两列伸进伸出的冲突;又没有连续奇数个0。
}
}
//第三部分:dp开始
memset(f, 0, sizeof f);
//全部初始化为0,因为是连续读入,这里是一个清空操作。
//类似上面的state[j].clear()
f[0][0] = 1 ;// 这里需要回忆状态表示的定义
//按定义这里是:前第-1列都摆好,且从-1列到第0列伸出来的状态为0的方案数。
//首先,这里没有-1列,最少也是0列。
//其次,没有伸出来,即没有横着摆的。即这里第0列只有竖着摆这1种状态。
for (int i = 1; i <= m; i ++) { //遍历每一列:第i列合法范围是(0~m-1列)
for (int j = 0; j < (1<<n); j ++) { //遍历当前列(第i列)所有状态j
for (auto k : state[j]) // 遍历第i-1列的状态k,如果“真正”可行,就转移
f[i][j] += f[i-1][k]; // 当前列的方案数就等于之前的第i-1列所有状态k的累加。
}
}
//最后答案是什么呢?
//f[m][0]表示 前m-1列都处理完,并且第m-1列没有伸出来的所有方案数。
//即整个棋盘处理完的方案数
cout << f[m][0] << endl;
}
}
(2)最短Hamilton路径
给定一张 n 个点的带权无向图,点从 0∼n−1 标号,求起点 0 到终点 n−1 的最短 Hamilton 路径。
Hamilton 路径的定义是从 0 到 n−1 不重不漏地经过每个点恰好一次。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=20,M=1<<N;
int f[M][N],w[N][N];//w表示的是无权图
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>w[i][j];
memset(f,0x3f,sizeof(f));//因为要求最小值,所以初始化为无穷大
f[1][0]=0;//因为零是起点,所以f[1][0]=0;
for(int i=0;i<1<<n;i++)//i表示所有的情况
for(int j=0;j<n;j++)//j表示走到哪一个点
if(i>>j&1)
for(int k=0;k<n;k++)//k表示走到j这个点之前,以k为终点的最短距离
if(i>>k&1)
f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);//更新最短距离
cout<<f[(1<<n)-1][n-1]<<endl;//表示所有点都走过了,且终点是n-1的最短距离
//位运算的优先级低于'+'-'所以有必要的情况下要打括号
return 0;
}
7.树形DP-没有上司的舞会
Ural 大学有 N 名职员,编号为 1∼N。
他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。
每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。
现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。
在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 6010;
int n;
int happy[N]; //每个职工的高兴度
int f[N][2]; //上面有解释哦~
int e[N],ne[N],h[N],idx; //链表,用来模拟建一个树
bool has_father[N]; //判断当前节点是否有父节点
void add(int a,int b){ //把a插入树中
e[idx] = b,ne[idx] = h[a],h[a] = idx ++;
}
void dfs(int u){ //开始求解题目
f[u][1] = happy[u]; //如果选当前节点u,就可以把f[u,1]先怼上他的高兴度
for(int i = h[u];~i;i = ne[i]){ //遍历树
int j = e[i];
dfs(j); //回溯
//状态转移部分,上面有详细讲解~
f[u][0] += max(f[j][1],f[j][0]);
f[u][1] += f[j][0];
}
}
int main(){
scanf("%d",&n);
for(int i = 1;i <= n;i ++) scanf("%d",&happy[i]); //输入每个人的高兴度
memset(h,-1,sizeof h); //把h都赋值为-1
for(int i = 1;i < n;i ++){
int a,b; //对应题目中的L,K,表示b是a的上司
scanf("%d%d",&a,&b); //输入~
has_father[a] = true; //说明a他有上司
add(b,a); //把a加入到b的后面
}
int root = 1; //用来找根节点
while(has_father[root]) root ++; //找根节点
dfs(root); //从根节点开始搜索
printf("%d\n",max(f[root][0],f[root][1])); //输出不选根节点与选根节点的最大值
return 0;
}
8.记忆化搜索-滑雪
给定一个 R 行 C 列的矩阵,表示一个矩形网格滑雪场。
矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。
一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。
当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。
下面给出一个矩阵作为例子:
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9
在给定矩阵中,一条可行的滑行轨迹为 24−17−2−1。
在给定矩阵中,最长的滑行轨迹为 25−24−23−…−3−2−1,沿途共经过 25 个区域。
现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 310;
int n,m; //网格滑雪场的行和列
int f[N][N]; //状态转移式
int h[N][N]; //网格滑雪场
int dx[4] = {-1,0,1,0};
int dy[4] = {0,1,0,-1};
int dp(int x,int y){
int &v = f[x][y]; //Y总说的小技巧,等于把f[x][y]简化成了v,如果v发生变化,f[x][y]也会随之变化
if(v != -1) return v; //如果已经计算过了,就可以直接返回答案
v = 1; //注意v要先赋值为1哦~
for(int i = 0;i < 4;i ++){ //四个方向
int xx = x + dx[i];
int yy = y + dy[i];
if(xx >= 1 && xx <= n && yy >= 1 && yy <= m && h[x][y] > h[xx][yy]){ //判断这点是否能走
v = max(v,dp(xx,yy) + 1); //更新
}
}
return v; //别忘了返回v啊(被坑了
}
int main(){
cin>>n>>m; //输入滑雪场行和列
for(int i = 1;i <= n;i ++){
for(int j = 1;j <= m;j ++){
cin>>h[i][j]; //读入滑雪场数据
}
}
memset(f,-1,sizeof f);
int res = 0; //最后答案
for(int i = 1;i <= n;i ++){
for(int j = 1;j <= m;j ++){
//因为这个人可以在任意一点开始滑,所以要遍历一遍滑雪场
res = max(res,dp(i,j)); //更新答案
}
}
cout<<res<<endl;
return 0;
}
六、贪心
1.区间问题
(1)区间选点
给定 N 个闭区间 [ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。
输出选择的点的最小数量。
位于区间端点上的点也算作区间内。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l, r;
bool operator< (const Range &W)const
{
return r < W.r;
}
}range[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d%d", &range[i].l, &range[i].r);
sort(range, range + n);
int res = 0, ed = -2e9;
for (int i = 0; i < n; i ++ )
if (ed < range[i].l)
{
res ++ ;
ed = range[i].r;
}
printf("%d\n", res);
return 0;
}
(2)最大不相交区间数量
给定 N 个闭区间 [ai,bi],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。
输出可选取区间的最大数量。
#include <iostream>
#include <algorithm>
using namespace std;
const int N=1e5+10;
struct Range{
int l,r;
bool operator < (const Range ra){
return r<ra.r;
}
}range[N];
int n;
int main(){
cin>>n;
for(int i=0;i<n;i++) cin>>range[i].l>>range[i].r;
sort(range,range+n);
int res=0,ed=-2e9;
for(int i=0;i<n;i++){
if(ed<range[i].l){
res++;
ed=range[i].r;
}
}
cout<<res<<endl;
}
(3)区间分组
给定 N 个闭区间 [ai,bi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。
输出最小组数。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100100;
int n;
int b[2 * N], idx;
int main()
{
scanf ("%d", &n);
for(int i = 0; i < n; i ++)
{
int l, r;
scanf("%d %d", &l, &r);
b[idx ++] = l * 2;//标记左端点为偶数。
b[idx ++] = r * 2 + 1;// 标记右端点为奇数。
}
sort(b, b + idx);
int res = 1, t = 0;
for(int i = 0; i < idx; i ++)
{
if(b[i] % 2 == 0) t ++;
else t --;
res = max(res, t);
}
printf ("%d\n", res);
return 0;
}
(4)区间覆盖
给定 N 个闭区间 [ai,bi] 以及一个线段区间 [s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。
输出最少区间数,如果无法完全覆盖则输出 −1。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int n;
struct Range
{
int l, r;
bool operator< (const Range &W)const
{
return l < W.l;
}
}range[N];
int main()
{
int st, ed;
scanf("%d%d", &st, &ed);
scanf("%d", &n);
for (int i = 0; i < n; i ++ )
{
int l, r;
scanf("%d%d", &l, &r);
range[i] = {l, r};
}
sort(range, range + n);
int res = 0;
bool success = false;
for (int i = 0; i < n; i ++ )
{
int j = i, r = -2e9;
while (j < n && range[j].l <= st)
{
r = max(r, range[j].r);
j ++ ;
}
if (r < st)
{
res = -1;
break;
}
res ++ ;
if (r >= ed)
{
success = true;
break;
}
st = r;
i = j - 1;
}
if (!success) res = -1;
printf("%d\n", res);
return 0;
}
2.Huffman树-合并果子
在一个果园里,达达已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。
达达决定把所有的果子合成一堆。
每一次合并,达达可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。
可以看出,所有的果子经过 n−1 次合并之后,就只剩下一堆了。
达达在合并果子时总共消耗的体力等于每次合并所耗体力之和。
因为还要花大力气把这些果子搬回家,所以达达在合并果子时要尽可能地节省体力。
假定每个果子重量都为 1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使达达耗费的体力最少,并输出这个最小的体力耗费值。
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
int main()
{
int n;
scanf("%d", &n);
priority_queue<int, vector<int>, greater<int>> heap;
while (n -- )
{
int x;
scanf("%d", &x);
heap.push(x);
}
int res = 0;
while (heap.size() > 1)
{
int a = heap.top(); heap.pop();
int b = heap.top(); heap.pop();
res += a + b;
heap.push(a + b);
}
printf("%d\n", res);
return 0;
}
3.排序不等式-排队打水
有 n 个人排队到 1 个水龙头处打水,第 i 个人装满水桶所需的时间是 ti,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
typedef long long LL;
int n;
int a[N];
int main()
{
cin>>n;
for (int i = 0; i < n; i ++ )cin>>a[i];
sort(a, a+n);
LL res = 0, t=0;
for (int i = 0; i < n; i ++ ){
res += t;
t += a[i];
}
cout<<res<<endl;
return 0;
}
4.绝对值不等式-货仓选址
在一条数轴上有 N 家商店,它们的坐标分别为 A1∼AN。
现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。
为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。
#include <algorithm>
using namespace std;
const int N = 100005;
int n, res;
int a[N];
int main()
{
scanf("%d", &n);
for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]);
sort(a, a + n);
for (int i = 0; i < n; i ++ ) res += abs(a[i] - a[n >> 1]);
printf("%d\n", res);
return 0;
}
5.推公式-耍杂技的牛
农民约翰的 N 头奶牛(编号为 1..N)计划逃跑并加入马戏团,为此它们决定练习表演杂技。
奶牛们不是非常有创意,只提出了一个杂技表演:
叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。
奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。
这 N 头奶牛中的每一头都有着自己的重量 Wi 以及自己的强壮程度 Si。
一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。
您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
typedef pair<int, int> PII;
const int N = 5e4 + 5;
PII a[N];
int main()
{
int n;
cin >> n;
for(int i = 0; i < n; i ++ )
{
int x, y;
scanf("%d %d", &x, &y);
a[i].first = x + y;
a[i].second = y;
}
sort(a, a + n);
ll res = -1e18, sum = 0;
for(int i = 0; i < n; i ++ )
{
sum -= a[i].second;
res = max(res, sum);
sum += a[i].first;
}
cout << res << endl;
return 0;
}