題目鏈接:點擊這裏
01揹包問題描述:
有 件物品和一個容量爲 的揹包。放入第 件物品耗費的費用是 ,得到的價值是 。求解將哪些物品裝入揹包可使價值總和最大。
這是最基礎的揹包問題,特點是:每種物品僅有一件,可以選擇放或不放。
用子問題定義狀態:即 表示前 件物品恰放入一個容量爲 的揹包可以獲得的最大價值。
則其狀態轉移方程便是:
這個方程非常重要,基本上所有跟揹包相關的問題的方程都是由它衍生出來的。
所以有必要將它詳細解釋一下:“將前 件物品放入容量爲 的揹包中”這個子問題,若只考慮第 件物品的策略(放或不放),那麼就可以轉化爲一個只和前 件物品相關的問題。
- 如果不放第 件物品,那麼問題就轉化爲“前 件物品放入容量爲 的揹包中”,價值爲 ;
- 如果放第 件物品,那麼問題就轉化爲“前 件物品放入剩下的容量爲 的揹包中”,此時能獲得的最大價值就是 再加上通過放入第 件物品獲得的價值 。
注意到 只與之前的狀態 有關,所以可以枚舉 從 到 , 從 到 ,通過邊界 (即前 件物品放入任何容量的揹包中都只能獲得價值 )就可以把整個 數組遞推出來。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int v[N], w[N];
int f[N][N];
int n, m;
int main()
{
cin>>n>>m;
for(int i = 1; i <= n; ++i) cin>>v[i]>>w[i];
for(int i = 1; i <= n; ++i)
{
for(int j = 0; j <= m; ++j)
{
f[i][j] = f[i-1][j];
if(j >= v[i]) f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
}
}
cout<<f[n][m]<<endl;
return 0;
}
以上方法的時間和空間複雜度均爲 ,其中時間複雜度應該已經不能再優化了,但空間複雜度卻可以優化到 。
先考慮上面講的基本思路如何實現,肯定是有一個主循環 ,每次算出來二維數組 的所有值。
那麼,如果只用一個數組 ,能不能保證第 次循環結束後 中表示的就是我們定義的狀態 呢?
是由 和 兩個子問題遞推而來,能否保證在推 時(也即在第 次主循環中推 時)能夠取用 和 的值呢?
事實上,這要求在每次主循環中我們以 的遞減順序計算 ,這樣才能保證計算 時 保存的是狀態 的值。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int v[N], w[N];
int f[N];
int n, m;
int main()
{
cin>>n>>m;
for(int i = 1; i <= n; ++i) cin>>v[i]>>w[i];
for(int i = 1; i <= n; ++i)
{
for(int j = m; j >= v[i]; --j)
{
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout<<f[m]<<endl;
return 0;
}
【補充】
我們看到的求最優解的揹包問題題目中,事實上有兩種不太相同的問法。
有的題目要求“恰好裝滿揹包”時的最優解,有的題目則並沒有要求必須把揹包裝滿。
一種區別這兩種問法的實現方法是在初始化的時候有所不同。
-
如果是第一種問法,要求恰好裝滿揹包,那麼在初始化時除了 爲 ,其它 均設爲 ,這樣就可以保證最終得到的 是一種恰好裝滿揹包的最優解。
-
如果並沒有要求必須把揹包裝滿,而是隻希望價格儘量大,初始化時應該將 全部設爲 。
這是爲什麼呢?
可以這樣理解:初始化的 數組事實上就是在沒有任何物品可以放入揹包時的合法狀態。
-
如果要求揹包恰好裝滿,那麼此時只有容量爲 的揹包可以在什麼也不裝且價值爲 的情況下被“恰好裝滿”,其它容量的揹包均沒有合法的解,屬於未定義的狀態,應該被賦值爲 了。
-
如果揹包並非必須被裝滿,那麼任何容量的揹包都有一個合法解“什麼都不裝”,這個解的價值爲 ,所以初始時狀態的值也就全部爲 了。
這個小技巧完全可以推廣到其它類型的揹包問題。