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\)
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\) 建立聯絡。
-
\(sta\) 的第 \(i\) 位是 \(0\)。
顯而易見地,\(sta\) 與 \(x\) 的第 \(i\) 位均為 \(0\)。因此 \(x\) 僅有 \(0\) 至 \(i-1\) 位與 \(sta\) 不同,故有 \(S(sta,i)=S(sta,i-1)\)。
-
\(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)\)。