揹包問題終結篇(上)

揹包問題介紹
揹包問題是一個非常典型的考察動態規劃應用的題目,對其加上不同的限制和條件,可以衍生出諸多變種,若要全面理解動態規劃,就必須對揹包問題瞭如指掌。

首先記住解決動態規劃的三個基本要素:
最優子結構
邊界條件
狀態轉移方程

1.0-1揹包問題
即限定每個物品要麼拿(1個)要麼不拿(0個)
典型問題描述:

一個小偷面前有一堆(n個)財寶,每個財寶有重量w和價值v兩種屬性,而他的揹包只能攜帶一定重量的財寶(Capacity),在已知所有財寶的重量和價值的情況下,如何選取財寶,可以最大限度的利用當前的揹包容量,取得最大價值的財寶(或求出能夠獲取財寶價值的最大值)

問題分析:
這個問題有兩個維度,一個是當前物品i,另一個是當前容量c,於是我們可以用f[n,c]來表示將n個物品放入容量爲C的揹包可以得到的最大收益,而第i個物品無非拿與不拿兩種情況,拿的情況下,f[i][c]=f[i-1][c-w[j]+v[i]],這個公式表示在第i個物品選擇拿的情況下,同時還有前i-1的物品的選擇的最優子結構,不拿的情況下,f[i][c]=f[i - 1][c],而且要使盡量拿的價值最大。
因此可以表示爲:

f[i][c] = max( f[i - 1][c], f[i - 1][c - w[i]] + v[i] )
這便是我們的最優子結構,即不拿第 i 件物品和拿第 i 件物品中的最大值,當然,這裏要保證w[i] <= c,否則f[i][c] = f[i - 1][c]。

代碼如下:

#include<stdio.h>
int f[100][100];
 int n;
 int Maxweight;
int max(int a,int b)
{
 if(a>b)
 return a;
 else
 return b;
} 
int bag0_1(int v[100],int w[100],int n,int c)
{
 int i,j;
 for(i=0;i<n;i++)
 f[i][0]=0;
 for(j=1;j<=c;j++)
 {
  if(j>=w[0])
  f[0][j]=v[0];
  else
  f[0][j]=0;
  } 
  for(i=1;i<n;i++)
  {
   for(j=1;j<=c;j++)
   {
    if(j<w[i])
    {
     f[i][j]=f[i-1][j];
  }
  else
  {
   f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+v[i]);
  }
   }
  }
  for(i=0;j<n;i++)
  {
  for(j=0;j<=c;j++)
  printf("%d ",f[i][j]);
  printf("\n");
  }
  return f[n-1][c];
}
int main()
{
 int i,j;
 int Maxvalue;
 int value[100];
 int weight[100];
 scanf("%d",&n);
 scanf("%d",&Maxweight);
 for(i=0;i<n;i++)
 scanf("%d",&value[i]);
 for(j=0;j<n;j++)
 scanf("%d",&weight[j]);
 Maxvalue=bag0_1(value,weight,n,Maxweight);
 printf("小偷可以拿走的最大價值爲:%d",Maxvalue);
 return 0;
 } 

在這裏插入圖片描述

這樣不是最優的解,我們還可以空間優化,使用一維數組解決問題,同時從右往左填表。

int bag0_1(int v[100],int w[100],int n,int c)
{
 int i,j;
     for(int i = 0; i < n; i++){
        for(int j = c; j >= w[i]; j--)
            f[j] = max(f[j], f[j - w[i]] + v[i]);
    }
    return f[c];
}

這樣相當於永遠只保存一行數據,根據前面數組前面的數據更新後面的,最後就得到了上面圖片的最後一行。

題目會要求我們輸出最優解,而不只是最優解的答案,這時我們就無法在空間上對算法進行優化了,因爲我們需要每一次變化中保存的值,以回溯最優解(以上題的小偷爲例子)

 i = n;
 j = Maxweight;
 while(i>0){
    if(f[i][j] == f[i - 1][j])
    printf("未選第 %d 件物品\n",i);
    else if(f[i][j] == f[i - 1][j - w[i]] + v[i]) {
    printf("選第 %d 件物品\n",i);
        j -= w[i];
    }
    i--;
}

這裏是要求揹包裝有最大價值的物品,沒有規定必須將揹包裝滿,如果規定揹包必須裝滿,那麼除了f[0]初始化爲0,其他的f[1~C]都要初始化爲INT_MIN,可以理解爲沒有物品時,如果揹包容量爲0,那麼什麼都不裝就是剛好裝滿,價值爲0,而如果揹包容量大於0,說明初始情況除了f[0]外我們哪種情況都裝不滿,因此把那些無解的情況初始化爲負無窮。

  1. 完全揹包問題

如果不限定每種物品的數量,同一樣物品想拿多少拿多少,則問題稱爲無界或完全揹包問題。如果一件物品沒有件數限制,那麼我們可以取0、1、2、…至多可以取C/w[i]件,按照之前的分析,狀態轉移方程可以改寫爲f[i][j] = max( f[i - 1][j], f[i - 1][j - k * w[i]] + k * v[i] )其中k需滿足0 <= kw[i] <= j,那麼此時的時間複雜度就變成了O(nC*Σ(C/w[i]))

代碼如下:

int absolute_bag(int v[100],int w[100],int n,int c)
{
 int i,j;
  for(i=1;i<=n;i++)
  {
   for(j=1;j<=c;j++)
   {
    if(j<w[i])
    {
     f[i][j]=f[i-1][j];
  }
  else
  {
   int k;
   int maxV =j/w[i];
                    for(k=0;k<maxV+1;k++){
                        f[i][j]=max(f[i-1][j],f[i-1][j-k*w[i]]+k*v[i]);
             }
  }
   }
  }
  return f[n][c];
}

很明顯可以對它進行優化,先回想一下0-1揹包問題中的兩層循環,第一層爲0至n-1,第二層從右至左C至w[i],而這裏從右至左更新的原因,是爲了保證第 i 件物品的狀態一定由第 i - 1 件物品的狀態得來,也就是說,考慮第 i 件物品時,依據的是一個一定沒有選中過 i - 1 件物品的結論,因此如果將第二層循環改爲從左至右,由w[i]至C,就變成了選第 i 件物品時依然從已經拿過第 i 件物品的結論中遞推,此時的狀態轉移方程可以寫爲:

f[i][j] = max( f[i - 1][j], f[i][j - w[i]] + v[i] )

注意這裏變成了i,我們不再需要k這個變量

於是我們便可以寫出優化後解決完全揹包問題的代碼:

int absolute_bag(int v[100],int w[100],int n,int c)
{
 int i,j;
     for(i = 0; i < n; i++){
        for(j = w[i]; j<=c; j++)
             f[j] = max(f[j-w[i]]+v[i],f[j]);
    }
    return f[c];
}

理解了上面的兩個狀態轉移方程,就可以利用0-1揹包問題的解決思路,順利解決完全揹包問題。可以看到這和0-1揹包問題的寫法幾乎一致,不同的只是第二層循環變成了從左至右更新。

  1. 多重揹包問題

如果限定物品i最多隻能拿m[i]個,則問題稱爲有界或多重揹包問題。類似的,此時的狀態轉移方程可以寫爲:f[i][j] = max{f[i -
1][j - k * w[i]] + k * v[i] | 0 ≤ k ≤ m[i]}

代碼如下:

int double_bag(int v[100],int w[100],int num[100],int n,int c)
{
 int i,j;
  for(i=1;i<=n;i++)
  {
   for(j=1;j<=c;j++)
   {
    if(j<w[i])
    {
     f[i][j]=f[i-1][j];
  }
  else
  {
   int k;
   int maxV =min(num[i],j/w[i]);
                    for(k=0;k<maxV+1;k++){
                        f[i][j]=max(f[i-1][j],f[i-1][j-k*w[i]]+k*v[i]);
             }
  }
   }
  }
  return f[n][c];
}
  1. 混合三種揹包問題

如果物品中既有最多隻能拿m[i]個物品,又有不限數量的物品,又有隻有一件的物品,我們該如何選取呢?這個問題看起來非常複雜,但是其實仔細分析一下,我們可以將前面的三種揹包問題作爲三種不同的情況來將複雜的問題簡單化。這裏就可以體現出我們前面將三種情況抽象成函數的意義了:

int f[100];
for(int i = 0; i < n; i++){
    if(m[i] == 1)               // 爲0-1揹包
        bag0_1(v,w,n,c)
    else if(m[i] == INT_MAX)    // 爲完全揹包
       absolute_bag(v,w,n,c)
    else                        // 爲多重揹包
        double_bag( v,w,num, n,c)
}

參考優秀博客如下
上篇:揹包問題上
下篇:揹包問題下

另一篇: 揹包問題總結

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章