1. 程式人生 > 其它 >SOS DP 學習筆記

SOS DP 學習筆記

SOS DP 學習筆記

0.0 前言

本文大部分譯自 CF 部落格上的原文。Link here

0.1 前置知識

  • 狀壓 DP

1.0 簡介

SOS DP,全稱 Sum over Subsets dynamic programming,意為子集和 DP,用來解決一些涉及子集和計算的問題。

1.1 例題引入

給定一個含 \(2^N\) 個整數的集合 A,我們需要計算:對於每個集合 \(x\in A\),求 \(x\) 中所有元素 \(i\)\(A[i]\) 的和,即求:

\(\begin{gather*}{F[sta]=\sum\limits_{i\subseteq sta} A[i]}\end{gather*}\)

1.2 解決方案

1.2.1 樸素演算法

直接按照題意模擬即可,複雜度為 \(O(4^N)\)

for(int sta=0;sta<(1<<N);sta++)
  	for(int i=0;i<(1<<N);i++)
      	if((sta&i)==i)F[sta]+=A[i];

1.2.2 列舉子集

對於每個狀態,我們只遍歷它的子集而去除了無關狀態。如果一個狀態的二進位制位上只有 \(k\)\(1\),我們只需列舉它的 \(2^k\) 個子集。這樣的狀態一共有 \(\dbinom{N}{k}\) 個,因此總迭代次數 \(=\sum\limits_{k=0}^N{\dbinom{N}{k}2^k}=(1+2)^N=3^N\)

,時間複雜度即為 \(O(3^N)\)

for(int sta=0;sta<(1<<N);sta++)
{
    F[sta]=A[0];
	for(int i=sta;i>0;i=(i-1)&sta)
		F[sta]+=A[i];
}

1.2.3 SOS DP

上面列舉子集的方法有明顯的缺陷:當一個狀態的二進位制位上有 \(k\)\(0\) 時,它將在其他(不包含本身)狀態迭代時被訪問 \(2^k-1\) 次,存在重複的計算。

而產生這種現象的原因就是:我們沒有在 \(A[x]\) 被不同 \(F[sta]\) 利用時建立一定的聯絡。我們應新增另一個狀態來避免上述的重複計算。

定義狀態 \(S(sta)=\{x|x\subseteq sta\}\)。現在我們把這個集合劃分為不相交的組。

\(S(sta,i)=\{x|x\subseteq sta \&\& sta\oplus x<2^{i+1}\}\)。我們將二進位制位數從 \(0\) 開始從低位向高位表示,那集合 \(S(sta,i)\) 就表示只有第 \(i\) 位以及更低位與 \(sta\) 不同的 \(x\) 的集合。

舉個例子:\(S(\)1011010\(,3)=\{\)1011010\(,\)1010010\(,\)1011000\(,\)1010000\(\}\) 。其中 \(sta\) 中的 1010 即為 \(sta\) 的第 \(3\) 至第 \(0\) 位,集合中的元素的加粗部分都與 \(sta\) 保持一致。

讓我們嘗試將 \(sta\)\(x\) 建立聯絡。

  1. \(sta\) 的第 \(i\) 位是 \(0\)

    顯而易見地,\(sta\)\(x\) 的第 \(i\) 位均為 \(0\)。因此 \(x\) 僅有 \(0\)\(i-1\) 位與 \(sta\) 不同,故有 \(S(sta,i)=S(sta,i-1)\)

  2. \(sta\) 的第 \(i\) 位是 \(1\)

    那麼 \(S(sta,i)\) 就有兩部分組成:

    第一部分:\(x\) 的第 \(i\) 位為 \(0\),即為 \(S(sta\oplus 2^i,i-1)\)

    第二部分:\(x\) 的第 \(i\) 位為 \(1\),即為 \(S(sta,i-1)\)

下圖描述瞭如何將 \(S(sta,i)\) 集合相互關聯。任何集合 \(S(sta,i)\) 的元素都是其子樹中的葉子。\(\color{red}{紅色}\)字首表示的這一部分對其所有子結點都是公共的,而\(\color{black}{黑色}\)部分允許不同。

請注意,這些關係形成一個有向無環圖,而不一定是有根樹(請考慮當 \(sta\) 不同但 \(i\) 相同時)

在實現了這些關係之後,我們可以很容易地寫出相應的動態規劃。

for(int sta=0;sta<(1<<N);sta++)
{
    dp[sta][-1]=A[sta];// 葉結點
	for(int i=0;i<N;i++)
	{
		if(sta&(1<<i))
			dp[sta][i]=dp[sta][i-1]+dp[sta^(1<<i)][i-1];
		else dp[sta][i]=dp[sta][i-1];
	}
	F[sta]=dp[sta][N-1];
}

上述的演算法浪費了較多空間,注意到每次 \(i\) 這一維都由 \(i-1\) 轉移而來,因此可以採取滾動陣列的方式優化空間。

for(int i=0;i<(1<<N);i++)
	F[i]=A[i];
for(int i=0;i<N;i++)
	for(int sta=(1<<N)-1;sta>=0;sta--)
	    if(sta&(1<<i))
			F[sta]+=F[sta^(1<<i)];

值得注意的是,在此處 \(sta\) 這一維可以採用正序列舉的方式。

原因:……

上述演算法的時間複雜度即為 \(O(N·2^N)\)

2.0 推薦習題