dp 套 dp(dp of dp)
- Update on 2025.8.10:加入了自动机部分以及两道题。
dp 套 dp
dp of dp 的题的形式一般是让你求满足...条件的...的数量。
直接 dp 可能不是很好用一个状态来刻画是否满足条件。
但是判定一个东西是否满足条件可以用一个比较简单的 dp 来处理。
这样我们可以用内层 dp 的结果作为外层 dp 的状态,就容易刻画出满足...条件这个限制了。
这么说还是很抽象,所以我们引入几道例题。
I. [TJOI2018] 游园会
这应该算是 dp 套 dp 板子。
下面称输入给出的奖章串为 \(s\),要求的兑奖串为 \(t\)。
会发现题目中的 \(t\) 有三个限制:
- 长度为 \(N\),且只有
N
,O
,I
三个字符。 - 与 \(s\) 的 LIS 为 \(len\)。
- 不能出现子串
NOI
。
会发现限制 \(1\) 可以直接在 dp 转移的时候满足,限制 \(3\) 直接多加一维状态 \(0/1/2\) 表示现在的后缀和 NOI
的匹配位数即可。
即我们的 dp 状态应该是 \(dp_{i,S,0/1/2}\) 表示填到 \(t\) 的第 \(i\) 位,当前状态是 \(S\),后缀和 NOI
的匹配位数是 \(0/1/2\) 的方案数。
但是限制 \(2\) 极其不好记录状态,因为你需要知道当前匹配到 \(s\) 的第几位,而直接记录匹配到 \(s\) 的第几位的话又容易算重。
考虑我们是怎么判定一个给定的 \(t\) 和 \(s\) 的 LIS 是否为 \(i\) 的。
显然就是先求出他们的 LIS 再判断。
这是一个经典 dp,设 \(f_{i,j}\) 表示 \(t[1,i]\) 和 \(s[1,j]\) 匹配的 LIS,转移:
\(f_{i,j}=\max(f_{i-1,j},f_{i,j-1},f_{i-1,j-1}+(t_i==s_j))\)。
发现我们要计算出 \(f\) 第 \(i\) 行的所有值,需要的只是 \(t_i\) 和 \(f\) 第 \(i-1\) 行的所有值。
所以最外层 dp 的状态的 \(S\) 我们直接让他表示内层 dp 的第 \(i\) 行的所有值。
外层 dp 的转移就枚举下一个位置 \(t_{i+1}\) 是什么,并对第二维 \(S\) 再做一遍 LIS 的那个 dp,得到新的状态 \(S'\)(即内层 dp \(f\) 数组的第 \(i+1\) 行)。
这样就转移成功了。
但是很明显 \(S\) 的数量太多了,时间空间双爆炸。
但显然并不是所有的 \(S\) 都是合法的。
因为 \(0\le f_{i,j}-f_{i,j-1}\le 1\),所以 \(S\) 表示的序列的差分数组每一位都一定是 \(0/1\)。
因此状态 \(S\) 可以改为记录内层 dp \(f\) 数组的第 \(i\) 行的差分数组。
进一步地,可以直接状压成一个二进制数。
这样 \(S\) 的总数就是 \(2^K\) 了。
状态数是 \(O(N2^K)\),转移在 \(O(1)\) 枚举完 \(t_{i+1}\) 之后需要对内层 dp 进行 \(O(K)\) 的转移。
复杂度 \(O(NK2^K)\)。
转移好函数的封装并不难写,实现参考了第一篇题解。
注意滚动数组并稍微剪枝。
因为在进行外层 dp 转移的时候还要对内层 dp 进行一次转移,所以叫 dp 套 dp
。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5,mod=1e9+7;
inline int read(){
int w=1,s=0;
char c=getchar();
for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
return w*s;
}
int n,k,dp[2][(1<<15)+5][3],f1[20],f2[20],ans[20];
char s[20];
int Hash(int f[20]){ //把一个数组的差分数组状压
int S=0;
for(int i=0;i<k;i++) S+=(f[i+1]-f[i])*(1<<i);
return S;
}
void decrypt(int f[20],int S){ //解压缩
for(int i=0;i<k;i++) f[i+1]=S>>i&1;
for(int i=1;i<=k;i++) f[i]+=f[i-1];
}
void DP(int S,int newi,int newj,char c,int val){
decrypt(f1,S);
for(int i=1;i<=k;i++) f2[i]=max({f2[i-1],f1[i],f1[i-1]+(c==s[i])});
int S2=Hash(f2);
(dp[newi&1][S2][newj]+=val)%=mod;
}
signed main(){
n=read(),k=read();
scanf("%s",s+1);
dp[0][0][0]=1;
for(int i=0;i<n;i++){
for(int S=0;S<(1<<k);S++) dp[i&1^1][S][0]=dp[i&1^1][S][1]=dp[i&1^1][S][2]=0;
for(int S=0;S<(1<<k);S++){
if(dp[i&1][S][0]){ //这个剪枝很重要
DP(S,i+1,1,'N',dp[i&1][S][0]);
DP(S,i+1,0,'O',dp[i&1][S][0]);
DP(S,i+1,0,'I',dp[i&1][S][0]);
}
if(dp[i&1][S][1]){
DP(S,i+1,1,'N',dp[i&1][S][1]);
DP(S,i+1,2,'O',dp[i&1][S][1]);
DP(S,i+1,0,'I',dp[i&1][S][1]);
}
if(dp[i&1][S][2]){
DP(S,i+1,1,'N',dp[i&1][S][2]);
DP(S,i+1,0,'O',dp[i&1][S][2]);
}
}
}
for(int S=0;S<(1<<k);S++){ //显然 S 的 1 的个数就是这个内层 dp DP 出来的 LIS 的长度
(ans[__builtin_popcount(S)]+=((dp[n&1][S][0]+dp[n&1][S][1])%mod+dp[n&1][S][2])%mod)%=mod;
}
for(int i=0;i<=k;i++) printf("%d\n",ans[i]);
return 0;
}
II. 开心消消乐
题面
数据范围: \(N\le 1e5,T\le 10,N 是奇数\)。
考虑如何判断一个没有 ?
的序列合不合法。
题目中的操作不好顺序处理,转换一下:
相当于把原序列分成 \(\lfloor \frac{n}{2} \rfloor\) 个块 \([1,2],[3,4],[5,6],...,[n-2,n-1]\) 和最后一个数。
并维护一个栈,栈里面存储的是块。
我们一次考虑每个块,如果当前考虑的块是 \((x,y)\),那相当于有两种操作:
- 把 \(x\) 和栈中所有块依次合并,直到栈中只剩一个数 \(z\),将 \((z,y)\) 放入栈。
- 直接把 \((x,y)\) 丢入栈。
考虑完所有块后让最后一个数和栈中的块依次合并,最后得到的数就是结果。
判断就是要判断是否存在一种操作方式使得最后可以得到 \(1\)。
你会发现,我们其实不在乎这个栈具体长什么样,只在乎当我们把一个数 \(x\) 和栈中的所有块合并完之后会得到什么数。
即我们只需要维护栈所对应的函数 \(f:x\to (a,b)\) 表示当 \(x=0\) 时,会得到 \(a\),当 \(x=1\) 时会得到 \(b\)。
并且对于这两种操作都可以快速更新出新的 \(f\)。
于是我们考虑 dp 设 \(dp_{i,a,b}\) 表示是否可以在考虑完第 \(i\) 个块之后得到函数 \(f:x\to (a,b)\)。
转移显然,于是我们完成了判定。
对于计数,因为这个判定的 dp 的后两维只有 \(4\) 种情况,且 dp 值天然地只有 \(0/1\) 两种情况。
所以直接上 dp of dp 即可。
状态数是 \(O(2^4 \times n)\)。
点击查看代码
#include<bits/stdc++.h>
#define PII pair<int,int>
#define fi first
#define se second
using namespace std;
const int N=1e5+5,mod=998244353;
inline int read(){
int w=1,s=0;
char c=getchar();
for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
return w*s;
}
int T,n;
char c[10],s[N];
int calc(char x,char y,char z){
int u=x-'0',v=y-'0',w=z-'0';
return c[(w<<2)+(v<<1)+u]-'0';
}
int dp[N][(1<<4)+5];
bool f1[2][2],f2[2][2];
int Hash(bool f[2][2]){
return (f[0][0]<<3)+(f[0][1]<<2)+(f[1][0]<<1)+f[1][1];
}
void decrypt(bool f[2][2],int S){
f[0][0]=S>>3&1;
f[0][1]=S>>2&1;
f[1][0]=S>>1&1;
f[1][1]=S&1;
}
void DP(int newi,int S,char x,char y,int val){
decrypt(f1,S);
f2[0][0]=f2[0][1]=f2[1][0]=f2[1][1]=false;
for(int a=0;a<=1;a++){
for(int b=0;b<=1;b++){
//转移 1:先放 x,再放 y
if(x=='0') f2[calc(a+'0',y,'0')][calc(a+'0',y,'1')]|=f1[a][b];
else f2[calc(b+'0',y,'0')][calc(b+'0',y,'1')]|=f1[a][b];
//转移 2:把 (x,y) 直接丢进栈里
int a1=calc(x,y,'0'),b1=calc(x,y,'1');
if(a1==0&&b1==0) f2[a][a]|=f1[a][b];
else if(a1==0&&b1==1) f2[a][b]|=f1[a][b];
else if(a1==1&&b1==0) f2[b][a]|=f1[a][b];
else f2[b][b]|=f1[a][b];
}
}
int S2=Hash(f2);
(dp[newi][S2]+=val)%=mod;
}
void work(){
memset(dp,0,sizeof dp);
dp[0][4]=1;
for(int i=1;i<n;i+=2){
int id=(i+1)/2;
for(int S=0;S<(1<<4);S++){
if(s[i]=='?'&&s[i+1]=='?'){
DP(id,S,'0','0',dp[id-1][S]);
DP(id,S,'0','1',dp[id-1][S]);
DP(id,S,'1','0',dp[id-1][S]);
DP(id,S,'1','1',dp[id-1][S]);
}
else if(s[i]=='?'){
DP(id,S,'0',s[i+1],dp[id-1][S]);
DP(id,S,'1',s[i+1],dp[id-1][S]);
}
else if(s[i+1]=='?'){
DP(id,S,s[i],'0',dp[id-1][S]);
DP(id,S,s[i],'1',dp[id-1][S]);
}
else DP(id,S,s[i],s[i+1],dp[id-1][S]);
}
}
int id=(n-1)/2,ans=0;
for(int S=0;S<(1<<4);S++){
decrypt(f1,S);
if(s[n]!='1') if(f1[1][0]||f1[1][1]) (ans+=dp[id][S])%=mod;
if(s[n]!='0') if(f1[0][1]||f1[1][1]) (ans+=dp[id][S])%=mod;
}
printf("%d\n",ans);
}
signed main(){
scanf("%d",&T);
while(T--){
scanf("%s%s",c,s+1);
n=strlen(s+1);
work();
}
return 0;
}
III. Bus Analysis
考虑给你了一个序列 \(t\) 怎么算答案,然后 dp of dp 即可。
首先把贡献都除以二,最后再把答案 \(\times 2\) 没有任何影响。
朴素的 dp 是设 \(f_i\) 表示覆盖前 \(i\) 个点的最小代价。
转移时,如果区间 \([t_{j+1},t_i]\) 的长度 \(\le 20\) 则有转移 \(f_j+1 \to f_i\);如果长度 \(\le 75\) 则有转移 \(f_j+3 \to f_i\)。
这么看的话,如果想要转移 \(f_i\) 我们需要至多 \(i\) 前面 \(75\) 个 \(j\) 的 DP 值。
继续压缩,会发现这 \(75\) 个 \(j\) 中的任意两个 \(x,y\) 都满足 \(f_x\) 和 \(f_y\) 相差不超过 \(3\)。
所以其实有用的 DP 值只有三种。
于是我们内层 dp 其实只需要维护四个值:
- \(w\):表示 \(f_i\)。
- \(x\): 表示最大的 \(j\) 满足 \(f_j=f_i-1\)。
- \(y\): 表示最大的 \(j\) 满足 \(f_j=f_i-2\)。
- \(z\): 表示最大的 \(j\) 满足 \(f_j=f_i-3\)。
注:理论上讲我们还需要维护 \(u\) 表示最大的 \(j\) 满足 \(f_j=f_i\),但显然 \(u=i\)。
转移是简单的,先算出 \(f_{i+1}\),然后用 \({i,x,y,z}\) 去得到新的 \((x',y',z')\)。
那么我们的外层 dp 就是 \(f_{i,w,x,y,z}\) 表示内层 dp 的结果是 \((w,x,y,z)\) 的方案数,显然会炸。
不过发现我们记录 \(w\) 的唯一用处就是计算最后的答案。
但是我们只需要计算所有 \(w\) 的和即可,这启发我们在 \(w\) 改变的时候直接把变化值加到最后的答案里就可以了。
这样状态就变成 \(f_{i,x,y,z}\) 了,\(O(n\times 75^3)\) 因为常数小可以通过。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e3+5,mod=1e9+7;
inline int read(){
int w=1,s=0;
char c=getchar();
for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
return w*s;
}
int n,t[N],f[2][76][76][76],pos[10],ans,p[N];
void DP1(int i,int x,int y,int z,int res){
if(x) (x+=1)%=76;
if(y) (y+=1)%=76;
if(z) (z+=1)%=76;
(f[i&1^1][x][y][z]+=res)%=mod;
}
int solve(int now,int lst,int res){
int len=t[now]-t[lst+1]+1;
if(len<=20) return res+1;
else if(len<=75) return res+3;
else return n+5;
}
void DP2(int i,int x,int y,int z,int res){
int g=solve(i+1,i,4);
if(x) g=min(g,solve(i+1,i-x,3));
if(y) g=min(g,solve(i+1,i-y,2));
if(z) g=min(g,solve(i+1,i-z,1));
(ans+=1ll*res*p[n-i-1]%mod*(g-4)%mod)%=mod;
pos[1]=pos[2]=pos[3]=0;
if(z>=1&&z<=74) pos[g-1]=z+1;
if(y>=1&&y<=74) pos[g-2]=y+1;
if(x>=1&&x<=74) pos[g-3]=x+1;
pos[g-4]=1;
(f[i&1^1][pos[1]][pos[2]][pos[3]]+=res)%=mod;
}
signed main(){
n=read();
p[0]=1;
for(int i=1;i<=n;i++) t[i]=read(),p[i]=p[i-1]*2%mod;
f[0][0][0][0]=1;
for(int i=0;i<n;i++){
memset(f[i&1^1],0,sizeof f[i&1^1]);
for(int x=0;x<=min(i,75);x++){
for(int y=0;y<=min(i,75);y++){
for(int z=0;z<=min(i,75);z++){
if(!f[i&1][x][y][z]) continue;
DP1(i,x,y,z,f[i&1][x][y][z]);
DP2(i,x,y,z,f[i&1][x][y][z]);
}
}
}
}
printf("%d\n",ans*2%mod);
return 0;
}
总结
先总结一下,dp of dp 的基本步骤一般是:
- 确定内层 dp;
- 压缩内层 dp;
- 嵌套外层 dp。
其中第三步比较简单,第一步跟 dp of dp 没啥关系。
大部分题目的第二步需要发现内层 dp 的一些性质(如 I,III),但是也有少数的 dp 本来内层的状态数比较少(如 II)。
当然也有这样一类题目,你只需要爆搜就可以发现内层 dp 有用的状态不多。
自动机与 dp 套 dp
dp 套 dp 的另一个名字叫 自动机DP,因为你可以把内层 DP 看成一个自动机,内层 DP 的每个状态就是自动机上的一个状态(或者说节点),内层 DP 的每种转移就对应了自动机上的转移(或者说边),那么外层 DP \(f_{i,u}\) 表示的就可以认为是考虑了前 \(i\) 个 ... 后,位于自动机点 \(u\) 上的方案数(或者权值和什么的)。
而上面所提到的第二步就是在减少自动机的状态个数。
从自动机的角度会更好理解 dp 套 dp,下面有两道更贴合 自动机DP 这个名字的例题。
免责声明: 虽然下面两题的最后一步都是"然后通过搜索可以发现有用的状态数只有...种",但是在搜索这一步前都经历了大量的性质分析与状态压缩,因此不包括在我上面所说的"只需要爆搜的题目"中,相反,这两道都是 dp of dp 的绝世好题。
IV. [ZJOI2019] 麻将
注意题目中即使是相同类型的牌也是互相区分的。
我们先规定形如 \(i,i,i\) 的面子叫刻子,形如 \(i,i+1,i+2\) 的面子叫顺子。
先思考当给你一副牌的时候怎么判断他胡没胡,或者说我们要构造一个麻将自动机。
我们先不考虑七个对子的情况。
一个比较显然的发现是我们不在乎抽中的牌之间的顺序是什么样的,我们只在乎每种牌已经抽中了多少张。
于是我们不妨按牌的大小从小到大考虑每种牌,设 DP \(f_{i,j,k}\) 表示考虑到了大小为 \(i\) 的牌,预留了 \(j\) 对 \(i-1,i\) 来与 \(i+1\) 形成顺子,又另外预留了 \(k\) 个 \(i\) 来与 \(i+1\) 形成半顺子,此时最多能凑出多少个面子。
转移的时候如果大小为 \(i+1\) 的有 \(x\) 张牌,那么首先要分出 \(j+k\) 个来与预留的拼在一起,然后再枚举 \(y\in [0,x-j-k]\) 表示剩下的牌中要预留多少个和 \(i+2\) 形成半顺子,最后还剩 \(x-j-k-y\) 张牌尽可能的凑刻子,即:\(f_{i,j,k}+i+\lfloor \frac{x-j-k-y}{3} \rfloor \to f_{i+1,k,y}\)。
然后因为胡牌的时候还需要一个对子,于是把上面的状态拆成两个状态 \(f0,f1\) 分别表示当前是否已经预留了一个对子。
那么如果 \(x\ge 2\) 的话就可以从 \(f0\) 加上 \(x-2\) 个牌转移到 \(f1\)。
如果 \(f1\) 的某个 DP 值 \(\ge 4\) 就代表胡了。
为了使我们最后自动机上的节点数尽可能的少,我们需要优化一下这个 DP:
- DP 值显然可以跟 \(4\) 取 \(min\)。
- 注意到 \(3\) 个相同的顺子等价于 \(3\) 个刻子,所以 \(j,k,y\) 实际上可以只取到 \(\le 2\)。
接着我们开始构造麻将自动机,对于这个自动机上的一个状态,首先我们需要记录两个 DP 数组 \(f0,f1\)(注意 \(i\) 这一维是不用存的),我们可以用两个 \(3\times 3\) 的矩阵来表示当前的 DP 值。然后我们还需要加上七对子这种情况,我们只需要在每个状态里多记录一个 \(c\) 表示目前抽到了多少种牌数 \(\ge 2\) 的牌。
自动机上的一条转移边 \(x\) 就代表下一种类型的牌有 \(x\) 张。
一个状态是终止状态(就是胡牌的状态)当且仅当 \(c\ge 7\),或者 \(f1\) 中的某个值 \(=4\),我们可以把所有终止状态都设为一个特殊状态 \(t\)。
从初始状态开始暴力搜索可以发现这样构建的自动机状态数已经不是很多了,我的状态数是:\(2092\)。
然后开始思考原问题,这个期望不是很好算,我们考虑计算 \(g_i\) 表示摸了 \(i\) 张牌仍然没有胡的方案数,由于上面的 DP 中我们没有考虑牌的顺序,所以这里的 \(g\) 我们也不考虑顺序,那么最后的答案是:
设 \(cnt_i\) 表示初始有多少张大小为 \(i\) 的牌。
设 \(dp_{i,j,u}\) 表示考虑到了大小为 \(i\) 的牌,目前从 \(4n-13\) 张牌中摸到的牌有 \(j\) 张,位于自动机上的点 \(u\) 的方案数。
转移枚举大小为 \(i+1\) 的牌摸到了 \(x\) 张,那么总共就有 \(x+cnt_{i+1}\) 张牌,转移为:\(dp_{i,j,u}\times \binom{4-cnt_{i+1}}{x} \to dp_{i+1,j+x,v}\),
其中 \(v\) 为自动机上 \(u\) 走 \(x+cnt_{i+1}\) 这条转移边到达的状态。
最后 \(g_i=\sum_{u\ne t} dp_{n,i,u}\)。
点击查看代码
#include<bits/stdc++.h>
#define Debug puts("-------------------------")
#define eb emplace_back
using namespace std;
const int N=100+5,M=2091+5,inf=0x3f3f3f3f,mod=998244353;
inline int read(){
int w=1,s=0;
char c=getchar();
for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
return w*s;
}
void chkmax(int &x,int y){x=(y>x)?y:x; x=(x<0)?(-inf):x;}
void init(int a[3][3]){for(int i=0;i<=2;i++) for(int j=0;j<=2;j++) a[i][j]=-inf;}
struct Matrix{
int a[3][3];
void clear(){init(a);}
};
Matrix operator + (const Matrix &A,int &x){
Matrix B; B.clear();
for(int i=0;i<=2;i++){
for(int j=0;j<=2;j++){
if(A.a[i][j]==-inf) continue;
for(int k=0;k<=2;k++){
if(i+j+k<=x){
chkmax(B.a[j][k],min(4,A.a[i][j]+i+(x-i-j-k)/3));
}
}
}
}
return B;
}
Matrix merge(Matrix A,Matrix B){for(int i=0;i<=2;i++) for(int j=0;j<=2;j++) chkmax(A.a[i][j],B.a[i][j]); return A;}
struct state{ int c; Matrix f0,f1; };
state s,t,node[M];
int tot,G[M][5];
map<vector<int>,int> id;
vector<int> Hash(state u){
vector<int> v; v.eb(u.c);
for(int i=0;i<=2;i++) for(int j=0;j<=2;j++) v.eb(u.f0.a[i][j]),v.eb(u.f1.a[i][j]);
return v;
}
bool check(state u){
if(u.c>=7) return true;
for(int i=0;i<=2;i++) for(int j=0;j<=2;j++) if(u.f1.a[i][j]>=4) return true;
return false;
}
state operator + (const state &u,int &x){
state v; int y=x-2;
v.c=u.c+(y>=0);
v.f0=u.f0+x,v.f1=(y>=0)?merge(u.f1+x,u.f0+y):(u.f1+x);
if(check(v)) v=t;
return v;
}
void build(){
t.c=-1,s.c=0,s.f0.clear(),s.f1.clear(),s.f0.a[0][0]=0;
node[0]=t,node[1]=s,id[Hash(t)]=0,id[Hash(s)]=1;
tot=1;
for(int i=1;i<=tot;i++){
state u=node[i];
for(int x=0;x<=4;x++){
state v=u+x;
if(!id.count(Hash(v))){
id[Hash(v)]=++tot,node[tot]=v;
}
G[i][x]=id[Hash(v)];
}
}
cerr<<tot<<'\n';
}
void add(int &x,int y){x=(x+y>=mod)?(x+y-mod):(x+y);}
int n,cnt[N],pre[N],f[2][N<<2][M],g[N<<2],C[5][5],fact[N<<2],inv[N<<2];
void Init(){
fact[0]=inv[0]=inv[1]=1;
for(int i=1;i<=4*n-13;i++) fact[i]=1ll*fact[i-1]*i%mod;
for(int i=2;i<=4*n-13;i++) inv[i]=1ll*(mod-mod/i)*inv[mod%i]%mod;
for(int i=2;i<=4*n-13;i++) inv[i]=1ll*inv[i-1]*inv[i]%mod;
for(int i=0;i<=4;i++) for(int j=0;j<=i;j++) C[i][j]=1ll*fact[i]*inv[j]%mod*inv[i-j]%mod;
}
signed main(){
build();
n=read();
for(int i=1,w,t;i<=13;i++) w=read(),t=read(),cnt[w]++;
Init();
for(int i=1;i<=n;i++) pre[i]=pre[i-1]+4-cnt[i];
f[0][0][1]=1;
for(int i=0;i<n;i++){
for(int j=0;j<=pre[i+1];j++) for(int k=1;k<=tot;k++) f[i&1^1][j][k]=0;
for(int j=0;j<=pre[i];j++){
for(int k=1;k<=tot;k++){
if(!f[i&1][j][k]) continue;
for(int x=0;x<=4-cnt[i+1];x++){
add(f[i&1^1][j+x][G[k][x+cnt[i+1]]],1ll*f[i&1][j][k]*C[4-cnt[i+1]][x]%mod);
}
}
}
}
int ans=0;
for(int i=1;i<=4*n-13;i++){
for(int k=1;k<=tot;k++) add(g[i],f[n&1][i][k]);
add(ans,1ll*g[i]*fact[i]%mod*fact[4*n-13-i]%mod);
}
printf("%d\n",(1ll*ans*inv[4*n-13]%mod+1)%mod);
return 0;
}
V. [NOI2022] 移除石子
首先思考最简单的问题:\(l_i=r_i,k=0\)。
对于操作二区间长度 \(\ge 3\) 的限制,他就等价于只进行长度 \(=3,4,5\) 的操作二,我们把每个操作二挂到第一个位置上。
于是一种最暴力的 DP 就是设 \(f_{i,a,b,c,d}\) 表示考虑完了前 \(i-1\) 个位置,\(i-4,i-3,i-2,i-1\) 上的延伸到 \(i\) 的操作二个数分别是 \(a,b,c,d\),前 \(i-1\) 个位置能否操作完。
转移时枚举 \(i\) 开始的操作二个数 \(x\),如果 \(a_i-a-b-c-d-x<0\) 或者 \(=1\) 那么就不行,否则那 \(a\) 个操作二显然不会再延伸到 \(i+1\),但是我们还需要决策那 \(b+c\) 个操作二是否需要延伸,因此还要再枚举两个 \(b',c'\) 表示延伸下去的操作二个数,转移到 \(f_{i+1,b',c',d,x}\)。
显然两个相同的操作二可以转换成若干个操作一,所以 \(a\le 1,b\le 2,c,d\le 3\);进一步的,如果一个位置同时有一个长度为 \(4\) 和 \(5\) 的操作二,那么可以转化成一个这个位置的操作一和下一个位置的长度为 \(3,4\) 的操作二,所以有 \(a\le 1,b\le 1,c,d\le 2\)。
但是这太暴力了,我们可以把 \(a,b,c\) 合并到一起,设 \(f_{i,j,k}\) 表示考虑完了前 \(i-1\) 个位置,\(i-4,i-3,i-2\) 上的延伸到 \(i\) 的操作二总共有 \(j\) 个,\(i-1\) 上的操作二有 \(k\) 个,前 \(i-1\) 个位置能否操作完,转移同样先枚举从 \(i\) 开始的操作二个数 \(x\),如果 \(a_i-j-k-x<0\) 或者 \(=1\) 就不行,否则就枚举 \(j\) 个操作二中延伸到 \(i+1\) 的个数 \(y\),转移到 \(f_{i+1,k+y,x}\)。最后如果 \(f_{n+1,0,0}=true\) 那就有解。
根据上面的分析,\(j\le 4,k\le 2\)。
然后你大胆猜测 \(j\) 的范围可以更小,实际上 \(j\) 的范围只需要取到 \(\le 2\) 即可,举个例子(\(pos(len)\) 表示在位置 \(pos\) 上进行长度为 \(len\) 的操作二):对于 \(i-4(5),i-3(4),i-2(3),i-2(5)\) 这 \(4\) 个操作,可以转化成 \(i-4(3),i-3(3),i(3)\) 以及三个操作一,此时就把这种情况下原本需要 \(j=4\) 的转化成了 \(j=0\)(注意只有起始位置 \(\le i-2\) 且延伸到 \(i\) 的操作二需要算进 \(j\))。同理对于其他 \(j>2\) 的情况都可以把他们转化成 \(j\le 2\) 的情况。
然后思考 \(k>0\) 的情况,这个恰好很讨厌,考虑能不能变成至少:
(1) \(k=1\):假设 \(k=0\) 的时候可行,但是 \(k=1\) 的时候不可行了:
- 如果进行了一次操作一,那可以直接把这枚石子加到那次操作一上。
- 如果进行了一次长度 \(>3\) 的操作二,那可以把这个操作二长度减一,再加上一个操作一。
- 如果只进行了长度为 \(3\) 的操作二,但是 \(n>3\),那么可以把这个操作二的长度加一。
所以 \(k=0\) 可行,但是 \(k=1\) 不行的情况,只可能是 \(n=3\) 且 \(a_1=a_2=a_3=1\) 或者 \(a_i=0\)。
(2) 对于 \(k\ge 2\),显然如果 \(k'=x\le k-2\) 时可行那么 \(k\) 一定行,否则假设 \(k-1\) 时可行,但是 \(k\) 时不可行:根据上面的分析,当且仅当加完 \(k-1\) 个石子后局面是 1 1 1
或者 0 0 0 ... 0
,显然不可能加了 \(k-1\) 个石子后局面是全零,而如果局面是三个 1
,完全可以撤销一个,然后把另外两个 1
变成 2
,用两次操作一。
因此综上所述,在特判掉 \(k=1\) 的两个情况后,就可以直接把恰好改成至少。
类似的设计 DP 设 \(g_{i,j,k}\) 表示要让 \(f_{i,j,k}=1\) 前 \(i-1\) 个石堆至少要添加多少石子,转移时同样枚举 \(x,y\) 用 \(g_{i,j,k}+F(a_i-x-j-k)\) 转移到 \(g_{i+1,k+y,x}\),其中:
最后看是否有 \(g_{n+1,0,0}\le k\) 即可。 突然意识到变量名重名了,不过应该能看懂
学会了判定之后,我们要开始计数了,由于 \(j,k,x\le 2\) 所以对于 \(\ge 8\) 的 \(a_i\) 是等价的。
而实际上对于 \(\ge 6\) 的 \(a_i\) 其实就是等价的了,除了对拍,我也不知道怎么证明这个东西。所以枚举 \(a_i\) 的值只有 \(6\) 种。
考虑 dp of dp,和上一题类似的,我们把这个过程建成自动机,每个状态存储一个 \(3\times 3\) 的矩阵,然后通过搜索可以发现有用的状态只有 \(8765\) 种。
于是你就过了,复杂度是 \(O(n\times 8765)\)。
点击查看代码
#include<bits/stdc++.h>
#define Debug puts("-------------------------")
#define eb emplace_back
using namespace std;
const int N=1e3+5,M=8765+5,Max=101,mod=1e9+7;
inline int read(){
int w=1,s=0;
char c=getchar();
for(;c<'0'||c>'9';w*=(c=='-')?-1:1,c=getchar());
for(;c>='0'&&c<='9';s=s*10+c-'0',c=getchar());
return w*s;
}
int T,n,k,l[N],r[N],tot,G[M][7];
struct state{
int f[3][3];
void Init(){for(int j=0;j<=2;j++) for(int k=0;k<=2;k++) f[j][k]=Max;}
}beg,node[M];
map<vector<int>,int> mp;
void chkmin(int &x,int y){x=min(x,y);}
int calc(int x){return (x<0)?(-x):((x==1)?1:0);}
state operator + (const state &u,int val){
state v; v.Init();
for(int j=0;j<=2;j++){
for(int k=0;k<=2;k++){
for(int x=0;x<=2;x++){
for(int y=k;y<=k+j&&y<=2;y++){
chkmin(v.f[y][x],u.f[j][k]+calc(val-j-k-x));
}
}
}
}
return v;
}
vector<int> Hash(state u){
vector<int> v;
for(int i=0;i<=2;i++) for(int j=0;j<=2;j++) v.eb(u.f[i][j]);
return v;
}
void build(){
beg.Init(),beg.f[0][0]=0;
node[++tot]=beg,mp[Hash(beg)]=tot;
for(int i=1;i<=tot;i++){
state u=node[i],v;
for(int j=0;j<=6;j++){
v=u+j;
if(!mp.count(Hash(v))) node[++tot]=v,mp[Hash(v)]=tot;
G[i][j]=mp[Hash(v)];
}
}
}
int f[N][M];
void add(int &x,int y){x+=y; if(x>=mod) x-=mod;}
signed main(){
build();
T=read();
while(T--){
int maxn=0,ming=INT_MAX;
n=read(),k=read();
for(int i=1;i<=n;i++) l[i]=read(),r[i]=read(),maxn=max(maxn,l[i]),ming=min(ming,r[i]);
memset(f,0,sizeof f);
f[1][1]=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=tot;j++){
if(!f[i][j]) continue;
for(int x=0;x<=5;x++) add(f[i+1][G[j][x]],f[i][j]*(l[i]<=x&&x<=r[i]));
add(f[i+1][G[j][6]],1ll*f[i][j]*max(0,r[i]-max(l[i],6)+1)%mod);
}
}
int ans=0;
for(int u=1;u<=tot;u++) if(node[u].f[0][0]<=k) add(ans,f[n+1][u]);
if(k==1) add(ans,mod-(maxn==0)-(n==3&&ming>=1&&maxn<=1));
printf("%d\n",ans);
}
return 0;
}