假如给我十个模板
今天是2022年8月26日。距离csp初赛还有三个星期,距离csp复赛还有不到两个月。
除模拟题外,所有的编程题都是对一个或几个题型的综合。这种“题型”最原始的解法或思想我们称之为模板。
如果有十个模板可以带进考场,我会选择哪些?
1.中国剩余定理
-
- 对于同余问题,在满足一定条件的情况下可以直接使用中国剩余定理快速求解。
- 如果考场上遇到相关数论题,可以迅速完成代码框架搭建,留更多的时间给推导做题方法。
-
#include<bits/stdc++.h> using namespace std; #define ll long long const int h=10000001; /* 回忆一下扩欧 对于ax=1 mod p 可得(ax-1)=p*(-y) a*x+y*p=1 那么只要a与y互质,就可以求出一组x,y x就是我们要求的逆元 */ ll exgcd(ll a,ll b,ll &x,ll &y){ //if(a<b) // swap(a,b); ll xa=1,xb=0,ya=0,yb=1; ll xr,yr,r,p; while(b!=0){ p=a/b; r=a%b; xr=xa-ya*p; yr=xb-yb*p; xa=ya; xb=yb; ya=xr,yb=yr; a=b,b=r; } x=xa,y=xb; return a; } /* 关于crt 求解同余方程组 x=a1 mod m1 x=a2 mod m2 ...... x=ak mod mk 令M=m1*...*mk,Mi=M/mi 令ti=inv[Mi] mod mi 则有一个解x0=a1*inv[M1]*M1+...+ak*inv[Mk]*Mk 则对于这个问题,我们有最小解 证明:毕竟对于所有Mj,j!=i,Mj|mi,只需证ai*Mi^-1*Mi-ai|mi 那么转化为ai*(inv[Mi]*Mi-1) inv[Mi]*Mi=1 mod mi 成立 最小解即为:x0%M */ ll a[h],m[h]; int main(){ //ll l,r; //ll g=exgcd(46,240,l,r); //cout<<g<<" "<<l<<' '<<r; int n; ll maxx=0,M=1; cin>>n; for(int i=1;i<=n;i++){ scanf("%d%d",&m[i],&a[i]); M*=m[i]; } ll ans=0; //cout<<M<<endl; //turn(M); for(int i=1;i<=n;i++){ ll l,r; ll u=exgcd(M/m[i],m[i],l,r); ans+=a[i]*(((l%m[i])+m[i])%m[i])*(M/m[i]); } cout<<ans%M; return 0; }
2.扩展中国剩余定理
-
- 毕竟,不是所有的同余问题模数都互质,对于那些条件更广泛的问题,我们需要扩展中国剩余定理。
- 而且,其中包含的部分数论小工具对于解题而言也都可能发挥一定的作用。
-
#include<bits/stdc++.h> using namespace std; #define ll long long /* 扩展中国剩余定理 首先,这玩意和中国剩余定理是两个东西 更确切地说,你只需要会求拓展欧几里得即可 对于两个同余方程 x=r1 mod m1 x=r2 mod m2 将其转化为 x=m1*k1+r1 x=m2*k2+r2 转化为m1*k1+r1=m2*k2+r2 得到不定方程m1*k1-m2*k2=r2-r1 若这个方程有解,gcd(m1,m2)|r2-r1 否则无解 若有解,设d=gcd(m1,m2),p1=m1/d,p2=m2/d 那么将原方程转化为p1*k1-p2*k2=(r2-r1)/d 这时,p1,p2互质,方程右边是整数 可以求出一组k1,k2 令l1=k1/((r2-r1)/d),l2=... 求出p1*l1+p2*l2=1的解 那么k1就是l1*(r2-r1)/d 于是k1=l1*(r2-r1)/d,k2=-l2*(r2-r1)/d 拼出x=r1+k1*m1=r1+l1*(r2-r1)/d*m1 最终的表达式x=r1+l1*(r2-r1)/gcd(m1,m2)*m1 x就是原来方程组的特解 原方程组的解系即为x+z*lcm(m1,m2) zEZ 就是x0=x mod lcm(m1,m2) */ ll gcd(ll a,ll b){ if(a<0) a*=-1; if(b<0) b*=-1 ; if(a<b) swap(a,b); ll r; while(b!=0){ r=a%b; a=b,b=r; } return a; } ll lcm(ll a,ll b){ return a*b/gcd(a,b); } ll exgcd(ll a,ll b,ll &x,ll &y){ if(a<0) a*=-1; if(b<0) b*=-1; ll xa=1,ya=0,xb=0,yb=1; ll xr,yr,p,r; //a=b*p1+r1 //r=a-p1*b while(b!=0){ p=a/b,r=a%b; xr=xa-p*xb; yr=ya-p*yb; xa=xb,ya=yb; xb=xr,yb=yr; a=b,b=r; } x=xa,y=ya; return a; } int main(){ ll n; //由是,只需要连续求解即可 cin>>n; ll x,r1,r2,m1,m2,l1,l2; cin>>m1>>r1; for(int i=2;i<=n;i++){ cin>>m2>>r2; ll d=gcd(m1,m2); ll p1=m1/d,p2=m2/d; ll k1=((r2-r1)%m2+m2)%m2;////为什么是m2????????? exgcd(p1,p2,l1,l2); //if(k1<0) // k1*=-1; r1=r1+l1*k1/gcd(m1,m2)*m1; m1=lcm(m1,m2); } x=(r1%m1+m1)%m1; cout<<x<<endl; return 0; }
3.lucas定理
-
- 组合数问题不论在初赛还是在复赛都极有可能出现,也是许多数学问题的难点所在。
- 如果在第一题或者第二题忽然分析出组合数取模的裸题,那就直接拿全分,如果自己在考场现场推导,费时还有可能出错。
-
#include<bits/stdc++.h> using namespace std; #define ll long long /* 又...回到了这里... 在一次又一次理解失败后,我终于放弃了证明lucas lucas定理: if(b=0) return 1; lucas(a,b,p)=lucas(a/p,b/p,p)*c(a%p,b%p,p) 而此处的组合数计算为:c(m,n) if m>n return 0; else return ((n!(m^(p-2)%p))%p)*((n-m)!^(p-2)%p)%p; 记住了吗? いくてす! */ ll jc[100001]; ll qpow(ll x,ll y,ll p){ ll a=x; ll ans=1; while(y>0){ if(y&1) ans=(ans*a%p); y>>=1; a=a*a%p; } return ans%p; } ll c(ll n,ll m,ll p){ if(n<m) return 0; return ((jc[n]*qpow(jc[m],p-2,p)%p)*qpow(jc[n-m],p-2,p)%p);// } ll lucas(ll x,ll y,ll p){ if(y==0) return 1; return lucas(x/p,y/p,p)*c(x%p,y%p,p)%p;//这里一定要取模 } int main(){ int t; cin>>t; ll n,m,p; jc[0]=1; for(int i=1;i<=t;i++){ cin>>n>>m>>p; for(int j=1;j<=p;j++) jc[j]=(j*jc[j-1]%p); cout<<lucas(n+m,n,p)<<endl; } return 0; }
4.扩展lucas定理
-
- 理由同上,可以解决扩展的组合数取模问题。
-
#include <bits/stdc++.h> using namespace std; #define ll long long struct T { ll num,p; }; vector <T> p; ll anns=0,n,m,P,fac[1000010],cnt=0; void exgcd(ll a,ll b ,ll &x, ll &y)//扩欧 { if(b==0){x=1,y=0;return;} exgcd(b,a%b,y,x); y-=a/b*x; return ; } void pre()//分解p { int res=P; for(int i=2;i<=sqrt(P);i++) { if(res<i) break; if(res%i==0) { ll cnt=1; while (res%i==0){cnt=cnt*i;res/=i;} p.push_back({i,cnt}); } } if(res>1) p.push_back({res,res}); } ll quick_pow(ll a,ll b,ll mod)//快速幂 { if(b==0) return 1; if(b==1) return a%mod; if(b&1) return a%mod*quick_pow(a,b-1,mod)%mod; ll cun=quick_pow(a,b/2,mod)%mod; return cun*cun%mod; } ll preres(ll n,ll p,ll pe)//处理不含p的乘积 { ll ans=1; if(n==0) {return ans;} ll rou=1; ll rem=1; for (ll i=1;i<=pe;i++){if(i%p) rou=rou*i%pe;} rou=quick_pow(rou,n/pe,pe); for(ll i=pe*(n/pe);i<=n;i++){if(i%p) rem=rem*(i%pe)%pe;} return preres(n/p,p,pe)*rou%pe*rem%pe; } ll prepow(ll n,ll p)//处理每一次的指数 { if(n<p) return 0; return prepow(n/p,p)+(n/p); } ll inv(ll a,ll b)//求逆元 { ll ans,p; exgcd(a,b,ans,p); return (ans%b+b)%b; } void crt(ll m,ll a1)//合并答案 { ll Mul=P; ll pre=Mul/m; ll nipre; nipre=inv(pre,m); anns=(anns+a1*pre%Mul*nipre)%Mul; } int main() { scanf("%lld%lld%lld",&n,&m,&P); //n!/(m!(n-m)!) pre(); for(int i=0;i<p.size();i++)//枚举约数 { ll mo=p[i].num,pe=p[i].p; ll resn=preres(n,mo,pe),resm=preres(m,mo,pe),resM=preres(n-m,mo,pe);//求n! m! (n-m)!中不含mo的部分的乘积 ll ans; ll nim,niM; nim=inv(resm,pe); niM=inv(resM,pe);//求逆元 ans=quick_pow(mo,prepow(n,mo)-prepow(m,mo)-prepow(n-m,mo)/*mo的次数*/,pe)*resn%pe*nim%pe*niM%pe; crt(pe,ans);//合并答案 } printf("%lld",anns); return 0; } /*15 12 60*/
5.矩阵快速幂
-
- 很多简单递推的数列问题都可以放进矩阵中快速向后扩展,如果使用该算法可以在很多数列题中直接拿全分,性价比极高。
- 而这种问题通常的难点也是推出公式而不是写一个矩阵快速幂,所以有了这个模板可以空出更多时间给推导矩阵做法。
-
#include<bits/stdc++.h> using namespace std; #define ll long long ll mod=1e9+7; ll n,m,k; ll a[101][101],ans[101][101],update[101][101]; void expand(){ for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ ll sum=0; for(int l=1;l<=n;l++){ sum+=a[i][l]*a[l][j]; sum%=mod; } sum%=mod; update[i][j]=sum; } } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) a[i][j]=(update[i][j]%mod); } void mix(){ for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ ll sum=0; for(int l=1;l<=n;l++){ sum+=ans[i][l]*a[l][j]; sum%=mod; } sum%=mod; update[i][j]=sum; } } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) ans[i][j]=update[i][j]; } int main(){ cin>>n>>k; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){ cin>>a[i][j]; if(i==j) ans[i][j]=1; } //int u=1; while(k>0){ if(k&1) mix(); expand(); k=k>>1; } for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ cout<<(ans[i][j]%mod)<<' '; } cout<<endl; } //1 1 1 1 //1 1 1 1 //1*1+ return 0; }
6.后缀自动机
-
- 很多字符串问题可以使用后缀自动机解决,然而自动机的码量往往不小。
- 其他的字符串算法较之SAM较为简单,KMP、manacher、trie树都不难理解,所以在众多字符串算法中我选择SAM。
7.可持久化线段树
-
- 数据结构题码量巨大,细节很多,一旦出一个小错整题报废。
- 在众多数据结构中,当属可持久化最为生疏,有了可持久化线段树,就可以轻易变成可持久化数组,可持久化并查集等等,泛用性很强。
-
#include<bits/stdc++.h> using namespace std; const int h=25000001; int fa[h]; struct leaf{ int leftson,rightson; int ans; }tree[h]; int tot=0; int file[h]; int mirrow(int root){ tree[++tot]=tree[root]; return tot; } int build(int root,int l,int r){ root=++tot; if(l==r){ tree[root].ans=fa[l]; return root; } int mid=(l+r)/2; tree[root].leftson=build(tree[root].leftson,l,mid); tree[root].rightson=build(tree[root].rightson,mid+1,r); return root; } int modify(int root,int l,int r,int x,int ch){ root=mirrow(root); if(l==r){ tree[root].ans=ch; return root; } int mid=(l+r)/2; if(x<=mid) tree[root].leftson=modify(tree[root].leftson,l,mid,x,ch); else tree[root].rightson=modify(tree[root].rightson,mid+1,r,x,ch); return root; } int query(int root,int l,int r,int x){ if(l==r) return tree[root].ans; int mid=(l+r)/2; if(x<=mid) return query(tree[root].leftson,l,mid,x); else return query(tree[root].rightson,mid+1,r,x); } int n,m; int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&fa[i]); file[0]=build(0,1,n); for(int i=1;i<=m;i++){ int r,op,p; scanf("%d%d%d",&r,&op,&p); if(op==1){ int x; scanf("%d",&x); file[i]=modify(file[r],1,n,p,x); } else{ file[i]=file[r]; cout<<query(file[i],1,n,p)<<endl; } } return 0; }
8.树链剖分
-
- 树上权值修改操作必备算法,内置线段树,倍增lca,处理树上问题是一个很好的选择、
- 然而在考场上硬着头皮不出错打完整套轻重链剖分有一定难度,而且细节很多,所以需要这样一个模板。
-
#include<bits/stdc++.h> using namespace std; #define ll long long const ll h=1000001; inline ll read() { int s = 0, w = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') w= -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { s = s * 10 + ch - '0'; ch = getchar(); } return s * w; } ll line[h],val[h]; int siz[h],dep[h],dfn[h],son[h],top[h],father[h]; int n,m,s; ll mod; struct node{ ll l,r,v,lz; }tree[h*4]; void pushup(int root){ tree[root].v=tree[root*2].v+tree[root*2+1].v; return; } void build(int start,int end,int root){ tree[root].l=start;tree[root].r=end; tree[root].lz=0; if(start==end){ tree[root].v=line[start];return; } ll mid=(start+end)/2; build(start,mid,root*2); build(mid+1,end,root*2+1); pushup(root); } void pushdown(int root){ if(tree[root].lz!=0){ tree[root*2].lz+=tree[root].lz; tree[root*2+1].lz+=tree[root].lz; tree[root*2].v+=tree[root].lz*(tree[root*2].r-tree[root*2].l+1); tree[root*2+1].v+=tree[root].lz*(tree[root*2+1].r-tree[root*2+1].l+1); tree[root].lz=0; }return; } ll query(ll root,ll l,ll r){ if(tree[root].l>=l&&tree[root].r<=r){ return tree[root].v; }pushdown(root); ll mid=(tree[root].l+tree[root].r)/2;ll res=0; if(mid>=l) res+=query(root*2,l,r); if(mid<r) res+=query(root*2+1,l,r); return res; } void update(ll root,ll l,ll r,ll p){ if(tree[root].l>=l&&tree[root].r<=r){ tree[root].lz+=p; tree[root].v+=p*(tree[root].r-tree[root].l+1); return; }pushdown(root); ll mid=(tree[root].l+tree[root].r)/2; if(mid>=l) update(root*2,l,r,p); if(mid<r) update(root*2+1,l,r,p); pushup(root); } int head[h],last[h],to[h],tot=0; void add(int x,int y){ last[++tot]=head[x]; head[x]=tot; to[tot]=y; } void dfs1(int now,int fa){ father[now]=fa; dep[now]=dep[fa]+1; siz[now]=1; for(int i=head[now];i;i=last[i]){ int nex=to[i]; if(nex==fa) continue; dfs1(nex,now); siz[now]+=siz[nex]; if(siz[son[now]]<siz[nex]) son[now]=nex; } } int tott=0; void dfs2(int now,int fa,int up){ dfn[now]=++tott; line[dfn[now]]=val[now]; top[now]=up; if(!son[now]) return; dfs2(son[now],now,up); for(int i=head[now];i;i=last[i]){ int nex=to[i]; if(nex==son[now]||nex==fa) continue; dfs2(nex,now,nex); } } void tmodify(int x,ll ad){ //cout<<dfn[x]<<" "<<dfn[x]+siz[x]-1<<endl; update(1,dfn[x],dfn[x]+siz[x]-1,ad); } void pmodify(int x,int y,ll ad){ while(1){ if(top[x]==top[y]) break; if(dep[top[x]]<dep[top[y]]) swap(x,y); update(1,dfn[top[x]],dfn[x],ad); x=father[top[x]]; } if(dep[x]<dep[y]) swap(x,y); update(1,dfn[y],dfn[x],ad); } ll pquery(int x,int y){ ll cnt=0; while(1){ if(top[x]==top[y]) break; if(dep[top[x]]<dep[top[y]]) swap(x,y); cnt+=query(1,dfn[top[x]],dfn[x]); cnt%=mod; x=father[top[x]]; } if(dep[x]<dep[y]) swap(x,y); cnt+=query(1,dfn[y],dfn[x]); return cnt%mod; } ll tquery(int x){ return query(1,dfn[x],dfn[x]+siz[x]-1)%mod; } int main(){ scanf("%d%d%d%d",&n,&m,&s,&mod); int a,b; for(int i=1;i<=n;i++) scanf("%lld",&val[i]); for(int i=1;i<n;i++){ scanf("%d%d",&a,&b); add(a,b),add(b,a); } dfs1(s,0); dfs2(s,0,s); build(1,n,1); int op; int x,y; ll z; for(int i=1;i<=m;i++){ scanf("%d",&op); if(op==1){ scanf("%d%d%lld",&x,&y,&z); pmodify(x,y,z); } if(op==2){ scanf("%d%d",&x,&y); printf("%lld\n",pquery(x,y))%mod; } if(op==3){ scanf("%d%lld",&x,&z); tmodify(x,z); } if(op==4){ scanf("%d",&x); printf("%lld\n",tquery(x)%mod); } } return 0; }
9.最高标号预流推进网络流算法
-
- 现阶段网络最大流问题最优解
10.模拟退火
-
- 遇到一些“很奇怪“的问题的时候,有时甚至难以模拟,这种时候只能硬着头皮建模赌一把。
- 万一跑出来了呢?
这是对于我而言很重要的十个模板,有一半都是数学内容,的确,在八月之前,我还从没有接触过计算机上的数论题,初赛复习更是被这些东西深深困扰。
说句闲话,数学真的是是很重要的一门学科。
其实如果再给我一点空间,我还会考虑高精度,但是一般情况下考试的时候写高精能拿到的分数并不多,更多时候可能会选择把时间拿来优化算法。
以上。