1. 程式人生 > 其它 >dp套dp學習筆記

dp套dp學習筆記

簡介

我都還不會怎麼簡介啊。

注,該篇是對著 dp套dp學習筆記 - dead_X - 部落格園 (cnblogs.com) 的,如有雷同,並非巧合。

dp 和 dp套dp 的關係

  • dp套dp 實際上就是將內層 dp 的結果作為外層 dp 的狀態。

感覺根據這個定義根本搞不懂呢。

舉個例子

BZOJ3864 Hero meet devil

題目大意

給定一個由 \(\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}\)

似乎以前見過),即 \(g_{\{a_i\}}\) 表示 \(i\in[0,|S|]\) 時值為 \(a_i\) 的情況下的方案數,我們考慮這個東西也是可以轉移的,且狀態數是 \(O(|S|^{|S|})\) 的。我們如果考慮暴力在這個 DAG 上跑長度為 \(n\) 的路徑方案數,複雜度是 \(O(n|S|^{|S|})\) 的,暫且不能通過。

然後發現數組 \(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

P4590 [TJOI2018]遊園會

題目大意

給定一個由 \(\text{NOI}\) 組成的字串 \(S\) ,考慮有多少長度為 \(n\) 的由 \(\text{NOI}\) 組成的字串滿足他們的最長公共子序列長度等於 \(i\in[0,|S|]\) ,且不存在子串 \(\text{NOI}\)\(|S|\le 15,n\le 1000\)

分析

基本做法可以確定和上面是類似的,只不過需要多加兩維記錄一下不要連續以 \(\text{NOI}\) 的形式走即可。

程式碼略。

例題3

P5279 [ZJOI2019]麻將

咕咕咕。