计数 dp 部分例题(一~五部分)
数え上げテクニック集 笔记。题目的代码难度都不高。
引言
OI 中有三大专题:dp,数据结构,图论。而在这三大专题中,因为 dp 是从小问题的解法上升至大问题的解法的关键;所以 dp,在这三大专题中,优先性是第一位的。如果在从小问题的解法上升至大问题时,难免于时间和空间的超限,会使得这种题目值得深究;而在 OI 中,人们总是会深究简化所用时间和空间的方法,我们称这种方法为 dp 优化;而在这篇文章中,就会专门对那些耗费大量时间和空间的解法,处心积虑,找出对应的 dp 优化方法。
一、状态设计和简化(状態をまとめる)
例题1:Unhappy Hacking
题意
有一个空串,可以进行下面三种操作:
- 在末尾加入一个 。
- 在末尾加入一个 。
- 删去末尾的数,如果串为空则忽略。
求 次操作后满足整个串等于某个给定的串 的方案数模 。。
解法1(本人的 ** 解法)
某次操作可以删去原有匹配好的串的一部分,考虑消除这种操作带来的影响。
此时可以对于每个 ,在填好的串为最后一次和长度为 的前缀相同的时候进行统计。显然之后的操作不包括将前 个字符删除,所以在最后一次匹配长度为 的前缀时,一定只会删去新加的字符然后加入一个对应的字符。这两次匹配之间加入/删除的操作可以形成一个合法的括号序列。设 为进行了 次操作,形成了长度为 的前缀的方案数,则转移为 ,(其中 为长为 的合法括号序列数,也就是 卡塔兰数)是一个卷积的形式,可以使用 MTT + 快速幂解决。(懒得写。)
注意 需要 特别计算(可能对空串进行删除操作)。
解法2(题解区首页解法)
考虑只在删除操作处计算对应的贡献。显然我们只关心未被删除的部分能否构成 ,此时如果某个插入/删除序列中有 次删除操作删除了某个数,则其对应了 种实际方案。
设 为进行了 次操作,得到了长为 的串的方案数。如果下一次操作为删除,则 (特别的,);如果下一次操作为加入,则 ()。
代码
点此查看代码
#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
题意
有一个 个点的图,目前一条边都没有。
有一个人在 号点要进行 次移动,终点不必是 号点,假设第 次从 移动到 ,那么在 与 之间连一条有向边。可以有 。
问有多少种序列能满足:最终 个点组成的图是一个强连通图。答案对 取模。。
解法
考虑形成强连通图的一个充要条件:对于某个点 ,满足所有点都能从 到达,所有点也都有到达 的路径。所以在构造时,令 ,则每次回到 之后均会使得之前从 出发能到达的点现在能到达 。(显然只需要考虑之前不能到达 的点)
设 为在第 次操作时,目前有 个点能到达 ,而距上次从 出发已经经过了 个不能到达 的点。转移时考虑从当前点能走到哪个点。如果走到了已经经过的 个点,则 ;如果走到了还没有经过的 个点,则 ;如果走到了能到达 的 个点,则 。
代码
点此查看代码
#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
题意
有一个 的排列 。现在对于每个 ,给出 和 的大小关系(),求可能的 的数量模 。。
解法
考虑建一张二分图,第 个左部点向所有可能的 的取值的右部点连边,则问题变成了求该二分图的最大匹配数。显然可以删去所有满足 的对应 ,则之后的每个 ,方便之后的讨论。
显然无论单独看哪一部分的点,钦定每个点匹配的点时都会十分困难(无法确定其对之后的影响)。考虑每次同时处理第 个左部点/右部点,且对于一对匹配点,只在编号更大的点统计贡献(避免后效性)。设 为考虑了前 对点,且 个左部点和 个右部点未匹配的方案数(显然 个左部点只向编号更大的右部点连边)。如果 ,则该左部点只能向编号更大的右部点连边,;否则其需要向编号更小的右部点连边,。注意右部点可能和之前的 个左部点之一匹配,可能不匹配,所以还有 ,。注意到合法的 dp 状态一定满足 ,所以可以合并后两维。
代码
注意某些日本远古题需要文末换行。
点此查看代码
#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
题意
有一个长为 的数组 。设 为 的一个子集,求满足 的所有 的数量模 。。
解法
考虑枚举每个最小值对应的方案。此时一定需要选择小于最小值的所有数,不能选择最小值,然后选择另外一些大于最大值的数。此时可以从大到小枚举每个最大值,使用背包维护对应方案数。
代码
点此查看代码
#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)
在对排列计数的过程中,如果暴力统计当前还有多少个数没有插入,则需要状压 种可能的情况。如果每个排列对应的贡献只和相邻元素的大小关系有关,则此时可以考虑 DP 升序/降序枚举每个元素插入的位置,保证已知相邻元素的大小关系。
例题2
题意
有 栋楼房,高度从 到 不等。求所有楼房有多少种可能的高度,使得从左端能看到 栋楼房,从右端能看到 栋楼房。。
解法
考虑将所有楼房按照高度从大到小依次加入。此时每次新加入的楼房只有在序列的开头或结尾时才能造成贡献,而在插入中间时没有贡献。设 为加入了高度为 的楼房,从左端能看到 栋,从右端能看到 栋的方案数。
例题3:CWOI 19th November 2022 T4
3. 按照区间起/终点大小选择区间(区間は終点でソート)
可以用于解决使用区间覆盖某些位置的问题。
例题4
题意
有 个在 内的区间。求有多少种选择区间的方式使得选择的区间的并为 。。
解法
(原文内解法有误)将所有区间按照左端点升序排序后,选择的一些区间一定会覆盖 的某个前缀。设 为使用前 个区间覆盖 的方案数,设第 个区间为 ,则转移即为 。可以使用权值线段树优化计算。
三、改写条件(条件の言い換え)
某些时候题目所给出的条件比较复杂,此时需要提炼出一个足够严谨而简洁的充要条件代替之。
例题1:A or...or B Problem
题意
求 中的若干数(至少一个)的按位或有多少种。。
解法
考虑去掉二进制数 的 LCP,然后找出一个最大的 满足 ,然后讨论 和 的对应答案。
显然 内取任意数的按位或一定小于 ,所以这部分内对答案造成的贡献为 ;而对于 ,考虑满足 的最大 ,此时 ,将它们和 按位或可得 内的所有数;且 内的所有数按位或均小于 ,所以这部分对答案造成的贡献为 。
然后考虑分别取 和 内的任意数按位或(就是取 和 中各一个数按位或)的数量。此时显然这个值的最小值为 ,最大值为 ,且 可以取遍 内的所有数。将所有答案区间取并即可得出答案。
代码
点此查看代码
#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
题意
有 个 和 个 。每次会删去 个数,然后加入它们的平均数。求最后留下的数的可能的值的数量对 取模。。
解法
设最后留下的数为 。考虑使用 进制写出 ,则 的所有位上的数字和和 在模 意义下同余。证明考虑将所有加法不进行仅为操作时,所有位上的数字之和刚好为 。同时考虑使用相同的方法操作,但是将 和 互换,最后得到的数为 ,且对应的所有位上的数字和和 在模 意义下同余。(由于 ,所以后者一定会发生)
此时如果存在对应的 ,设 为 的第 位, 为 的第 位;则可以每次选择一个非 的 减一,然后将 加上 ,直到 。同时对 也进行这样的操作。然后考虑整个合并的过程可以看成建出一棵 叉树的过程。然后考虑操作之前的 和 ,设 为满足 的最大 ,则 ,且 ,可以构造出一棵合法的二叉树,其中每个 对应了深度为 的所有叶节点(可能为 )。然后在将某个 减一,将 加 的过程可以看成将一个叶节点变成一棵直径为 的 叉树的过程;所以操作后的 也可以建出对应的 叉树,每一种 均为合法。
注意 需要最后一位非 ,需要特判。
代码
点此查看代码
#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
题意
求有多少个 的排列可以被分成两个单增的子序列。。
解法
枚举某种划分方案对应的序列并不划算,因为某个序列可以有多种划分方案。考虑对于某个排列 如何判断其是否满足条件。设 为考虑了前 个数,且 不在的另一个序列的末尾最小值为多少。如果新的 ,则 ,否则需要 ,。
考虑构造排列时,在排列中通过依次插入 的数来计算排列数量(避免算重,且确定大小关系可以更好转移)。显然每次插入后某个子序列的末尾一定是当前最后一个数。在插入完前 个数后,需要维护在这个序列内 另一个子序列的末尾的最小值(以免在使用其他划分方法时算重)的位置(维护后面的元素的插入方法)。设 为插入前 个数,且另一个子序列末尾的最小值离末尾有 个元素时的方案数,转移时看 是否插入在末尾,有 ,可以使用后缀和优化计算。
所以在统计满足某种限制对应的序列数量时,由于贪心对应的策略会保证某个序列不会有多种贪心操作的方案,所以可以把目前贪心决策对应的状态作为 dp 状态,可以保证不重复。
例题2:Salaj
题意
有 个点。现在需要进行 次操作,每次操作选择两个点 满足 边不存在,然后连上 的有向边。设 为第 次操作后图上的强连通分量个数,求有多少种 。。
解法(暂缺,不会证明)
知道如何证明者可以在下面补充证明。
补一下 巨佬 的证明:
代码(暂缺)
点此查看代码
五、根据事件找准参数(場合分けのテクニック)
在对某些元素计数的时候,为了去重,如果不能使用容斥原理在计算完后去重,则应该找到一个能够和元素一一确定的一个量。
例题1
题意
有一个长为 的数组 。现在需要选出若干数分别分到 集合,满足 ;求有多少种分的方法满足 。 中的数互不相同。
解法
排序后的 中, 和 一定会在两个不交的前后缀内。如果单独在前后缀内分别选择就会出现重复。考虑去重问题:设 为 强制选择了 ,且选择的所有数之和为 的方案数。转移时可以使用普通背包。然后在每个后缀同样进行一次背包即可。
例题2:Piling Up
题意
一开始有 个颜色为黑白的球,但不知道黑白色分别有多少, 次操作,每次先拿出一个球,再放入黑白球各一个,再拿出一个球,最后拿出的球按顺序排列会形成一个颜色序列,求颜色序列有多少种。答案对 取模。。
解法
设 为 次操作后,剩余 个白球的对应序列数。然而这样做会算重,例如序列 白 白
会同时在 里面出现。
然后考虑只让某个序列在一个 dp 值内出现,但是同时需要维护每个 dp 值对应的转移。考虑实际上某个序列对应了原有的白球需要在某个区间里面,且这个区间一定会随着序列的长度而减小。此时可以直接取区间左端点对应的 dp 值计入答案。转移时设 表示 次操作,剩余 个白球,且白球是否被取完的序列数,最后取 为答案即可。
代码
点此查看代码
#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; }
本文来自博客园,作者:Fran-Cen,转载请注明原文链接:https://www.cnblogs.com/Fran-CENSORED-Cwoi/p/17034363.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框架的用法!