我们先说一下 \(dp\) 套 \(dp\) 大概是个什么东西。
感性理解一些,你现在有一个动态规划数组 \(g\),然后你的 \(f\) 用 \(g\) 的某种方式作为下标进行转移。
事实上,这个 \(g\) 需要满足单调性,然后相当于你是在一个 \(DAG\) 上做 \(dp\)。为什么要满足单调性,否则有可能出现环,有环代表你的状态有后效性,有后效性则无法转移。
游园会
首先最长公共子序列的动态规划是好做的,这里就不说了。
这里设最长公共子序列的动态规划数组为 \(g\)。
我们先设 \(f_{i,{w_1,w_2,\cdots,w_k}}\) 为 长度为 \(i\) 且 \(g_{i,j}=w_j\) 的字符串个数。
但是你发现 \(w\) 你存不下来。于是我们考虑有没有什么性质能够版主我们优化。
显然有 \(g_{i,j}-g_{i,j-1}\le 1\)。于是你考虑 \(f\) 的后面维度不存储真正意义上的 \(w\),而存储 \(w\) 的差分数组,于是每一位都是 \(0\) 或者 \(1\)。状态数共 \(n\times 2^k\),可以接受。
但是你又发现,我们知道 \(s_i\)(题目给出),也就是说当前的 \(g_i\) 只跟 \(t_i\) 和 \(g_{i-1}\) 有关。于是我们在做 \(g\) 的转移时只记 \(g_{i-1}\) 就行(就是滚动数组优化)。
然后是我们的 \(f\) 也可以用滚动数组优化,否则空间会爆。
代码:
#include<bits/stdc++.h>
#define int long long
#define mod 1000000007
#define N 40005
using namespace std;
int n,k,f[2][N][3],g[2][20],cnt[N],res[20];
char s[20];
void solve1(int state){
memset(g[0],0,sizeof g[0]);
for(int i=0;i<k;i++){
g[0][i+1]=state>>i&1;
}
for(int i=1;i<=k;i++){
g[0][i]+=g[0][i-1];
}
}
int solve2(){
int state=0;
for(int i=1;i<=k;i++){
int x=g[1][i]-g[1][i-1];
state|=x*(1<<i-1);
}
return state;
}
void solve(int cur,int state,char c,int now,int las){
if(las==0)return;//没有贡献不用转移
if(c=='I'&&now==2)return;//包含了子串NOI
int ne=c=='N'?1:0;//如果是N,下一个就从I匹配,否则从N
if(now==0&&c=='N')ne=1;//如果当前应匹配第0个且是N,后跳一位
else if(now==1&&c=='O')ne=2;//同上
solve1(state);//把差分记录的状态还原
memset(g[1],0,sizeof g[1]);//先清空
for(int i=1;i<=k;i++){
if(s[i]==c){//如果s_i=t_j,可以使用特殊转移
g[1][i]=max(g[1][i],g[0][i-1]+1);//这里的g_{0,i-1}就是lcs_{i-1,j-1}
}
g[1][i]=max({g[1][i],g[0][i],g[1][i-1]});//否则使用一般转移(g_{0,i}就是lcs_{i,j-1},g_{1,i-1}就是lcs_{i-1,j}
}
int res=solve2();//再把当前数组弄成差分状态存进去
(f[cur][res][ne]+=las)%=mod;//继承上一个
}
signed main(){
cin>>n>>k>>s+1;
int cur=0;
f[0][0][0]=1;
for(int i=1;i<=n;i++){
cur^=1;
memset(f[cur],0,sizeof f[cur]);
for(int j=0;j<1<<k;j++){
for(int p=0;p<3;p++){
char c=p==0?'N':p==1?'O':'I';
solve(cur,j,c,0,f[cur^1][j][0]);
solve(cur,j,c,1,f[cur^1][j][1]);
solve(cur,j,c,2,f[cur^1][j][2]);
}
}
}
for(int i=1;i<1<<k;i++){
cnt[i]=cnt[i>>1]+(i&1);//cnt是这个数二进制表示有几个1,这里有几个1代表lcs长度就是几
}
for(int i=0;i<1<<k;i++){
for(int j=0;j<3;j++){
(res[cnt[i]]+=f[cur][i][j])%=mod;
}
}
for(int i=0;i<=k;i++){
cout<<res[i]<<'\n';
}
return 0;
}
小 N 的独立集
独立集是指,一堆点,他们两两之间没有边。最大权独立集是指,所有的独立集中,权值之和最大的那个。
这道题我们从部分分说起。
- \(n\le 8\)
枚举所有可能的权值分配。然后参照这道题写个动态规划就行了。
- \(n\le 100\)
首先 \(f\) 数组与这道题的设立相同。
我们考虑将 \(f\) 数组的值设为状态。
设 \(g_{u,v_0,v_1}\) 为 \(f_{u,0}=v_0,f_{u,1}=v_1\) 的方案数。
我们枚举 \(i,j,p,q\) 分别为 \(f_{u,0},f_{u,1},f_{v,0},f_{v,1}\)。
于是有 \(g'{u,i+\max(p,q),j+p}\rightarrow g'{u,i+\max(p,q),j+p}+g_{u,i,j}+g_{v,p,q}\)。
- \(n\le 1000\)
上述转移无法通过的一个重要原因是,我们的状态复杂度过高,所以我们要优化状态。
我们重新定义 \(f_{u,1/0}\) 为 \(u\) 子树内是否强制不选 节点 \(u\) 时的最大权值。于是有 \(0\le f_{u,0}-f_{u,1}\le k\)。因为两者之差不超过 \(u\) 点的权值,而权值不超过 \(k\),这样就优化掉了一个 \(n\)。
于是 \(g_{u,v_1,d}\) 为 \(u\) 子树内 \(f_{u,0/1}\) 分别为 \(v_1+d,v_1\) 的方案数,然后还是枚举 \(i,j,p,q\),有:\(g'{u,i+p+q,\max(i+j+p,i+p+q)-(i+p+q))}\rightarrow g'{u,i+p+q,\max(i+j+p,i+p+q)-(i+p+q))}+g_{u,i,j}+g_{v,p,q}\)。