目錄
如上圖是一個LeetCode的經典問題,0-1揹包問題
1.0-1揹包問題的分析
嘗試下面的算法
暴力解法:每一件物品都可以放進揹包,也可以不放進揹包,時間複雜度爲O((2^n)*n),需要耗費太久的時間 X
貪心算法:優先放入平均價值最高的物品,如下圖例子 X
假設有一個容量爲5的揹包
(1)此時如果採用貪心算法,則應該是先放入6,佔了一個容量,再放入10,佔了2個容量,此時一共佔用了3個容量,無法繼續放入第3個物品,此時貪心算法的結果就是16
(2) 但如果我們不放入1,只放入2,3物品,則此時揹包容量剛好填滿,價值爲22。此時我們剛好放棄了平均價值最大的物品,
因此貪心算法是不正確的
(1)狀態方程
F(n,C):考慮將n個物品放進容量爲C的揹包,使得價值最大聲
狀態轉移方程分析:
狀態有兩種,一種是該物品放進揹包,一種是不放進揹包,直接考慮後面的物品,兩種狀態取大值即可
F(i,C) = F(i-1,C) 不放進揹包,直接考慮後面的物品
=v(i) + F(i-1,C-w(i))該物品放進揹包
狀態轉移方程:F(i,C)= max(F(i-1,C),v(i) + F(i-1,C-w(i)))
2.遞歸算法
class Solution {
private $w,$v; //使其成爲成員變量
public function knapsack01($w,$v,$c){
$len = count($w);
$this->w = $w;
$this->v = $v;
return $this->bestValue($len-1,$c);
}
/**
* [用[0...index]的物品,填充容積爲c的揹包的最大價值]
* @param [type] $index [考慮到的物品的下標]
* @param [type] $c [剩餘的容量]
*/
private function bestValue($index,$c){
if($index < 0 || $c <= 0) return 0;
$res = $this->bestValue($index-1,$c); //不放入該物體,直接考慮後面的物品放入
if($c >= $this->w[$index]) //如果該物體能放得下該揹包,則放入,並與上面的策略取大值
$res = max($res, $this->v[$index] + $this->bestValue($index-1,$c-$this->w[$index]));
return $res;
}
}
$q = new Solution();
$w = [1,2,3];
$v = [6,10,12];
$c = 5;
var_dump($q->knapsack01($w,$v,$c));
3.記憶化搜索
遞歸解答中存在大量的重疊子結構問題,可以利用index 和 剩餘容量 c 作爲記憶化數組的下標
class Solution {
private $w,$v; //使其成爲成員變量
private $memo = []; //初始化記憶化數組
public function knapsack01($w,$v,$c){
$len = count($w);
$this->w = $w;
$this->v = $v;
return $this->bestValue($len-1,$c);
}
/**
* [用[0...index]的物品,填充容積爲c的揹包的最大價值]
* @param [type] $index [考慮到的物品的下標]
* @param [type] $c [剩餘的容量]
*/
private function bestValue($index,$c){
if($index < 0 || $c <= 0) return 0;
if(isset($this->memo[$index][$c])) //檢索是否已經檢索過
return $this->memo[$index][$c];
$res = $this->bestValue($index-1,$c); //不放入該物體,直接考慮後面的物品放入
if($c >= $this->w[$index]) //如果該物體能放得下該揹包,則放入,並與上面的策略取大值
$res = max($res, $this->v[$index] + $this->bestValue($index-1,$c-$this->w[$index]));
$this->memo[$index][$c] = $res; //保存記憶化數組
return $res;
}
}
$q = new Solution();
$w = [1,2,3];
$v = [6,10,12];
$c = 5;
var_dump($q->knapsack01($w,$v,$c));
4.動態規劃
在二維數組中使用動態規劃,模擬填充過程.
行爲物品,列爲容量
(1)填充第一行,容量爲0的時候,不能填充,因此該點的價值爲0。容量爲1的時候,可以填充物品0,此時價值爲6,往後的所有點的最大價值都爲6
(2)填充第二行,每一個元素點都有兩種可能,一種爲放入該物品,另一種爲不放入該物品
- 0點,無法放入物體,最大價值依舊是0;下標1,無法放入1號物品,因此最大價值爲放入1號下標之前的最大收益,爲6
- 下標2,可以放入物品1,因此有兩種情況,不放入該物品時,所獲最大價值爲該容量的上一個最大收益,爲6;放入該物體時,價值則等於該物體的價值10加上當前容量減去該物體所佔體積的(2-2)的容量位置的最大收益的值,即爲10+0 = 10 > 6,則下標2位置的最大收益變爲 10
- 以此類推,一直到容量5,不放入該物體時,當前容量的最大收益爲6;放入該物體時,10 + 6 = 16 > 6 ,因此該下標的最大收益爲16
(3)以此類推,填充第三行,到容積爲3時才能放入該物體,到最後一個下標5時,如果不放入該物體,則最大收益爲上一行容量的最大收益;如果放入該物體,則最大收益爲,該物體所獲收益12 + 容量-該物體佔用容量的容量下標所獲最大收益10 10+12=22>16,因此最終答案爲22
class Solution {
public function knapsack01($w,$v,$c){
$len = count($w); //求數組長度
$dp = []; //初始化動態規劃二維數組
for($j = 0; $j <= $c; ++$j) //初始化第一行數據
$dp[0][$j] = $j >= $w[0]? $v[0]: 0;
for($i = 1;$i < $len; ++$i){ //從第二行開始冬天規劃
for($j = 0;$j <= $c; ++$j){
$dp[$i][$j] = $dp[$i-1][$j]; //不放入物品的策略
if($j >= $w[$i]) //如果物品可以放入,則取與物品可以放入的最大值
$dp[$i][$j] = max($dp[$i][$j], $v[$i] + $dp[$i-1][$j-$w[$i]]);
}
}
return $dp[$len-1][$c]; //最終,最後一個元素未所求答案
}
}
$q = new Solution();
$w = [1,2,3];
$v = [6,10,12];
$c = 5;
var_dump($q->knapsack01($w,$v,$c));
5.優化1——空間複雜度O(2C)
原本的動態規劃的時間複雜度爲O(n*c) ,空間複雜度爲O(n*c)
狀態轉移方程:F(i,C)= max(F(i-1,C),v(i) + F(i-1,C-w(i)))
第i行元素只依賴於第i-1行元素,所以理論上,只需要保持兩行元素即可,空間複雜度O(2*c) = O(c)
我們可以定義兩行,第一行都在處理偶數的數,第二行都是奇數
通過節省空間,可以解決的問題範圍就大大增加了
class Solution {
public function knapsack01($w,$v,$c){
$len = count($w); //求數組長度
$dp = []; //初始化動態規劃二維數組
for($j = 0; $j <= $c; ++$j) //初始化第一行數據
$dp[0][$j] = $j >= $w[0]? $v[0]: 0;
for($i = 1;$i < $len; ++$i){ //從第二行開始冬天規劃
for($j = 0;$j <= $c; ++$j){
$dp[$i%2][$j] = $dp[($i-1)%2][$j];//不放入物品的策略
if($j >= $w[$i]) //如果物品可以放入,則取與物品可以放入的最大值
$dp[$i%2][$j] = max($dp[$i%2][$j], $v[$i] + $dp[($i-1)%2][$j-$w[$i]]);
}
}
return $dp[($len-1)%2][$c]; //最終,最後一個元素未所求答案
}
}
$q = new Solution();
$w = [1,2,3];
$v = [6,10,12];
$c = 5;
var_dump($q->knapsack01($w,$v,$c));
6.優化2——空間複雜度O(C)
根據上面的圖例,每一次更新都會參考上面和左邊的內容,右邊的內容不會進行操作
因此我們可以只開闢一個一維的長度爲C的數組,從右往左進行動態規劃
不僅可以節省時間複雜度,$j 只需遍歷到比當前物體佔用位置大的下標,再小就放不下去
還可以節省空間複雜度,O(C),每次都與自身(即不放入當前物體)和放入物體後取大值
簡潔代碼
class Solution {
public function knapsack01($w,$v,$c){
$len = count($w); //求數組長度
$dp = []; //初始化動態規劃二維數組
for($j = 0; $j <= $c; ++$j) //初始化第一行數據
$dp[$j] = $j >= $w[0]? $v[0]: 0;
for($i = 1;$i < $len; ++$i){ //從第二個物品開始,從右往左開始動態規劃
for($j = $c;$j >= $w[$i]; --$j){ //$j爲當前的容量,當前的容量要當前物品所佔用的地方,否則放不下去
$dp[$j] = max($dp[$j], $v[$i] + $dp[$j-$w[$i]]);//跟原先的自己(即不放入該物體)和放入該物體後比較
}
}
return $dp[$c]; //最終,最後一個元素爲所求答案
}
}
$q = new Solution();
$w = [1,2,3];
$v = [6,10,12];
$c = 5;
var_dump($q->knapsack01($w,$v,$c)); //22
7.0-1揹包問題的變種
完全揹包問題:每個物品可以無限使用
多重揹包問題:每個物品不止1個,有num(i)個
多維費用揹包問題:要同時考慮物品的體積和重量兩個維度
物品之間互相排斥/互相依賴
……各種約束,腦瓜疼