计数 dp 部分例题(六~十部分)
六、转化求和顺序(線形和への分解)
例题1
题意
有一个长为 的数组 。求在 中选择 个数的所有方案中,每次选择的所有数的中位数的和。 为偶数。
解法
设 。排序后,设在最中间的两个数为 ,则 在所有中位数里出现了 次,则答案为 ,化简可以变成 ,后缀和优化计算即可。
例题2
求 。。
解法
考虑将每个 的每一位拆开,然后合并每一位。设 为满足 且第 位为 的数的数量(显然可以 计算),则第 位造成的贡献为 。
例题3:Many Easy Problems
给定一棵大小为 的无根树,定义 ,对于所有大小为 的点集,求出能够包含它的最小连通块大小之和。对于 的所有 ,求出 。。(,原根为 )
解法
考虑将“能够包含每个大小为 的点集的最小连通块大小之和”在每个点而非每个连通块处计算贡献,则其可以转化为“对于每个点,求作为包含某个大小为 的点集的最小连通块中包含了该点的连通块数量”。
设以某个点 为根时,其的每棵子树内大小分别为 ,则某个最小连通块内不包含 的充要条件是内部每个点都在 儿子的子树内, 对 的贡献即为 , 即为 。设 ,则 。将 拆成 的形式,令 ,则 ,就是标准的卷积形式了(差卷积)。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=200010; const int maxd=maxn<<1; const int maxp=524300; const int md=924844033,G=5; int n,i,j,k,u,v,t,p,le,lm; int h[maxn],c[maxp],d[maxp]; int r[maxp],fac[maxn],inv[maxn]; struct edge{int to,nxt;}E[maxd]; inline int Pow(int d,int z){ int r=1; do{ if(z&1) r=(1LL*r*d)%md; d=(1LL*d*d)%md; }while(z>>=1); return r; } int dfs(int p,int f){ int lp,to,sz=1,nw; for(lp=h[p];lp;lp=E[lp].nxt){ to=E[lp].to; if(to==f) continue; nw=dfs(to,p); sz+=nw; ++c[nw]; } ++c[n-sz]; return sz; } inline void Add(int &x,int y){x-=((x+=y)>=md)*md;} void DFT(int *f){ for(i=1;i<t;++i) if(i<r[i]) swap(f[i],f[r[i]]); for(i=1,le=2;le<=t;i=le,le<<=1){ v=Pow(G,(md-1)/le); for(j=0;j<t;j+=le){ for(k=0,u=1;k<i;++k,u=(1LL*u*v)%md){ p=(1LL*f[i+j+k]*u)%md; Add(f[i+j+k]=f[j+k],md-p); Add(f[j+k],p); } } } } inline int C(int x,int y){return ((1LL*fac[y]*inv[x])%md*inv[y-x])%md;} int main(){ fac[0]=1; for(i=1;i<maxn;++i) fac[i]=(1LL*fac[i-1]*i)%md; inv[maxn-1]=Pow(fac[maxn-1],md-2); for(i=maxn-1;i;--i) inv[i-1]=(1LL*inv[i]*i)%md; scanf("%d",&n); for(i=1;i<n;++i){ scanf("%d%d",&u,&v); E[++t]={u,h[v]}; h[v]=t; E[++t]={v,h[u]}; h[u]=t; } dfs(1,0); c[0]=0; d[0]=fac[n]; for(i=1;i<=n;++i){ d[i]=inv[n-i]; c[i]=(1LL*c[i]*fac[i])%md; } for(u=n<<1,t=1;t<u;t<<=1); for(le=t>>1,i=0;i<t;++i) r[i]=(r[i>>1]>>1)+(i&1)*le; DFT(c); DFT(d); for(i=0;i<t;++i) c[i]=(1LL*c[i]*d[i])%md; DFT(c); reverse(c+1,c+t); u=Pow(t,md-2); for(i=1,j=n+1;i<=n;++i,++j){ v=((1LL*c[j]*u)%md*inv[i])%md; Add(v=md-v,(1LL*n*C(i,n))%md); printf("%d\n",v); } return 0; }
七、置换群相关知识(部分群のテクニック)
置换群相关知识可以用于解决某些不可重计数问题。然而自认为后面的两个题的解法和置换群相关知识联系不大,需要者可以自行看 OI Wiki。不过这里的题目都是和“如何交换两个可交换的位置以尽量少影响其他位置/找出能够表示其他所有操作的单位操作”的思路有关。
例题1:Reverse Grid
题意
有一个 的矩阵 ,可以进行若干次操作,每次选择一行或一列翻转,求最后能够生成的本质不同的矩阵有多少种。 矩阵内的元素均为小写字母。
解法
考虑某个 只会变到 的位置。(对于 为奇数的情况,可以直接乘上中间能否变化的方案数,则其他元素都有四种可能出现的位置)所以这四个元素可以看成一个四元组整体处理。设四个位置的数分别为 ,则可以通过下面的方式使得只有 之间的顺序进行变化(箭头表示行/列内其他元素的顺序):
同理其他任意三个元素也可以按照这样的方式变化,打表可以发现这些元素出现的顺序有 种。同时可以发现将同在一行/一列的元素调换位置可得所有 种方式中另外 种出现方式,意味着某行被翻转将导致若干个四元组能够变换的位置同时变化。注意如果某个四元组内部出现了相同的数,则它们一定可以在某次变化后出现在同一行,它们在被翻转某行/某列后能够变换出的顺序一样,可以把对应的贡献先乘上;否则在翻转某行/某列后对应能够变换出的顺序会变化。
考虑将每一行和每一列看成点,然后对于某个四元组 ,在第 行和第 列之间连边。在翻转某行/某列之后对应出边的四元组状态也会改变,所以可以对于某个连通块 求出某棵生成树,然后对于树上 种状态,自顶向下确定某行/某列是否翻转(会有两种等效的方式),进一步确定其他所有边的状态(只会有一种)。时间复杂度为 。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=210; const int maxt=maxn<<1; const int md=1000000007; int n,m,i,j,k,p,w,a,b,ans=1,c[4]; int fa[maxt],siz[maxt]; char s[maxn][maxn]; int Find(int x){ if(x==fa[x]) return x; return fa[x]=Find(fa[x]); } inline void Merge(int x,int y){ x=Find(x); y=Find(y); if(x==y) return; if(siz[x]>siz[y]) swap(x,y); fa[x]=y; siz[y]+=siz[x]; siz[x]=1; } int main(){ scanf("%d%d",&n,&m); for(i=1;i<=n;++i) scanf("%s",s[i]+1); a=(n>>1)+1; b=(m>>1)+1; if(n&1){ for(i=1;s[a][i]==s[a][m-i+1]&&i<=m;++i); if(i<=m) ans<<=1; } if(m&1){ for(i=1;s[i][b]==s[n-i+1][b]&&i<=n;++i); if(i<=n) ans<<=1; } for(i=1,j=a+b-2;i<=j;++i) fa[i]=i,siz[i]=1; for(i=1;i<a;++i){ for(j=1;j<b;++j){ char t[4]={s[i][j],s[i][m-j+1], s[n-i+1][j],s[n-i+1][m-j+1]}; sort(t,t+4); w=24; for(k=c[p=0]=1;k<4;++k) w/=(++c[p+=(t[k]!=t[k-1])]); if(p==3) Merge(i,a+j-1),w=12; ans=(1LL*ans*w)%md; memset(c,0,(p+1)<<2); } } for(i=1,j=a+b-2;i<=j;++i) for(w=siz[i];--w;ans-=((ans<<=1)>=md)*md); printf("%d\n",ans); }
例题2:Rotate 3x3
题意
有一个 的网格,第 行第 列的数为 。可以进行若干次操作,每次将某个 的子矩阵旋转 ,求能否经过若干次操作得出某个 矩阵。。
解法
显然每一列的数不会改变,且已知每一列被翻转的次数的奇偶性,则可以将每一列压成一个数,可以用正负号表示对应的列是否翻转。此时每次操作即为取三个相邻的数一起取反,然后交换两边的数。
显然奇数/偶数位置内部才能进行变换,然后对于某两个数交换位置时,我们可以选择中间的任意一个奇偶性不同的数变号。证明考虑对于两个数 和中间奇偶性不同的数 ,可以先将 移动到 位置(会使得中间一段数全部变号,且从 开始每两个数交换一次;最后整体向左平移),再将 移动到 位置(同理,但是整体向右平移);对 进行操作;最后把 移回 原来的位置。这种操作覆盖了给出的操作,所以可以表示所有操作。同理可以在不改变其他数的情况下将任意两个奇偶性相同的数变号。端点位置也可以变号,考虑让其变成非端点位置变号再移回去即可。
综上,可以设计出任意一个方案使得每个数到达对应位置,然后判断奇数/偶数位置的负数个数是否均为偶数即可。设计方案时,令 为第 行的数的绝对值,然后考虑一张 个点的有向图,每个 向 连一条边(显然有向图由若干个简单环组成);则每次对于某个 的 将 和 交换位置时,对应在有向图上就是将 移出所在的简单环,所以在不超过 次操作后一定可以将每个数换到原来的位置。最后注意在每次操作后维护奇数/偶数位置的负数个数。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=100010; int n,i,j,x,y,z; bool c[2]; int a[maxn][3],r[maxn]; int main(){ scanf("%d",&n); c[0]=c[1]=1; for(i=0;i<3;++i) for(j=1;j<=n;++j) scanf("%d",a[j]+i); for(i=1;i<=n;++i){ x=a[i][0]; y=a[i][1]; z=a[i][2]; j=y/3+1; if(y%3!=2||(j^i)&1|| !((y==x+1&&z==y+1)|| (y==x-1&&z==y-1))){ printf("No\n"); return 0; } r[i]=j; c[i&1]^=(y<x); } for(i=1;i<=n;++i){ x=!(i&1); while(j=r[i],i!=j){ swap(r[i],r[j]); c[x]^=1; } } if(c[0]&&c[1]) printf("Yes\n"); else printf("No\n"); return 0; }
八、递归定义的运用(再帰的な定義の利用)
在分形相关的题目中,可以将 级分形的对应内容拆成 级分形的内容以 DP 计算。
例题:Chaotic Polygons
题意
求 阶谢尔宾斯基三角形(如下)内简单闭合回路数量。。
解法
考虑该三角形内的回路有两种:
- 只经过其中一个 阶三角形内的。
- 同时经过三个 阶三角形并绕回来的。
令 为 阶三角形的答案,而 为从某个顶点到另一个顶点的简单路径数量,则 。同时从某个顶点到相邻顶点的简单路径也有两种:
- 只经过两个 阶三角形的。
- 同时经过三个 阶三角形的。
此时 。注意简单回路的限制,路径上不能有环,所以需要减去成环(经过某个顶点两次)的方案数。设 为从某个顶点到另一个顶点 且经过第三个顶点 的简单路径数,则要经过某个顶点两次时只能选择在两个 阶三角形内强制经过第三个顶点,此时 ,同理 。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int md=1000000007; int n,i,s=1,a=2,b,c=1,d; int main(){ scanf("%d",&n); for(i=1;i<=n;++i){ b=(1LL*a*a)%md; d=md-(1LL*c*c+md-b)%md; s=(1LL*b*a+3LL*s)%md; a=(1LL*a*d+b)%md; c=(1LL*c*d)%md; } printf("%d\n",s); }
九、数位 dp(桁 DP について)
例题1:Zig-Zag Numbers
题意
求满足下列条件的十进制数 的个数模 :
- 且 。
- 是 的倍数。
- 在十进制表示下,如果位数不为 ,则不能有相邻两位相同,且如果第 位大于第 位则需要第 位小于第 位。
。
解法
套路题,记 为填好了前 位,第 位为 ,模 的值为 ,是否大于 位,当前所填后缀是否大于 的当前后缀的数的个数即可。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=510; const int md=10000; int n,m,i,j,k,p,u,v,c,f,g,ans; int dp[2][maxn][10][2][2]; char a[maxn],b[maxn]; inline void Add(int &x,int y){x-=((x+=y)>=md)*md;} int Solve(char *x){ n=strlen(x+1); if(!n) return 0; auto X=dp[0],Y=dp[1]; int ret=0; memset(dp,0,sizeof(dp)); c=x[1]^'0'; for(j=0;j<10;++j) X[j%m][j][j>c][0]=1; for(j=1;j<10;++j) ret+=X[0][j][0][0]; if(n==1) return ret; for(j=1;j<10;++j) ret+=X[0][j][1][0]; c=x[2]^'0'; for(j=u=0;j<10;++j){ f=j>c; g=j>=c; for(k=0;k<j;++k){ v=(u+k)%m; p=k%m; Add(Y[v][j][f][1],X[p][k][0][0]); Add(Y[v][j][g][1],X[p][k][1][0]); } for(++k;k<10;++k){ v=(u+k)%m; p=k%m; Add(Y[v][j][f][0],X[p][k][0][0]); Add(Y[v][j][g][0],X[p][k][1][0]); } u+=10; } for(j=1;j<10;++j) ret+=Y[0][j][0][0]+Y[0][j][0][1]; if(n==2) return ret; for(j=1;j<10;++j) ret+=Y[0][j][1][0]+Y[0][j][1][1]; swap(X,Y); for(i=3,v=100%m;i<=n;++i){ memset(Y,0,sizeof(dp[0])); c=x[i]^'0'; for(p=0;p<m;++p){ for(j=0,u=p;j<10;++j){ f=j>c; g=j>=c; for(k=0;k<j;++k){ Add(Y[u][j][f][1],X[p][k][0][0]); Add(Y[u][j][g][1],X[p][k][1][0]); } for(++k;k<10;++k){ Add(Y[u][j][f][0],X[p][k][0][1]); Add(Y[u][j][g][0],X[p][k][1][1]); } u-=((u+=v)>=m)*m; } } for(j=1;j<10;++j){ Add(ret,Y[0][j][0][0]); Add(ret,Y[0][j][0][1]); if(i!=n){ Add(ret,Y[0][j][1][0]); Add(ret,Y[0][j][1][1]); } } v=(10*v)%m; swap(X,Y); } return ret; } int main(){ scanf("%s%s%d",a+1,b+1,&m); reverse(a+1,a+strlen(a+1)+1); reverse(b+1,b+strlen(b+1)+1); for(i=1;a[i]=='0';++i) a[i]='9'; --a[i]; if(a[i]=='0') a[i]=0; Add(ans=md-Solve(a),Solve(b)); printf("%d\n",ans); return 0; }
例题2
题意
有 种硬币,第 种硬币为 元。现在每种硬币都有无穷个,求使用这些硬币凑出 元的方案数。。
解法
考虑在每次不只多增加一个硬币而是 个(或者是对硬币数量二进制拆分)。此时对于每个 ,每种硬币只能有选择 个或者不选择两个选项(可以使用背包处理对应的方案);而在选择后需要当前硬币面额总和和 的后 位相等。此时可以对 的每一位使用数位 dp 进行转移,转移时需要维护当前面额除以 的商方便找出合法的 dp 值。
十、dp 优化(偏实现而非思想)(高速化のテクニック)
1. 前缀和优化(累積和の利用)
2. 数据结构优化(データ構造の利用)
3. 利用之前的数组(配列の使いまわし)
例题:Division into Two
题意
有一个长为 的单增序列 。求将其划分为两个子序列 ,满足 中任意相邻两项之差不小于 , 中任意相邻两数之差不小于 的方案数。。
解法
设 为考虑了前 个数,且 不在的子序列末尾为 , 是否在 子序列的方案数。转移有:
考虑在枚举到 时,只会更新 的值为 的某个前缀和,且如果 过小则新的 会赋为 ,否则新的 可以直接赋值为 。此时某个 被赋为 则 都会为 。此时在计算 时可以直接使用 ,维护最后一次被赋为 的对应右端点即可。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=100010; const int md=1000000007; int n,i,s0,s1,l0,l1,r0=-1,r1=-1; int d0[maxn],d1[maxn],t0[maxn],t1[maxn]; long long a,b,w,s[maxn]; inline void Add(int &x,int y){x-=((x+=y)>=md)*md;} int main(){ scanf("%d%lld%lld%lld",&n,&a,&b,s+1); s0=s1=t0[0]=t1[0]=1; for(i=1;i<n;++i){ scanf("%lld",&w); s[i+1]=w; while(w-s[l0+1]>=b) ++l0; while(w-s[l1+1]>=a) ++l1; if(l1>r1) Add(d0[i],t1[min(l1,i-1)]); if(l0>r0) Add(d1[i],t0[min(l0,i-1)]); if(w-s[i]<a) r0=i-1,s0=0; if(w-s[i]<b) r1=i-1,s1=0; Add(s0,d0[i]); Add(s1,d1[i]); t0[i]=s0; t1[i]=s1; } Add(s0,s1); printf("%d\n",s0); return 0; }
4. FFT(高速フーリエ変換)
板子
点此查看代码
void DFT(int *f){ for(i=1;i<t;++i) if(i<r[i]) swap(f[i],f[r[i]]); for(i=1,le=2;le<=t;i=le,le<<=1){ v=Pow(G,(md-1)/le); for(j=0;j<t;j+=le){ for(k=0,u=1;k<i;++k,u=(1LL*u*v)%md){ p=(1LL*f[i+j+k]*u)%md; Add(f[i+j+k]=f[j+k],md-p); Add(f[j+k],p); } } } }
属于需要背的知识。原根表。常见的原根形如 原根为 , 原根为 。
5. 高维前缀和/FMT(高速ゼータ変換)
计算 时,考虑分开处理每一位的贡献,设 为 且 满足第 位以上部分 相等,第 位以下部分 ,则有 ;转移时如果 的 位为 ,则需要加上 部分,表示此时的 可以取 位为 。最后目标 dp 值即为 值。
6. FWT(And と Add の畳み込み)
此处用了一种较为清奇(?)的思路。
(1) 或卷积(求 )
考虑多项式乘法的过程:我们会先对序列进行 DFT,然后相乘,最后进行 IDFT。此时可以使用类似的方式,用某种序列表示需要操作的序列。
令 ,则 。而 等效于 是 的子集,所以 当且仅当 均是 的子集,也就是说 。求 可以使用上述的方法,而从 变回 时,考虑上述 dp 过程的逆过程:在从 推到 时,如果某个 的第 位为 则 ,否则 ,直接 dp 即可。
(2) 与卷积(求 )
仍然可以考虑上述的过程,设 ,则同样有 当且仅当 ,所以仍然有 。求 时仍然可以使用上面 dp 的思路,但是转移时有 。从 推回 同理。
(3) 异或卷积(求 )
考虑异或的性质,令 为 在二进制表示下的位数,可以发现 ,同理有 。令 ,则 。 也可以使用上述的 dp 方式计算。不过有更简洁的方式计算:设 ,则 。同或卷积可以直接将求得的 翻转即可。
(4) 子集卷积(求 )
考虑第二个性质等效于 。可以把所有下标按照 的不同值分开处理,设 为 ,则 (暴力计算即可)。
7. 简单剪枝(簡単な枝刈り)
本文来自博客园,作者:Fran-Cen,转载请注明原文链接:https://www.cnblogs.com/Fran-CENSORED-Cwoi/p/17052570.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?