赞
踩
闲来无事,利用阿里云做了个图床,已经将图片全部上传了。
快速:选择一个数,让数组中比他小的靠左,比他大的靠右,然后在左边右边同样进行操作。注意边界问题。O(nlogn)
一般选择mid
= l+r+1 >> 1,因为是用dowhile,所以设置i和j都是l和r往外一个。当i<j时做完了i++和j–,说明此时两边都出现了错误,所以swap(a[i],a[j])然后由于while(i < j)因此继续做。当i >= j 说明左边都小于a[mid],右边都大于a[mid]了,然后对于左边和右边再进行quick_sort,但是要注意左边是l,i-1,右边是i,r
#include<iostream> using namespace std; const int N = 1e6 + 10; int q[N]; int n; void quick_sort(int q[],int l,int r){ if(l>=r){ return; } int x = q[(l+r+1)/2], i = l-1, j = r+1;//如果取左端点,则下面为q,l,j q,j+1,r,如果取右端点,则下面为q,l,i-1 q,i,l (因为会出现死循环) while(i<j){ do i++; while(q[i]<x); do j--; while(q[j]>x); if( i < j ) swap(q[i],q[j]); } quick_sort(q,l,i-1); quick_sort(q,i,r); } int main(){ scanf("%d",&n); for(int i=0;i<n;i++) scanf("%d",&q[i]); quick_sort(q,0,n-1); for(int i=0;i<n;i++) printf("%d ",q[i]); }
归并:先二分,让左边的和右边的排好大小后,再合并,合并时i <= mid && j <= r,如果左边大,就先放右边的,右边大就先放左边的,然后最后在进行没放完的放进去。可以两边都试试,因为一定是只有一边没放完,那么放完的那边就不会再进行了,然后让q数组从l开始到r结束,把temp数组中的复制一遍。
O(nlogn)
#include<iostream> using namespace std; const int N = 1000010; int q[N],tmp[N]; int n; void merge_sort(int q[],int l,int r){ if(l>=r){ return; } int mid = (l+r)/2; merge_sort(q,l,mid); merge_sort(q,mid+1,r); int k=0,i=l,j=mid+1; while(i<=mid && j<=r){ if(q[i]<=q[j]) tmp[k++]=q[i++]; else tmp[k++]=q[j++]; } while(i<=mid) tmp[k++]=q[i++]; while(j<=r) tmp[k++]=q[j++]; for(i=l,j=0;i<=r;i++,j++){ q[i]=tmp[j]; } } int main(){ scanf("%d",&n); for(int i=0;i<n;i++) scanf("%d",&q[i]); merge_sort(q,0,n-1); for(int i=0;i<n;i++) printf("%d ",q[i]); }
逆序对就是当右边比左边小的时候就有逆序了,每次进行res += mid - i + 1;i代表了左边进行到哪里了。
只要有二段性的问题都可以使用二分去做。(二段性,在某段情况下满足条件的,其他的一连串情况不满足),比如牛棚问题,关押罪犯等,二分通常都是和别的算法联合起来用的。
两个模板:第一个是找红色的边界的,第二个是找绿色的边界的
//check(mid)表示mid是否满足要找的那边的性质。
//红色边界
//找符合条件的右端点
int bsearch_1(int l,int r){
while(l<r){
int mid = (l+r+1)/2;
if(check(mid)) l=mid;
else r = mid-1
}
return l;
}
//绿色边界
//找符合条件的左端点
int bsearch_2(int l,int r){
while(l<r){
int mid = (l+r)/2;
if(check(mid)) r=mid;//符合条件的左端点
else l = mid+1;
}
return r;
}
区别在于mid的选择以及r=mid-1和l=mid+1
mid的+1与否主要是找到最后两个r=l+1是否会死循环,左边端点不加1会死循环,因为找左边的端点最终是在左侧找到,就会经历r=l+1的情况,那么true就是l=mid=(l+r)/2=l,就一直死循环了,因此找左端点为mid = (l+r+1)/2
同理对右边的端点同样,因为是找右边的端点,因此最后一定是执行,r=mid的,如果mid=(l+r+1)/2,则r=mid=(r-1+r+1)/2=r,因此找右端点为mid=(l+r)/2
起始位置就是找右边≥x的端点,结束位置就是找左边≤x的端点,对应找右边的端点和左边的端点。
#include<iostream> using namespace std; const int N = 10e5+10; int q[N],n,m; int main(){ scanf("%d%d",&n,&m); for(int i=0;i<n;i++) scanf("%d",&q[i]); while(m--){ int x,l,r; scanf("%d",&x); //起始位置 l=0;r=n-1; while(l<r){ int mid = l+r >>1; if(q[mid]>=x) r=mid; else l = mid+1; } if(q[l]!=x){ cout<<"-1 -1"<<endl; } else{ cout<<l<<" "; l=0;r=n-1; //终止位置 while(l<r){ int mid = l + r + 1 >>1; if(q[mid]<=x) l=mid; else r = mid-1; } cout<<r<<endl; } } }
二分是只需要左边的数据有右边没有的特性就可以使用,不需要说一定是排序问题,比如左边的数都有1,右边的数没有1,找有1的数的右边界也是可以的。
无需考虑边界问题,直接二分即可
注意:解决问题的时候,精度最好比需要的要求小数点多两位,比如要求4位小数,精度就设置10-6,
题:开根问题
大整数一般用数组来存。从小位开始存。如123 在数组中为 3 2 1。
加法就是注意相同位数相加数%10存本位,然后t = /10,然后放到下一位就行。
#include<iostream> #include<vector> using namespace std; vector<int> add(vector<int> &A,vector<int> &B){//加引用是为了加快速度,如果不加引用也可以但是会先拷贝一遍就会慢很多 vector<int> C; int t=0; 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> A,B; cin>>a>>b; for(int i = a.size()-1;i>=0;i--) A.push_back(a[i]-'0');//从小位开始存 for(int i = b.size()-1;i>=0;i--) B.push_back(b[i]-'0'); vector<int> C = add(A,B); for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]); return 0; }
首先判断谁大,然后进行减法,用A-B,同样此处的t代表了当前的情况,t = A[i]-B[i]-t,如果t<0就把t变为-1,代表进了一位。注意最后输出的时候要去掉前面的0
#include<iostream> #include<vector> using namespace std; bool cmp(vector<int> &A,vector<int> &B){ if(A.size()!=B.size()) return A.size()>B.size(); for(int i=A.size()-1;i>=0;i--){ if(A[i]!=B[i]){ return A[i]-B[i];//从高位开始比较依次到最小位,只要有位数不同,就返回谁大谁小。 } } return true; } vector<int> sub(vector<int> &A,vector<int> &B){ vector<int> C; int t=0; for(int i=0;i<A.size();i++){ t=A[i]-t; if(i<B.size()) t-=B[i]; C.push_back((t+10)%10); if(t<0) t=1;//和下面我自己处理的是一样的想法,但是他这和我的符号相反而已。 else t=0; } while(C.size()>1&&C.back() == 0) C.pop_back();//这一步没考虑到,但是结果出来看到有0,肯定会改,很妙,C.size() ==0的时候当然要留一个0, // for(int i=0;i<A.size();i++){ // if(A[i]<B[i]){ // C.push_back(A[i]-B[i]+10+t); // t=-1; // } // else{ // C.push_back(A[i]-B[i]+t); // } // } return C; } int main(){ string a,b; vector<int> A,B; cin>>a>>b; for(int i = a.size()-1;i>=0;i--) A.push_back(a[i]-'0');//从小位开始存 for(int i = b.size()-1;i>=0;i--) B.push_back(b[i]-'0'); vector<int> C; if(cmp(A,B)){ C=sub(A,B); for(int i=C.size()-1;i>=0;i--){ printf("%d",C[i]); } } else{ C=sub(B,A); printf("-"); for(int i=C.size()-1;i>=0;i--){ printf("%d",C[i]); } } return 0; }
#include<iostream> #include<vector> using namespace std; vector<int> mul(vector<int> A,int b){ int t=0; vector<int> C; for(int i=0;i<A.size() || t;i++){//这里他合在一起了,原本是要单独把超过了A.size()的t拿出来,在C的前面加上t%10,....知道t=0,代表加完了,他合在一起,然后下面那个t+=A[i]*b;用了个if判断要不要执行。妙 if(i<A.size()) t+=A[i]*b;//每次的t等于自己这一位乘b加上下面进的t。 C.push_back(t%10);//自己这一位的个位数,剩下的往上进. t/=10; } 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'); vector<int> C; C = mul(A,b); for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]); }
#include<iostream> #include<vector> #include<algorithm> using namespace std; vector<int> div(vector<int> A,int b,int &r){ vector<int> C; r=0; for(int i=A.size()-1;i>=0;i--){ r = r*10+A[i]; C.push_back(r/b); r%=b; } reverse(C.begin(),C.end()); while(C.size()>1 && C.back()==0) C.pop_back(); return C; } int main(){ string a; int b; cin>>a>>b; vector<int> A; for(int i=a.size()-1;i>=0;i--) A.push_back(a[i]-'0'); vector<int> C; int r; C = div(A,b,r); for(int i=C.size()-1;i>=0;i--) printf("%d",C[i]); cout<<endl; cout<<r<<endl; }
当实现题目要求的过程中需要多次用到从某个位置到某个位置的和时使用,因为生成前缀和Sn只需要O(n),之后的查询只需要O(1),否则的话每次计算都需要O(n),n次就是O(n^2),其实就是前n项和
一维数组前缀和:
#include<iostream> using namespace std; const int N=1e6+10; int n,m; int a[N],s[N]; int main(){ //los::sync_with_stdio(false);//这个可以提高cin的速度但不能用scanf了 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]); } return 0; }
二维数组前缀和:
#include<iostream> using namespace std; const int N = 1e4+10; int n,m,q; int 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("%d",&a[i][j]);3 for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) s[i][j]=s[i][j-1]+s[i-1][j]-s[i-1][j-1]+a[i][j]; while(q--){ int l1,r1,l2,r2; scanf("%d %d %d %d",&l1,&r1,&l2,&r2); printf("%d",s[l2][r2]-s[l2][r1-1]-s[l1-1][r2]+s[l1-1][l2-1]); } return 0; }
有数组a1,a2,…,an,构造b1,b2,…,bn的前缀和数组为an。
不管是前缀和还是差分,都是为了在进行大量操作是降低时间复杂度。尽管这些操作可能都是很简单的,但是会占用很多的时间成本,因此产生了前缀和 和 差分数组,牢记前缀和 和 差分等价于前n项和数组Sn和数列数组an。
核心为insert。
一维数组差分:
#include<iostream> using namespace std; const int N = 1e6+10; int n,m; int a[N],b[N]; void insert(int l,int r,int c){ b[l]+=c; b[r+1]-=c; } 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++) insert(i,i,a[i]); while(m--){ int l,r,c; scanf("%d %d %d",&l,&r,&c); insert(l,r,c); } for(int i=1;i<=n;i++) b[i]+=b[i-1]; for(int i=1;i<=n;i++) printf("%d",b[i]); return 0; }
二维数组差分:
#include<iostream> using namespace std; const int N = 1e4+10; int n,m,q; int a[N][N],b[N][N]; void insert(int x1,int y1,int x2,int y2,int c){ b[x1][y1]+=c; b[x2+1][y1]-=c; b[x1][y2+1]-=c; b[x2+1][y2+1]+=c; } int main(){ scanf("%d %d %d",&n,&m,&q); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) scanf("%d",&a[i][j]); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) insert(i,j,i,j,a[i][j]); while(q--){ int x1,y1,x2,y2,c; scanf("%d %d %d %d %d",&x1,&y1,&x2,&y2,&c); insert(x1,y1,x2,y2,c); } for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) b[i][j]+=b[i-1][j]+b[i][j-1]-b[i-1][j-1]; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ printf("%d ",b[i][j]); } printf("\n"); } return 0; }
核心思想:
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
O(n^2)
将上面的朴素算法优化到O(n)
双指针的算法一定是O(n^2),但是在过程中的优化剪枝可以很大的降低时间复杂度,
模板:
for(i=0,j=0;i<n;i++){
while(j<1 && check(i,j)) j++;
//每道题的具体逻辑
}
单个数组:
最长连续重复子序列
#include<iostream> using namespace std; int main(){ string a; int max=0,i,j; cin>>a; for(i=0;i<a.size()-1;i++){ j=i; while(a[j]==a[j+1]){ j++; } if(j-i+1>max) max=j-i+1; i=j; } printf("%d",max); }
最长连续不重复子序列
此处把j放在i的左侧也很妙,动态数组也很妙。
#include<iostream> using namespace std; const int N =1e6+10; int a[N]; int s[N]; int n; int main(){ cin>>n; for(int i=0;i<n;i++) scanf("%d",&a[i]); int res=0; for(int i=0,j=0;i<n;i++){//利用s存储很妙,这样子在后面的判断里根据s[a[i]]>1,判断是否重复,然后重复就s[a[j]]-- ,然后后移,这样子知道a[j]的值等于刚加进来的a[i]的值就可以停止,这个时候j-i这个区间就没有重复了 s[a[i]]++; while(s[a[i]]>1){ s[a[j]]--; j++; } res = max(res,i-j+1); } cout<<res<<endl; return 0; }
取某个数二进制下k位数的值。(a>>k)&1
lowbit(x):取x二进制下最后一个1的数(x & (~x)),如lowbit(10101000)=1000,=8,随意Lowbit操作可以找到x中1的个数。
通常是一个很大的范围但是里面的东西很稀疏,因此将其映射到一个小的范围了,并对每个下标有一个映射,之后通过下标找到映射后的就可以进行在原范围内同样的了。
区间离散化,
vector<int> alls; // 存储所有待离散化的值 sort(alls.begin(), alls.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 }
例题区间和
#include<iostream> #include <vector> #include <algorithm> #include<bits/stdc++.h> using namespace std; typedef pair<int,int> PII; const int N = 300010; int n,m; int a[N],s[N]; vector<int> alls; vector<PII> add,query; int find(int x){ int l=0,r=alls.size()-1; while(l<r){ int mid = (l+r)>>1; if(alls[mid]>=x) r = mid; else l= mid+1; } return r+1; } int main(){ cin>>n>>m; for(int i=0;i<n;i++){ int x,c; cin>>x>>c; add.push_back({x,c}); alls.push_back(x); } for(int i=0;i<m;i++){ int l,r; cin>>l>>r; query.push_back({l,r}); alls.push_back(l); alls.push_back(r); } sort(alls.begin(),alls.end()); alls.erase(unique(alls.begin(),alls.end()),alls.end()); for(auto item : add){ int x = find(item.first); a[x]+=item.second; } for(int i=1;i<=alls.size();i++) s[i] = s[i-1]+a[i]; for(auto item: query){ int l = find(item.first), r=find(item.second); cout<<s[r]-s[l-1]<<endl; } return 0; }
由于这个要在gcc11上才能运行,因此我只在ACWing上利用在线环境编译过,auto也要在较高版本上才能用
交集区间合并。
#include<iostream> #include <algorithm> #include <vector> 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(seg.second,ed); if(st != -2e9) res.push_back({st,ed}); } 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; return 0; }
head表示头结点。e[n]表示当前结点的value,ne[n]表示存储的下一个结点的位置,即e[ne[i]]表示i结点下一个结点的value。
#include<iostream> using namespace std; const int N = 1e6; //head头结点 //e[i]表示结点i的值 //ne[i]表示结点i的下一个结点是多少 //idx表示当前已经到了哪个结点 //用数组模拟数据结构是因为如果用结构体,然后new 结点的话,时间是很长的,长度稍微长点就会超时。 int e[N],ne[N],head,idx; void init(){ head = -1; idx = 0; } //将x插到头结点 void add_to_head(int x){//把x存到现在到的那个结点,然后把那个结点放在head头指针的后面,idx++就新的结点生成了。 e[idx]=x ne[idx]=head; head = idx; idx++; } void remove(int k){ ne[k]=ne[ne[k]]; } int main(){ }
直接判断输入的是什么后执行操作即可
#include<iostream> using namespace std; const int N=1e6+10; int l[N],r[N],e[N]; int n,index; void init(){ r[0]=1; l[1]=0; index=2; } //在下标是k的右边加入结点x。 //直接在下标是k的右边加是存在问题的。 void add(int k,int x){ e[index] = x; l[index] = k; r[index] = r[k]; r[k] = index; l[r[index]] = index; index++; } void remove(int k){ r[l[k]] = r[k]; l[r[k]] = l[k]; } int main(){ init(); add(0,5); add(0,10); for(int i=r[0];i!=1;i=r[i]){ cout<<e[i]<<endl; } }
算法一:
简单的两个for循环,第一个循环代表从0到n的第i个值。第二个for循环是从i到0找第一个小于i的值。
算法二:
利用单调栈,当存在 j < i , s t k j > s t k i j<i,stk_j>stk_i j<i,stkj>stki时,此时的 j j j是没有意义的,应该直接删掉的,因为对于后面的 k > i k>i k>i,对于 k k k的最近的小于 s t k k stk_k stkk的一定是 s t k i stk_i stki因此可以在栈中直接把j给删除了。如图:
显然,这样的图,对于后面还没有画出来的点,所有超过了最低点的都是无用的,所以最终有用的点图为:
仅有三个点时有用的,这三个点存在栈中,其他的都pop出来。
算法实现如下:
#include<iostream> using namespace std; const int N = 1e6+10; int n; int stk[N],tt; int main(){ cin>>n; for(int i=0;i<n;i++){ int x; cin>>x; while(tt && stk[tt] >= x) tt--;//如果栈内的>=x,说明这些点对于i后面的来说是没有意义的,因此pop出去,直到目前栈中的都比x小,然后进行下一步。 if(tt) cout<<stk[tt] << ' ';//如果栈没空了,则说明目前栈顶的就比x小,那么cout,否则则说明没有比x小的,那么就cout<<-1 else cout<<-1<<' '; stk[++tt]=x;//把x压入栈中,做为新的栈顶元素和后面的比较 } return 0; } //可以把cout,cin改为printf,scanf,可以变为原来的十分之一的时间左右
#include<iostream> using namespace std; const int N = 1e6+10; int a[N],q[N]; int n; int main(){ scanf("%d %d",&n,&k); for(int i=0;i<n;i++) scanf("%d",&a[i]); int hh=0;tt=-1;//tt是队头,队列里面放的是编号。 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--; //队内有元素,判断队尾是否大于现在的这个点的值,如果大于,说明目前这个点是没有意义的,因此要pop这个队尾,和单调栈极其的相似,不过是现在加了个hh的队头用于判断元素个数。 q[ ++ tt] = i;//把i这个点加入队列里去。 if(i>=k-1) printf("%d ",a[q[hh]]);//这个队列中队头的点的数值即为滑动窗口的最小值。 } //最大值则反过来即可。 printf("\n") ; 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]]); } return 0; }
暴力:
S[N],P[m];
for(int i=1;i<=n;i++){
bool flag = true;
for(int j=1;j<=m;j++)
if(s[i+j-1]!=p[j]){
flag=false;
break;
}
}
优化:
第二个for循环不用每次都从1开始,由KMP匹配字符串可以得到如果从i开始不匹配,那么从i=next[i]开始循环即可。next[i]=j的意思是P[1j]=P[i-j+1i]
#include<iostream> using namespace std; const int N = 1e4+10, M = 1e5+10; int n,m; char p[N],s[M]; int ne[N]; //生成next数组和匹配的过程是一样的。因为同样是字符串匹配,只是这里的字符串匹配是同一个字符串在匹配,当j=ne[j]后,p[i]==p[j+1],意味着j的下一个字符是可以和i匹配上的,那么i的ne[i]=j+1。代表着i的最大前缀和字符串是j+1。 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); j = ne[j];//找出所有 } } return 0; }
每次的匹配成功可以让下一次有了前面匹配的基础,如果匹配不成功,则变为ne[j],那么同样拥有前面匹配成功的基础,知道你的ne[j]=0,这意味着你没有前面的基础了,那么你如果匹配成功,那你的ne[i]=1,否则你没有ne[i],所以你的ne[i]=0;这就是构造next[]数组的精髓。
用于高效存储和查找字符串集合的数据结构
类似关联规则中建造FP树。
存储:在结点打上标记代表这个地方有单词,如abcdef则在最左边的那个f处做个标记,现在要插入一个abc则在c那里加一个标记。
查找,按照步骤来,如abc,现在去查,虽然能完整走完,但是c这里没有标记,所以表示没有这个单词,如果是abcf,则是走完,也代表没有这个单词
#include<iostream> using namespace std; const int N = 100010; int son[N][26],cnt[N],idx;//son代表着第p个结点的的儿子结点是谁,cnt代表这个单词出现了几次。idx代表着第几个数组,标是0的点,既是根结点,又是空节点。 比如abc,son[0][0]就是存的a,然后son[0][0]=1,代表着cnt[1]即为a单词出现的次数 char str[N]; void insert(char str[]){ int p=0; for(int i=0;str[i];i++){ int u = str[i]-'a'; if(!son[p][u]) son[p][u] = ++idx; p = son[p][u]; } cnt[p]++; } int query(char std[]){ 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; }
son[i] [j]代表着第idx=i的位置的子结点[j]是否存在,同时记录他的idx。比如现在存入abcd和bcd和abcf,
首先存入abcd
son[0] [0] = ++idx = 1;
son[1] [1] = ++idx = 2;
son[2] [2] = ++idx = 3;
son[3] [3] = ++idx = 4;
cnt[4]++;
这样就储存完了abcd,在这基础上再储存bcd
son[0] [1] = ++idx = 5;
son[5] [2] = ++idx = 6;
son[6] [3] = ++idx = 7;
cnt[7]++;
这样就储存完了bcd,在这基础上在储存abcf,
son[0] [0]存在,所以直接做p = son[0] [0] =1;
son[p] [1]因为p=1,所以存在,所以直接做 p = son[1] [1] = 2;
son[2] [2]存在,所以做p = son[2] [2]=3;
son[3] [4] 不存在,所以son[3] [4] = ++idx = 8;
然后cnt[8]++;
可以看到idx只是提供了p走向下一个位置的方位,以及作为cnt数组提供数量的一个坐标。如son[1] [7]=10,代表着下一个结点走到了son[10] [u],如果son[1] [7]就是结尾,则cnt[10]++;
例题:
解题思想:
首先将所有数字以二进制形式存入trie树中,如3:11,2:10,5:101。找最大值,然后从最前面的0开始往后搜索,如果最大值开始有数字,而该层有不同位的,就把地址改到不同位的层去,再从不同层的位比较是否数字相同,如果也有不同位的,然后再改地址,如图
#include<iostream> #include<algorithm> using namespace std; int const N=100010,M=31*N; int n; int a[N]; int son[M][2],idx; //M代表一个数字串二进制可以到多长 void insert(int x) { int p=0; //根节点 for(int i=30;i>=0;i--) { int u=x>>i&1; /取X的第i位的二进制数是什么 x>>k&1(前面的模板) if(!son[p][u]) son[p][u]=++idx; ///如果插入中发现没有该子节点,开出这条路 p=son[p][u]; //指针指向下一层 } } int search(int x) { int p=0;int res=0; for(int i=30;i>=0;i--) { ///从最大位开始找 int u=x>>i&1; if(son[p][!u]) 如果当前层有对应的不相同的数 { ///p指针就指到不同数的地址 p=son[p][!u]; res=res*2+1; ///*2相当左移一位 然后如果找到对应位上不同的数res+1 例如 001 } /// 010 else --->011 //刚开始找0的时候是一样的所以+0 到了0和1的时候原来0右移一位,判断当前位是同还是异,同+0,异+1 { p=son[p][u]; res=res*2+0; } } return res; } int main() { cin.tie(0); cin>>n; idx=0; for(int i=0;i<n;i++) { cin>>a[i]; insert(a[i]); } int res=0; for(int i=0;i<n;i++) { res=max(res,search(a[i])); ///search(a[i])查找的是a[i]值的最大与或值 } cout<<res<<endl; }
将两个集合合并、询问两个元素是否在一个集合当中。
不用并查集第二个操作是O(1),但是第一个操作要O(n)。用了并查集近乎都是O(1)
并查集的基础利用树来存储。同一颗树代表了同一个集合,根结点的编号就是这个集合的信息。每个结点存储它的父节点,p[x]表示x的父节点。
那么就有:
问题1:如何判断树根:if(p[x]==x)
问题2:如何判断x的集合编号:while(p[x]!=x) x = p[x];(路径压缩的优化)
问题3:如何合并两个集合:令一个树的父亲结点为p[x]=y即可,也就是让一个树的根结点指向另一个树的根结点,那么就合并了。
#include<iostream> using namespace std; const int N = 1e5+10; int p[N],n,m; 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];//用字符串的读入时不用考虑空格啥的影响,比如这里你输入n,m时会有空格,如果是char s,那么在读入时就得考虑空格或者回车了。 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)) cout<<"Yes"<<endl; else cout<<"No"<<endl; } } }
例题:连通块中点的数量
#include<iostream> using namespace std; const int N = 1e5+10; int p[N],n,m,size[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++){ size[i]=1; p[i] = i; } while(m--){ char op[2];//用字符串的读入时不用考虑空格啥的影响,比如这里你输入n,m时会有空格,如果是char s,那么在读入时就得考虑空格或者回车了。 int a,b; scanf("%s",op); if(op[0]=='C') { scanf("%d%d",&a,&b); if(find(a)==find(b)) continue;//判断是否在同一个集合中,防止做了翻倍。 size[find(b)]+=size[find(a)]; p[find(a)] = find(b); } else if (op[1]=='1') { scanf("%d %d",&a,&b); if(find(a)==find(b)) cout<<"Yes"<<endl; else cout<<"No"<<endl; } else{ scanf("%d",&a); cout<<size[find(a)]<<endl; } } }
基本操作:
1.插入一个数
2.求集合当中的最小值
3.删除最小值
4.删除任意一个元素
5.修改任意一个元素
堆是一颗完全二叉树(除最后一层非空,且最后一层从左到右)下标从1开始比较方便计算儿子结点,左儿子=2k,右儿子=2k+1
小根堆:根是最小值。
利用其完全二叉树的性质用一维数组存储。
操作down(x):
void down(int k)
{
int t = k;
if(k*2<=size && heap[k*2] < heap[t]) t = k*2;
if(k*2+1<size && heap[k*2+1] <heap[t]) t = k*2+1;
if(t!=k){
int temp = heap[k];
heap[k] = heap[t];
heap[t] = temp;
//或者swap(heap[k],heap[t]);
down(t);
}
}
操作up(x):则是从下往上移动。
void up(int k){
while(heap[k]<heap[k/2]){
swap(heap[k],heap[k/2]);
k/=2;
}
}
有了up和down后就可以做上面的五种操作了;
插入一个数就是在最后一个结点的后面插入一个数,然后up
heap[++size] = x;up(size);
求集合中最小值则是heap[1];
删除最小值则是把最后一个元素覆盖到第一个元素,然后–size;down(1)
删除任意一个元素heap[k] = heap[size];size–;down(k);up(k);
修改任意一个元素heap[k] = x;down(k);up(k);
堆排序:
#include<iostream> using namespace std; const int N = 1e6+10; int heap[N],size,n,m; void down(int k){ //自己写的 // int zko = 2*k,yok = 2*k+1; // if(yok<=size){ // if(heap[zko]<heap[yok] && heap[zko]<heap[k]){ // int temp = heap[k]; // heap[k] = heap[zko]; // heap[zko] = temp; // down(zko); // } // else(heap[yok] < heap[zko] && heap[yok] <heap[k]){ // int temp = heap[k]; // heap[k] = heap[yok]; // heap[yok] = temp; // down(yok); // } // } // else if(zko>=size && yok <size){ // if(heap(zko)<heap[k]){ // int temp = heap[k]; // heap[k] = heap[zko]; // heap[zko] = temp; // } // } //y总写的,秒的一逼,首先判断左儿子是否存在,然后判断是否小于自己,小于的话,最小的就是左儿子,然后再让第一步中的最小值和右儿子比较,如果右儿子最小,则最小值等于右儿子,t记录最小值的结点 //令heap[k]=heap[t],heap[t]=heap[k],然后再进行down(t),但是要先判断t==k是否,如果是则retkrn; int t = k; if(k*2<=size && heap[k*2] < heap[t]) t = k*2; if(k*2+1<size && heap[k*2+1] <heap[t]) t = k*2+1; if(t!=k){ int temp = heap[k]; heap[k] = heap[t]; heap[t] = temp; //或者swap(heap[k],heap[t]); down(t); } } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&heap[i]); size=n; for(int i=n/2;i;i--) down(i); while(m -- ){ printf("%d ",heap[1]); heap[1] = heap[size--]; down(1); } return 0; }
模拟堆:
#include<iostream> #include<string.h> using namespace std; const int N = 1e6+10; int ph[N],hp[N];//ph[k]第k个插入的点的下标.hp[k]堆里的第k个点时第几次插入的。 int heap[N],size,n,m; void heap_swap(int i,int j){ swap(heap[i],heap[j]); swap(ph[hp[i]],ph[hp[j]]); swap(hp[i],hp[j]); } void down(int k){ int t = k; if(k*2<=size && heap[k*2] < heap[t]) t = k*2; if(k*2+1<size && heap[k*2+1] <heap[t]) t = k*2+1; if(t!=k){ heap_swap(k,t); down(t); } } void up(int k){ while(k/2 && heap[k]<heap[k/2]){ heap_swap(k,k/2); k/=2; } } int main(){ scanf("%d",&n); while(n -- ){ char op[10]; int k,x; cin>>op; if(!strcmp(op,"I")){ scanf("%d",&x); size++; m++; ph[m] = size; hp[size] = m; heap[size] = x; up(size); } else if(!strcmp(op,"PM")) printf("%d",heap[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]; heap[k] = x; down(k),up(k); } } return 0; }
把一个庞大的空间通过哈希函数小的空间里。
离散化是一种特殊的哈希方式
开放寻址法:
开放寻址法和拉链法类似,但是这里就是没有拉链这样子的处理冲突的方式,开放寻址法的数组是比拉链法的大小大的,大概是2-3倍左右的大小,哈希的方式同样是h(x),这里处理冲突是往下一个顺位走,比如h(x) = 3,然后在3这里有人,就往4放,如果还有人,就依次到没有人的地方
开一个数组,然后按照哈希函数把原始映射的现在的数组中。如果存在冲突,则在数组的该位置后面加上一个结点,类似一个拉链。
例题:
拉链法:
#include<iostream> #include<cstring> using namespace std; const int N = 1e5+3;//取靠近需要空间的最小的质数 int h[N],e[N],ne[N],idx; //h[i]中存的是第i个数组位置指向的结点。e[i]是第i个结点的值,ne[i]是第i个结点的下一个结点。 //插入是让x插在第i个数组位置的第一个,即ne[idx]=h[k] h[k] = idx; void insert(int x){ int k = (x % N + N ) % N;//担心有负数 e[idx] = x; ne[idx] = h[k]; h[k] = idx++; } bool find(int x){ int k = (x%N+N)%N; for(int i = h[k];i != -1; i = ne[i]){ if(e[i] == x) return true; } return false; } int main(){ int n; cin>>n; memset(h,-1,sizeof h); while(n--){ char op[2]; int x; scanf("%s%d",op,&x); if( *op == 'I') insert(x); else{ if(find(x)) puts("Yes"); else puts("No"); } } return 0; }
开放寻址法:
#include<iostream> #include<cstring> using namespace std; const int N = 2e5+3,null = 0x3f3f3f3f; int h[N],n; int find(int x){ int k = (x % N + N) % N; while(h[k] != null && h[k] != x){ k++; if(k==N) k = 0; } return k; } int main(){ cin>>n; memset(h,0x3f,sizeof h);//memset是把每个字节都赋成0x3f,由于int是4字节,那么就是0x3f3f3f3f, //在拉链法中的-1,就是每个字节都是-1,由于-1表示是都是1,那么4个字节都是1,也就意味着最终的也是-1 //如果是0,那么就是每个字节都是0,4个字节也就是0,一般就这几种赋值的方法 while(n--){ char op[2]; int x; scanf("%s%d",op,&x); int k = find(x); if(*op == 'I') h[k] = x; else { if(h[k] != null) puts("Yes"); else puts("No"); } } }
首先预处理出来所有字符串的前缀哈希值
把字符串看成一个p进制的数
注意事项:(不能产生冲突)这就意味着p最最最小得大于26,不能映射为0,同样是为了防止出现冲突。
①不能映射为0
②Rp足够好,不存在冲突 通常p=131或者13331,Q = 2 64 2^{64} 264
例题:
#include<iostream> #include<math.h> using namespace std; typedef unsigned long long ULL; const int N = 1e5+10,P = 131; int n,m; char str[N]; ULL h[N],p[N];//h[i]代表的是这个字符串str的前i个字符对应的哈希值,比如JXUFEACWING ,h[1]则为"J"代表的哈希值,h[2]则为"JX"代表的哈希值,且h[2] = h[1] * p + str[2] //p[i]单纯代表了p的i次方而已 ,因此有get(l,r) = h[r] - h[l-1] * p [r-l+1],因为h[r]是p^r次方的,h[l-1]是p^l-1次方的,所以要*p^r-l+1次方,然后用h[r]-h[l-1]*p[r-l+1]才剩下l到r区间的哈希值。且此时的值为p^r-l+1次方的范围。 ULL get(int l, int r){ return h[r] - h[l-1] * p[r-l+1];//这里把p[r-l+1]改为pow(P,r-l+1)也是可以的 } int main(){ scanf("%d%d%s",&n,&m,str+1); p[0] = 1; for(int i = 1; i <= n; i ++){ p[i] = p[i - 1] * P; h[i] = h[i - 1] * P +str[i]; } while(m -- ){ int l1,r1,l2,r2; scanf("%d%d%d%d",&l1,&r1,&l2,&r2); if(get(l1,r1) == get(l2,r2)) puts("Yes"); else puts("No"); } return 0; }
size()长度;empty()是否为空;clear()清空;front()/back()第一个或者最后一个;push_back()/pop_back()插入或者删除最后一个数;begin()/end()
pair<int,pair<int,int>> 三元组
当需要存储两种不同的元素时,需要排序的放在first里不需要排序的放在second
p.first/p.second第一个或者第二个元素;支持比较运算
substr(1,2)第一个参数是初始位置,第二个参数是长度,下标从0开始,如果长度太长,则会输出到最后一个位置为止,也可以省略第二个参数,代表从初始位置到最后一个位置;c_str();size();empty();clear()
push()向队尾插入;front()返回队头元素;back()返回队尾元素;pop()弹出队头元素
push()插入一个元素;top()返回堆顶元素;pop()弹出堆顶元素
push()向栈顶插入一个元素;top()返回栈顶元素;pop()弹出栈顶元素
size();empty();clear();front();back();push_back()/pop_back();push_front()/push_front();begin()/end()
set中不能有重复元素,如果有插入重复元素,这个操作会被忽略掉,multiset可以有重复元素
size();empty();clear();begin()/end() ++,-- 但会前驱和后继
set/multiset:
insert()插入一个数;find()查找一个数;count()返回一个数的个数;erase(),如果是输入一个数x,就是删除所有数x 时间是O(k+logn),如果输入时一个迭代器,删除这个迭代器;lower_bound()/upper_bound(),l返回大于等于x的最小的数的迭代器,u返回大于x的最小的数的迭代器
map/multimap:
insert() 插入的数是一个pair;erase()输入的参数是pair或者迭代器;find();[]时间复杂度是O(logn)
和上面的类似,但是增删改查的时间复杂度是O(1),缺点是没有lower_bound()/upper_bound(),迭代器的++、–
存一个1000*1000bool类型的矩阵。可以省8倍的空间
主要是为了让内存符合需要。正常的1000*1000的 bool矩阵大小=100MB,太大了,超出了题目限制,用bitset则可以变为12.5,就可以压缩大小。
bitset<100000> s;~,&,|,^,>>,<<,==,!=,【】,count()返回有多少个1,any()判断是否至少有一个1;none() 判断是否全为0,set() 把所有位置成1;set(k,v)把k位设为v
栈,空间少,O(h),
全排列:
#include<iostream> using namespace std; int path[10],n; int v[10],sum; void dfs(int u){ if(u == n){ for(int i = 0; i < n; i ++) printf("%d ",path[i]+1); sum++; cout<<endl; return ; }; for(int i=0;i<n;i++){ if(v[i] != 1){ path[u] = i; v[i] = 1; dfs(u+1); v[i] = 0; } } } int main(){ cin>>n; dfs(0); cout<<sum; return 0; }
n皇后问题:
第一种搜索顺序:和全排列一样(一排一排放)
#include<iostream> using namespace std; char g[20][20]; int n; bool col[20],dg[20],udg[20]; void dfs(int u){ if(u == n){ for(int i=0;i<n;i++) puts(g[i]); puts(""); return; } for(int i = 0; i < n; i ++){ if(!col[i] && !dg[u+i] && !udg[n-u+i]){//为什么要用u+i,是因为,当放了u个的时候代表到了第u+1个地方,此时的i代表了,在第几格,合起来就表示了是在第几条对角线上,具体可以看上方图像,dg同理。这也是为什么要开2*10的空间大小的原因。dg[u+i]代表的是正的斜对角线不能有相同的,udg[n-u+i]代表反的斜对角线不能有相同的 g[u][i] = 'Q'; col[i] = dg[u+i] = udg[n-u+i] = 1; dfs(u+1); col[i] = dg[u+i] = udg[n-u+i] = 0; g[u][i] = '.'; } } } int main(){ cin>>n; for(int i = 0;i<n;i++){ for(int j=0;j<n;j++){ g[i][j] = '.'; } } dfs(0); return 0; }
第二种搜索顺序:一个格子一个格子枚举
每个格子都是放或者不放两种情况
#include<iostream> using namespace std; char g[20][20]; int n; bool row[20],col[20],dg[20],udg[20]; 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(g[i]); puts(""); } return ; } dfs(x,y+1,s); if(!row[x] && !col[y] && !dg[x + y] && ! udg[x -y +n]){//和dfs的一排一排不同,因此这里还多了一个row用于去除同一排的情况 g[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; g[x][y] = '.'; } } int main(){ cin>>n; for(int i = 0;i<n;i++){ for(int j=0;j<n;j++){ g[i][j] = '.'; } } dfs(0,0,0); return 0; }
队列,空间大O(2^h),最短性
例题:走迷宫
#include<iostream> #include<algorithm> #include<cstring> using namespace std; typedef pair<int ,int> PII; const int N = 110; int g[N][N]; int d[N][N]; int n,m; PII q[N * N]; int bfs(){ int hh = 0,tt = 0; 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){ q[++tt] = {x,y}; d[x][y] = d[t.first][t.second] + 1;//上一个位置的状态加1,其实很想dp了 } } } 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++){ scanf("%d",&g[i][j]); } } cout<<bfs()<<endl; }
图的存储方式:邻接矩阵,邻接表
例题:树的重心
#include<iostream> #include<cstring> #include<algorithm> using namespace std; const int N = 1e5+10,M = N * 2; int h[N], e[M], ne[M],idx; bool v[M]; int ans = N,n; void add(int a, int b){ e[idx] = b;//idx代表数组的位置,e[idx]代表存了哪个结点。ne[idx]代表这个结点的下个结点是谁,如5:->1->3,ne[idx] = 3,e[idx] = 1 ne[idx] = h[a]; h[a] = idx ++; } int dfs(int u){//e[i]表示i这个结点和哪个结点有关联,比如现在5:->1->3->4,代表着5和1,3,4有边 //dfs就是先找到5,然后访问5的所有关联的点,也就是h[5]连的是idx,但e[idx]=1,这里就是让j=e[idx] = 1,然后对h[1]再进行深度遍历 v[u] = true; int sum = 1,res = 0; for(int i = h[u]; i != -1; i = ne[i]){ int j = e[i];//res代表着当前遍历过的儿子结点中最大的数量。sum=遍历过的儿子的数量的和。 if(!v[j]){ int s = dfs(j); res = max(res,s); sum += s; } }//这个是儿子结点最大的那个的数量和非儿子结点中最大的。 res = max(res,n-sum);//n-size(A)-size(B)-...size(M)-1,1是自己本身的结点。 ans = min(ans,res); return sum;//dfs()返回值是sum,在dfs的过程中寻找ans和res,res是dfs(i),i这个结点去除后各部分最大的,ans是所有dfs(i)中res最小的。 /*9 1 2 1 4 1 7 2 8 2 5 4 3 4 6 3 9 比如4的儿子为3,6,则dfs(4)=dfs(3)+dfs(6),dfs(3)=dfs(9)+1,dfs(6)=1,这个1是sum原本设的值,很妙,因此dfs(4)=3,然后两个儿子分别为2和1 然后res则是计算两个儿子中最大的,在循环外边再和n-sum比较,就得到了去除4这个结点时的各连通块最大值,再拿ans和res做比较,找到最小的作为ans, 在进行操作的同时,每次dfs(4)会同时对每个子结点也做dfs,因此对于有向图,只需要dfs(i),i为根结点就能得到最小的ans了,如果不是根结点的话就dfs不全所有结点,所以一定得是根结点。 当然对于无向图来说就没有必要了,事实上对于无向图好像并不需要再和n-sum比较。不对不对,对于无向图也是需要的,因为在dfs后,设置了v[i]把这个点注销了,那么就不存在反过来再计算的情况了,因此得再比较 */ } void bfs(){ } int main(){ cin>>n; memset(h,-1,sizeof h); for(int i = 0;i<n;i++){ int a,b; cin>>a>>b; add(a,b); add(b,a); } dfs(1); cout<<ans; return 0; }
例题:图中点的层次
最短路(广搜,Dijkstra)
#include<iostream> #include<cstring> #include<algorithm> using namespace std; const int N = 1e5+10; int h[N],e[N],ne[N],idx,n,m; int d[N],q[N]; void add(int a,int b){ e[idx] = b; ne[idx] = h[a]; h[a] = idx ++; } int bfs(){ int hh = 0,tt = 0; q[0] = 1; memset(d,-1,sizeof d); d[1] = 0; while(hh<=tt){ int t = q[hh ++ ]; for(int i = h[t]; i != -1; i = ne[i]){ int j = e[i];//一定得注意这里,这里是e[i],而不是直接用i,因为i是在数组中的位置idx,而不是实际的结点,e[idx]才是idx这个位置对应的结点。 if(d[j] == -1){ d[j] = d[t] + 1; q[ ++ tt] = j; } } } return d[n]; } int main(){ cin>>n>>m; memset(h,-1,sizeof h); for(int i = 0; i < m; i ++){ int a,b; cin>>a>>b; add(a,b); } cout<<bfs()<<endl; return 0; }
这里因为边的长度都是1,所以用广搜而不是Dijkstra。
例题:有向图的拓扑序列
计算所有点的入度,首先将入度为0的放入队列,然后如果队列不空,就弹出队头,然后将队头元素存在和其他节点的点的度-1,如果此时那个结点的度=0了,那么也把这个结点入队。直至队列为空,说明没有入度为0的结点了,那么拓扑序列就不包括这些点,且重新输出队列从0->hh对应的结点值,就是拓扑序列了。
#include<iostream> #include<cstring> #include<algorithm> using namespace std; const int N = 1e5+10; int q[N],ne[N],e[N],idx; int h[N],m,n,d[N]; void add(int a,int b){ e[idx] = b; ne[idx] = h[a]; h[a] = idx ++; } bool topsort(){ int hh = 0, tt = 0; for(int i =1;i <= n; i ++){ if(d[i] == 0){ q[tt ++ ] = i; } } while(hh <= tt ){ int t = q[hh ++ ]; for(int i = h[t]; i != -1; i = ne[i]){ int j = e[i]; d[j]--; if(d[j] == 0){ q[tt ++ ] = j; } } } return tt == n; } int main(){ cin>>n>>m; memset(h,-1,sizeof h); for(int i =0; i < m; i ++){ int a,b; cin>>a>>b; d[b]++; add(a,b); } if(topsort()){ for(int i = 0; i < n; i++){ cout<<q[i]<<" "; } } else cout<<"-1"<<endl; return 0; }
朴素Dijkstra算法O(n^2)和堆优化版Dijkstra算法O(mlogn),m代表边数,n代表点数
朴素Dijkstra:
①初始化距离dis[1] = 0,dis[i] = +无穷
②for i:0~n,s为当前已确定最短距离的点
③(1)先找到不在s中的点集t,然后在t中找到最短的距离的点i,(2)然后用min(i到其他点的距离加上i本身的距离,其他点的距离)更新其他点的距离,(3)并把i设为已确定的最短距离的点。
对于未优化的③(1)总共进行了n^2次,(2)进行了n次,(3)进行了m次,因此是O(n^2)
朴素
#include<iostream> #include<cstring> #include<algorithm> using namespace std; const int N = 510; int n,m; int g[N][N]; int dist[N]; bool v[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(!v[j] && (t == -1 || dist[t] > dist[j])){ t = j; } } v[t] = true; for(int j = 1; j <= n; j++){//用这个点去更新其他的所有点的距离, 其实可以不用更新v[j] == true的点。 dist[j] = min(dist[j],dist[t]+g[t][j]); } } if(dist[n] == 0x3f3f3f3f) return -1; return dist[n]; } int main(){ cin>>n>>m; memset(g,0x3f,sizeof g); while(m--){ int a, b, c; cin>>a>>b>>c; g[a][b] = min(g[a][b],c); } int t = dijkstra(); cout<<t<<endl; return 0; }
如果把距离由原始的数组的形式写成小根堆的形式,那么对于③(1)总共就是n次,(2)是n次,但是(3)用堆更新每个点的距离是logn,然后mlogn是总次数。因此是O(mlogn)
堆优化
#include<iostream> #include<cstring> #include<algorithm> #include<queue> using namespace std; typedef pair<int,int> PII; const int N = 1e5+10; int n,m; int h[N],e[N],ne[N],w[N],idx; int dist[N]; bool v[N]; void add(int a,int b,int c){ e[idx] = b; w[idx] = c; ne[idx] = h[a]; h[a] = idx ++; } int dijkstra(){ memset(dist,0x3f,sizeof dist); dist[1] = 0; priority_queue<PII,vector<PII>, greater<PII>> heap;//生成一个优先队列。 heap.push({0,1});//second是编号,first是距离。把1这个点放到队列中,且距离=0 while(heap.size()){ auto t = heap.top();//找到队列的头。 heap.pop();//弹出头 int ver = t.second, distance = t.first;//记录距离最小值的距离和编号 if(v[ver]) continue;//如果该点是确定了最小距离的,就只弹出,但是不进行更新距离。 v[ver] = true; for(int i = h[ver]; i != -1; i = ne[i]){//如果是没有确定最小距离的。利用他来更新距离。此时的图是用邻接表来存储的。 int j = e[i]; if(dist[j] > distance + w[i]){ dist[j] = distance + w[i]; heap.push({dist[j],j}); } } } if(dist[n] == 0x3f3f3f3f) return -1; return dist[n]; } int main(){ cin>>n>>m; memset(h,-1,sizeof h); while(m--){ int a, b, c; cin>>a>>b>>c; add(a,b,c); } int t = dijkstra(); cout<<t<<endl; return 0; }
Bellman-Ford O(nm);SPFA 一般O(m),最坏情况下是O(nm)
如果有边数限制就只能用Bellman-Ford
为什么Bellman-Ford能应对负权边的问题?
这是由于在Dijkstra算法当中,如果存在负权边就意味着可能要推翻所有的已确定的v[ver],那么势必会导致出现异常。
但是在Bellman-Ford算法中,是对每一条边进行遍历,所有的dist都是在实时变化的,而不存在已经确定的情况,因此可以进行负权边的最短路求解。
Bellman-Ford
①n次迭代
②每次迭代循环所有边m
再更新到各个边的最短距离前,得先备份一遍,防止串起来导致的最短距离出现问题,比如从1->2=1,从2->3=1,从1->3=3,当进行第一次循环所有边确定最短距离时,首先更新了1->2=1,此时1->3应该=3的,因为第一次循环意味着只经过了一个边,但是由于更新导致距离错乱,就是1->3=1->2+2->3=2了,但是这应该是两次循环才能达到的,类似数据库中的数据混乱,因此得进行拷贝,然后用在利用拷贝的数组在原数组上进行更新。
③然后更新到各个边的最短距离
因此时间复杂度是O(nm)
例题(有边数限制的最短路)
#include<iostream> #include<cstring> #include<algorithm> using namespace std; const int N = 510, M = 10010; int n, m, k; int dist[N], backup[N]; struct Edge{ int a, b, w; }; Edge edges[M]; int bellman_ford(){ memset(dist,0x3f,sizeof dist); dist[1] = 0; for(int i = 0; i < k; i ++){ memcpy(backup,dist ,sizeof dist); for(int j = 0; j < m; j ++){ int a = edges[j].a, b = edges[j].b, w = edges[j].w ; dist[b] = min(dist[b], backup[a] + w); } } if (dist[n] > 0x3f3f3f3f / 2) return -0x3f3f3f3f; return dist[n]; } int main(){ cin>>n>>m>>k; for(int i = 0;i < m; i ++){ int a,b,w; cin>>a>>b>>w; edges[i] = {a,b,w}; } int t = bellman_ford(); if(t == -0x3f3f3f3f) cout<<"impossible"; else cout<<t<<endl; return 0; }
SPFA
实际上利用宽搜对Bellman-Ford进行优化,因为在Bellman-Ford中的dist[b] = min(dist[b],dist[a]+w)中只有当dist[a]变小了,dist[b]才有可能变小
优化如下:
首先将起点放入队列中,将dist[a]变小了的放入队列中,更新变小了a的所有的边,比如原本dist[5]是0x3f,由于起点1->5=3,所以dist[5]=3,因为dist[a=5]变小了,这个时候就可以把a放入队列中,然后对a能走的所有边进行一个距离更新的操作,比如5能去3,6,10,那么就对dist[3],dist[6],dist[10]进行更新操作,如果同样有变小的,如dist[3],那么就把3进队列(当然,得看下队列中是否有3,有3就无需再进队列了),然后进行同样操作,如果队列不空,直至无dist[a]变化,且队列中没有元素了。
#include<iostream> #include<cstring> #include<algorithm> #include<queue> using namespace std; typedef pair<int,int> PII; const int N = 1e5+10; int n,m,k; int h[N],e[N],ne[N],w[N],idx; int dist[N]; bool v[N]; void add(int a,int b,int c){ e[idx] = b; w[idx] = c; ne[idx] = h[a]; h[a] = idx ++; } int spfa(){ memset(dist,0x3f,sizeof dist); dist[1] = 0; queue<int> q; q.push(1); v[1] = true; while(q.size()){ int t = q.front(); q.pop(); v[t] = false; for(int i = h[t]; i != -1; i = ne[i]){ int j = e[i];//e[i]是i点对应的结点,w[i]是t与i点对应结点的边权 if(dist[j] > dist[t] + w[i]){ dist[j] = dist[t] + w[i]; if(!v[j]){ q.push(j); v[j] = true; } } } } if(dist[n] == 0x3f3f3f3f) return -1; return dist[n]; } int main(){ cin>>n>>m; memset(h,-1,sizeof h); while(m--){ int a, b, c; cin>>a>>b>>c; add(a,b,c); } int t = spfa(); if(t == -1) cout<<"impossible"; else cout<<t<<endl; return 0; }
spfa判断是否存在负环
#include<iostream> #include<cstring> #include<algorithm> #include<queue> using namespace std; typedef pair<int,int> PII; const int N = 1e5+10; int n,m,k; int h[N],e[N],ne[N],w[N],idx; int dist[N], cnt[N];//判断负环就是在这个加个cnt表示从1-x经历了多少条边,当cnt[i]>=n时代表从1->i的边数大于等于n,由于只有n各点,最长不成环图只有n-1个点,当>=n时代表着一定成环了,一定成环也就一定成负环,否则不会成环 bool v[N]; void add(int a,int b,int c){ e[idx] = b; w[idx] = c; ne[idx] = h[a]; h[a] = idx ++; } bool spfa(){ memset(dist,0x3f,sizeof dist); dist[1] = 0; queue<int> q; for(int i = 1; i <= n; i ++){ q.push(i); v[i] = true; } while(q.size()){ int t = q.front(); q.pop(); v[t] = false; for(int i = h[t]; i != -1; i = ne[i]){ int j = e[i];//e[i]是i点对应的结点,w[i]是t与i点对应结点的边权 if(dist[j] > dist[t] + w[i]){ dist[j] = dist[t] + w[i]; cnt[j] += 1; if(cnt[j] >= n) return true; if(!v[j]){ q.push(j); v[j] = true; } } } } return false; } int main(){ cin>>n>>m; memset(h,-1,sizeof h); while(m--){ int a, b, c; cin>>a>>b>>c; add(a,b,c); } if(spfa()) cout<<"Yes"; else cout<<"No"<<endl; return 0; }
Floyd算法 O(n^3)
基于动态规划
用邻接矩阵存储所有边d[i,j],
第一重循环k从1->n
第二重循环i从1->n
第三重循环j从1->n
更新d[i,j] = min(d[i,j],d[i,k]+d[k,j])
本来是d[m,i,j] = min(d[m-1,i,j],d[m-1,i,k]+d[m-1,k,j])
但是代表状态的m可以去掉,因此变为了d[i,j]=min(d[i,j],d[i,k]+d[k,j])
具体含义代表m状态下的i到j的最短距离=m-1状态下的i到j的最短距离或者m-1状态下的i到k的最短距离+k到j的最短距离。
#include<iostream> #include<cstring> #include<algorithm> using namespace std; const int N = 210,INF = 1e9; int n,m,q; int d[N][N]; void floyd(){ for(int k = 1; k <= n; k ++){ for(int i = 1; i <= n; i ++){ for(int j = 1; j <= n ; j ++){ d[i][j] = min(d[i][j],d[i][k]+d[k][j]); } } } } int main(){ cin>>n>>m>>q; for(int i= 1; i <= n; i ++){ for(int j = 1; j <= n; j ++){ if(i == j) d[i][j] = 0; else d[i][j] = INF; } } while(m--){ int a,b,w; cin>>a>>b>>w; d[a][b] = min(d[a][b],w); } floyd(); while(q--){ int a,b; cin>>a>>b; if(d[a][b] > INF / 2) cout<<"impossible"<<endl; else cout<<d[a][b]<<endl; } return 0; }
①距离设置为无穷
②i从0到n的for循环
找到集合外距离最近的点i
用i更新其他点到集合的距离,这是因为在连通块中的点已经更新过了,因此只需要用i更新一遍就是连通块到其他点的最短距离。
v[i] = true
集合是表示已经处于连通块中的点了。
#include<iostream> #include<cstring> using namespace std; const int N = 510, INF = 0x3f3f3f3f; int n,m; int g[N][N]; int dist[N];//此处的dist[i]代表着i点到连通块的最短距离 bool v[N]; int prim(){ memset(dist,0x3f,sizeof dist); int res = 0; for(int i = 0;i < n; i ++){ int t = -1; for(int j = 1; j <= n; j ++){ if(!v[j] && (t == -1 || dist[t] > dist[j])) t = j;//如果是第1次循环,直接把t=1放进去。//其实我感觉可以直接把1放进去然后省略一次循环由n变为n-1次 } if(i && dist[t] == INF) return INF;//如果不是第1次循环且最短距离是INF,说明这个树是不连通。 if(i) res += dist[t]; for(int j = 1; j <= n; j ++ ) dist[j] = min(dist[j],g[t][j]);//每次更新都是让dist[j]更新为已经连通块整体与未连通块的最小值,这也是和Dijkstra算法的区别所在,Dijkstra是起点和其他点的最小距离 v[t] = true; } return res; } int main(){ cin>>n>>m; memset(g,0x3f,sizeof g); while(m--){ int a,b,w; cin>>a>>b>>w; g[a][b] = g[b][a] = min(g[a][b],w); } int t = prim(); if(t == INF) cout<<"impossible"<<endl; else cout<<t<<endl; }
添边的算法。
①将所有边按照权重从小到大排序 O(mlogm),这个就是算法瓶颈,但是由于排序的系数m是比较小的,因此kruskal相对于其他的mlogn的算法来说是要快的。
②枚举每条边a-b,权重是c,如果当前a,b不连通,就将这条边加入集合中,利用并查集,总共m次就是O(m)。
#include<iostream> #include<cstring> #include<algorithm> using namespace std; const int N = 100010; int p[N]; int n,m; struct Edge{ int a,b,w; bool operator< (const Edge &W) const{ return w < W.w; } }edges[N]; int find(int x){ if(p[x] != x) p[x] = find(p[x]); return p[x]; } int main(){ cin>>n>>m; for(int i = 0; i < m; i ++){ int a,b,w; cin>>a>>b>>w; edges[i] = {a,b,w}; } sort(edges , edges + m); for(int i = 1;i <= n; i ++) p[i] = i; int res = 0, cnt = 0; for(int i = 0; i < m; i ++){ int a = edges[i].a, b = edges[i].b, w = edges[i].w; a = find(a) , b = find(b); if(a != b){ p[a] = b; res += w; cnt += 1; } } if(cnt < n - 1) cout<<"impossible"<<endl; else cout<<res<<endl; }
判断是否为二分图:染色法O(m+n)
二分图的定义是能将顶点分为两个集合,且仅在集合间有边,集合内部是没有边的图称为二分图
具体算法:
①对于每个点利用深度优先遍历(dfs)染色
②没有出现矛盾说明是不含奇数环的,所以是二分图
出现了矛盾说明含奇数环,所以不是二分图
#include<iostream> #include<cstring> #include<algorithm> using namespace std; const int N = 100010, M = 200010; int h[N],ne[M],e[M],idx; int color[N]; 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){ //注释的是我写的,现在的是y总的,我的好像出了错误,有些情况不行,通过不了, /* 5 5 5 5 1 2 1 4 2 3 2 4 */ // bool flag = true; // color[u] = c; // for(int i = h[u]; i != -1; i = ne[i]){ // int j = e[i]; // if(color[j] == 0) flag = dfs(j,-c); // else if(color[j] == c){ // return false; // } // } // return flag; color[u] = c; for(int i = h[u]; i != -1; i = ne[i]){ int j = e[i]; if(color[j] == 0) { if(!dfs(j,-c)) return false; } else if(color[j] == c) return false; } return true; } int main(){ cin>>n>>m; memset(h,-1,sizeof h); while(m--){ int a,b; cin>>a>>b; add(a,b),add(b,a); } bool flag = true; for(int i = 1; i <= n; i ++){ if(!color[i]){ if(!dfs(i, 1)){ flag = false; break; } } } if(flag) cout<<"Yes"<<endl; else cout<<"No"<<endl; }
匈牙利算法O(mn),实际远小于O(mn)
求二分图的最大匹配:没有两条边用同一点叫匹配,最大边个数就是最大匹配。
①从某个集合的第一个点开始选边,找第一条和另一个集合点的边,然后第二个点选边,如果第二个点只有一个边且第一个选了,那么就尝试第一个点有没有换的边,如果有,则第一个点换一条边,然后让第二个点使用这条边。
#include<iostream> #include<algorithm> #include<cstring> using namespace std; const int N = 510, M = 100010; int n1,n2,m; int h[N],e[M],ne[M],idx; int match[N]; bool v[N];//v[i]代表着此次过程中i以及被搜索过了,match[i]代表第i个人匹配的是谁,也就是右边的点是左边谁的匹配 void add(int a,int b){ e[idx] = b; ne[idx] = h[a]; h[a] = idx ++; } bool find(int x){ for(int i = h[x]; i != -1; i = ne[i]){ int j = e[i]; if(!v[j]){//j是否搜索过,如果没有就查看j是否有匹配,如果j没有匹配,那么就让j匹配x,如果j有匹配,就查看j匹配的那个点有没有别的匹配的点可以去匹配,如果有,那么j匹配的就匹配另一个,而j就变成没有匹配的,然后j去匹配新的。如果都所有x有边的点都不能匹配,那么就返回false,res就不++。返回true的话res++; v[j] = true; if(match[j] == 0 || find(match[j])){ match[j] = x; return true; } } } return false; } int main(){ cin>>n1>>n2>>m; while(m--){ int a,b; cin>>a>>b; add(a,b); } int res = 0; for(int i =1; i <= n1; i ++){ memset(v,false,sizeof v); if(find(i)) res ++; } cout<<res<<endl; return 0; }
质数的判定(试除法)O(sqrt(n))
bool is_prime(int x){
if(x < 2 ) return false;
for(int i = 2; i <= n / i; i ++)//用sqrt(n)由于这个函数运行较慢,所以不好,写成i*i <= n当n特别大的时候可能会导致i溢出
if(x % i == 0) return false;
return true;
}
分解质因数法O(sqrt(n))
从小到大枚举所有数,判断是否是n的质因数,如果是,则把i和次数列出来,直到sqrt(n)
void divide(int x){
for(int i = 2; i <= x / i; i ++){
if(x % i == 0){// i 一定是质数,因为在1~i-1的因数已经提取出来了,那么对i来说能让x%i==0()因数,说明i一定是质因数了。
int s = 0;
while(x % i == 0){
x /= i;
s ++;
}
printf("%d %d\n",i,s);
}
}
if(x > 1) printf("%d %d\n",x,1);
}
筛法(从2到n一共有多少质数):把不符合要求的筛出去,留下的就是符合要求的。
朴素是(n(1/2+1/3+…+1/n)nlnnnlogn)
从2开始往n挑,如果i没有被筛出去,就说明i是质数(因为i之前的数没有人的倍数是i),记录下i,同时把到n中i的倍数删除,
const int N = 1e6+10; bool v[N]; int primes[N],cnt; void get_prime(int n){ for(int i = 2; i <= n; i ++){ if(!v[i]) primes[cnt ++] = i;//如果i是false证明i不是i之前的数的倍数,说明i是质数, 就把i加入到primes数组中,同时再把i到n中,i的倍数删掉。 for(int j = i + i; j <= n; j += i ) v[i] = true; } } int main(){ int n; cin>>n; get_primes(n); cout<<cnt<<endl; }
埃式筛法:
优化(当一个数不是质数的时候就不需要筛掉他的倍数)
原本是O(n(1/2+1/3+…+1/n)),由于只需要算质数的,根据质数定理,从1-n中有n/lnn个质数,因此为O(n(1/2+…+1/n)/lnn~nloglogn约等于n)
void get_prime(int n){
for(int i = 2; i <= n; i ++){
if(!v[i]){
primes[cnt ++] = i;
for(int j = i + i; j <= n; j += i ) v[i] = true;
}
}
}
线性筛法
优化(遍历比n/i小的所有已存入primes的质数,对这些质数与i的积数筛掉,直到i是某个质数的倍数时停止)
核心:n只会被最小质因子筛掉
//primes数组是从
void get_prime3(int n){
for(int i = 2; i <= n; i ++){
if(!v[i]) primes[cnt ++] = i;
for(int j = 0; primes[j] <= n / i; j ++ ){
v[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
(1)试除法求一个数的所有约数
#include<iostream> #include<vector> #include<algorithm> 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; vector<int> res = get_divisors(x); for(auto t : res) cout<<t<<" "; cout<<endl; } return 0; }
(2)约数个数
若N = p 1 a 1 p 2 a 2 . . . p n a n p_1^{a_1}p_2^{a_2}...p_n^{a_n} p1a1p2a2...pnan,那么N的约数个数有 ( a 1 + 1 ) ( a 2 + 1 ) . . . ( a n + 1 ) (a_1+1)(a_2+1)...(a_n+1) (a1+1)(a2+1)...(an+1)
#include<iostream> #include<unordered_map> #include<algorithm> using namespace std; const int mod = 1e9+7; typedef long long LL; //求约数的个数 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 prime : primes) res = res * (prime.second + 1) % mod; cout<<res; return 0; }
(3)约数之和
若N = p 1 a 1 p 2 a 2 . . . p n a n p_1^{a_1}p_2^{a_2}...p_n^{a_n} p1a1p2a2...pnan,那么N的约数之和为 ( p 1 0 + p 1 1 + . . . + p 1 a 1 ) . . . ( p n 0 + p n 1 + . . . + p n a n ) (p_1^0+p_1^1+...+p_1^{a_1})...(p_n^0+p_n^1+...+p_n^{a_n}) (p10+p11+...+p1a1)...(pn0+pn1+...+pnan),
#include<iostream> #include<unordered_map> #include<algorithm> using namespace std; const int mod = 1e9+7; typedef long long LL; //求约数的个数 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 prime : primes){ int p = prime.first , a = prime.second; LL t = 1; while(a --) t = (t * p + 1) % mod; res = res * t % mod; } cout << res << endl; return 0; }
(4)欧几里得算法求最大公约数O(logn)
(a,b) = (b,a mod b)
因为a mod b = a - [a/b]*b,所以(b,a mod b) = (b,a - [a/b]*b]),如果d是a,b的最大公约数,所以d也是a - [a/b]*b]的最大公约数,所以(a,b) = (b,a mod b)
#include<iostream> using namespace std; int n; int name(int a,int b ){ if(b>a) swap(a,b); while(a % b){ int temp = b; b = a % b; a = b; } return b; } //y总的算法 int gcd(int a, int b){ return b ? gcd(b , a % b) : a;//b如果不是0,返回gcd(b, a % b),如果b是0(则表示上一层的a % b == 0),则返回a(上一层的b) } int main(){ cin>>n; while(n -- ){ int a , b; cin >> a >> b; int t = gcd(a,b); cout << t << endl; } }
f(n)表示1~n中与n互质的数的个数
f(6) = 2
若N = p 1 a 1 p 2 a 2 . . . p n a n p_1^{a_1}p_2^{a_2}...p_n^{a_n} p1a1p2a2...pnan,那么f(N) = N(1-1/p1)(1-1/p2)…(1-1/pn)
证明:容斥原理
①从1~N中去除p1,p2,…,pn的所有倍数(N-N/p1-N/p2-…-N/pn)
②加上所有pi*pj的倍数(+N/p1*p2+N/p1*p3+…N/p1*pn+…+N/pn-1*pn)
③再减去所有pi*pj*pk的倍数(-N/p1*p2*p3-…-N/pn-2*pn-1*pn)
④…n。
类比于p(AUBUC) = p(A)+P(B)+P©-p(AB)-P(AC)-P(BC)+P(ABC)
然后将上式展开就可以得到f(N) = N(1-1/p1)(1-1/p2)…(1-1/pn)
#include<iostream> #include<algorithm> using namespace std; int main(){ int n; cin >> n; while(n -- ){ int x; cin >> x; int res = x; for(int i = 2; i <= x / i; i ++ ){ if(x % i == 0){ res = res / i * (i - 1); while(x % i == 0) x /= i; } } if(x > 1) res = res / x * (x - 1); cout << res << endl; } return 0; }
利用筛法求1~n中的数的欧拉函数
首先利用线性,如果i是质数,则i的欧拉函数等于i-1,对于现存的所有质数
如果i是某个质数的倍数,则f(i)中的是包括了这个质数的,那么i*这个质数的欧拉函数f(i * primes[j]) = primes[j] * f(i)
如果i不是所有质数的倍数,那么i * 这个质数的欧拉函数f(i * primes[j]) = primes[j] * (1 - 1/primes[j]) * f(i)
#include<iostream> #include<algorithm> using namespace std; const int N = 1e6+10; typedef long long LL; int n; int primes[N], cnt; int phi[N]; bool v[N]; LL get_eulers(int n){ phi[1] = 1; for(int i = 2;i <= n; i ++){ if(!v[i]){ primes[cnt ++ ] = i; phi[i] = i - 1; } for(int j = 0; primes[j] <= n / i; j ++){ v[primes[j] * i] = true; if(i % primes[j] == 0){ phi[primes[j] * i] = primes[j] * phi[i]; break; } phi[primes[j] * i] = phi[i] * (primes[j] - 1); } } LL res = 0; for(int i = 1; i <= n; i ++) res += phi[i]; return res; } int main(){ cin >> n; cout << get_eulers(n)<<endl; return 0; }
欧拉定理:若a和n互质,则有 a f ( n ) m o d n = 1 a^{f(n)} mod n = 1 af(n)modn=1
证明:n中所有与n互质的数为a1…af(n)共f(n)个,那么a*a,…,a*an与n互质,且a*ai%n互不相同,而小于n且与n互为质数的数总共只有f(n)个,所以 a f ( n ) a 1 . . . a f ( n ) m o d n 一定且只能等于 a 1 . . . a f ( n ) m o d n a^{f(n)}a_1...a_{f(n)} mod n 一定且只能等于 a_1...a_{f(n)} mod n af(n)a1...af(n)modn一定且只能等于a1...af(n)modn
同时约掉后面的,则 a f ( n ) m o d n = 1 a^{f(n)} mod n = 1 af(n)modn=1
是一种能够快速O(logk)求出 a k m o d p a^k mod p akmodp的算法。当k很大的时候可以使用,如范围在1-10^9时,因为普通的算法在O(k)超过了一千万了,因此不行。
思想:
首先预处理出来 a 2 0 , a 2 1 , . . . , a 2 l o g k a^{2^0},a^{2^1},...,a^{2^{logk}} a20,a21,...,a2logk%p的结果
然后将a^k用上述表示后乘起来再做模运算即可。而 a 2 0 , a 2 1 , . . . , a 2 l o g k a^{2^0},a^{2^1},...,a^{2^{logk}} a20,a21,...,a2logk很好处理得到,后一位是前一位的平方。 a 2 l o g k = ( a 2 l o g k − 1 ) 2 a^{2^{logk}}=(a^{2^{logk-1}})^2 a2logk=(a2logk−1)2,因此预处理只需要logk的时间,转化为k转化为2进制也是logk的时间,因此整个算法的复杂度就是O(logk)
#include<iostream> #include<math.h> #include<algorithm> using namespace std; typedef long long LL; //a^k % p int qmi(int a, int k, int p){ int res = 1; while(k){//如果k不等于0,如果当前k的二进制为1,则让现在的模了剩下的数 = 之前模了剩下*当前位置模了剩下的数再取模 if(k % 2) res = (LL) res * a % p; k /= 2;//二进制进一位 a = (LL) a * a % p;//上一个模了剩下的数的平方再 取模 } 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; }
快速幂求逆元
给定一个ai找到逆元x使得ai*x=1(mod p)
由于此处的p是质数,所以根据欧拉定理中的费马定理,ai^(p-1) % p = 1,所以x=a^(p-2),所以就是输入一个数a和模数p,然后求a的p-2次幂mod p,当b是p的倍数的时候,无解。
#include<iostream> #include<math.h> #include<algorithm> using namespace std; typedef long long LL; //a^k % p int qmi(int a, int k, int p){ int res = 1; while(k){ if(k % 2) res = (LL) res * a % p; k /= 2; a = (LL) a * a % p; } return res; } int main(){ int n; scanf("%d",&n); while(n -- ){ int a, k, p; scanf("%d %d", &a, &p); int res = qmi(a, p - 2, p); if(a % p) printf("%d\n",res); else printf("impossible\n"); } return 0; }
裴蜀定理
有一对正整数a,b,那么一定存在非零整数x,y,使得ax+by=gcd(a,b)
核心:通过递归最后一层得到当时的x和y并传回到倒数第二层,对倒数第二层确定好对应的系数位置,然后对第二层时的等式进行整理,更新x和y,此时可以看到只需更新y。
当欧几里得算法return的时候,b=0,return a,也就是到了gcd(a,0)=a,此时ax+by=gcd(a,0),即x=1,y=0,然后递归回上一层时,由于是gcd(b,a % b)传进去的,所以此时是b为a,a % b = 0,所以返回上一层就是y=1,x=0,也就是对应的x,y每返回一层得交换,因此int d = exgcd(b,a%b,y,x),那么在倒数第二层的情况是by+a%bx = d,也就是by+(a-[a/b]*b)x = d,整理得ax+b(y-[a/b]*x) = d,则x不用变,y变为y-[a/b]*x
#include<iostream> 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); y -= a / b * x ; return d; } int main(){ int n; scanf("%d",&n); while(n --){ int a,b,x,y; scanf("%d%d",&a,&b); exgcd(a,b,x,y); printf("%d %d\n",x,y); } return 0; }
例题:线性同余方程(当bi为1,且mi为质数时,就变成了快速幂求逆元了,这里是普遍情况下的求法)
#include<iostream> 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); y -= a / b * x ; return d; } int main(){ int n; scanf("%d",&n); while(n --){ int a,b,m; scanf("%d%d%d",&a,&b,&m); int x,y; int d = exgcd(a,m,x,y);//扩展和原的返回值都是前两个值的最大公因数,只是扩展欧几里得算法还拓展了返回x,y,使得ax+by = d ,d就是最大公因数。 if(b % d) printf("impossible\n"); else printf("%d %d\n",(LL) x * (b / d) % m);//x * b/gcd(a,m) % m,因为b可能是gcd的倍数,所以用x*b/gcd 才是ax+my = d的解,还得mod m } return 0; }
若存在 m 1 , m 2 , . . . , m n 两两互质 m_1,m_2,...,m_n两两互质 m1,m2,...,mn两两互质,对于方程组 x = a i ( m o d m i ) , i = 1 , 2 , . . . , n x=a_i(mod\space{}m_i),i=1,2,...,n x=ai(mod mi),i=1,2,...,n,有解 x = a 1 M 1 M 1 − 1 + a 2 M 2 M 2 − 1 + . . . + M n M n − 1 x=a_1M_1M_1^{-1}+a_2M_2M_2^{-1}+...+M_nM_n^{-1} x=a1M1M1−1+a2M2M2−1+...+MnMn−1,其中 M = m 1 m 2 . . . m n M = m_1m_2...m_n M=m1m2...mn, M i = M m i , M i − 1 表示 M i 模 m i 的逆 M_i=\frac{M}{m_i},M_i^{-1}表示M_i模m_i的逆 Mi=miM,Mi−1表示Mi模mi的逆, M i − 1 M_i^{-1} Mi−1可以用扩展欧几里得算法求
(1)十万组询问求 C a b C_a^b Cab,1<a,b<2000
如果直接进行运算的话是100000X2000=2^ 8超过了1^8所以会超时,因此不能用这个方法。
最终选择先预处理出来所有的 C a b C_a^b Cab,然后进行n次询问,由于预处理所有的 C a b C_a^b Cab,只需要2000^ 2次(两层for循环,同时 C a b = C a − 1 b + C a − 1 b − 1 C_a^b=C_{a-1}^b+C_{a-1}^{b-1} Cab=Ca−1b+Ca−1b−1),因此是4*10^6,可以解决。
#include<iostream> using namespace std; const int N = 2010, mod = 1e9 + 7; int e[N][N]; void init(){ for(int i = 0; i < N; i ++) for(int j = 0; j <= i; j ++ ) if(!j) e[i][j] = 1; else e[i][j] = (e[i - 1][j - 1] + e[i - 1][j]) % mod; } int main(){ init(); int n; scanf("%d",&n); while(n -- ){ int a, b; scanf("%d%d",&a,&b); cout<<e[a][b]<<endl; } return 0; }
(2)a,b较大为10000时
首先预处理出i!和(i!)的逆元,然后利用 C a b = a ! b ! ( a − b ) ! C_a^b=\frac{a!}{b!(a-b)!} Cab=b!(a−b)!a!,
#include<iostream> using namespace std; typedef long long LL; const int N = 1e5 + 10, mod = 1e9 + 7; int fact[N], infact[N];//逆元用快速幂来求。 //首先预处理出i!%mod以及(i!)%mod的逆元。 //为什么要用逆元的原因是(i mod p)/(b mod p) 不等于i/b int qmi(int a,int k,int p){ int res = 1; while(k){ if(k % 2) res = (LL) res * a % p; a = (LL) a * a % p; k = k / 2; } return res; } int main(){ fact[0] = infact[0] = 1; for(int i = 1; i <= N; i ++){//次数qmi的意思是i的逆元,由于mod为质数,所以能用这种方法,否则需要找一个大于100010的质数去做。 fact[i] = (LL)(fact[i - 1] * i) % mod; infact[i] = (LL)infact[i - 1] * qmi(i,mod - 2, mod) % mod; } int n; scanf("%d",&n); while(n --){ int a,b; scanf("%d%d",&a,&b); cout << (LL) fact[a] * infact[b] % mod * infact[a - b] % mod << endl; } return 0; }
(3)a,b很大10^ 18,但是只询问了20组,1<p<10^5
卢卡斯定理: C a b = C a m o d p b m o d p C a / p b / p C_a^b=C_{a\space mod\space p}^{b\space mod \space p}C_{a/p}^{b/p} Cab=Ca mod pb mod pCa/pb/p,时间复杂度为 l o g p N p l o g p = p l o g N l o g p log_pN\space plogp=plogNlogp logpN plogp=plogNlogp,
证明:p为质数,对于a来说存在 a = a 0 p 0 + . . . + a k p k a=a_0p^0+...+a_kp^k a=a0p0+...+akpk,对b来说存在 b = b 0 p 0 + . . . + b k p k b = b_0p^0+...+b_kp^k b=b0p0+...+bkpk,由于 ( 1 + x ) a = ( 1 + x ) a 0 . . . ( 1 + x ) a k (1+x)^a=(1+x)^{a_0}...(1+x)^{a_k} (1+x)a=(1+x)a0...(1+x)ak,对于左边来说xb的系数则为$C_a^b$,而右边xb的系数则为 C a 0 b 0 . . . C a k b k C_{a_0}^{b_0}...C_{a_k}^{b_k} Ca0b0...Cakbk,因此卢卡斯定理得证
#include<iostream> using namespace std; typedef long long LL; const int mod = 1e9 + 7; int p; int qmi(int a,int k){ int res = 1; while(k){ if(k % 2) res = res * a % p; k /= 2; a = (LL) a * a % p; } return res; } int C(int a,int b){ int res = 1; for(int i = 1, j = a; i <= b; i ++, j -- ){ res = (LL) res * j % p; res = (LL) res * qmi(i, p - 2) % p; } return res; } int lucas(LL a, LL b){ if(a < p && b < p) return C(a % p, b % p); return (LL)C(a % p, b % p) * lucas(a / p, b / p) % p; } int main(){ int n; cin >> n; while(n -- ){ LL a, b; cin >> a >> b >> p; cout << lucas(a, b) << endl; } return 0; }
(4)a,b不是很大,但结果不%任何数,要高精度。
思路:
①分解质因数
C a b = a ! b ! ( a − b ) ! C_a^b=\frac{a!}{b!(a-b)!} Cab=b!(a−b)!a!对上面的进行分解质因数,对下面的分解质因数,如果质因数p在上面有就是+1,如果在下面有就是-1。最终得到一个质因数个数的数组。
②高精度乘法。
#include<iostream> #include<vector> using namespace std; const int N = 5010; int primes[N],cnt; int sum[N]; bool v[N]; void get_primes(int n){ for(int i = 2; i <= n; i ++){ if(!v[i]) primes[cnt ++] = i; for(int j = 0; primes[j] <= n / i ;j ++){ v[i * primes[j]] = true; if(i % primes[j] == 0) break; } } } 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> e; int t = 0; for(int i = 0; i < a.size(); i ++){ t = t + a[i] * b; e.push_back(t % 10); t /= 10; } while(t){ e.push_back(t % 10); t /= 10; } return e; } 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(b, p) - get(a - b, p); } for(int i = 0; i < cnt; i ++){ cout<<sum[i]<<endl; } vector<int> res; res.push_back(1); for(int i = 0; i < cnt; i ++){ for(int j = 0; j <sum[i]; j ++){ res = mul(res,primes[i]); } } for(int i = res.size() - 1; i >= 0; i --){ cout<<res[i]; } cout<<endl; }
6个1,6个0,从(0,0)到(6,6)的路径数有 C 12 6 C_{12}^6 C126条,不符合规矩的就是触碰到了红线的路径,而触摸到了红线的路径就等于从(0,0)到(5,7)的路径,因此方案数= C 12 6 − C 12 5 C_{12}^6-C_{12}^5 C126−C125
,对应本题的方案数即为 C 2 n n − C 2 n n − 1 = ( 2 n ) ! n ! n ! − ( 2 n ) ! ( n − 1 ) ! ( n + 1 ) ! = 1 n + 1 C 2 n n C_{2n}^n-C_{2n}^{n-1}=\frac{(2n)!}{n!n!}-\frac{(2n)!}{(n-1)!(n+1)!}=\frac{1}{n+1}C_{2n}^{n} C2nn−C2nn−1=n!n!(2n)!−(n−1)!(n+1)!(2n)!=n+11C2nn
#include<iostream> using namespace std; typedef long long LL; const int mod = 1e9 + 7; int n; int qmi(int a, int k, int p){ int res = 1; while(k){ if(k % 2) res = (LL) res * a % p; a = (LL) a * a % p; k /= 2; } return res; } int C(int a, int b){ int res = 1; for(int i = 1, j = a; i <= b; j --, i ++){ res = (LL) res * j % mod; res = (LL) res * qmi(i, mod - 2, mod) % mod; } return res; } int main(){ cin >> n; int res = (LL)C(2 * n, n) / (n + 1) % mod; cout<<res; }
内容:对集合 S 1 , S 2 , . . . , S n S_1,S_2,...,S_n S1,S2,...,Sn, S 1 ∪ S 2 ∪ . . . ∪ S n = S 1 + S 2 + . . . + S n − S 1 ∩ S 2 − S 1 ∩ S 3 − . . . − S 1 ∩ S n − . . . − S n − 1 ∩ S n + S 1 ∩ S 2 ∩ S 3 . . . + ( − 1 ) n − 1 S 1 ∩ S 2 ∩ . . . ∩ S n S_1∪S_2∪...∪S_n=\\S_1+S_2+...+S_n\\-S_1∩S_2-S_1∩S_3-...-S_1∩S_n-...-S_{n-1}∩S_n\\+S_1∩S_2∩S_3...+(-1)^{n-1}S_1∩S_2∩...∩S_n S1∪S2∪...∪Sn=S1+S2+...+Sn−S1∩S2−S1∩S3−...−S1∩Sn−...−Sn−1∩Sn+S1∩S2∩S3...+(−1)n−1S1∩S2∩...∩Sn
共2^n-1项。
例题:能被整除的数
可以看到n的范围在10^9,如果用循环一定是会超时的。因此选择使用容斥定理
素数i的集合 S i = i , 2 i , . . . , n i S_i={i,2i,...,ni} Si=i,2i,...,ni
对所有的素数p_i的集合利用容斥原理求并集个数即为能被整除的数的个数,时间复杂度为(2m)此处m在16以内,因此最大为65536。可以接受。而集合的个数是[n/p],有多个素数的倍数就是[n/(p1p2…pj)],求集合的个数是O(m)的,最终所有的时间复杂度是O(m*2m)=220=106,可以接受的。
#include<iostream> using namespace std; typedef long long LL; const int N = 20; int n, m; int p[N]; int main(){ cin >> n >> m; for(int i = 0; i < m; i ++) cin >> p[i]; int res = 0; for(int i = 1; i < 1 << m; i ++){//由于是从m个素数中选择任意个,因此有2^n-1种情况 ,利用位运算来选择要选的素数。如i=1,就是00001,只选择第一个素数,当i=2的时候就是00010,只选择第二个素数,当i=5的时候就是00101,选择第三个和第一个素数,而且根据1的个数来确定是加还是-,如果是偶数就是-,奇数就是+。 int t = 1, cnt = 0; for(int j = 0; j < m; j ++){ if(i >> j & 1){ cnt ++; if((LL)t * p[j] > n){ t = -1; break; } t *= p[j]; } } if(t != -1){ if(cnt % 2) res += n / t; else res -= n / t; } } cout << res << endl; return 0; }
做DP问题需要考虑的:
两个方面,一是用什么状态表示(一维,二维,…,n维数组),表示完后要考虑两个问题,一个是这个集合的含义,一个是这个表示的属性,如i个物品在体积为j的情况下的最大值。二是状态计算(从上一个状态到下一个状态的变化是什么,也就是状态转移方程)。
0 1背包(每件物品只能用一次)
#include<iostream> using namespace std; const int N = 1010; int v[N], w[N]; int dp[N][N]; int n, V; int f[N]; int main(){ cin >> n >> V ; for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i]; for(int i = 1; i <= n; i ++){ //未优化的二维的dp算法 for(int j = 1; j <= V; j ++){ dp[i][j] = dp[i - 1][j]; if(j >= v[i]) dp[i][j] = max(dp[i][j], dp[i - 1][j - v[i]] + w[i]); } //优化后的一维的DP算法 //首先把i去除,从二维的可以看到 for(int j = V; j >= v[i]; j --){ // dp[j] = dp[j] 但是由于这个式子恒成立,所以去除。 f[j] = max(f[j],f[j - v[i]] + w[i]);//这个式子直接去除了i轴,但是直接去除和之前显然是不等价的,因为dp[i-1]变成里dp[i],但是如果是从后往前更新,那么dp[i]中存的就是上一次的dp[i-1],因此将for循环改为从后往前就可以用直接删了i轴的维度了。 } } cout << dp[n][V] << endl; cout << f[V] << endl; return 0; }
完全背包(每件物品有无限个)
#include<iostream> using namespace std; const int N = 1010; int dp[N][N]; int p[N]; int f[N][N]; int v[N], w[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 ++){ int k = 0; for(int j = 0; j <= m; j ++){ //未优化的算法,此时由于多了一层k循环,这个循环最大可能是N的,因此最终时间复杂度为O(n^3) = 1e9会超时。因此想办法把k的循环去除,那么如何优化掉k呢 f[i][j] = f[i - 1][j]; //之前的是没有变化的,从这个k的for循环中可以看到,我们是找到第i-1个状态下j,j-v[i],j-2*v[i],...j-k*v[i]中最大的,并且让f[i][j]=这个最大值 //那么f[i][j-v[i]]呢,由上述的表达式可以知道f[i][j-v[i]]是在j-v[i],j-2*v[i],...j-(k-1)*v[i]中最大的,那么f[i][j]与f[i][j-v[i]]的区别就是没有把f[i-1][j]和之前的比较得出最大值,而由于f[i][j-v[i]]就是之前那些值中的最大值,那么就只需要把f[i-1][j]和f[i][j-v[i]]比较即可得到对于f[i][j]来说的之前的最大值了 for(int k = 0; j - k * v[i] >= 0; k ++){ f[i][j] = max(f[i][j],f[i - 1][j - k * v[i]]+k * w[i]); } //优化后的算法 dp[i][j] = dp[i - 1][j]; if(j >= v[i]) dp[i][j] = max(dp[i][j],dp[i][j - v[i]] + w[i]); //还能优化为一维的吗?可以看到当i轴去除后,就变成了 if(j >= v[i]) p[j] = max(p[j],p[j - v[i]] + w[i]); } } cout << p[m] << endl; return 0; }
可以看到优化后的01背包问题和完全背包问题只有顺序有不同的了,01背包问题的j是从后往前,而完全背包问题是从前往后,为什么顺序不同呢?
我认为01背包问题从后往前是因为只需要-一个v[i],从后往前就能够-了,且由于要利用现有的一维数组保留i-1个物品时的状态。而从前往后则会让后来的串联起来,导致后面的改变不是在i-1个物品时的状态了,因此需要从后往前。
而完全背包问题是需要将前面改为了i个物品状态的时候再更新,也就是由于数量是无限的所以需要串联,因此导致后面的改变需要在前面更新的基础上更新,因此需要从前往后走。
多重背包问题(每件物品的个数不一样)
多重背包问题和完全背包问题很像,但是由于数量是有限的,因此在挑选最大值的时候是从j,j-v[i],…,j-num[i]*v[i]这样一个范围挑选,类似于滑动窗口求最大值。
朴素做法:(当n,V和S不是很大的时候可以用,100左右,O(nVS)
#include<iostream> using namespace std; const int N = 110; int n, V; int v[N], w[N], s[N]; int dp[N][N]; int main(){ cin >> n >> V; for(int i = 1; i <= n; i ++) cin >> v[i] >> w[i] >> s[i]; for(int i = 1; i <= n; i ++){ for(int j = 1; j <= V; j ++){ dp[i][j] = dp[i - 1][j]; //朴素做法,在完全背包问题中加入一个数量小于s[i]的限制即可 for(int k = 0; j - k * v[i] >= 0 && k <= s[i]; k ++){ dp[i][j] = max(dp[i][j],dp[i - 1][j - k * v[i]]+ k * w[i]); } } } cout << dp[n][V] << endl; return 0; }
优化算法(将第i个物品s[i]个数量分解成logsi个物品,现在对于这些物品就是01背包问题了,而01背包问题的复杂度为O(nV),现在的V没变,但是n变为了n*logs个,因此优化后的算法复杂度为O(nVlogs),当n=1000,s,v=2000的时候就是1000*2000*log2000 = 2*10^7,可以接受)
#include<iostream> using namespace std; const int N = 11000, M = 2010;//为什么N开到11000是因为原来的n有1000个,而log2000的上取整是11,因此11000才不会发生越界 int n, V; int v[N], w[N], s[N]; int dp[N]; int main(){ cin >> n >> V; int cnt = 0; //在读入vws的时候就直接把这个转换为01背包问题,那么cnt就是新的数量n。 //为什么能这么转换是因为将一个数量s的物品转换为n组数量为a[i]的物品后,对这些物品的选择与否能够数量为s的物品选择任意个的结果。 //如s = 10,则转化为由个数1,2,4,3变成的新的四组物品,这四组物品的vi分别为v,2v,4v,3v,wi分别为w,2w,4w,3w //而新的四组物品的01背包问题,能够组成原始多重背包问题j~j-10v+10w的最大值 //比如7的时候的最大值,可以选择第1、2、3组,就可以构成7, //比如5的时候的最大值,可以选择第1、3组。 //为什么任意的s能够由一个二进制的组构成,是因为第一项能够构成0~1,加上第二项能够构成2~3,也就是0~3,加上第三项能够构成4~7,也就是0~7,加上第k项能够构成c~c+2^k-1=s,而c一定是小于等于2^k,否则能够把c分解成2^k+c1,此时c1作为新的c。而c小于等于2^k,而除开c之前的数能够构成2^k-1,因此能够构成0~s的任意一个数(类似用二进制优化) 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 = V; j >= v[i]; j --){ dp[j] = max(dp[j],dp[j - v[i]] + w[i]); } } cout << dp[V] << endl; return 0; }
分组背包问题(有N组物品,每组物品有若干个,每组物品只能选一个)
核心思想:
和01背包问题一样,对于每一组i的物品k,在体积j大于v[i][k]的时候,对每一个该组i的物品k都进行最大值的判断,找到最大值,将值赋予f[j]。比如f[1-10] = 1-10,现在有第二组一个物品k=1为v=2,w=3,那么f[10] = max(f[10],f[8]+3) = 11,第二组第二个物品为k=2,v=2,w=5,那么就有f[10] = max(f[10],f[8]+5) = 13,这样子来更新。
#include<iostream> using namespace std; const int N = 110; int n, V; int v[N][N], w[N][N], s[N]; int dp[N]; int main(){ cin >> n >> V; for(int i = 1; i <= n; i ++){ cin >> s[i]; for(int j = 1; j <= s[i]; j ++){ cin >> v[i][j] >> w[i][j]; } } for(int i = 1; i <= n; i ++){ for(int j = V; j >= 0; j --){ for(int k = 1; k <= s[i]; k ++){ if(v[i][k] <= j){ dp[j] = max(dp[j],dp[j - v[i][k]] + w[i][k]); } } } } cout << dp[V] << endl; return 0; }
数字三角形:
#include<iostream> using namespace std; const int N = 510, INF = 1e9; int g[N][N]; int dp[N][N]; int n; int main(){ cin >> n; for(int i = 1; i <= n; i ++){ for(int j = 1; j <= i; j ++){ cin >> g[i][j]; } } for(int i = 0; i <= n; i ++){ for(int j = 0; j <= i + 1; j ++){ dp[i][j] = - INF; } } dp[1][1] = g[1][1]; for(int i = 2; i <= n; i ++){ for(int j = 1; j <= i ; j ++){ dp[i][j] = max(dp[i - 1][j - 1] , dp[i - 1][j]) + g[i][j]; } } int res = - INF; for(int i = 1; i <= n; i ++){ res = max(res, dp[n][i]); } cout << res << endl; return 0; }
最长上升子序列(输出了最长序列)
#include<iostream> #include<cstring> using namespace std; const int N = 1010; int n; int a[N], dp[N], m[N]; int main(){ cin >> n; memset(m,-1,sizeof m); for(int i = 1; i <= n; i ++) cin >> a[i]; for(int i = 1; i <= n; i ++){ dp[i] = 1; for(int j = 1; j < i; j ++){ if(a[j] < a[i]){ if(dp[i] < dp[j] + 1){ m[i] = j; dp[i] = dp[j] + 1; } } } } int res = 0, index = -1; for(int i = 1; i <= n; i ++){ if(res < dp[i]){ res = dp[i]; index = i; } } cout << res << endl; for(int i = index; i != -1; i = m[i]){ cout << a[i] << " "; } cout << endl; }
最长上升子序列(数据量更大的时候)
分析序列的性质,可以发现,如果设q[i]为子序列长度为i时的值,有q[j]>q[i]当j>i时恒成立,而当新进入的一个数字,如果这个数字比q[j]大而比q[j+1]小,那么子序列长度为j+1的子序列的值就应该是这个新进入的数字的值,比如3,4,1,2,6。q[1] = 3,q[2] = 4,而现在来了一个1,进行比较发现1<q[1],那么q[1]更新=1,对新数字2来说,由于q[2]>2>q[1],因此q[2] = 2,对于新数字6来说,6>q[2],因此q[3] = 6,而最长子序列就是数组q的长度3。
#include<iostream> using namespace std; const int N = 1e5 + 10; int a[N], q[N]; int n; int dp[N]; int main(){ cin >> n; for(int i = 0; i < n; i ++) cin >> a[i]; int len = 0; q[0] = - 2e9; for(int i = 0; i < n; i ++){ int l = 0, r = len; while(l < r){ int mid = l + r + 1>> 1; if(q[mid] < a[i]) l = mid; else r = mid - 1; } len = max(len, r + 1); q[r + 1] = a[i]; } cout << len << endl; return 0; }
最长公共子序列
#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; scanf("%s%s",a + 1, b + 1); for(int i = 1; i <= n; i ++){ for(int j = 1; j <= m; j ++){ dp[i][j] = max(dp[i - 1][j],dp[i][j - 1]); if(a[i] == b[j]) dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1); } } cout << dp[n][m] <<endl; return 0; }
编辑距离
石子合并
#include<iostream> #include<math.h> using namespace std; const int N = 5010, INF = 1e9; int a[N], s[N]; int dp[N][N];//从i到j堆石子合并的代价 int n; int main(){ cin >> n; for(int i = 1; i <= n; i ++) cin >> a[i]; for(int i = 1; i <= n; i ++) s[i] = s[i-1] + a[i]; for(int i = 0; i <= n; i ++){ for(int j = 0; j <= n; j ++){ dp[i][j] = INF; if(i >= j) dp[i][j] = 0; } } //不用len来做。 // for(int i = n; i >= 1; i --){//注意i得从n开始,因为dp[i][j] = dp[i][k] + dp[k][j]总是需要先用到后面的,因此如果是从前面开始,用到dp[k][j]时,dp[k][j]还是INF,会导致最终的答案偏大 // for(int j = i + 1; j <= n; j ++){ // for(int k = i; k < j; k ++){ // dp[i][j] = min(dp[i][j],dp[i][k] + dp[k + 1][j] + s[j] - s[i - 1]); // } // } // } //用len来做 for(int len = 1; len <= n; len ++){ for(int i = 1; i + len - 1 <= n; i ++){ int j = i + len - 1; if(j == i) dp[i][j] = 0; else{ for(int k = i; k <= j; k ++){ dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + s[j] - s[i - 1]); } } } } // for(int i = 1; i <= n; i ++){ // for(int j = 1; j <= n; j ++){ // cout << dp[i][j] << " "; // } // cout << endl; // } cout<<dp[1][n]<<endl; }
整数划分
#include<iostream> using namespace std; const int N = 1010, mod = 1e9 + 7; int n; int dp[N][N]; int main(){ cin >> n; dp[0][0] = 1; for(int i = 1; i <= n; i ++){ for(int j = 1; j <= i; j ++){ dp[i][j] = dp[i - 1][j - 1] + dp[i - j][j] % mod; } } int res = 0; for(int i = 1; i <= n; i ++) res += dp[n][i] % mod; cout << res << endl; return 0; }
计数问题
对于解决某个区间的问题,我们经常用0n的情况-0m的情况,以获得n~m的情况,类似于前缀和的思想
#include<iostream> #include<vector> #include<algorithm> #include<math.h> using namespace std; int count(int n, int x){ if(!n) return 0; vector<int> num; while(n){ num.push_back(n % 10); n /= 10; } n = num.size(); int res = 0; for(int i = 0; i < n; i ++){ //依次统计各个数位上的x的个数,总和即为所有x的个数。 if(i < n - 1){ //前面有数字且在000-abc-1的情况 int sum = 0; for(int j = n - 1; j > i; j --){ sum = sum * 10 + num[j]; } // cout << "sum = " << sum << endl; if(x) res += sum * pow(10, i); else res += (sum - 1) * pow(10, i);//由于没有前导0,所以如果x是0的话需要-1. // cout << "res = " << res << endl; //计算后面的情况,即前面数字为abc时,第i个数如果==x,那么还要加上efg + 1种情况,如果第i个数>x,则后面的efg为任何数都可以就是pow(10,3)中情况。,如果第i个数<x,则无情况出现 sum = 0; for(int j = i - 1; j >= 0; j --){ sum = sum * 10 + num[j]; } if(num[i] == x) res += sum + 1; else if(num[i] > x) res += pow(10, i); } else{ //对于前面没有数字的时候,就没有000-abc-1的情况,直接计算后面的情况,如果第i个数==x,但是x==0,说明最前面的数是0,这是不符合规范的,所以这个和之前的比较出现问题的地方在x==0,因此在if判断中加上x!=0即可。其他的数是没有影响的,如果是0就无情况就行。 int sum = 0; for(int j = i - 1; j >= 0; j --){ sum = sum * 10 + num[j]; } if(num[i] == x && x != 0) res += sum + 1; else if(num[i] > x && x != 0) res += pow(10,i); } } return res; } int force_count(int n, int x){ int res = 0; for(int i = 1; i <= n; i ++){ for(int j = i; j; j /= 10){ if(j % 10 == x) res ++; } } return res; } int main(){ int n, m; while(cin >> n >> m, m || n){ if(n > m) swap(n,m); // cout << count(10,0); for(int i = 0; i <= 9; i ++) cout << count(m, i) - count(n - 1, i) << " "; cout << endl; } }
为什么叫状态压缩DP是因为这里的j和k是10进制的数,但是被我们看成是2进制的形式。用二进制的形式来表示是否伸出来的那个状态如10011,用整数表示某个状态,因此叫状态压缩。
#include<iostream> #include<cstring> using namespace std; const int N = 12, M = 1 << N; int n, m; long long dp[N][M]; bool v[M]; int main(){ int n, m; while(cin >> n >> m , n || m){ memset(dp, 0, sizeof dp); //先预处理出来哪些j和k是可以匹配上的。 for(int i = 0; i < 1 << n; i ++){ v[i] = true; int cnt = 0;//连续0的个数。 for(int j = 0; j < n; j ++){ if(i >> j & 1){//如果i的二进制表示中为1,则判断之前的0的个数有多少个,如果是奇数个,则v[i]=false,也就是i不合法。 if(cnt & 1) v[i] = false; cnt = 0;//如果是偶数个就令cnt=0,因为从下一个开始又要重新计数了。 } else cnt ++;//如果i的二进制当前不为1,则记连续的0个数++。 } if(cnt & 1) v[i] = false;//由于最后还有可能剩下一连串的0的个数,因此还要判断剩下的0是否是偶数。 } //上面预处理完了以后,下面就是dp的过程了。 dp[0][0] = 1; for(int i = 1; i <= m; i ++){//i从1到m,代表了从第一列到第m列 for(int j = 0; j < 1 << n; j ++){//这里是第i列伸出来的j,由于要表现所有的xxxxx情况,如10011,01100,因此j是< 1 << n的。 for(int k = 0; k < 1 << n; k ++){//收集所有满足第i列伸出来的为j的所有第i-1列伸出来的为k的情况。并计算和。 if((j & k) == 0 && v[j | k]){//看当前的j和k是否能契合,j & k是10011,01100能否合在一起,如果两个地方都有1就是冲突的。j | k代表,都是1的情况,如10011,01100,合就是11111,没有连续的0,因此是合法的,如果是10000,01000,合就是11000,有三个连续的0,就是不合法的。为什么用|是因为考虑的是i-1列,需要i-1列的所有空白的格子只能是偶数个,因为如果是奇数个,那么只能填横的格子,但是这样就和i的状态为j冲突了,因此是j|k不能有连续的格子。 dp[i][j] += dp[i - 1][k];//把j和k看成一个二进制的数,j由i-1状态下的k转移而来,由于能契合当前j的k有很多个,因此这里多了一个k层的循环,是为了寻找符合这种j的所有的k,然后总和就是i列伸出的为j的总方案数。 } } } } cout << dp[m][0] << endl; } return 0; }
最短Hamilton路径dp[i , j]表示从0走到j,经过了i的所有路径。i是二进制表示,表示通过了哪些点,如10011通过了1 4 5。
那么状态转移方程是什么样的呢?
由于dp[i , j]是从0走到j,经过了i的所有路径,那么在前一个状态就应该是没有从0走到k,经过了i除去j的所有路径,而由于i是二进制,所以i除去j的所有路径就是i-j,所有dp[i , j] = min(dp[i - j , k]),k 从0到n-1
#include<iostream> #include<cstring> using namespace std; const int N = 21, M = 1 << N ; int a[N][N], n;//从0走到j,经过了i这些点的所有路径 int dp[M][N]; int main(){ cin >> n; for(int i = 0; i < n; i ++) for(int j = 0; j < n; j ++) cin >> a[i][j]; memset(dp, 0x3f, sizeof dp); dp[1][0] = 0; for(int i = 0; i < 1 << n; i ++){//i为二进制的数,000000-111111的范围。 for(int j = 0; j < n; j ++){//j是到达的点。 if(i >> j & 1){//i >> j & 1 是为了判断i中有哪些结点是可以通过的,只有到过了那个结点才能让前一个状态为没到过那个结点的。 for(int k = 0; k < n; k ++){//k从0到n-1,代表是结点0到n-1, if((i - (1 << j)) >> k & 1){//如果i去掉了j这个结点还有哪些结点是经过的,如果结点是经过的,那么dp[i][j] = dp[经过i这些点 除了 j这个结点][到k] + 从k这个结点到j的路径的最小值就是所求。 //如i为11010,j=2,那么i - j << 1就是11010-00010=11000,这就是除去了j这个结点后还经过的结点,对于这些经过的结点再加上k到j的路径,就是通过i这些结点到j的路径。寻找这些路径的最小值,就是需要的值。比如k=4,那么就是dp[11000][4] + a[4][2]。 dp[i][j] = min(dp[i][j], dp[i - (1 << j)][k] + a[k][j]); } } } } } cout << dp[(1 << n) - 1][n - 1] << endl; return 0; }
状态压缩的DP问题这种状态表示的含义有点难理解,还有就是状态转移的情况中有些无意义的情况要舍去。
没有上司的舞会
#include<iostream> #include<cstring> #include<algorithm> using namespace std; const int N = 6010; int n; int h[N], e[N], ne[N], idx; int dp[N][N]; int happy[N]; bool has_father[N]; void add(int a, int b){ e[idx] = b; ne[idx] = h[a]; h[a] = idx ++ ; } void dfs(int u){ dp[u][1] += happy[u]; for(int i = h[u]; i != -1; i = ne[i]){ int j = e[i]; dfs(j); dp[u][1] += dp[j][0]; dp[u][0] += max(dp[j][0],dp[j][1]); } } int main(){ cin >> n; for(int i = 1; i <= n; i ++) cin >> happy[i]; memset(h, - 1, sizeof h); for(int i = 1; i < n; i ++){ int a, b; cin >> a >> b; has_father[a] = true; add(b, a); } int root = 1; while(has_father[root]) root ++; dfs(root); cout << max(dp[root][0], dp[root][1]) << endl; return 0; }
滑雪
//y总的算法 #include<iostream> #include<cstring> using namespace std; const int N = 310; int a[N][N]; int dp[N][N]; int n, m; int dx[4] = {1, - 1, 0, 0} , dy[4] = {0, 0, 1, - 1}; int f(int x, int y){ int &v = dp[x][y];//&v的话之后对v的运算也就是对dp[x][y]的运算。 if(v != -1) return v;//如果v不为-1代表已经访问过dp[x][y]了。这个感觉有没有无所谓,因为dp[x][y]不可能二次访问,因为从(x,y)到(x_now,y_now),由于严格单调,所以后面的条件应该能够限制不可能再返回去。刚刚经过了思考,这样子好像可以节省一些时间。 v = 1; for(int i = 0; i < 4; i ++){ int x_now = x + dx[i], y_now = y + dy[i]; if(a[x_now][y_now] > a[x][y] && x_now >= 1 && x_now <= n && y_now >=1 && y_now <= m){ v = max(v, f(x_now, y_now) + 1); } } return v; } int main(){ cin >> n >> m; for(int i = 1; i <= n; i ++) for(int j = 1; j <= m; j ++) cin >> a[i][j] ; memset(dp, -1, sizeof dp); int res = 0; for(int i = 1; i <= n; i ++){ for(int j = 1; j <= m; j ++){ res = max(res, f(i , j)); } } for(int i = 1; i <= n; i ++){ for(int j = 1; j <= m; j ++){ cout << dp[i][j] << " "; } cout << endl; } cout << res << endl; return 0; } //类似我之前用的深度优先搜索的算法。我那个是每次到了return的时候看看res = max(res, 当前的路径深度)。然后最终输出res。 // #include<iostream> // #include<cstring> // using namespace std; // const int N = 310; // int a[N][N]; // int dp[N][N]; // int n, m, res; // int dx[4] = {1, - 1, 0, 0} , dy[4] = {0, 0, 1, - 1}; // void dfs(int x,int y,int lujin){ // int cnt = 0; // for(int i = 0; i < 4; i ++){ // int x_now = x + dx[i], y_now = y + dy[i]; // if(a[x_now][y_now] > a[x][y] && x_now >= 1 && x_now <= n && y_now >=1 && y_now <= m){ // cnt ++; // dfs(x_now, y_now, lujin + 1); // } // } // if(!cnt) res = max(res, lujin); // } // int main(){ // cin >> n >> m; // for(int i = 1; i <= n; i ++) // for(int j = 1; j <= m; j ++) // cin >> a[i][j] ; // for(int i = 1; i <= n; i ++){ // for(int j = 1; j <= m; j ++){ // dfs(i,j,1); // } // } // cout << res << endl; // }
区间选点
#include<iostream> #include<algorithm> using namespace std; const int N = 1e5 + 10; int n; struct Range{ int l, r; bool operator <(const Range R){ return r < R.r; } }range[N]; int main(){ cin >> n; for(int i = 0; i < n; i ++){ int l, r; cin >> l >> r; range[i] = {l, r}; } sort(range, range + n); int end = - 2e9; int res = 0; for(int i = 0; i <= n; i ++){ if(range[i].l > end){ end = range[i].r; res ++ ; } } cout << res << endl; return 0; }
区间分组
#include<iostream> #include<algorithm> #include<cstring> using namespace std; const int N = 1e5 + 10; int n,max_R[N]; struct Range{ int l, r; bool operator <(const Range R){ return l < R.l; } }range[N]; int main(){ cin >> n; for(int i = 0; i < n; i ++){ int l, r; cin >> l >> r; range[i] = {l, r}; } sort(range, range + n); // memset(max_R, 0x3f, sizeof max_R); for(int i = 0 ; i < n; i ++){ max_R[i] = - 2*1e9; } max_R[0] = range[0].r; int cnt = 1; for(int i = 1; i < n; i ++){ int flag = 1; for(int j = 0; j < cnt; j ++){ if(range[i].l > max_R[j]){ max_R[j] = range[i].r; flag = 0; break; } } if(flag) max_R[cnt ++] = range[i].r; } cout << cnt << endl; return 0; }
区间覆盖
#include<iostream> #include<algorithm> #include<cstring> using namespace std; const int N = 1e5 + 10; int n,max_R[N]; bool success; struct Range{ int l, r; bool operator <(const Range R){ return l < R.l; } }range[N]; int main(){ int s, t; cin >> s >> t; cin >> n; for(int i = 0; i < n; i ++){ int l, r; cin >> l >> r; range[i] = {l, r}; } sort(range, range + n); // memset(max_R, 0x3f, sizeof max_R); int res = 0; for(int i = 0; i < n; i ++){ int j = i, r = -2e9; while(j < n && range[j].l <= s){ r = max(r, range[j].r); j ++ ; } if(r <= s){//这里只是判断每次往左是否有增加,只要增加了就不是-1,而不是判断最终是否覆盖了ed。 res = -1; break; } res ++; if(r >= t){ success = true;//成功了就是覆盖了ed,否则就是没有覆盖ed break; } s = r; i = j - 1; } if(success) cout << res << endl; else cout << "-1" << endl; return 0; }
合并果子
#include<iostream> #include<algorithm> #include<queue> using namespace std; const int N = 10010; int a[N]; int n; int main(){ cin >> n; priority_queue<int, vector<int>, greater<int>> heap;//定义一个小根堆。 while(n --){ int x; cin >> 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); } cout << res << endl; return 0; }
排队打水
#include<iostream> #include<algorithm> using namespace std; typedef long long LL; const int N = 1e5 + 10; 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; for(int i = 0; i < n; i ++) res += a[i] * (n - i - 1); cout << res << endl; return 0; }
耍杂技的牛
我在这个题想的贪心算法证明是这样的。
假设牛从上往下依次为1,2…,n,则有 x 1 = 0 − s 1 x 2 = w 1 − s 2 = x 1 + w 1 + s 1 − s 2 x 3 = w 1 + w 2 − s 3 = x 2 + w 2 + s 2 − s 3 x i = x i − 1 + w i − 1 + s i − 1 − s i x_1=0-s_1 \\x_2=w_1-s_2=x_1+w_1+s_1-s_2\\x_3=w_1+w_2-s_3=x_2+w_2+s_2-s_3\\x_i=x_{i-1}+w_{i-1}+s_{i-1}-s_i x1=0−s1x2=w1−s2=x1+w1+s1−s2x3=w1+w2−s3=x2+w2+s2−s3xi=xi−1+wi−1+si−1−si
因此有 w i − 1 + s i − 1 − s i w_{i-1}+s_{i-1}-s_i wi−1+si−1−si尽可能小时, x i x_i xi最小,所以按照 w i − 1 + s i − 1 < w i + s i w_{i-1}+s_{i-1}<w_i+s_i wi−1+si−1<wi+si来从小到大排序。
#include<iostream> #include<algorithm> using namespace std; const int N = 50010; int a[N], s[N]; int n; struct Cow{ int w, s; bool operator <(Cow c){ return s + w < c.s + c.w; } }cow[N]; int main(){ cin >> n; for(int i = 0 ; i < n; i ++ ){ int w, s; cin >> w >> s; cow[i] = {w,s}; } sort(cow, cow + n); for(int i = 1; i <= n; i ++) s[i] = s[i - 1] + cow[i - 1].w; int res = -2e9; for(int i = 0; i < n ; i ++){ res = max(res, s[i] - cow[i].s); } cout << res << endl; return 0; }
string line;
stringstreams ssin(line);
auto p;
while(ssin >> p){
}
在数组的函数不需要太大的时候最好是使用数组本身做映射,因为在利用map的时候,时间会用的太长了,除非是数组的范围需要开到很大,而需要映射的点不多的时候再用map
当题目中需要我们求某点到某些点的最短路等涉及到多个点的情况的时候,通常我们会选择虚拟出一个超级源点或者超级汇点,其与需要的多个点的距离为0,那么从这个点到某个点的最短路就是某个点到某些点的最短路,如果源点和汇点都是虚拟的超级点,那么就是从某些点到某些点的最短路。
在最短路中和最小生成树中总会出现。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。