【演算法導論】貪心演算法之揹包問題
在討論貪心演算法時,我們先了解貪心演算法與動態規劃之間的區別與聯絡,後面我們將發現可以用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