簡單揹包問題+0/1揹包問題+DP

簡單揹包問題
  首先不好意思,前段時間在做項目,現在項目結束了,有時間了,來爲大家更新下算法!
看題:
小紅和小明在魔法石礦裏挖到了很多的魔法石,他們有一個揹包,可以放入的重量爲S,現有N件魔法石,重量分別爲W1,W2,W3,…Wn,各重量均爲正整數,從N件魔法石中挑選若干件,使得放入揹包的重量之和正好爲S。若成功,則輸出放入揹包的物品,否則輸出“Failed!”。
【輸入格式】
 第一行兩個整數即S和N,其中S<1000,N<32.第二行爲N個整數,即N件物品的重量。
【輸出格式】
 若成功,則輸出放入揹包的物品,以空格間隔,否則輸出“Failed!”.
【輸入樣例】
10  5
12 3 4 5 6
【輸出樣例】
4 6



枚舉算法

  該問題可以轉化爲某個物品取或不取的問題,以1代表取,以0代表不取,窮舉出所有的可能性,輸出總質量==S的組合即可,下面是輸出所有符合條件的組合:
//簡單揹包問題——枚舉算法
#include<iostream>
using namespace std;
int N,S;
int W[40];           		//初始化每個物品的重量 
int flag[40]={0};			//標記數組


void Print()				//打印結果 
{
	for(int i=0;i!=N;i++)
	if(1 == flag[i])
		cout<<W[i]<<" ";
	cout<<endl; 
} 

int main()
{
	int sum,all_count=1;
	cin>>S>>N;
	for(int i=0;i<N;i++)
		cin>>W[i];
	for(int i=0;i!=N;i++)	//計算所有可能性次數,即2的n次方 
		all_count *= 2;
		
	
	for(int num=0;num<=all_count;++num)
	{
		for(int i=0;i!=N;i++)	//列舉所有flag數組可能 
			if(flag[i] == 0)
			{
				flag[i]=1;
				continue;
			}
			else
			{
				flag[i]=0;
				break;
			}
			sum = 0;			//本次重量初始化爲0 
			
	for(int i=0;i!=N;i++)		//按標記計算所有選中物品重量和 
		if(flag[i] == 1)
			sum += W[i];
	if(sum == S)				//打印方案 
		Print();
	} 
	return 0;
	
}

下面我們來引入另一種算法


遞歸算法

  解決此題最可能的方法是一個一個將物品放入揹包內實驗,設布爾函數knapsack(s,n)表示剩下n個物品中裝滿剩下重量爲s的揹包,如果有解,返回1,否則返回0.實驗過程應該如下所述:
(1)取最後一個物品Wn,調用knapsack(s,n);
(2)如Wn=s,結束程序,輸出結果(n,Wn);
(3)如Wn<s,且n>1,則求knapsack(s-Wn,n-1);
(4)如Wn>s,且n>1,刪除Wn,從剩下n-1中繼續找,即knapsack(s,n-1)。
還可以得知遞歸結束的條件應爲:
(1)Wn=s(正好放入的物品重量等於揹包能裝的重量)
(2)Wn!=s(無解)
(3)n<=0(再沒有物品可試)
  但實際上問題並不是這麼簡單,因爲所選取並放入的物品Wn很可能導致無法獲得正確結果。例如s=10,
物品重量分別爲1,6,2,7,5,如果第一次選擇Wn=5放入揹包後,則後面再怎麼選擇也不可能成功,正確的做法是排除
Wn=5,從Wn=7開始纔可能有正確答案,即7+2+1=10。
  因此Wn是否有效還要看後續的knapsack(s-Wn,n-1)是否有解,如果無解,說明先前取得Wn不合適,就要放棄Wn,在剩餘物品中
重新開始挑選,即knapsack(s,n-1)。
參考代碼如下:

//簡單揹包問題——遞歸算法
#include<iostream>
using namespace std;
#define MAXN 40

int W[MAXN];		//各物品重量

int knapsack(int s,int n)		//s爲剩餘重量,n爲剩餘可選物品數 
{
	if(s == 0)					//如果正好裝滿 
		return 1;	
	if(s<0 || (s>0 && n<1))		//如s<0 或 n<1 則不能完成 
		return 0;
	if(knapsack(s-W[n],n-1))	//從後往前裝,裝上W[n]後,若剩餘物品仍有解 
	{
		cout<<W[n]<<" ";		//則裝進第n個包,並輸出 
		return 1;
	}
	return knapsack(s,n-1);		//如裝了第n個包後,導致無解,則刪除該包,嘗試第n-1個 
} 



int main()
{
	int S,N;
	cin>>S>>N;
	for(int i=1;i<=N;++i)
		cin>>W[i];
	if(knapsack(S,N))
		cout<<"\n";
	else
		cout<<"Failed!\n";
	
}

大家是不是對揹包問題有一定了解了,下面我們引入另一種揹包0/1揹包問題

0/1揹包問題

小紅和小明有一個最多能裝m千克的揹包,有n塊魔法石,它們的重量分別是W1,W2,...Wn,它們的價值分別是爲C1,C2,...Cm。若每種魔法石只有一件,問能裝入的最大總價值。
【輸入示例】
  第一行爲兩個整數m和n,以下n行中,每行兩個整數Wi,Ci,分別代表第i件物品的重量和價值
【輸出格式】
  輸出一個整數,即最大價值
【輸入樣例】
8 3
2 3
5 4
5 5
【輸出樣例】
8
動態規劃算法

在這道題裏,物品或者被裝入揹包,或者不被裝入揹包,只有兩種選擇。因此被稱爲0/1揹包問題。我們可以使用窮舉組合、貪心算法,但是,用窮舉組合可能會超時,貪心法不穩定,往往的不出最優解。由此需要考慮動態規劃算法(DP)。


使用動態規劃算法解決如下:
設f(i,x)表示前i件物品,揹包容量爲x時的最優價值;
則f(i,x)=max{f(i-1,x-W[i])+C[i],f(i-1,x)}
f(n.m)即爲最優解,邊界條件爲f(0,x)=,f(i,0)=0;
如果對上述公式不理解,我們使用題目中的樣例用表格法來分析:
根據邊界條件f(0,x)=0,f(i,0)=0;表格如下所示:

 試放入第一個物品,即W=2,C=3試一下在各個重量段所能取得的最大價值如表所示:


  如下表所示,試放入第二個物品,即w=5,c=4,試一下在各個重量段所能取得的最大價值:

 由於第二個物品重量爲5,所以在1~4重量段時,值仍爲3,當重量段>=5時,可以嘗試放入改物品,即比較改重量段減去5時的價值
加該物品的價值,與不放第二個物品時的總價值哪個大,取其最大值。以後讀入的物品一次類推。
 例如:f(2,5)=max(f(1,(5-5))+4,f(1,5))
     =max(f(1,0)+4,f(1,5))
     =4
 F(2,7)=max(f(1,(7-5))+4,f(1,7))
       =max(f(1,2)+4,f(1,7))
       =7
 以此法試放入第三個物品,即w=5,c=5時,表格如圖所示:



最終答案爲8
完整代碼
// 0/1揹包問題
#include<iostream>
#include<cstdlib>
using namespace std;


int i,j,m,n;
int w[200],c[200],f[200][200];

int main()
{
	cin>>m>>n;
	for(i=1;i<=n;i++)
		cin>>w[i]>>c[i];
	for(i=1;i<=n;i++)
		for(j=1;j<=m;j++)
			if(j >= w[i])
				f[i][j]=max(f[i-1][j-w[i]]+c[i],f[i-1][j]);
			else
				f[i][j]=f[i-1][j];
	cout<<f[n][m]<<endl;
	return 0;
} 

到這裏就結束了,希望此篇文章,能夠對大家有所幫助,也希望的算法精益求精,不斷突破瓶頸;下次爲大家講解N皇后問題!

点赞