计数 dp 部分例题(六~十部分)

六、转化求和顺序(線形和への分解)

例题1

题意

有一个长为 n 的数组 a。求在 a 中选择 k 个数的所有方案中,每次选择的所有数的中位数的和。n105,k 为偶数。

解法

B=k21。排序后,设在最中间的两个数为 ai,aj,则 12(ai+aj) 在所有中位数里出现了 (i1B)(nj1B) 次,则答案为 12i=1nj=i+1n(ai+aj)(i1B)(nj1B),化简可以变成 12i=1n(i1B)(ai(j=i+1n(nj1B))+j=i+1n(nj1B)aj),后缀和优化计算即可。

例题2

x=0ny=0mxXORyn,m109

解法

考虑将每个 XOR 的每一位拆开,然后合并每一位。设 Bx,j,0/1 为满足 x 且第 j 位为 0/1 的数的数量(显然可以 O(1) 计算),则第 j 位造成的贡献为 2j(Bn,j,0Bm,j,1+Bn,j,1Bm,j,0)

例题3:Many Easy Problems

给定一棵大小为 n 的无根树,定义 f(i),对于所有大小为 i 的点集,求出能够包含它的最小连通块大小之和。对于 i=1n 的所有 i,求出 f(i)mod924844033n2×105。(924844033=221×441+1,原根为 5

解法

考虑将“能够包含每个大小为 i 的点集的最小连通块大小之和”在每个点而非每个连通块处计算贡献,则其可以转化为“对于每个点,求作为包含某个大小为 i 的点集的最小连通块中包含了该点的连通块数量”。

设以某个点 u 为根时,其的每棵子树内大小分别为 sizu,1,sizu,2,,sizu,su,则某个最小连通块内不包含 u 的充要条件是内部每个点都在 u 儿子的子树内,uf(i) 的贡献即为 (ni)j=1su(sizu,ji)f(i) 即为 n(ni)u=1nj=1su(sizu,ji)。设 ci=u=1nj=1su[sizu,j=i],则 f(i)=n(ni)j=1ncj(ji)。将 (ji) 拆成 j!i!(ji)! 的形式,令 Fj=(nj)!,则 j=1ncj(ji)=j=1n(cjj!)Fn+ij,就是标准的卷积形式了(差卷积)。

代码

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

题意

有一个 H×W 的矩阵 a,可以进行若干次操作,每次选择一行或一列翻转,求最后能够生成的本质不同的矩阵有多少种。H,W200, 矩阵内的元素均为小写字母。

解法

考虑某个 ai,j 只会变到 aHi+1,j,ai,Wj+1,aHi+1,Wj+1 的位置。(对于 H,W 为奇数的情况,可以直接乘上中间能否变化的方案数,则其他元素都有四种可能出现的位置)所以这四个元素可以看成一个四元组整体处理。设四个位置的数分别为 a,b,c,d,则可以通过下面的方式使得只有 a,b,c,d 之间的顺序进行变化(箭头表示行/列内其他元素的顺序):

同理其他任意三个元素也可以按照这样的方式变化,打表可以发现这些元素出现的顺序有 12 种。同时可以发现将同在一行/一列的元素调换位置可得所有 4! 种方式中另外 12 种出现方式,意味着某行被翻转将导致若干个四元组能够变换的位置同时变化。注意如果某个四元组内部出现了相同的数,则它们一定可以在某次变化后出现在同一行,它们在被翻转某行/某列后能够变换出的顺序一样,可以把对应的贡献先乘上;否则在翻转某行/某列后对应能够变换出的顺序会变化。

考虑将每一行和每一列看成点,然后对于某个四元组 {ai,j,aHi+1,j,ai,Wj+1,aHi+1,Wj+1}(2iH,2jW),在第 i 行和第 j 列之间连边。在翻转某行/某列之后对应出边的四元组状态也会改变,所以可以对于某个连通块 S 求出某棵生成树,然后对于树上 2|S|1 种状态,自顶向下确定某行/某列是否翻转(会有两种等效的方式),进一步确定其他所有边的状态(只会有一种)。时间复杂度为 O(HW)

代码

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

题意

有一个 3×n 的网格,第 i 行第 j 列的数为 3(j1)+i。可以进行若干次操作,每次将某个 3×3 的子矩阵旋转 180o,求能否经过若干次操作得出某个 3×n 矩阵。n105

解法

显然每一列的数不会改变,且已知每一列被翻转的次数的奇偶性,则可以将每一列压成一个数,可以用正负号表示对应的列是否翻转。此时每次操作即为取三个相邻的数一起取反,然后交换两边的数。

显然奇数/偶数位置内部才能进行变换,然后对于某两个数交换位置时,我们可以选择中间的任意一个奇偶性不同的数变号。证明考虑对于两个数 i,j 和中间奇偶性不同的数 k,可以先将 i 移动到 k1 位置(会使得中间一段数全部变号,且从 i+1 开始每两个数交换一次;最后整体向左平移),再将 j 移动到 k+1 位置(同理,但是整体向右平移);对 i,k,j 进行操作;最后把 i,j 移回 j,i 原来的位置。这种操作覆盖了给出的操作,所以可以表示所有操作。同理可以在不改变其他数的情况下将任意两个奇偶性相同的数变号。端点位置也可以变号,考虑让其变成非端点位置变号再移回去即可。

综上,可以设计出任意一个方案使得每个数到达对应位置,然后判断奇数/偶数位置的负数个数是否均为偶数即可。设计方案时,令 ri 为第 i 行的数的绝对值,然后考虑一张 n 个点的有向图,每个 iri 连一条边(显然有向图由若干个简单环组成);则每次对于某个 iriirirri 交换位置时,对应在有向图上就是将 ri 移出所在的简单环,所以在不超过 n 次操作后一定可以将每个数换到原来的位置。最后注意在每次操作后维护奇数/偶数位置的负数个数。

代码

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

八、递归定义的运用(再帰的な定義の利用)

在分形相关的题目中,可以将 K 级分形的对应内容拆成 K1 级分形的内容以 DP 计算。

例题:Chaotic Polygons

题意

n 阶谢尔宾斯基三角形(如下)内简单闭合回路数量。n105

解法

考虑该三角形内的回路有两种:

  • 只经过其中一个 n1 阶三角形内的。
  • 同时经过三个 n1 阶三角形并绕回来的。

Snn 阶三角形的答案,而 An 为从某个顶点到另一个顶点的简单路径数量,则 Sn=3Sn1+An13。同时从某个顶点到相邻顶点的简单路径也有两种:

  • 只经过两个 n1 阶三角形的。
  • 同时经过三个 n1 阶三角形的。

此时 An=An13+An12。注意简单回路的限制,路径上不能有环,所以需要减去成环(经过某个顶点两次)的方案数。设 Bn 为从某个顶点到另一个顶点 且经过第三个顶点 的简单路径数,则要经过某个顶点两次时只能选择在两个 n1 阶三角形内强制经过第三个顶点,此时 An=An13+An12An1Bn12,同理 Bn=Bn1An12Bn13

代码

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

题意

求满足下列条件的十进制数 x 的个数模 10000

  • xAxB
  • xM 的倍数。
  • x 在十进制表示下,如果位数不为 1,则不能有相邻两位相同,且如果第 i+2 位大于第 i+1 位则需要第 i+1 位小于第 i 位。

A,B10500,M500

解法

套路题,记 dpi,j,0/1,r,0/1 为填好了前 i 位,第 i 位为 j,模 M 的值为 r,是否大于 i1 位,当前所填后缀是否大于 A1/B 的当前后缀的数的个数即可。

代码

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

题意

n 种硬币,第 i 种硬币为 ai 元。现在每种硬币都有无穷个,求使用这些硬币凑出 X 元的方案数。n20,ai30,X1018

解法

考虑在每次不只多增加一个硬币而是 2k 个(或者是对硬币数量二进制拆分)。此时对于每个 k,每种硬币只能有选择 2k 个或者不选择两个选项(可以使用背包处理对应的方案);而在选择后需要当前硬币面额总和和 X 的后 k+1 位相等。此时可以对 X 的每一位使用数位 dp 进行转移,转移时需要维护当前面额除以 2k 的商方便找出合法的 dp 值。

十、dp 优化(偏实现而非思想)(高速化のテクニック)

1. 前缀和优化(累積和の利用)

2. 数据结构优化(データ構造の利用)

3. 利用之前的数组(配列の使いまわし)

例题:Division into Two

题意

有一个长为 n 的单增序列 a。求将其划分为两个子序列 x,y,满足 x 中任意相邻两项之差不小于 Ay 中任意相邻两数之差不小于 B 的方案数。n105

解法

dpi,j,0/1 为考虑了前 i 个数,且 ai 不在的子序列末尾为 ajai 是否在 y 子序列的方案数。转移有:

dpi+1,i,0=dpi,0,1+ai+1ajAdpi,j,1dpi+1,i,1=dpi,0,0+ai+1ajBdpi,j,0dpi+1,j,0=[ai+1aiA]dpi,j,0dpi+1,j,1=[ai+1aiB]dpi,j,1

考虑在枚举到 ai 时,只会更新 dpi,i1 的值为 dpi1 的某个前缀和,且如果 ai+1ai 过小则新的 dpi,0dpi,i2 会赋为 0,否则新的 dpi,0dpi,i2 可以直接赋值为 dpi1,0dpi,i2。此时某个 dpi,j 被赋为 0k>i,dpk,j 都会为 0。此时在计算 dpi 时可以直接使用 dpi1,维护最后一次被赋为 0 的对应右端点即可。

代码

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

属于需要背的知识。原根表。常见的原根形如 998244353 原根为 3924844033 原根为 5

5. 高维前缀和/FMT(高速ゼータ変換)

计算 fS=TSgT 时,考虑分开处理每一位的贡献,设 dpi,Sjfjj 满足第 i 位以上部分 S,j 相等,第 i 位以下部分 jS,则有 dpi,Sdpi1,S;转移时如果 Si 位为 1,则需要加上 dpi,Sxor2i 部分,表示此时的 j 可以取 i 位为 0。最后目标 dp 值即为 f 值。

6. FWT(And と Add の畳み込み)

此处用了一种较为清奇(?)的思路。

(1) 或卷积(求 Ci=jork=iAjBk

考虑多项式乘法的过程:我们会先对序列进行 DFT,然后相乘,最后进行 IDFT。此时可以使用类似的方式,用某种序列表示需要操作的序列。

o(A)i=xori=iAx,则 o(C)i=(jork)ori=iAjBk。而 xori=i 等效于 xi 的子集,所以 (jork)ori 当且仅当 j,k 均是 i 的子集,也就是说 Ci=AiBi。求 o(A/B) 可以使用上述的方法,而从 o(C) 变回 C 时,考虑上述 dp 过程的逆过程:在从 dpi 推到 dpi1 时,如果某个 S 的第 i 位为 1dpi,S=dpi1,S+dpi1,Sxor2i=dpi1,S+dpi,Sxor2i,否则 dpi,S=dpi1,S,直接 dp 即可。

(2) 与卷积(求 Ci=jandk=iAjBk

仍然可以考虑上述的过程,设 a(A)i=xandi=iAx,则同样有 (jandk)andi=i 当且仅当 jandi=kandi=i,所以仍然有 a(C)i=a(A)ia(B)i。求 a(A) 时仍然可以使用上面 dp 的思路,但是转移时有 dpi,S=dpi1,S+[Sand2i=0]dpi1,Sor2i。从 a(C) 推回 C 同理。

(3) 异或卷积(求 Ci=jxork=iAjBk

考虑异或的性质,令 c(x)x 在二进制表示下的位数,可以发现 (c(j)+c(k)c(jxork))mod2=0,同理有 (c(jandi)+c(kandi)c((jxork)andi))mod2=0。令 xp(A)i=c(jandi)mod2=pAj,则 xp(C)i=j,k{0,1}[jxork=p]xj(A)ixk(B)ix0/1 也可以使用上述的 dp 方式计算。不过有更简洁的方式计算:设 x(A)i=x0(A)ix1(A)i,则 x(C)i=x(A)ix(B)i。同或卷积可以直接将求得的 C 翻转即可。

(4) 子集卷积(求 Ci=jork=i,jandk=0AjBk

考虑第二个性质等效于 c(j)+c(k)=c(i)。可以把所有下标按照 c 的不同值分开处理,设 op(A)ic(j)=p,jandi=iAj,则 op(C)i=j+k=poj(A)iok(B)i(暴力计算即可)。

7. 简单剪枝(簡単な枝刈り)

posted @   Fran-Cen  阅读(66)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示