1. 程式人生 > 其它 >【演算法】01揹包

【演算法】01揹包

先看題目

物品不能分隔,必須全部取走或者留下,因此稱為01揹包
(只有不取和取兩種狀態)

看第一個樣例

我們需要把4個物品裝入一個容量為10的揹包

我們可以簡化問題,從小到大入手分析

weight	value
2		1
3		3
4		5
7		9

先考慮物品數量為1的情況:

把前1件物品放入容量為1的揹包,此時得到的最大價值是:0
把前1件物品放入容量為2的揹包,此時得到的最大價值是:1
把前1件物品放入容量為3的揹包,此時得到的最大價值是:1
......
把前1件物品放入容量為10的揹包,此時得到的最大價值是:1

把前2件物品放入容量為1的揹包,此時得到的最大價值是:0
把前2件物品放入容量為2的揹包,此時得到的最大價值是:1
把前2件物品放入容量為3的揹包,此時得到的最大價值是:3
把前2件物品放入容量為4的揹包,此時得到的最大價值是:3
把前2件物品放入容量為5的揹包,此時得到的最大價值是:4
把前2件物品放入容量為6的揹包,此時得到的最大價值是:4
......
把前2件物品放入容量為10的揹包,此時得到的最大價值是:4

把前3件物品放入容量為1的揹包,此時得到的最大價值是:0
把前3件物品放入容量為2的揹包,此時得到的最大價值是:1
把前3件物品放入容量為3的揹包,此時得到的最大價值是:3
把前3件物品放入容量為4的揹包,此時得到的最大價值是:5
把前3件物品放入容量為5的揹包,此時得到的最大價值是:5
把前3件物品放入容量為6的揹包,此時得到的最大價值是:6
......
把前3件物品放入容量為10的揹包,此時得到的最大價值是:9

即:先從物品的數量開始列舉,對i件物品列舉所對應的揹包容量j,考慮它的最大價值f[i][j]

for(int i=1;i<=n;i++){//總共有n件物品
  for(int j=1;j<=m;j++){//揹包的最大容量是m
    
  }
}

對於一個f[i][j]:

(1)假設我們不選擇第i件物品,則f[i][j] = f[i-1][j],相當於把前i-1件物品放入這個容量為j的揹包,得到的最大值;

(2)假設我們選擇第i件物品,則這個物品剩餘的空間就是j-w[i],前一次迴圈的時候必定會算出這個值來,因此可以讓:f[i][j] = f[i-1][j-w[i]] + v[i]

兩種選擇中,我們保留最優解,避免下標出現負數(即j-w[i]可能為負數)

for(int i=1;i<=n;i++){
      for(int j=1;j<=m;j++){
            f[i][j]=f[i-1][j];
            if(j>=w[i])f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);
      }
}
printf("%d\n",f[n][m]);

其實這道題還有更多的做法,例如遞迴,從大到小的做法

上面的做法其實是遞推,時間複雜度是O(n*m),相比遞迴好一點,使用這種演算法只要保證n*m在107以內即可。空間複雜度則是O(n*m)

其實上,f陣列是可以壓縮成一維陣列的

對於一個數f[i][j],實際上是隻用到了第i-1行

因此,經過簡單的優化,只需要一行即可,把上一行的資訊移動下來,刪掉所有相關第一維度的東西,看看會變成什麼樣:

f[j]=f[j-w[i]]; //直接存到下一行中
f[j]=f[j];
f[j]=max(f[j],f[j-w[i]]);

對於一維陣列的問題所在:迴圈的順序會發生變化,如果繼續按照從左到右的順序進行迴圈的話,我們會發現,每一個數字都會被後面的資料所引用,因此需要從後往前來遞推。這樣,後面的值更新的時候不會被第二次使用,更新前面的值都不會被影響到。

反正記住是從後往前就行了

完整版的程式碼

for(int i=1;i<=n;i++){
    for(int j=m;j>=w[i];j--){
        //此處寫j>=w[i]避免下標產生負數
        //相當於if(j>=w[i]) f[j]=max(f[j],f[j-w[i]]+v[i]);
        f[j]=max(f[j],f[j-w[i]]+v[i]);
    }
}