DP套DP
DP套DP
顾名思义,外面是一个 \(DP\), 而这个 \(DP\) 的状态里还有一个 \(DP\) 。
引例(P10614):
给定长度为 \(k\) 的字符串 \(s\) ,对于 \(i\in [0,k]\) ,求有多少种长度为 \(n\) 字符串 \(t\) ,使得 \(s,t\) 的最长公共子序列(\(LCS\))的长度为 \(i\) 。
\(1\le k \le 15\) ,\(1\le n \le 1000\)
发现要满足的条件本生就需 \(DP\) 来算。
那么首先最简单的暴力就是把所有字符串枚举出来,然后 \(DP\) 来算。时间复杂度 \(O(z^n \times n \times k)\) ,\(z\) 是字符集大小。
接着考虑怎么用计数 \(DP\) 解决,大概思路就是设 \(dp[i][]\) 表示 \(t\) 的前 \(i\) 位____的贡献,关键就是考虑第二维怎么设计。
尝试将第二维表示为最长公共子序列的长度,但这样会发现根本没法转移。
原因是每加入一个数,最长公共子序列的长度都是需要重新 \(DP\) 计算的。
发现 \(k\) 非常小,那考虑直接把第二维的状态设为一整个 \(DP\) (实际代码中一般以编码的形式存),这样每加入一个数时,都可以继续推导内层的 \(DP\) 进行转移 。
然后先考虑怎么 \(dp\) 算两个字符串的最长公共子序列(即 \(LCS\) ),这个显然可以设 \(h[i][j]\) 表示 \(t\) 前 \(i\) 个字符,\(s\) 前 \(j\) 个字符的最长公共子序列,然后转移如下:
那么转移时怎么转移呢?
比如我们要从 \(dp[i][g]\) 去转移( \(g\) 是 \(dp\) 数组, 这里等同于 \(h[i][j]\))。
那么可以转移到 \(dp[i][f]\) ,其中 \(f[j]=max(g[j-1]+[t[i]=s[j]],f[j-1],g[j])\)
实际代码中由于 \(g[j-1]\) 与 \(g[j]\) 的差不会超过 \(1\) ,所以 \(g\) 的差分数组就只由 \(0,1\) 组成,所以可以直接用 \(2\) 进制来表示这个数组。
可以先预处理出 \(trans[state][c]\) 表示当前状态 \(state\),加入字符 \(c\),能转移到的状态。
查询时遍历每个状态 \(state\) ,统计该状态下的 \(LCS\) 长度 \(l\) ,然后将 \(dp[n][state]\) 的值累加到 \(ans[l]\) 中。
时间复杂度:\(O(n\times 2^k \times z \times k)\) ,当 \(k\le15\) 时可行。
代码:
int dp[2][(1<<15)+10];
int g[20],f[20];
char match[4]={'A','C','G','T'};
int get_trans(int state,char c){
//还原dp
g[0]=0;
for (int i=1;i<=k;i++) g[i]=g[i-1]+((state>>(k-i))&1);
//转移
f[0]=0;
for (int i=1;i<=k;i++) f[i]=max({g[i],f[i-1],g[i-1]+(c==s[i])});
//压缩
int new_state=0;
for (int i=1;i<=k;i++){
int cha=f[i]-f[i-1];
new_state+=cha*(1<<(k-i));
}
return new_state;
}
int trans[(1<<15)+10][4];
ll ans[50];
void check(ll state,ll v){
ll sum=0;
for (int i=1;i<=k;i++){
(sum+=((state>>(k-i))&1))%=mod;
}
(ans[sum]+=v)%=mod;
}
int main(){
cin>> T;
while(T--){//多测
cin >> s;
k=s.length();
s=' '+s;
cin >> n;
for (int state=0;state<=(1<<k)-1;state++){
for (int i=0;i<4;i++){
trans[state][i]=get_trans(state,match[i]);
}
}
int res=0;
memset(dp,0,sizeof dp);
dp[res][0]=1;//滚动优化
for (int i=0;i<n;i++){
memset(dp[res^1],0,sizeof dp[res^1]);
for (int state=0;state<=(1<<k)-1;state++){
for (int j=0;j<4;j++){
dp[res^1][trans[state][j]]+=dp[res][state];//外层转移
if (dp[res^1][trans[state][j]]>=mod){
dp[res^1][trans[state][j]]-=mod;
}//取模,只是为了更快,没别的目的
}
}
res^=1;
}
memset(ans,0,sizeof ans);
for (int state=0;state<=(1<<k)-1;state++){
check(state,dp[res][state]);
}
for (int i=0;i<=k;i++){
cout << ans[i] << endl;
}
}
return 0;
}
还有一道几乎完全一样的题:
P4590
这道题只是多了一个不能出现 \(NOI\) 的限制。
实际上就是外层 \(dp\) 转移时多一个维度和一些判断。
如:
for (int i=0;i<n;i++){
memset(dp[res^1],0,sizeof dp[res^1]);
for (int state=0;state<=(1<<k)-1;state++){
for (int p=0;p<3;p++){//表示当前与NOI匹配到了多少位
if (!dp[res][state][p]) continue;//剪枝优化
for (int j=0;j<3;j++){
int np;//表示新的匹配上的位数
if (j==p){
if (p==2) continue;
np=p+1;
}
else np=(j==0)?1:0;
dp[res^1][trans[state][j]][np]+=dp[res][state][p];//正常转移
if (dp[res^1][trans[state][j]][np]>=mod){
dp[res^1][trans[state][j]][np]-=mod;//依旧取模变减法,更快
}
}
}
}
res^=1;
}
例二:P15301
求满足最长上升子序列为 \(l\) 的 \(1-n\) 排列的个数(这道题有一些变体,即要求最长上升子序列之一是给定的序列)。\(n\le 15\)
首先有一个关键是如何去刻画求最长上升子序列的 \(dp\) 转移。
- 方法一:
暂时不考虑变体:
设 \(d[i]\) 表示长度为 \(i\) 的最长上升子序列的末尾的数的最小值。易发现 \(d\) 有序。
则新放进来一个数 \(x\) 时,若 \(d[j]\) 是第一个大于 \(x\) 的数,那么令 \(d[j]\) 等于 \(x\) 。
这样就更新了 \(d\) 。原理也比较简单:\(x\) 可以接在 \(d[j-1]\) 后面形成长度为 \(j\) 的上升子序列 。
由于 \(d\) 有序,用一个二进制数 \(state\) 来存每个数是否在 \(d\) 中出现过,这样 \(state\) 和 \(d\) 可以互相唯一确定 。
但是存下这个还不够,因为排列限定每个数只能用一次,所以还需要用一个状态 \(used\) 来记录每个数是否出现过。
由于 \(state \in used\) 所以说总状态数不超过 \(3^n\) 。
可以用三进制编码来存这两个状态,具体来说,如果第 \(x\) 位为:
\(1\): 表示 \(x\) 不属于 \(state\) ,也不属于 \(used\)
\(2\): 表示 \(x\) 属于 \(state\) ,但不属于 \(used\)
\(3\): 表示 \(x\) 属于 \(state\) 和 \(used\)
外层枚举 \(i\) 表示前 \(i\) 个数,然后枚举三进制状态 \(state\) ,内层再枚举当前这个数填 \(j\) 。
但这样时间复杂度是 \(O(n^23^n)\) ,当 \(n\) 为 \(15\) 时过不了。
但是发现每一层的状态只由上一层的状态转移过来。
所以类似 \(bfs\) 的,每次只考虑这一层中在队列的那些状态去转移,总共状态就是 \(3^n\) 的,总时间复杂度就是 \(O(n\,3^n)\)
这是可以过的。
现在考虑变体:
其实就是强制选的时候要给定序列里的元素要有序,那就直接结合 \(used\) 判断一下这个元素能不能加就行了。时间复杂度不变。
- 方法二:
方法一的时间复杂度并不优秀。
考虑将数值从小到大依次插入到序列中(这是处理排列计数的一种常用方法),
定义 \(f[i][j]\) 表示插入前 \(i\) 个数后,以前 \(j\) 个位置的元素结尾的最长上升子序列长度。
将 \(i+1\) 插入到位置 \(x\) 后,新序列的长度为 \(i+1\) ,我们需要计算新的 \(f[i+1][j]\) ,
那么:
若 \(j<x\): \(f[i+1][j]=f[i][j]\)
若 \(j=x\): \(f[i+1][j]=f[i][j-1]+1\) ,因为新加进来的数比之前都大,所以直接放在后面。
若 \(j>x\) : \(f[i+1][j]=max(f[i][j-1],f[i+1][x])\) 要么取之前的值(原来是第 \(j-1\) 个位置),要么取新产生的值。
当然同上题一样,对于每个 \(i\) ,\(f[i][j]\) 相邻两项的差都是 \(0,1\)
所以依旧差分,转化为二进制。
转移仍然和之前一样。
由于还要满足变体的限制,所以外层 \(dp\) 额外开一维表示当前序列里面已经有多少个给定序列中的数。
继续转移就行了,大多都一样。
如果还是和方法一样优化一下转移,那么时间复杂度就是:
\(O(n^22^n)\)
优秀多了。

浙公网安备 33010602011771号