1. 程式人生 > >【演算法導論】貪心演算法之揹包問題

【演算法導論】貪心演算法之揹包問題

        在討論貪心演算法時,我們先了解貪心演算法與動態規劃之間的區別與聯絡,後面我們將發現可以用0、1揹包問題和部分揹包問題來比較貪心演算法和動態規劃的關係。

        我們知道,對於一個最優解問題,貪心演算法不一定能夠產生一個最優解。因為,如果想要採用貪心演算法得到最優解需要滿足兩個條件:貪心選擇性質、最優子結構

        貪心選擇性質:一個全域性最優解可以通過區域性最優解來得到。that is to say,當考慮如何做選擇時,我們只考慮對當前問題最佳的選擇而不考慮子問題的結果。

        最優子結構:全域性最優解包含子問題的最優解。

        貪心演算法和動態規劃的區別

:在動態規劃中,每一步都要做出選擇,但是這些選擇都依賴於子問題的解。因此,解動態規劃問題一般是自底向上,由子問題到問題。在貪心演算法中,我們總是做出當前的最好選擇,而這些選擇都不是依賴子問題,選擇後再解決選擇之後出現的子問題。因此,解貪心演算法問題一般是自頂向下,一個一個地做出貪心選擇。

        從上面的描述可知,動態規劃中解決問題,需要先解決子問題,因此可能用到遞迴(當然可以將遞迴化為非遞迴),計算複雜度要比貪心演算法高。從上面的理論解釋可能比較抽象,我們可以用具體的例項來說明問題。我們用經典的0、1揹包問題和部分揹包問題來看看動態規劃和貪心演算法的區別。兩個問題的描述如下:


用貪心選擇演算法來解決部分揹包問題正如上面所說的思想,十分簡單,在這裡就不給予程式實現。我們主要討論0、1揹包問題的最優解。

        下面我們例子來說明為什麼貪心選擇演算法不能解0、1揹包問題:假設揹包容量為116,序號為1-3的物品的重量和價格分別為:w[3]={100,14,10},p[3]={20,18,15}.其平均價值為{0.2,18/14,1.5},按照貪心演算法的話,選擇物品順序為:3,2,1,最終的選擇為3,2,其價值為33,但是實際的最優方案為:選擇1,2,其價值為38.

從這個例子中可以看出,在0、1揹包問題中,我們在選擇是否要加入一個物品時,必須將把該物品加進去的子問題和不加進去的子問題進行比較(選擇依賴子問題),這種方式的問題導致了許多重疊子問題,這是動態規劃的一個特點。

        下面我們用動態規劃來解0、1揹包問題:假設f[i][j]表示剩餘物品為i,i+1,……n,容量為j時的最大價值,例如以上面的例子來說明,f[0][10],表示物品為1,2,3,容量為116時的最大價值,f[1][116],表示物品為2,3,容量為116時的最大價值。我們目的是求f[0][116],利用動態規劃的思想,假設我們選擇1號物品,則最大價值為p[0]+f[1][116-100],如果不選1號,則最大價值為f[1][116],因此選不選1號則需要比較兩者的最大值。比較兩者的最大值需要求f[1][116]和f[1][116-100],這是重疊子問題。最終的表示式為:f[i][j]=f[i+1][j]>f[i+1][j-w[i]]+p[i].這個表示式可以遞迴求解,當然也可以迭代求解。

具體程式實現如下:

#include<stdio.h>

int Bag_0or1(int *w,int *p,int *flag,int n,int i,int y);
void Bag_0or1_iteration(int *w,int *p,int c,int n,int f[][116]);
void print(int *w,int *flag,int n,int c,int f[][116]);

void main()
{
	int w[]={100,14,10};//被註釋的部分是另外一個例項
	int p[]={20,18,15};
	int c=116;
	int n=2;
	//int w[]={2,2,6,5,4};
	//int p[]={6,3,5,4,6};
	//int c=10;
	//int n=4;//n為物品個數減一,是因為陣列從0開始。
	int i=0;
	int f[5][116]={0};
	int flag[5]={0};//flag為1表示選擇該物品
    printf("最大價值為:%d\n",Bag_0or1(w,p,flag,n,i,c));
	printf("選擇的物品為(1表示選擇,0表示未選擇):");
	printf("%d%d%d\n",flag[0],flag[1],flag[2]);
	 //Bag_0or1_iteration(w,p,c-1,n,f);
	 //printf("最大價值為:%d\n",f[0][c-1]);
	 //printf("選擇的物品為(1表示選擇,0表示未選擇):");
	 //print(w,flag,n,c-1,f);
	 //printf("\n");
}
/***************************************************\
函式功能:遞迴法解0/1揹包問題
輸入:    物品重量w、物品價格p,物品個數n、i,y表示剩餘容量為y,剩餘物品為i,i+1,……n
輸出:    揹包所能容下的最大價值
\***************************************************/
int Bag_0or1(int *w,int *p,int *flag,int n,int i,int y)//遞迴法
{
		if(i==n)//物品僅剩餘最後一件
		{
			if(y<w[n])
			{
				flag[n]=0;
				return 0;
			}
			else
			{
				flag[n]=1;
				return p[n];
			}
		}
		if(y<w[i])//當物品i加入後大於容量的情況
		{
			flag[i]=0;
			printf("ok");
			return Bag_0or1(w,p,flag,n,i+1,y);
		}
		if(Bag_0or1(w,p,flag,n,i+1,y)>(Bag_0or1(w,p,flag,n,i+1,y-w[i])+p[i]))
		//當物品i加入後還有剩餘容量的情況
		{
			flag[i]=0;
			return Bag_0or1(w,p,flag,n,i+1,y);
		}
		else
		{
			flag[i]=1;
			return Bag_0or1(w,p,flag,n,i+1,y-w[i])+p[i];
		}

}
/***************************************************\
函式功能:迭代法解0/1揹包問題
輸入:    物品重量w、物品價格p,物品個數n、i,y表示剩餘容量為y,剩餘物品為i,i+1,……n,
          f[i][j]表示剩餘物品為i,i+1,……n,容量為j時的最大價值
輸出:    無
\***************************************************/
void Bag_0or1_iteration(int *w,int *p,int c,int n,int f[][116])//迭代法
{
	
	for(int y=0;y<=c;y++)//初始化
		f[n][y]=0;
	for(int y=w[n];y<=c;y++)//這裡有很多y值根本用不到,但是由於不能知道y的取值,所以要考慮所有y的取值
		f[n][y]=p[n];

	for(int i=n-1;i>0;i--)
	{
		for(int y=0;y<=c;y++)
			f[i][y]=f[i+1][y];
		for(int y=w[i];y<=c;y++)//選擇當前物品i是否裝入
		{
			if(f[i+1][y]>(f[i+1][y-w[i]]+p[i]))
				f[i][y]=f[i+1][y];//不裝入
			else
				f[i][y]=f[i+1][y-w[i]]+p[i];//裝入
		}
	}
	f[0][c]=f[1][c];
	if(c>=w[0])//考慮是否將第一個物品裝入
	{
		if(f[0][c]>(f[1][c-w[0]]+p[0]))
			f[0][c]=f[0][c];
		else
			f[0][c]=f[1][c-w[0]]+p[0];
	}
}

/***************************************************\
函式功能:列印被選擇的物品
輸入:    物品重量w、是否被選擇的標誌flag,物品個數n、c為揹包容量
          f[i][j]表示剩餘物品為i,i+1,……n,容量為j時的最大價值
輸出:    無
\***************************************************/
void print(int *w,int *flag,int n,int c,int f[][116])
{
	for(int i=0;i<n;i++)
	{
		if(f[i][c]==f[i+1][c])//不裝入序號為i的物品的情況
			flag[i]=0;
		else
		{
			flag[i]=1;
			c=c-w[i];
		}
		flag[n]=(f[n][c])?1:0;
	}
	for(int j=0;j<=n;j++)
		printf("%d",flag[j]);

}

注:如果程式出錯,可能是使用的開發平臺版本不同,請點選如下連結: 解釋說明

作者:nineheadedbird