1. 程式人生 > 資訊 >小米電視高階化底氣在哪,潘俊:模式上先進,能做到一分錢一分貨

小米電視高階化底氣在哪,潘俊:模式上先進,能做到一分錢一分貨

揹包問題是動態規劃問題中常見的一個分支,常用的有 01揹包,完全揹包,和多重揹包,本篇文章詳解01揹包和完全揹包。

01揹包

問題描述:有一個容量為 \(m\) 的揹包,有 \(n\) 個物品每個只有一件,第 \(i\) 個物品佔用 \(w_i\) 單位空間、擁有 \(c_i\) 的價值,求問在這個揹包中放入若干物品使得不超出容量限制,所能獲得的最大價值是多少。

01揹包,顧名思義,0表示不選擇當前的物品,1表示選擇當前的物品。每種物品有選與不選兩種情況,如果用 DFS 取所有方案最大值的話複雜度 \(2^n\) 一定超時。用動態規劃的方法可以解決。

定義狀態:\(f_{i,j}\) 表示在只有 \(1\sim i\)

\(i\) 個物品的情況下選擇若干裝進一個容量為 \(j\) 的揹包所能獲得的最大價值。我們知道,當 \(i=0\) 時不能獲得價值(\(f_{0,(1,2,\ldots,m)}=0\)),當揹包沒有容量時也不能獲得價值(\(f_{(1,2,\ldots,n),0}=0\)),其實在全域性變數裡面定義 \(f\) 就可以解決了。

對於我們當前的 \(f_{i,j}\),可能選或不選。如果選第 \(i\) 個物品:那麼我們的價值就等於選的這個物品的價值再加上揹包中剩下容量裝 \(1\sim i-1\) 物品所能獲得的最大價值,即 \(f_{i,j}=c_i+f_{i-1,j-w_i}\);此時,顯然揹包要得裝得下這個物品才行,否則沒法選他。如果不選第 \(i\)

個物品:那麼我們的價值就等於用現在的揹包容量去裝 \(1\sim i-1\) 物品所能獲得的最大價值,即 \(f_{i,j}=f_{i-1,j}\)。我們在選與不選所得的答案中取一個最大值就是當前揹包的最大價值了。我們寫出以下程式碼:

#include <iostream>
using namespace std
const int N=1e3,M=5e4;
int f[N][M],c[N],w[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>w[i]>>c[i];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(j>=w[i]) //要的裝得下
                f[i][j]=max(f[i-1][j],c[i]+f[i-1][j-w[i]]);
            else //裝不下就相當於是f[i-1][j]了
                f[i][j]=f[i-1][j];
    cout<<f[n][m]<<endl;
    return 0;
}

看看這張圖片,其中標出了我們要取 \(\max\) 的兩個答案,我們發現他們都是在第 \(i-1\) 行也就是剛剛我們操作完的那一行。設想如果我們只用一個一維的陣列 \(f\) 儲存剛剛處理完的那些結果,那麼我們就可以在這個基礎上,獲取我們需要的資訊,從而 \(f_{i,j}=\max(f_{i-1,j},c_i+f_{i-1,j-w_i})\) 轉化成了 \(f_j=\max(f_j,c_i+f_{j-w_i})\)。好的,那請你想一想,我們 for(int j=1;j<=m;j++) 這個迴圈跟 for(int j=m;j>=1;j--) 對答案有影響嗎?是有的,因為如果用第一種(正向)那麼我們的 \(f_{j-w_i}\) 就已經不再是原來的 \(f_{j-w_i}\) 即之前所說的二維陣列中上一排的了,而是我們在二維陣列 \(f\) 中這一排的答案了,如果我們反向迴圈就可以消除這個問題。這就是所謂的“滾動陣列”,滾掉了一維。最終的程式碼:

#include <iostream>
using namespace std
const int N=1e3,M=5e4;
int f[M],c[N],w[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>w[i]>>c[i];
    for(int i=1;i<=n;i++)
        for(int j=m;j>=w[i];j++)
                f[j]=max(f[j],c[i]+f[j-w[i]]);
    cout<<f[m]<<endl;
    return 0;
}

完全揹包

問題描述:有一個容量為 \(m\) 的揹包,有 \(n\) 個物品每個有無數件,第 \(i\) 個物品佔用 \(w_i\) 單位空間、擁有 \(c_i\) 的價值,求問在這個揹包中放入若干物品使得不超出容量限制,所能獲得的最大價值是多少。

我告訴你,下面的程式碼就是答案。

#include <iostream>
using namespace std
const int N=1e3,M=5e4;
int f[M],c[N],w[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>w[i]>>c[i];
    for(int i=1;i<=n;i++)
        for(int j=w[i];j<=m;j++)
                f[j]=max(f[j],c[i]+f[j-w[i]]);
    cout<<f[m]<<endl;
    return 0;
}

顯然這就是滾動陣列之後的一維 \(f\) 陣列的程式碼,而它與01揹包的最終程式碼唯一的不同,就是我們是按照之前所說的“錯誤列舉順序”——正向迴圈的。為什麼這樣子在完全揹包是正確的呢?敬請看後文。

我們知道,反向列舉就是呼叫的二維陣列上一行的資訊,而正向列舉就是呼叫的二維陣列這一行算出來的資訊。對於這個揹包,我們同樣有選與不選兩種情況。當然裝不下就不用管,裝得下:

  • 不選的話,答案就不變,跟01揹包一樣的道理。
  • 選的話,也就是說我們至少需要選擇一個這種物品,那麼我們就選擇一個這種物品,剩下的就交給我們這一排算好的 \(f_{j-w_i}\) 就好了。

對於上面兩種情況取一個 \(\max\) 得到遞推關係式 \(f_j=\max(f_j,c_i+f_{j-w_i})\),但是這跟01揹包的關係式是本質不同的,完全揹包的關係式如果在二維陣列中應該是:\(f_{i,j}=\max(f_{i-1,j},c_i+f_{i,f-w_i})\)。邊界條件與01揹包相同:\(f_{0,(1,2,\ldots,m)}=0\)\(f_{(1,2,\ldots,n),0}=0\)

其實完全揹包還有另外的解法(複雜度不比剛才說的解法優)。其原理是這樣的:

我們可以把完全揹包問題化成01揹包問題,即每個物品其實最多隻能選 \(\lfloor \frac{m}{w_i} \rfloor\) 件,所以我們就把每個物品看作是 \(\lfloor \frac{m}{w_i} \rfloor\) 個佔用 \(w_i\) 單位空間、擁有 \(c_i\) 價值的物品,最後求解這個01揹包問題即可,這個複雜度是很有些高的。

我們想想,是不是真的有必要把他分成這麼多數量的同種物品?不要曲解我的意思,我的意思並不是說“只需要一個範圍內數量的物品就夠了,超出這個數量的物品都用不上”,我的意思是說我,我們可以把一些物品合併到一起,從而減少了總物品數量,優化了複雜度。

看:每一個自然數 \(n\) 都可以化成若干個 \(2\) 的冪次相加的形式;為什麼是這樣——因為 \(n\) 化成二進位制就只由 \(0,1\) 組成,而 \(2\) 的冪次化成二進位制就是一個 \(1\) 後面跟若干個 \(0\) 的形式(準確來說,\(2^k\) 的二進位制形式是一個 \(1\) 後面 \(k\)\(0\)),所以打比方說,十進位制數 \(5\) 的二進位制數是 \((101)_2=(100)_2+(1)_2=(4)_{10}+(1)_{10}\)。既然這樣,那麼我們說這個可以有無限多的物品我們取的次數就也可以化成若干個二進位制數相加的形式嘍,那我們就只需要配備:體積和價值都是原來的 \(2^k\) 倍的物品了。舉個例子就不那麼抽象:

我們當前這個物品體積為 \(3\),價值為 \(5\),可以取無數個,且 \(m=10\)。那麼我們知道他最多隻能取 \(\lfloor \frac{10}{3} \rfloor=3\) 個,那如果選他,他可能取 \(1\)\(2\)\(3\) 個,所以我們只需要配備 \(2^0\)\(2^1\) 這兩個物品,因為 \(1=2^0\)\(2=2^1\)\(3=2^0+2^1\)。所以我們轉成01揹包時把這個物品拆成的物品就應該有兩個:

\ 體積 價值
第一個 \(2^0\times 3=3\) \(2^0\times 5=5\)
第二個 \(2^1\times 3=6\) \(2^1\times 5=10\)

其實這樣還是沒有最經典的(最開頭談的)那個方法複雜度低,所以就不展示程式碼了。謝謝大家,希望看了這篇文章你能對揹包有更清楚的認識。