1. 程式人生 > 實用技巧 >《男人八題》之—多重揹包的單調佇列優化與二進位制優化

《男人八題》之—多重揹包的單調佇列優化與二進位制優化

  多重揹包就是揹包問題加了個次數每個物品可以選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;
}