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

数え上げテクニック集 笔记。题目的代码难度都不高。


引言

OI 中有三大专题:dp,数据结构,图论。而在这三大专题中,因为 dp 是从小问题的解法上升至大问题的解法的关键;所以 dp,在这三大专题中,优先性是第一位的。如果在从小问题的解法上升至大问题时,难免于时间和空间的超限,会使得这种题目值得深究;而在 OI 中,人们总是会深究简化所用时间和空间的方法,我们称这种方法为 dp 优化;而在这篇文章中,就会专门对那些耗费大量时间和空间的解法,处心积虑,找出对应的 dp 优化方法。


一、状态设计和简化(状態をまとめる)

例题1:Unhappy Hacking

题意

有一个空串,可以进行下面三种操作:

  • 在末尾加入一个 0
  • 在末尾加入一个 1
  • 删去末尾的数,如果串为空则忽略。

n 次操作后满足整个串等于某个给定的串 S 的方案数模 109+7n,|S|5×103

解法1(本人的 ** 解法)

某次操作可以删去原有匹配好的串的一部分,考虑消除这种操作带来的影响。

此时可以对于每个 i,在填好的串为最后一次和长度为 i 的前缀相同的时候进行统计。显然之后的操作不包括将前 i 个字符删除,所以在最后一次匹配长度为 i+1 的前缀时,一定只会删去新加的字符然后加入一个对应的字符。这两次匹配之间加入/删除的操作可以形成一个合法的括号序列。设 dpi,j 为进行了 i 次操作,形成了长度为 j 的前缀的方案数,则转移为 dpi,j=k=0i12dpi2k1,j12k(2kk)k+1,(其中 (2kk)k+1 为长为 2k 的合法括号序列数,也就是 卡塔兰数)是一个卷积的形式,可以使用 MTT + 快速幂解决。(懒得写。)

注意 dpi,0 需要 O(n2) 特别计算(可能对空串进行删除操作)。

解法2(题解区首页解法

考虑只在删除操作处计算对应的贡献。显然我们只关心未被删除的部分能否构成 S,此时如果某个插入/删除序列中有 k 次删除操作删除了某个数,则其对应了 2k 种实际方案。

dpi,j 为进行了 i 次操作,得到了长为 j 的串的方案数。如果下一次操作为删除,则 dpi,j2dpi1,j+1(特别的,dpi,0dpi1,0);如果下一次操作为加入,则 dpi,jdpi1,j1j>0)。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=5010;
const int md=1000000007;
int n,m,i,j,b,dp[2][maxn],*X=dp[0],*Y=dp[1]; char s[maxn];
inline int Plus(int x,int y){return x-=((x+=y)>=md)*md;}
int main(){
scanf("%d%s",&n,s+1);
m=strlen(s+1); dp[1][0]=1;
for(i=1;i<=n;++i){
swap(X,Y); Y[0]=Plus(X[0],Plus(X[1],X[1]));
for(j=1;j<=i;++j) Y[j]=Plus(X[j-1],Plus(X[j+1],X[j+1]));
}
printf("%d",Y[m]);
return 0;
}

例题2:Road of the King

题意

有一个 n 个点的图,目前一条边都没有。

有一个人在 1 号点要进行 m 次移动,终点不必是 1 号点,假设第 i 次从 u 移动到 v,那么在 uv 之间连一条有向边。可以有 u=v

问有多少种序列能满足:最终 n 个点组成的图是一个强连通图。答案对 109+7 取模。n,m300

解法

考虑形成强连通图的一个充要条件:对于某个点 u,满足所有点都能从 u 到达,所有点也都有到达 u 的路径。所以在构造时,令 u=1,则每次回到 1 之后均会使得之前从 1 出发能到达的点现在能到达 1。(显然只需要考虑之前不能到达 1 的点)

dpi,j,k 为在第 i 次操作时,目前有 j 个点能到达 1,而距上次从 1 出发已经经过了 k 个不能到达 1 的点。转移时考虑从当前点能走到哪个点。如果走到了已经经过的 k 个点,则 dpi+1,j,kk×dpi,j,k;如果走到了还没有经过的 njk 个点,则 dpi+1,j,k+1(njk)×dpi,j,k;如果走到了能到达 1j 个点,则 dpi+1,j+k,0j×dpi,j,k

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=310;
const int md=1000000007;
int n,m,i,j,k,b,c,w;
int dp[2][maxn][maxn];
int main(){
scanf("%d%d",&n,&m);
auto X=dp[0],Y=dp[1]; X[1][0]=1;
for(i=1;i<=m;++i){
for(j=1,b=n-1;j<=n;++j,--b){
for(k=0,c=b;k<=b;++k,--c){
w=X[j][k];
Y[j][k]=(1LL*k*w+Y[j][k])%md;
Y[j][k+1]=(1LL*c*w+Y[j][k+1])%md;
Y[j+k][0]=(1LL*j*w+Y[j+k][0])%md;
}
}
swap(X,Y); memset(Y,0,sizeof(dp[0]));
}
printf("%d",X[n][0]);
return 0;
}

例题3:Hakone

题意

有一个 1n 的排列 P。现在对于每个 i,给出 Pii 的大小关系(Pi>i/Pi=i/Pi<i),求可能的 P 的数量模 109+7n200

解法

考虑建一张二分图,第 i 个左部点向所有可能的 Pi 的取值的右部点连边,则问题变成了求该二分图的最大匹配数。显然可以删去所有满足 Pi=i 的对应 i,则之后的每个 Pii,方便之后的讨论。

显然无论单独看哪一部分的点,钦定每个点匹配的点时都会十分困难(无法确定其对之后的影响)。考虑每次同时处理第 i 个左部点/右部点,且对于一对匹配点,只在编号更大的点统计贡献(避免后效性)。设 dpi,j,k 为考虑了前 i 对点,且 j 个左部点和 k 个右部点未匹配的方案数(显然 j 个左部点只向编号更大的右部点连边)。如果 Pi+1>i+1,则该左部点只能向编号更大的右部点连边,dpi+1,j+1,k+1dpi,j,k;否则其需要向编号更小的右部点连边,dpi+1,j,kk×dpi,j,k。注意右部点可能和之前的 j 个左部点之一匹配,可能不匹配,所以还有 dpi+1,j,kj×dpi,j,kdpi+1,j1,k1j×k×dpi,j,k。注意到合法的 dp 状态一定满足 j=k,所以可以合并后两维。

代码

注意某些日本远古题需要文末换行。

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=210;
const int md=1000000007;
int n,i,j,t; char ch; bool p[maxn];
int dp[2][maxn],*X=dp[0],*Y=dp[1];
int main(){
scanf("%d",&n);
while(n--){
scanf(" %c",&ch);
if(ch=='-') continue;
p[++t]=(ch=='U');
}
X[0]=1;
for(i=0;i<t;++i,swap(X,Y)){
for(j=0;j<=i;++j) Y[j]=(1LL*X[j]*j)%md;
if(p[i+1]) for(j=0;j<=i;++j) Y[j+1]-=((Y[j+1]+=X[j])>=md)*md;
else for(j=1;j<=i;++j) Y[j-1]=(((1LL*j*j)%md)*X[j]+Y[j-1])%md;
}
printf("%d\n",X[0]);
return 0;
}

二、设计转移顺序(探索順の変更)

1. 降序枚举(大きい順に並べる)

例题1:My friends are small

题意

有一个长为 n 的数组 w。设 Sw 的一个子集,求满足 miniSi>WiSi 的所有 S 的数量模 109+7n200;wi,W104

解法

考虑枚举每个最小值对应的方案。此时一定需要选择小于最小值的所有数,不能选择最小值,然后选择另外一些大于最大值的数。此时可以从大到小枚举每个最大值,使用背包维护对应方案数。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=210;
const int maxm=10010;
const int md=1000000007;
int n,i,j,k,s,a,v,W,w[maxn],dp[maxm];
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
int main(){
scanf("%d%d",&n,&W);
for(i=1;i<=n;++i) scanf("%d",w+i),s+=w[i];
sort(w+1,w+n+1); dp[0]=1;
if(s-w[n]<=W&&s>=W) a=1; s-=w[n];
for(i=n-1,v=w[n];i;v=w[i--]){
s-=w[i];j=W-s;
for(k=W;k>=v;--k) Add(dp[k],dp[k-v]);
for(k=max(0,j-w[i]+1);k<=j;++k) Add(a,dp[k]);
}
printf("%d\n",a);
return 0;
}

2. 在排列中插入(順列は挿入 DP)

在对排列计数的过程中,如果暴力统计当前还有多少个数没有插入,则需要状压 O(2n) 种可能的情况。如果每个排列对应的贡献只和相邻元素的大小关系有关,则此时可以考虑 DP 升序/降序枚举每个元素插入的位置,保证已知相邻元素的大小关系。

例题2

题意

n 栋楼房,高度从 1n 不等。求所有楼房有多少种可能的高度,使得从左端能看到 L 栋楼房,从右端能看到 R 栋楼房。n300

解法

考虑将所有楼房按照高度从大到小依次加入。此时每次新加入的楼房只有在序列的开头或结尾时才能造成贡献,而在插入中间时没有贡献。设 dpi,j,k 为加入了高度为 1i 的楼房,从左端能看到 j 栋,从右端能看到 k 栋的方案数。

例题3:CWOI 19th November 2022 T4

3. 按照区间起/终点大小选择区间(区間は終点でソート)

可以用于解决使用区间覆盖某些位置的问题。

例题4

题意

n 个在 [0,X] 内的区间。求有多少种选择区间的方式使得选择的区间的并为 [0,X]n105

解法

(原文内解法有误)将所有区间按照左端点升序排序后,选择的一些区间一定会覆盖 [0,X] 的某个前缀。设 dpi,j 为使用前 i 个区间覆盖 [0,j] 的方案数,设第 i 个区间为 [Li,Ri],则转移即为 jLi,dpi1,jdpi,max(j,Ri)。可以使用权值线段树优化计算。

三、改写条件(条件の言い換え)

某些时候题目所给出的条件比较复杂,此时需要提炼出一个足够严谨而简洁的充要条件代替之。

例题1:A or...or B Problem

题意

[A,B] 中的若干数(至少一个)的按位或有多少种。A,B<260

解法

考虑去掉二进制数 A,B 的 LCP,然后找出一个最大的 2p 满足 A<2pB,然后讨论 [A,2p)[2p,B] 的对应答案。

显然 [A,2p) 内取任意数的按位或一定小于 2p,所以这部分内对答案造成的贡献为 2pA;而对于 [2p,B],考虑满足 2p+2qB<2p+2q+1 的最大 q,此时 v[2p,2p+2q),将它们和 2p+2q 按位或可得 [2p+2q,2p+2q+1) 内的所有数;且 [2p,B] 内的所有数按位或均小于 2p+2q+1,所以这部分对答案造成的贡献为 2q+11

然后考虑分别取 [A,2p)[2p,B] 内的任意数按位或(就是取 [A,2p)[2p,2p+2q+11] 中各一个数按位或)的数量。此时显然这个值的最小值为 2p+A,最大值为 2p+11,且 v[A,2p),vAND2p 可以取遍 [2p+A,2p+11] 内的所有数。将所有答案区间取并即可得出答案。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
long long a,b,p,q;
int main(){
scanf("%lld%lld",&a,&b);
if(a==b){putchar('1');return 0;}
for(q=a^b,p=1;p<=q;p<<=1);
--p; a&=p; b&=p;
for(p=1;p<=b;p<<=1); p>>=1;
for(q=1;p+q<=b;q<<=1);
printf("%lld",p+p+q-a-max(a,q));
return 0;
}

例题2:Eternal Average

题意

n0m1。每次会删去 k 个数,然后加入它们的平均数。求最后留下的数的可能的值的数量对 109+7 取模。n,m,k2×103

解法

设最后留下的数为 Z。考虑使用 k 进制写出 Z,则 Z 的所有位上的数字和和 m 在模 k1 意义下同余。证明考虑将所有加法不进行仅为操作时,所有位上的数字之和刚好为 m。同时考虑使用相同的方法操作,但是将 01 互换,最后得到的数为 1Z,且对应的所有位上的数字和和 n 在模 k1 意义下同余。(由于 (m+n1)mod(k1)=0,所以后者一定会发生)

此时如果存在对应的 Z,设 ciZ 的第 i 位,di1Z 的第 i 位;则可以每次选择一个非 0ci 减一,然后将 ci+1 加上 k,直到 ici=m。同时对 d 也进行这样的操作。然后考虑整个合并的过程可以看成建出一棵 k 叉树的过程。然后考虑操作之前的 cidi,设 l(c) 为满足 ci0 的最大 i,则 cl(c)+dl(c)=k,且 i<l(c),ci+di=k1,可以构造出一棵合法的二叉树,其中每个 ci+di 对应了深度为 i 的所有叶节点(可能为 0/1)。然后在将某个 ci 减一,将 ci+1k 的过程可以看成将一个叶节点变成一棵直径为 2k 叉树的过程;所以操作后的 c,d 也可以建出对应的 k 叉树,每一种 c 均为合法。

注意 c 需要最后一位非 0,需要特判。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=2010;
const int md=1000000007;
int n,m,k,i,j,a,b,s[maxn],dp[2][maxn][2];
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
int main(){
scanf("%d%d%d",&n,&m,&k);
dp[0][0][0]=1;
b=max(n,m)<<1;
auto X=dp[0],Y=dp[1];
for(i=1;i<=b;++i){
Add(s[0]=X[0][0],X[0][1]);
for(j=1;j<=m;++j){
Add(s[j]=X[j][0],X[j][1]);
Add(s[j],s[j-1]);
}
for(j=0;j<=m;++j){
Add(Y[j][0]=X[j][0],X[j][1]);
Y[j][1]=s[j-1]; if(j>=k) Add(Y[j][1],md-s[j-k]);
if(!((m-j)%(k-1))&&i*(k-1)-j+1>=0&&i*(k-1)-j+1<=n) Add(a,Y[j][1]);
}
swap(X,Y);
}
printf("%d",a);
return 0;
}

四、结合贪心思想(greedy からの帰着)

例题1

题意

求有多少个 1n 的排列可以被分成两个单增的子序列。n2000

解法

枚举某种划分方案对应的序列并不划算,因为某个序列可以有多种划分方案。考虑对于某个排列 p 如何判断其是否满足条件。设 dpi 为考虑了前 i 个数,且 maxj=1ipj 不在的另一个序列的末尾最小值为多少。如果新的 pi+1>maxj=1ipj,则 dpi+1=dpi,否则需要 pi+1>dpidpi+1=pi+1

考虑构造排列时,在排列中通过依次插入 1n 的数来计算排列数量(避免算重,且确定大小关系可以更好转移)。显然每次插入后某个子序列的末尾一定是当前最后一个数。在插入完前 i 个数后,需要维护在这个序列内 另一个子序列的末尾的最小值(以免在使用其他划分方法时算重)的位置(维护后面的元素的插入方法)。设 dpi,j 为插入前 i 个数,且另一个子序列末尾的最小值离末尾有 j 个元素时的方案数,转移时看 i+1 是否插入在末尾,有 dpi+1,j=k=j1i1dpi,k,可以使用后缀和优化计算。


所以在统计满足某种限制对应的序列数量时,由于贪心对应的策略会保证某个序列不会有多种贪心操作的方案,所以可以把目前贪心决策对应的状态作为 dp 状态,可以保证不重复。

例题2:Salaj

题意

n 个点。现在需要进行 n(n1) 次操作,每次操作选择两个点 u,v 满足 uv 边不存在,然后连上 uv 的有向边。设 ci 为第 i 次操作后图上的强连通分量个数,求有多少种 cn50

解法(暂缺,不会证明)

知道如何证明者可以在下面补充证明。

补一下 ZMJ 巨佬 的证明:

代码(暂缺)

点此查看代码

五、根据事件找准参数(場合分けのテクニック)

在对某些元素计数的时候,为了去重,如果不能使用容斥原理在计算完后去重,则应该找到一个能够和元素一一确定的一个量。

例题1

题意

有一个长为 n 的数组 a。现在需要选出若干数分别分到 A,B 集合,满足 iA,jB;i<j;求有多少种分的方法满足 iAi=jBjn,|a|100,a 中的数互不相同。

解法

排序后的 a 中,AB 一定会在两个不交的前后缀内。如果单独在前后缀内分别选择就会出现重复。考虑去重问题:设 Ai,j强制选择了 ai,且选择的所有数之和为 j 的方案数。转移时可以使用普通背包。然后在每个后缀同样进行一次背包即可。

例题2:Piling Up

题意

一开始有 n 个颜色为黑白的球,但不知道黑白色分别有多少, m 次操作,每次先拿出一个球,再放入黑白球各一个,再拿出一个球,最后拿出的球按顺序排列会形成一个颜色序列,求颜色序列有多少种。答案对 109+7 取模。n,m3×103

解法

dpi,ji 次操作后,剩余 j 个白球的对应序列数。然而这样做会算重,例如序列 白 白 会同时在 dp1,0dp1,m1 里面出现。

然后考虑只让某个序列在一个 dp 值内出现,但是同时需要维护每个 dp 值对应的转移。考虑实际上某个序列对应了原有的白球需要在某个区间里面,且这个区间一定会随着序列的长度而减小。此时可以直接取区间左端点对应的 dp 值计入答案。转移时设 dpi,j,0/1 表示 i 次操作,剩余 j 个白球,且白球是否被取完的序列数,最后取 i=0mdpn,i,1 为答案即可。

代码

点此查看代码
#include <bits/stdc++.h>
using namespace std;
const int maxn=3010;
const int md=1000000007;
int n,m,i,j,w,b,dp[2][maxn][2];
inline void Add(int &x,int y){x-=((x+=y)>=md)*md;}
int main(){
scanf("%d%d",&n,&m);
auto X=dp[0],Y=dp[1]; X[0][1]=1;
for(i=1;i<=n;++i) X[i][0]=1;
for(i=1;i<=m;++i){
for(j=0;j<=n;++j){
if(j){
b=(j==1); w=X[j][0];
Add(Y[j-1][b],w);
Add(Y[j][b],w);
w=X[j][1];
Add(Y[j-1][1],w);
Add(Y[j][1],w);
}
if(j!=n){
w=X[j][0];
Add(Y[j][0],w);
Add(Y[j+1][0],w);
w=X[j][1];
Add(Y[j][1],w);
Add(Y[j+1][1],w);
}
}
swap(X,Y); memset(Y,0,(m+1)<<3);
}
for(i=w=0;i<=m;++i) Add(w,X[i][1]);
printf("%d",w);
return 0;
}
posted @   Fran-Cen  阅读(351)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示