《男人八題》之—多重揹包的單調佇列優化與二進位制優化
多重揹包就是揹包問題加了個次數每個物品可以選s次,很容易想到的程式碼就是在狀態轉換中再加一個迴圈從0到s,但這樣大資料會超時,所以有了很多
優化方案。
二進位制優化:
每個物品可以選的次數都是不同的,有一種想法是把這可以選s次的物品拆成s個物品,這樣就只涉及選與不選,就簡化成了01揹包問題但還是會超時,因
為拆成了s個但其實沒必要我們只需要拆成log2(n)個就可以了,轉換成二進位制,舉個例子,比如說7,7的二進位制是111,111以內的數都可以用100,10,1來表示,也就是十進位制的1,2,4,但7是特殊情況或者比如說10,同樣拆成1、2、4。1、2、4,可以表示7以內的數而我想表示10以內的書就只需要再加個3就可以了,dddd。
然後這樣程式碼複雜度就不是O(n*v*s)而是O(n*v*log2(s))。
程式碼:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 2010;
int f[N],n,m;
struct good
{
int w,v;
};
int main()
{
cin>>n>>m;
vector<good> Good;
good tmp;
for(int i = 1 ; i <= n ; i++ )
{
int v,w,s;
cin>>v>>w>>s;
for(int k = 1 ; k <= s ; k*=2 )
{
s-=k;
Good.push_back({k*w,k*v});
}
if(s>0) Good.push_back({s*w,s*v});
}
for(auto t : Good)
for(int j = m ; j >= t.v ; j--)
f[j] = max(f[j] , f[j-t.v]+t.w );
cout<<f[m]<<endl;
return 0;
}
單調佇列優化:
這個巨難我看大佬解析看懂的,原解析在這
作者:lys
連結:https://www.acwing.com/solution/content/6500/
來源:AcWing
首先先從完全揹包的思路出發dp[i][j] 表示將前 i 種物品放入容量為 j 的揹包中所得到的最大價值dp[i][j] = max(不放入物品 i,放入1個物品 i,放入2個物品 i, ... , 放入k個物品 i),這裡 k 要滿足:k <= s, j - k*v >= 0,不放物品 i = dp[i-1][j],放k個物品 i = dp[i-1][j - k*v] + k*w,dp[i][j] = max(dp[i-1][j], dp[i-1][j-v] + w, dp[i-1][j-2*v] + 2*w,..., dp[i-1][j-k*v] + k*w)。
然後我們優化成了一維,適當的調整迴圈條件,我們可以重複利用dp陣列來儲存上一輪的資訊,我們令 dp[j] 表示容量為j的情況下,獲得的最大價值,那麼,針對每一類物品 i ,我們都更新一下 dp[m] --> dp[0] 的值,最後 dp[m] 就是一個答案。
dp[m] = max(dp[m], dp[m-v] + w, dp[m-2*v] + 2*w, dp[m-3*v] + 3*w, ...)
接下來,我們把 dp[0] --> dp[m] 寫成下面這種形式
dp[0], dp[v], dp[2*v], dp[3*v], ... , dp[k*v]
dp[1], dp[v+1], dp[2*v+1], dp[3*v+1], ... , dp[k*v+1]
dp[2], dp[v+2], dp[2*v+2], dp[3*v+2], ... , dp[k*v+2]
...
dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j]
因為k*v+j一定可以等於m,所以很容易想到可以把這些分為j個類,每一類中的值,都是在同類之間轉換得到的,也就是說,dp[k*v+j] 只依賴於 { dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j] },因為我們需要的是{ dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j] } 中的最大值,可以通過維護一個單調佇列來得到結果。這樣的話,問題就變成了 j 個單調佇列的問題。所以,我們可以得到:
dp[j] = dp[j]
dp[j+v] = max(dp[j] + w, dp[j+v])
dp[j+2v] = max(dp[j] + 2w, dp[j+v] + w, dp[j+2v])
dp[j+3v] = max(dp[j] + 3w, dp[j+v] + 2w, dp[j+2v] + w, dp[j+3v])
...
但是,這個佇列中前面的數,每次都會增加一個 w ,所以我們需要做一些轉換
dp[j] = dp[j]
dp[j+v] = max(dp[j], dp[j+v] - w) + w
dp[j+2v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w) + 2w
dp[j+3v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w, dp[j+3v] - 3w) + 3w
...
這樣,每次入隊的值是 dp[j+k*v] - k*w
key:
維護佇列元素的個數,如果不能繼續入隊,彈出隊頭元素
維護佇列的單調性,即:尾值 >= dp[j + k*v] - k*w
程式碼:
#include <iostream>
#include <cstring>
using namespace std;
const int N = 20010;
int dp[N], pre[N], q[N];
int n, m;
int main() {
cin >> n >> m;
for (int i = 0; i < n; ++i) {
memcpy(pre, dp, sizeof(dp));
int v, w, s;
cin >> v >> w >> s;
for (int j = 0; j < v; ++j) {
int head = 0, tail = -1;
for (int k = j; k <= m; k += v) {
if (head <= tail && k - s*v > q[head])
++head;
while (head <= tail && pre[q[tail]] - (q[tail] - j)/v * w <= pre[k] - (k - j)/v * w)
--tail;
if (head <= tail)
dp[k] = max(dp[k], pre[q[head]] + (k - q[head])/v * w);
q[++tail] = k;
}
}
}
cout << dp[m] << endl;
return 0;
}