dp套dp學習筆記


1 dp 和 dp套dp

  • dp 套 dp 中的 dp 一定是 dp 套 dp 的基礎,而 dp 套 dp 也就是從 dp 的基礎上 dp 而來的。
  • 沒錯,上面這句話就是套娃。

為了方便大家理解,從這句話開始,dp套dp 將作為一個不加空格的詞,方便區分。

dp 的時候,我們一般會設計一個 dp 狀態,然后讓 dp 從某個狀態向某個狀態轉移,最終統計某些狀態最終的值。

而在 dp套dp 里面,我們就將內層 dp 的結果作為外層 dp 的狀態進行 dp。

2 正片

舉個例子:

  • \(1\)

  • 求長度為 \(n\),字符集為 \(\texttt{N},\texttt{O},\texttt{I}\),與給定字符串 \(S\)\(\text{LCS}\)\(len\) 的字符串數量。
  • \(|S|\leq15\)\(n\leq10^3\)

2.1 \(\text{LCS}\) 回顧

顯然對於字符串 \(A\)\(B\),我們記 \(\text{LCS}_{x,y}\)\(A\)\(x\) 位,\(B\)\(y\) 位的最長公共子序列長度,則

\[\text{LCS}_{x,y}=\left\{ \begin{aligned} \text{LCS}_{x-1,y-1}+1&&A_x=B_y\\ \max\{\text{LCS}_{x-1,y},\text{LCS}_{x,y-1}\}&&A_x\neq B_y\\ \end{aligned} \right.\]

2.2 轉化

第一部分最后一句中,我們提到外層 dp 的狀態就是內層 dp 的結果,於是一個最暴力的想法就是記錄所有 \(\text{LCS}\) 作為狀態。

  • 小補充:dp套dp 中內層的轉移應該是一個自動機的狀態,但是筆者對自動機理解不深,所以本文仍然用“狀態”和“轉移”描述自動機的邊。

此時,我們要記錄的是長度為 \(i\),和 \(S\)\(\text{LCS}\) 為某個數組的字符串數量。

然而我們發現,如果我們在某個字符串后面加一個新的字符,只會新生成一行 \(\text{LCS}\)而這一行 \(\text{LCS}\) 完全通過上一行生成!

(配圖,咕了)

於是我們只要記錄 \(\text{LCS}\) 最后一行為某個數組的字符串數量了。

然后我們還發現 \(\text{LCS}_{x+1,y}-\text{LCS}_{x,y}\) 只能是 \(0\) 或者 \(1\),於是我們還能差分最后一行得到一個 \(01\) 字符串並再次狀壓。

於是我們就能寫出很優美的 dp套dp 了。

算法復雜度 \(\text O(\large2^{|S|}m|\Sigma|)\)

  • 示例代碼(可以通過該題)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read()
{
    int x=0;char ch=getchar();
    while(ch<'0' || ch>'9') ch=getchar();
    while('0'<=ch && ch<='9'){x=x*10+(ch^48);ch=getchar();}
    return x;
}
const int p=1000000007;
int nxt[100003][4],f[2][100003],ans[1003];
char s[23];
const char ch[4]={'A','T','C','G'};
void solve()
{
	scanf("%s",s+1);
	int n=strlen(s+1),m=read(),st=1<<n;
	for(int i=0,tmp[23],res[23]; i<st; ++i) 
	{
		res[0]=tmp[0]=0;
		for(int j=1,k=i; j<=n; ++j,k>>=1) tmp[j]=tmp[j-1]+(k&1);
		for(int k=0,num; k<4; ++k)
		{
			num=0;
			for(int j=1; j<=n; ++j) res[j]=(s[j]==ch[k])?tmp[j-1]+1:max(tmp[j],res[j-1]),num+=(1<<(j-1))*(res[j]-res[j-1]);
			nxt[i][k]=num;
		}
	}
	memset(f,0,sizeof(f));
	f[0][0]=1;
	for(int i=0; i<m; ++i)
	{
		for(int j=0; j<st; ++j) f[(i&1)^1][j]=0;
		for(int j=0; j<st; ++j) for(int k=0; k<4; ++k) (f[(i&1)^1][nxt[j][k]]+=f[i&1][j])%=p;
	}
	for(int i=0; i<=n; ++i) ans[i]=0;
	for(int i=0; i<st; ++i) (ans[__builtin_popcount(i)]+=f[m&1][i])%=p;
	for(int i=0; i<=n; ++i) printf("%lld\n",ans[i]);
}
signed main()
{
	for(int T=read(); T--;) solve();
	return 0;
}

2.4 小試牛刀

  • \(2\)

  • 求長度為 \(n\),字符集為 \(\texttt{N},\texttt{O},\texttt{I}\)不包含子串 \(\texttt{NOI}\) 的字符串中,與給定字符串 \(S\)\(\text{LCS}\)\(len\) 的字符串數量。
  • \(|S|\leq15\)\(n\leq10^3\)

同步記錄是否有后綴是 \(\tt{N},\tt{NO}\) 即可。

  • 示例代碼(可以通過該題)。
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read()
{
    int x=0;char ch=getchar();
    while(ch<'0' || ch>'9') ch=getchar();
    while('0'<=ch && ch<='9'){x=x*10+(ch^48);ch=getchar();}
    return x;
}
const int p=1000000007;
int nxt[100003][3],f[2][100003][3],ans[1003];
char s[23];
const char ch[3]={'N','O','I'};
signed main()
{
	int m=read(),n=read(),st=1<<n;
	scanf("%s",s+1);
	for(int i=0,tmp[23],res[23]; i<st; ++i) 
	{
		res[0]=tmp[0]=0;
		for(int j=1,k=i; j<=n; ++j,k>>=1) tmp[j]=tmp[j-1]+(k&1);
		for(int k=0,num; k<3; ++k)
		{
			num=0;
			for(int j=1; j<=n; ++j) res[j]=(s[j]==ch[k])?tmp[j-1]+1:max(tmp[j],res[j-1]),num+=(1<<(j-1))*(res[j]-res[j-1]);
			nxt[i][k]=num;
		}
	}
	memset(f,0,sizeof(f));
	f[0][0][0]=1;
	for(int i=0; i<m; ++i)
	{
		for(int j=0; j<st; ++j) f[(i&1)^1][j][0]=f[(i&1)^1][j][1]=f[(i&1)^1][j][2]=0;
		for(int j=0; j<st; ++j) 
		{
			(f[(i&1)^1][nxt[j][0]][1]+=f[i&1][j][0]+f[i&1][j][1]+f[i&1][j][2])%=p;
			(f[(i&1)^1][nxt[j][1]][0]+=f[i&1][j][0]+f[i&1][j][2])%=p;
			(f[(i&1)^1][nxt[j][1]][2]+=f[i&1][j][1])%=p;
			(f[(i&1)^1][nxt[j][2]][0]+=f[i&1][j][0]+f[i&1][j][1])%=p;
		}
	}
	for(int i=0; i<=n; ++i) ans[i]=0;
	for(int i=0; i<st; ++i) (ans[__builtin_popcount(i)]+=f[m&1][i][0]+f[m&1][i][1]+f[m&1][i][2])%=p;
	for(int i=0; i<=n; ++i) printf("%lld\n",ans[i]);
	return 0;
}

3 你已經學會基本操作了,接下來請A掉這道題

首先,請和我一起虔誠地膜拜 zhouAKngyang,因為他一年前在考場上就爆切了這道題。

此題中,內層 dp 也比較難想,我們先考慮內層 dp,即給你一些牌,判斷有沒有和。

  • zhouAKngyang:你先想一會,不要馬上看題解。

可惜以 delta_X 的實力想 114514 年還是不會……

首先,不難發現我們只需要知道編號為 \(x\) 的牌的數量,不妨把一些牌的集合記為每種牌的出現次數,例如 \(\{1,1,1,2,3,4,5,6,7,8,9,9,9\}\) 記為 \(\{3,1,1,1,1,1,1,1,3\}\),第 \(i\) 張牌的出現次數為 \(cnt_i\)

我們記 \(dp_{i,0,x,y}\) 為考慮編號 \(\leq i\) 的牌,去掉 \(x\) 個編號為 \(i-1\)\(x+y\) 個編號為 \(i\) 的牌后,最多可以組成的面子數量,記 \(dp_{i,1,x,y}\) 為在這些牌中選一個雀頭后最多可以組成的面子數量。

我們在從 \(dp_{i}\) 轉移到 \(dp_{i+1}\) 的時候,不難發現 \(i+1\) 這種牌的貢獻只可能是這幾種:做順子的第一張(對應 \(y\)),做順子的第二張(對應 \(x\)),做順子的第三張(對應 \(dp_i\)\(x\) 轉移到 \(dp\) 值)和做刻子(直接轉移到 \(dp\) 值)。

如果 \(dp_{i,1}\) 的某個值已經為 \(4\),或者 \(dp_i\) 對應前 \(i\) 位已經有 \(7\) 個對子,我們就認定這個狀態可以是終止狀態了,不再繼續轉移。

由於三個相同的順子等價於三個刻子,我們發現 \(x,y<3\),因此 \(dp_i\) 只有 \(18\) 種可能。而由於某些神奇的原因,這 \(18\) 種可能只能組成大約 \(2000\)\(dp_i\)!於是現在只有 \(2000\) 個本質不同的 \(dp_i\) 了,我們就可以用 \(f_{st,j,k}\) 表示狀態為 \(st\),已經選了前 \(j\) 種共 \(k\) 張牌的情況數了。

時間復雜度 \(O(n^2|S|)\)\(S\) 代表 \(dp_i\) 的集合。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM