揹包問題是學習算法和數據結構時肯定會接觸到的,我老早就瞭解到這個問題,可直到今天看到《挑戰》書上才詳細瞭解這個問題.
該問題的題設和要求如上。
拿到這個問題,最先想到的思路就是利用遞歸針對每個物品是否放入揹包進行兩種情況下的搜索。詳細的源碼和解釋如下:
#include<iostream>
#include<algorithm>
#include<cstdlib>
using namespace std;
#define Max_N 100
int n;
int sumWeight;
int value[Max_N];
int weight[Max_N];
//從位置爲index的物品進行揹包問題,留給它們的重量爲leftWeight,返回這種情況下能裝的最大價值物品的總價值
int solve1(int index,int leftWeight)
{
int res=0;
if(index>=n) return 0; //物品已用完,剩下的重量裝不了物品,返回價值0
if(weight[index]>leftWeight) //當前位置的物品重量大於剩餘重量,則不選擇該物品,從它下一個物品進行揹包問題
res=solve1(index+1,leftWeight);
else
//還剩物品,並且當前位置的物品重量小於剩餘重量,則就取該物品和不取該物品兩種情況進行遞歸
res=max(solve1(index+1,leftWeight),solve1(index+1,leftWeight-weight[index])+value[index]);
return res;
}
int main()
{
cin>>n>>sumWeight;
for(int i=0;i<n;i++)
{
cin>>weight[i]>>value[i];
}
//初始化遞歸條件,從下標爲0的物品開始揹包問題,剩給它們的重量就是總重量 sumWeight
cout<<"揹包問題solve1()最大總價值:"<<solve1(0,sumWeight);
return 0;
}
上述程序的問題就在於其效率比較低,針對每個物品都分爲兩種情況下的分支進行遞歸,這樣其複雜度就爲O(2^n),那麼能不能優化呢?答案是可以的。
我們首先可以簡單地把這種遞歸樹用圖的形式表達出來(下面每個單元中的數字就對應遞歸函數solve1()中的兩個參數):
;
我們可以看到在參數爲(3,2)時,其之後的遞歸情況是不變的,那麼我們這時候在第一次遇到參數爲(3,2)的遞歸時,將其返回的結果記錄下來,下次再遇到參數爲(3,2)的遞歸時,直接獲得已經存儲的結果就行了,這樣就不用繼續遞歸。這樣看來,每種參數形式,都最多隻會調用一次,所以對於n*sumWeight種組合的參數形式,上述程序的複雜度只有O(n*sumWeight)。詳細代碼如下:
#include<iostream>
#include<algorithm>
#include<cstdlib>
#include<memory.h>
using namespace std;
#define Max_N 100
#define Max_M 10000
int n;
int sumWeight;
int value[Max_N];
int weight[Max_N];
int dp[Max_N][Max_M];
//從位置爲index的物品進行揹包問題,留給它們的重量爲leftWeight,返回這種情況下能裝的最大價值物品的總價值
int solve2(int index,int leftWeight)
{
//記憶化搜索,如果之前已經對這種遞歸情況搜索過,那麼直接返回值
if(dp[index][leftWeight]>=0)
return dp[index][leftWeight];
int res=0;
if(index>=n) return 0; //物品已用完,剩下的重量裝不了物品,返回價值0
if(weight[index]>leftWeight) //當前位置的物品重量大於剩餘重量,則不選擇該物品,從它下一個物品進行揹包問題
res=solve2(index+1,leftWeight);
else
res=max(solve2(index+1,leftWeight),solve2(index+1,leftWeight-weight[index])+value[index]);
//爲了進行遞歸話搜索,將這種情況下的遞歸結果存儲起來
return dp[index][leftWeight]=res;
//還剩物品,並且當前位置的物品重量小於剩餘重量,則就取該物品和不取該物品兩種情況進行遞歸
}
int main()
{
cin>>n>>sumWeight;
for(int i=0;i<n;i++)
{
cin>>weight[i]>>value[i];
}
//初始化記憶化搜索的數組,使用 memset將dp數組全部初始化爲-1,具體memeset的詳細使用,這裏不闡述,就記住只能初始化爲0或-1即可
//使用 memset需要導入 memory.h或者string.h頭文件
memset(dp,-1,sizeof(dp));
//初始化遞歸條件,從下標爲0的物品開始揹包問題,剩給它們的重量就是總重量 sumWeight
cout<<"揹包問題solve2()最大總價值:"<<solve2(0,sumWeight);
return 0;
}
這種形式下的程序複雜度對於題目中的數據規模已經完全足夠了。上述這種帶有存儲的搜索遞歸形式叫做記憶化搜索。
當然,想這種遞歸式搜索遍歷的的函數,其參數是不固定。上述兩種代碼只是擁有兩個參數,現在我們把當前已經裝好的物品的總價值也當做參數,可以得到如下的程序:
#include<iostream>
#include<algorithm>
#include<cstdlib>
using namespace std;
#define Max_N 100
int n;
int sumWeight;
int value[Max_N];
int weight[Max_N];
//從位置爲index的物品進行揹包問題,留給它們的重量爲leftWeight,之前0~index-1物品的揹包問題的總價值爲curValue
//返回這種情況下能裝的最大價值物品的總價值
int solve3(int index,int leftWeight,int curValue)
{
int res=0;
if(index>=n) return curValue; //物品已用完,剩下的重量裝不了物品,返回當前總價值
if(weight[index]>leftWeight) //當前位置的物品重量大於剩餘重量,則不選擇該物品,從它下一個物品進行揹包問題
res=solve3(index+1,leftWeight,curValue);
else
res=max(solve3(index+1,leftWeight,curValue),solve3(index+1,leftWeight-weight[index],curValue+value[index]));
return res;
//還剩物品,並且當前位置的物品重量小於剩餘重量,則就取該物品和不取該物品兩種情況進行遞歸
}
int main()
{
cin>>n>>sumWeight;
for(int i=0;i<n;i++)
{
cin>>weight[i]>>value[i];
}
//初始化遞歸條件,從下標爲0的物品開始揹包問題,剩給它們的重量就是總重量 sumWeight,當前總價值肯定爲0,因爲還沒有物品被選中
//這種把參數情況寫的比較全比較有利於進行遞歸式搜索下的剪枝
cout<<"揹包問題solve3()最大總價值:"<<solve3(0,sumWeight,0);
return 0;
}
這種參數比較全的遞歸在需要剪枝的情況下們可以根據當前獲得信息,不再由這條分支繼續遞歸下去,直接返回。這樣也可以提高程序效率。
PS:關於重點的動態規劃下的揹包問題的解決方案在下篇博文中會介紹~