dp套dp學習筆記
簡介
我都還不會怎麼簡介啊。
注,該篇是對著 dp套dp學習筆記 - dead_X - 部落格園 (cnblogs.com) 抄的,如有雷同,並非巧合。
dp 和 dp套dp 的關係
- dp套dp 實際上就是將內層 dp 的結果作為外層 dp 的狀態。
感覺根據這個定義根本搞不懂呢。
舉個例子
題目大意
給定一個由 \(\text{AGCT}\) 組成的字串 \(S\) ,考慮有多少長度為 \(n\) 的由 \(\text{AGCT}\) 組成的字串滿足他們的最長公共子序列長度等於 \(i\in[0,|S|]\) ,\(|S|\le 15,n\le 1000\)
分析
我們首先回憶一波 \(\text{LCS}\) 怎麼求,對於一般的最長公共子序列,我們只有 \(O(n^2)\) 的解法,具體如下:
\[f_{i,j}=\left\{\begin{matrix} f_{i-1,j-1}+1 & A_i=B_j \\ \max(f_{i-1,j},f_{i,j-1}) & A_i\ne B_j \end{matrix}\right. \]這很顯然。我們考慮就利用這個 dp 過程來作為我們外層 dp 的狀態。
我們不妨先嚐試用暴力的做法來實現這個東西,就假設我們在做暴力,那麼我們考慮的是暴力列舉當前的第二個串,然後用 dp 去 check 其的 \(\text{LCS}\)
我們就考慮設計方法二的 dp 狀態,具體的用 \(f_{T,i}\) 表示第二個串是 \(T\) ,長度為 \(|T|\) ,第一個串匹配到 \(i\) 的匹配值最大是多少。轉移的話每一次列舉新增的字元即可。
考慮上面這個做法如何優化?實際上我們發現 \(|T|\) 的狀態是一定向 \(|T|+1\) 的位置轉移的。換個想法,我們可以另設一個狀態 \(g_T\) 表示第二個串為 \(T\) 的時候,\(i\in[0,|S|]\) 的取值,這個 \(g_T\) 也是可以直接 dp 轉移的。
由於題目讓我們求的是對應長度的方案數,我們不妨嘗試將 \(\text{dp}\) 陣列的狀態與結果的位置交換(這個 \(\text{trick}\)
然後發現數組 \(a_i\) 中有很多狀態是無用狀態,具體觀察性質可以發現,其相鄰兩位的差只能為 \(0,1\) ,所以我們可以通過差分 \(a_i\) 來進一步減少狀態,最終複雜度應該就是 \(O(n2^{|S|})\) 的。
#include<bits/stdc++.h>
using namespace std;
const int N=17;
const int MOD=1e9+7;
int ADD(int x,int y){return x+y>=MOD?x+y-MOD:x+y;}
int TIME(int x,int y){return (int)(1ll*x*y%MOD);}
int n,m;
char s[N],t[4]={'A','G','C','T'};
int g[4][1<<N],f[2][1<<N],res[N];
int solve(){
scanf("%s%d",s,&m),n=strlen(s);
for(int i=0;i<(1<<n);++i){
int a[N],b[N];a[0]=(i&1);
for(int j=1;j<n;++j) a[j]=a[j-1]+((i>>j)&1);
for(int c=0;c<4;++c){
b[0]=max(a[0],(int)(s[0]==t[c]));
for(int j=1;j<n;++j)
b[j]=max(max(b[j-1],a[j]),a[j-1]+(s[j]==t[c]));
int I=b[0];
for(int j=1;j<n;++j) I|=((b[j]-b[j-1])<<j);
g[c][i]=I;
}
}
f[0][0]=1;
for(int j=1;j<(1<<n);++j) f[0][j]=0;
for(int i=1;i<=m;++i){
for(int j=0;j<(1<<n);++j) f[i&1][j]=0;
for(int j=0;j<(1<<n);++j){
for(int c=0;c<4;++c)
f[i&1][g[c][j]]=ADD(f[i&1][g[c][j]],f[(i-1)&1][j]);
}
}
for(int i=0;i<=n;++i) res[i]=0;
for(int i=0;i<(1<<n);++i){
int cnt=0,I=i;while(I) cnt+=(I&1),I>>=1;
res[cnt]=ADD(res[cnt],f[m&1][i]);
}
for(int i=0;i<=n;++i) printf("%d\n",res[i]);
return 0;
}
int main(){
int T;cin>>T;while(T--) solve();
return 0;
}
總結
我們再總體回顧一下上面的過程,除去最後的狀壓優化,前面的 dp 推導過程實際上是和很能體現 dp套dp 的性質的,具體的我們發現實際上我們就是通過先一步的 dp ,算出我們再新增一個字元的情況下,能夠轉移到的 dp 位置,這為我們後面的交換狀態與結果的 dp 提供了轉移的便捷性,然後我們再進行後者的 dp 。
求一個 dp 結果的方案數,我們可以利用 dp套dp 。
例題2
題目大意
給定一個由 \(\text{NOI}\) 組成的字串 \(S\) ,考慮有多少長度為 \(n\) 的由 \(\text{NOI}\) 組成的字串滿足他們的最長公共子序列長度等於 \(i\in[0,|S|]\) ,且不存在子串 \(\text{NOI}\) ,\(|S|\le 15,n\le 1000\) 。
分析
基本做法可以確定和上面是類似的,只不過需要多加兩維記錄一下不要連續以 \(\text{NOI}\) 的形式走即可。
程式碼略。
例題3
咕咕咕。