[dp 小計] SOSdp

g1ove發表於2024-04-24

復健 SOSdp(sum over subsets dynamic programming)。

引入

\(F(x)=\sum\limits_{u\subseteq x} A(u)\) 其中 \(A\) 為給定陣列,求出 \(\forall x, F(x)\)

思路一

暴力列舉子集,時間複雜度 \(O(4^n)\)

思路二

最佳化子集列舉,時間複雜度 \(O(3^n)\)

思路三

考慮 SOSdp 。
考慮到每次求 \(F(x)\) 時都是重複暴力列舉,這一步會浪費很多時間,同時,我們會聯想到 dp 的思想,利用上一次的結果加速轉移。

如果按照普通狀壓 dp 的思想,如果我們從一個狀態 \(mask\) 的所有子集轉移而來,很明顯會產生重複貢獻,如果從單一狀態轉移過來,列舉其它狀態時間複雜度只是一個 \(\frac{1}{2}\) 的常數,我們需要一種巧妙的 dp ,使每個貢獻都是有效的,並且快速就能轉移。

因此,我們設計一個新的狀態:\(g[mask][i]\) ,表示在考慮前 \(i\) 位已經確定的情況下,\(mask\) 的子集和。注意這的前 \(i\) 位是二進位制的前 \(i\) 位,是從小到大的。

我們來看這個 codeforces blog 上的圖。
其中,紅色部分是摁死部分,這一部分不能變動,也沒有計運算元集貢獻,黑色部分的子集和已經計算完畢,也就是說,我們任意取黑色部分的子集和紅色部分的全集的並集已經計算完畢了。
這張圖是十分的清晰易懂的。
舉個例子 , \(g[10010][1]=A(10000)+A(10010)\)

那麼我們看圖也知道怎麼轉移了。
注意這不是一棵樹,是一個 DAG 。

為什麼能做到不重不漏呢?因為我們加入了摁死紅色部分的限制條件,因此分開的兩個子問題是無交的。
這就很妙的解決了轉移的問題。時間複雜度 \(O(n2^n)\)

一種更通俗的理解就是,列舉每個 \(1\) 的位置,子問題就是不取當前的 \(1\) 和取當前的 \(1\)

一般的,子集 dp 都可以使用滾動陣列最佳化,在草稿紙上推好柿子就行。

子集 dp 在優秀的時間複雜度內能處理複雜的子集問題。