计数 dp 部分例题(十一~十五部分)
十一、矩阵的利用(行列を用いたテクニック)
1. 快速幂(二分累乗)
(1) 推导转移矩阵(行列の導出)
例题:Placing Squares
题解
(2) BM 优化递推(?)(コンパニオン行列の累乗)
(3) 多项式快速幂(多項式の累乗)
将转移矩阵看成乘上一个多项式的形式,则转移的合并可以从 优化到 。此时如果每个多项式都相同则可以直接使用快速幂,常数允许/模数为 NTT 模数时可以 进一步优化到 。
2. 行列式优化计数(行列式のテクニック)
十二、忽略小概率事件(小さい確率を無視する)
在用实数形式表示某个概率或期望时,可以将“极小的贡献”忽略不计,以达到更快速的效果。
例题:Ben Toh
题意
有一个变量 ,初始为 。现在需要对其执行 次操作:
- 有 的概率将 加上 。
- 有 的概率将 赋为 。
求第一次操作被执行的期望次数。
解法
考虑 。设 为在当前 时执行 次操作后 的期望值。转移有 ,初值显然为 ,目标为 。
考虑在 足够大的时候, 的贡献是可以忽略不计的,对之后的 dp 值的贡献也可以忽略不计,所以只需要维护每个 的前 项即可。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxl=30; const int maxn=100010; int n,i,j; double dp[2][maxl],sm[maxn],pw[maxl]; double w,a,*X=dp[0],*Y=dp[1]; int main(){ for(pw[0]=j=1;j<maxl;++j) pw[j]=pw[j-1]*0.5; for(i=1;i<maxn;++i){ for(j=0;j<maxl-1;++j) Y[j]=pw[j]*(X[j+1]+1)+(1-pw[j])*X[0]; Y[maxl-1]=(1-pw[maxl-1])*X[0]; sm[i]=Y[0]; a=0; swap(X,Y); for(j=0;j<maxl;++j) Y[j]=0; } for(;;){ scanf("%d",&n); if(!n) return 0; printf("%.8lf\n",sm[n]); } return 0; }
十三、二项式(二項係数のテクニック)
1. 常用公式(頻出公式集)
2. 转化为路径计数问题/组合数的几何意义(経路数への帰着)
例题1:BBQ Hard
题意
求 。。
解法
考虑 可以看成 到 的 只向上/向右走时的路径 数量之和,则原来的和式可以看成 ,前半部分为所有 到所有 路径数量之和,可以 dp 转移。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxx=2010; const int maxd=maxx<<1; const int maxv=maxx<<2; const int maxn=200010; const int md=1000000007; int n,i,j,a,b,ans,dp[maxd][maxd]; int x[maxn],y[maxn],fac[maxv],inv[maxv]; 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; } inline int C(int x,int y){return ((1LL*fac[y]*inv[x])%md*inv[y-x])%md;} inline void Add(int &x,int y){x-=((x+=y)>=md)*md;} int main(){ fac[0]=1; for(i=1;i<maxv;++i) fac[i]=(1LL*i*fac[i-1])%md; inv[maxv-1]=Pow(fac[maxv-1],md-2); for(i=maxv-1;i;--i) inv[i-1]=(1LL*inv[i]*i)%md; scanf("%d",&n); for(i=1;i<=n;++i){ scanf("%d%d",&a,&b); ++dp[maxx-a][maxx-b]; Add(ans,C(a<<1,(a+b)<<1)); x[i]=maxx+a; y[i]=maxx+b; } ans=md-ans; for(i=2;i<maxd;++i){ for(j=2;j<maxd;++j){ a=dp[i-1][j-1]; Add(dp[i][j-1],a); Add(dp[i-1][j],a); } } for(i=1;i<=n;++i) Add(ans,dp[x[i]][y[i]]); printf("%d\n",(1LL*ans*((md+1)>>1))%md); return 0; }
3. 旋转 45 度(45 度回転)
例题2:Don't worry. Be Together
题意
有 个点,第 个点的横纵坐标为 。起点为原点,每一步可以向上/下/左/右走一步。设 为走 步刚好能到达 的方案数,求 。。
解法
考虑 的情况。如果考虑在 轴方向正向/反向走了多少步的情况,则和式难以化简。
考虑将整个坐标系逆时针旋转 45 度,将某个点 对应到新坐标系上的 ,则在原图上向左/上/右/下走一步对应了在新的坐标系上向左下/左上/右上/右下走一步,新的坐标系上每次可以从某个点 走到 的位置,可以把横纵坐标分别独立出来看,则走 步到新坐标系上的点 (令 )的方案数为 。
注意模数不一定为质数,考虑将最后的答案写成 的形式,则上面的组合数可以拆成某个阶乘除以某个阶乘的形式,乘/除某个阶乘等效于对 前缀加/前缀减,最后处理整个答案时将每个 分解质因数则可以将答案处理出来。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=1000010; int n,m,i,j,t,x,y,u,w,a=1; int v[maxn],p[maxn],d[maxn]; long long pw[maxn]; inline int Pow(int d,long long z){ int r=1; do{ if(z&1) r=(1LL*r*d)%m; d=(1LL*d*d)%m; }while(z>>=1); return r; } int main(){ for(i=2;i<maxn;++i){ if(!v[i]) v[i]=p[++t]=i; for(j=1;j<=t;++j){ u=p[j]; if(v[i]<u||i*u>=maxn) break; v[i*u]=u; } } scanf("%d%d%d",&n,&t,&m); while(n--){ scanf("%d%d",&x,&y); u=x+y; w=x-y; if(u<0) u=-u; if(w<0) w=-w; if(u<w) swap(u,w); if(t<u||(u^t)&1){ printf("0\n"); return 0; } x=(t-u)>>1; y=(t-w)>>1; d[t]+=2; --d[x]; --d[y]; --d[t-x]; --d[t-y]; } x=0; for(i=maxn-1;i;--i){ if(!(x+=d[i])) continue; for(u=i;u!=1;){ w=v[u]; y=0; while(v[u]==w) ++y,u/=w; pw[w]+=1LL*y*x; } } for(i=2;i<maxn;++i){ if(!(u=pw[i])) continue; a=(1LL*a*Pow(i,u))%m; } printf("%d\n",a); return 0; }
4. 卡塔兰数(カタラン数)
十四、容斥原理(包除原理)
1. 使用对称性(?)(対称性を用いる場合)
例题1:~K Perm Counting
题意
求 的排列 中, 满足 的 的个数。。
解法
考虑容斥,计算钦定若干个 满足 的对应 的数量。此时可以以下标为左部点,元素为右部点构造二分图,则钦定 个满足 的对应方案数即为二分图上大小为 的匹配数量乘以 。考虑该二分图一定可以拆成 条链的形式,则对应匹配数量即为在这些链上选取 条边且满足选择的任意两条边不共点的方案数。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=2010; const int md=924844033; int n,i,j,k,u,v,d,dp[2][maxn<<1][2]; bool s[maxn<<1]; inline void Add(int &x,int y){x-=((x+=y)>=md)*md;} int main(){ scanf("%d%d",&n,&k); j=n%k; v=n/k; for(i=u=1;i<=k;++i){ d=v+(i<=j); s[u]=1; u+=d; s[u]=1; u+=d; } auto X=dp[0],Y=dp[1]; X[0][0]=1; --u; for(i=1;i<=u;++i){ for(j=0;j<=i;++j) Add(Y[j][0]=X[j][0],X[j][1]); if(!s[i]) for(j=0;j<i;++j) Add(Y[j+1][1],X[j][0]); swap(X,Y); memset(Y,0,sizeof(dp[0])); } u=0; d=j=1; for(i=n;~i;--i){ Add(v=X[i][0],X[i][1]); v=(1LL*v*d)%md; d=(1LL*d*(j++))%md; if(i&1) v=md-v; Add(u,v); } printf("%d\n",u); return 0; }
2. 使用 dp(DP を用いる場合)
例题2
题意
有一个平面直角坐标系,初始位置为 ,每次可以向横/纵坐标走一步,同时有 个点不能走,求走到 的方案数。。
解法
首先不可能直接计算如何走得合法的方案数。考虑从 入手计算不能走得合法得方案数。
显然某个点 能走到另一个不同的点 则一定有 不能走到 ,可以将所有点按照横/纵坐标排序后,每个点只需要考虑之前的点能否到达它。令 为从 走到 的方案数,设 为当前在点 ,经过了 个不能经过的点的方案数,则转移为 。显然最后每个 dp 对答案的贡献只和 的奇偶性相关,则可以把奇偶性相同的 的对应 合并。
3. 对因数的容斥(約数系包除)
例题3
题意
有 个数,求其所有非空子集的 之和。值域,。
解法
考虑从大到小判断每个数是多少个非空子集的 。显然可以先统计每个数 的倍数个数 ,则以其为 公因数 的非空子集为 个,但是需要减去 的所有倍数对应子集的个数。
例题4:Rotated Palindromes
题意
对于所有的长为 ,字符集为 内的整数的回文串 ,在任意次将 的开头的数字移到末尾的操作之后,求能够生成多少种不同的序列。。
解法
考虑将 的最小循环节移动到末尾之后会形成 本身,所以只需要讨论某个最小循环节带来的贡献即可。显然最小循环节为回文串(和反串相等),则需要考虑该回文串能否经过若干次循环移位之后形成新的回文串。设某个最小循环节 长为 ,如果 能够经过 次操作得到最小循环节 ,则 同样经过 次操作也会得到 (因为 的长为 的前缀和后缀为相等的回文子串)。而上述的 唯一,所以当且仅当 时 能变成 。综上,对于长为 的最小循环节,如果 为奇数则贡献为 ,否则贡献为 。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=2700; const int md=1000000007; int n,k,i,j,a,u,t,s[maxn],c[maxn]; 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; } inline void Add(int &x,int y){x-=((x+=y)>=md)*md;} int main(){ scanf("%d%d",&n,&k); u=sqrt(n); for(i=1;i<=u;++i){ if(!(n%i)){ s[++t]=i; s[++t]=n/i; } } if(u*u==n) --t; sort(s+1,s+t+1); for(i=1;i<=t;++i){ n=s[i]; u=Pow(k,(n+1)>>1); for(j=1;j<i;++j) if(!(n%s[j])) Add(u,c[j]); c[i]=md-u; if(!(n&1)) n>>=1; a=(1LL*u*n+a)%md; } printf("%d\n",a); return 0; }
十五、“难解”的计数问题(「解けない問題」を見極める)
#P 问题是一类对 NP 问题计数的问题,其中有一些计数问题是 #PC 的(可能对应的判定性问题不是 NPC 问题)(这种计数问题暂时不能找到多项式的解法):
所以我们在对计数问题求解时,需要尽量避免直接求解上述问题。
您觉得这几篇文章怎么样呢?今日进行探讨的内容只是 dp 优化方法的一小部分,除此之外我的随笔中还介绍有更多的 dp 优化的方法,我作为一名蒟蒻,衷心地欢迎您来博客园参观。
本文来自博客园,作者:Fran-Cen,转载请注明原文链接:https://www.cnblogs.com/Fran-CENSORED-Cwoi/p/17052572.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!