被leetcode401單週賽狠狠打敗了。就此記錄下bitset最佳化0-1揹包。
利用數字的二進位制操作對整體進行轉移
問題描述
點選連結直接獲取問題描述
求解思路
狀態定義
定義\(dp[i][j]\)表示能否從前i個數中得到總獎勵j。每次轉移對前一個狀態的j有特殊要求。
狀態轉移方程
考慮第i個物品選或不選:
- 不選第i個物品:\(dp[i][j] = dp[i-1][j]\) 。
- 選擇第i個物品:\(dp[i][j] = dp[i-1][j-w[i]]\) ,需要滿足\(j \geq w[i]\) 且\(j - w[i] < w[i]\) 。
狀態為能否從前i個數中得到獎勵,即$$dp[i][j] = dp[i-1][j]\ \cup\ dp[i-1][j-w[i]] $$
初始化方式
初始值\(dp[0][0] = true\) 。
所求答案為\(dp[n][j] = true\)中最大的j。
最佳化方式
對應的狀態個數為\(n^{2}\),當然是過不了當前\(n = 2 \times 10^{5}\) 的資料。需要最佳化掉其中一個維度。
-
由於狀態轉移中第\(i\)層一定從第\(i - 1\)層轉移過來,可以兩個陣列或者原地滾動進行最佳化。
但只省下空間,總狀態數還是那麼多,沒啥用 -
注意到\(dp[i][j]\)一定是從\(dp[i-1][j]\)和\(dp[i-1][j-w[i]]\)轉移過來,對應的\(0 \le j-w[i] < w[i]\)。也就是存在一一對應的關係,並且轉移方式為兩者取或。使用bitset對最佳化當前0/1揹包的狀態轉移。
用二進位制數表示每個狀態,二進位制數位j上的數字表示當前狀態下的j能否被取到。 對應的狀態轉移為了滿足限制條件,需要先將\(x\)中數位大於\(j\)的位置設定為0,再將當前的二進位制數\(x\)與左移\(w[i]\)位的\(x^{*}\)取或。初始狀態設定為1,代表開始只有0滿足要求。
bitset介紹
該部分內容搬運自 -Wallace-的文章
它類似於 bool 陣列,每個位置只有兩種值:0 或 1。
bitset 的實現方式是壓位,那麼一個大小為 n𝑛 的 bitset 的空間複雜度為 \(O(\frac{1}{ω}n)\)。其中 ω=32 或 64(系統位數)。
基本操作
bitset<N> f;//定義一個大小為N的bitset,下標範圍[0, N)
f.set(i);//在下標i設為1
f.reset(i);//將下標i設定為0
f.test(i);//判斷下標i處是否為1
f[i];//取值
除了建構函式,其餘操作均為\(O(1)\)。
進階操作
f.set(); // 全部置為 1
f.reset(); // 全部置為 0
f = g; // bitset 賦值
f &= g; // 將 f 對 g 做按位與操作
f |= g; // 將 f 對 g 做按位或操作
f ^= g; // 將 f 對 g 做按位異或操作
// 以及各種位運算操作
f.count(); // 計算 bitset 中 1 的個數
這些範圍操作都是\(O(\frac{1}{ω}n)\)的時間複雜度。bitset時空複雜度中的常數\(\frac{1}{w}\) 是bitset最佳化的關鍵。
程式碼案例
源自靈茶山艾府
bitset<100000>f{1};//初始化下標0的位置為1
for (int v : rewardValues) {
int shift = f.size() - v;
//bitset的區間操作帶有常數1/w
f |= f << shift >> shift << v;
}
for (int i = rewardValues.back() * 2 - 1; ; i--) {
//獲取最大的下標為1的位置
if (f.test(i)) {
return i;
}
}
使用bitset最佳化後,對應的時間複雜度為\((\frac{1}{w} n^{2})\) ,\(w\)對應為32或64,滿足資料範圍\(n = 2\times 10^{5}\)的要求。