计数 dp 部分例题(十一~十五部分)

十一、矩阵的利用(行列を用いたテクニック)

1. 快速幂(二分累乗)

(1) 推导转移矩阵(行列の導出)

例题:Placing Squares

题解

(2) BM 优化递推(?)(コンパニオン行列の累乗)

(3) 多项式快速幂(多項式の累乗)

将转移矩阵看成乘上一个多项式的形式,则转移的合并可以从 O(n3) 优化到 O(nlogn)。此时如果每个多项式都相同则可以直接使用快速幂,常数允许/模数为 NTT 模数时可以 进一步优化到 O(nlogn)

2. 行列式优化计数(行列式のテクニック)

LGV 引理

矩阵树定理

十二、忽略小概率事件(小さい確率を無視する)

在用实数形式表示某个概率或期望时,可以将“极小的贡献”忽略不计,以达到更快速的效果。

例题:Ben Toh

题意

有一个变量 x,初始为 0。现在需要对其执行 n 次操作:

  • 2x 的概率将 x 加上 1
  • 12x 的概率将 x 赋为 0

求第一次操作被执行的期望次数。

解法

考虑 dp。设 dpi,j 为在当前 x=j 时执行 i 次操作后 x 的期望值。转移有 dpi,j=2j(dpi1,j+1+1)+(12j)dpi1,0,初值显然为 j,dp0,j=0,目标为 j=0ndpi,j

考虑在 j 足够大的时候,dpi1,j+1+1 的贡献是可以忽略不计的,对之后的 dp 值的贡献也可以忽略不计,所以只需要维护每个 dpi 的前 O(log) 项即可。

代码

点此查看代码
#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. 常用公式(頻出公式集)

  • i=0n(ni)=2n
  • i=0n2(n2i)=2n1
  • i=0k(n+ii)=(n+i+1i)
  • i=0kj=0l(i+ji)=(k+l+2k+1)
  • i=0k(ni)(mki)=(n+mk)
  • i=0k(n+ii)(miki)=(n+m+1k)

2. 转化为路径计数问题/组合数的几何意义(経路数への帰着)

例题1:BBQ Hard

题意

i=1nj=i+1n(ai+aj+bi+bjai+aj)n2×105;ai,bi2×103

解法

考虑 (ai+aj+bi+bjai+aj) 可以看成 (ai,bi)(aj,bj)只向上/向右走时的路径 数量之和,则原来的和式可以看成 12(i=1nj=1n(ai+aj+bi+bjai+aj)i=1n(2ai+2bi2ai)),前半部分为所有 (ai,bi) 到所有 (ai,bi) 路径数量之和,可以 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

题意

n 个点,第 i 个点的横纵坐标为 (xi,yi)。起点为原点,每一步可以向上/下/左/右走一步。设 f(i) 为走 T 步刚好能到达 (xi,yi) 的方案数,求 i=1nf(i)modmod1mod109+7;n,T105;|xi|,|yi|106

解法

考虑 xi,yi0 的情况。如果考虑在 x/y 轴方向正向/反向走了多少步的情况,则和式难以化简。

考虑将整个坐标系逆时针旋转 45 度,将某个点 (x,y) 对应到新坐标系上的 (x+y,xy),则在原图上向左/上/右/下走一步对应了在新的坐标系上向左下/左上/右上/右下走一步,新的坐标系上每次可以从某个点 (x,y) 走到 (x±1,y±1) 的位置,可以把横纵坐标分别独立出来看,则走 T 步到新坐标系上的点 (X,Y) (令 X,Y0)的方案数为 (TTX2)(TTY2)

注意模数不一定为质数,考虑将最后的答案写成 i=2ici 的形式,则上面的组合数可以拆成某个阶乘除以某个阶乘的形式,乘/除某个阶乘等效于对 c 前缀加/前缀减,最后处理整个答案时将每个 i 分解质因数则可以将答案处理出来。

代码

点此查看代码
#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

题意

1n 的排列 p 中,i 满足 |pii|Kp 的个数。n2×103

解法

考虑容斥,计算钦定若干个 i 满足 |pii|=K 的对应 p 的数量。此时可以以下标为左部点,元素为右部点构造二分图,则钦定 c 个满足 |pii|=K 的对应方案数即为二分图上大小为 c 的匹配数量乘以 (nc)!。考虑该二分图一定可以拆成 2×min(K,nK) 条链的形式,则对应匹配数量即为在这些链上选取 c 条边且满足选择的任意两条边不共点的方案数。

代码

点此查看代码
#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

题意

有一个平面直角坐标系,初始位置为 (0,0),每次可以向横/纵坐标走一步,同时有 n 个点不能走,求走到 (X,Y) 的方案数。X,Y105,n2×103

解法

首先不可能直接计算如何走得合法的方案数。考虑从 n2×103 入手计算不能走得合法得方案数。

显然某个点 x 能走到另一个不同的点 y 则一定有 y 不能走到 x,可以将所有点按照横/纵坐标排序后,每个点只需要考虑之前的点能否到达它。令 p(i,j) 为从 i 走到 j 的方案数,设 dpi,j 为当前在点 i,经过了 j 个不能经过的点的方案数,则转移为 dpi,j=k=1i1dpk,j1p(k,i)。显然最后每个 dp 对答案的贡献只和 j 的奇偶性相关,则可以把奇偶性相同的 j 的对应 dpi,j 合并。

3. 对因数的容斥(約数系包除)

例题3

题意

n 个数,求其所有非空子集的 gcd 之和。值域,n105

解法

考虑从大到小判断每个数是多少个非空子集的 gcd。显然可以先统计每个数 i 的倍数个数 ci,则以其为 公因数 的非空子集为 2ci1 个,但是需要减去 i 的所有倍数对应子集的个数。

例题4:Rotated Palindromes

题意

对于所有的长为 n,字符集为 1k 内的整数的回文串 a,在任意次将 a 的开头的数字移到末尾的操作之后,求能够生成多少种不同的序列。n,k109

解法

考虑将 a 的最小循环节移动到末尾之后会形成 a 本身,所以只需要讨论某个最小循环节带来的贡献即可。显然最小循环节为回文串(和反串相等),则需要考虑该回文串能否经过若干次循环移位之后形成新的回文串。设某个最小循环节 S 长为 d,如果 S 能够经过 i 次操作得到最小循环节 T,则 S 同样经过 di 次操作也会得到 T(因为 S 的长为 min(i,di) 的前缀和后缀为相等的回文子串)。而上述的 i 唯一,所以当且仅当 i=d2S 能变成 T。综上,对于长为 d 的最小循环节,如果 d 为奇数则贡献为 d,否则贡献为 d2

代码

点此查看代码
#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 问题)(这种计数问题暂时不能找到多项式的解法):

  • 对一个 2-SAT 问题的合法赋值方案计数
  • 对二分图/一般图的最大匹配计数(对平面图的完美匹配的计数是有多项式解法的)
  • 计算矩阵的积和式
  • 对图的拓扑序计数
  • 对图的欧拉回路计数

所以我们在对计数问题求解时,需要尽量避免直接求解上述问题。


您觉得这几篇文章怎么样呢?今日进行探讨的内容只是 dp 优化方法的一小部分,除此之外我的随笔中还介绍有更多的 dp 优化的方法,我作为一名蒟蒻,衷心地欢迎您来博客园参观。

posted @   Fran-Cen  阅读(72)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示