1. 程式人生 > >0/1揹包問題(遞迴解決,遞推解決)

0/1揹包問題(遞迴解決,遞推解決)

0-1揹包問題: 

有N件物品和一個容量為V的揹包。第i件物品的費用是c[i],價值是w[i]。求解將哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大。

 這個問題的特點是:每種物品只有一件,可以選擇放或者不放。

輸入格式:V,NW1,V1W2,V2......輸出格式: X

幫助理解:

比如01揹包問題。

/* 一個旅行者有一個最多能用M公斤的揹包,現在有N件物品,它們的重量分別是W1,W2,...,Wn,它們的價值分別為P1,P2,...,Pn.若每種物品只有一件求旅行者能獲得最大總價值。輸入格式:M,NW1,P1W2,P2......輸出格式: */

因為揹包最大容量M未知。所以,我們的程式要從1到M一個一個的試。比如,開始任選N件物品的一個。看對應M的揹包,能不能放進去,如果能放進去,並且還有多的空間,則,多出來的空間裡能放N-1物品中的最大價值。怎麼能保證總選擇是最大價值呢?看下錶。

測試資料:10,33,44,55,6

c[i][j]陣列儲存了1,2,3號物品依次選擇後的最大價值.

這個最大價值是怎麼得來的呢?從揹包容量為0開始,1號物品先試,0,1,2,的容量都不能放.所以置0,揹包容量為3則裡面放4.這樣,這一排揹包容量為4,5,6,....10的時候,最佳方案都是放4.假如1號物品放入揹包.則再看2號物品.當揹包容量為3的時候,最佳方案還是上一排的最價方案c為4.而揹包容量為5的時候,則最佳方案為自己的重量5.揹包容量為7的時候,很顯然是5加上一個值了。加誰??很顯然是7-4=3的時候.上一排 c3的最佳方案是4.所以。總的最佳方案是5+4為9.這樣.一排一排推下去。最右下放的資料就是最大的價值了。(注意第3排的揹包容量為7的時候,最佳方案不是本身的6.而是上一排的9.說明這時候3號物品沒有被選.選的是1,2號物品.所以得9.)

從以上最大價值的構造過程中可以看出。

f(n,m)=max{f(n-1,m), f(n-1,m-w[n])+P(n,m)}


遞迴解決:(主要思考下面的遞迴公式是否能包含所有情況)

揹包問題具有最優子結構,令f(n,C)代表,有n個待選物品,揹包容量為C時的最優解,此時物品選擇向量為y=[y1,y2,…yn], 那麼當yn=1時,y’=[y1, y2, …yn-1],必然為f(n-1, C-wn)的物品選擇向量,當yn=0時,必然為f(n-1,C)的最優物品選擇向量。所以揹包問題可以由動態規劃來求解。

    根據上面的分析,我們可以得到如下的遞迴式:

    當wn>C時,  f(n,C)=f(n+

1,C);

    當wn<=C時,f(n,C) = max(f(n+1,C), vn+f(n+1, C-wn) );//f(n+1,C)當前物品沒有選擇,vn+f(n+1, C-wn)選擇了當前物品

    初始條件為:f(i, 0) = 0; f(0,i) = 0; f(0,0) = 0;

說明:如果理解不動,可以參考下面動態規劃實現中的那個圖仔細想想

程式實現:

import java.util.Scanner;

//遞迴解決的揹包問題
public class BeiBao01DG {

    private static int rl;//揹包容量
    private static int num;//物品個數
    private static int[] w;//物品重量(重量和價值一下標相關聯)
    private static int[] v;//物品價值
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Scanner sc=new Scanner(System.in);
        rl=sc.nextInt();
        num=sc.nextInt();
        w=new int[num];
        v=new int[num];
        for(int i=0;i<num;i++)
        {
            w[i]=sc.nextInt();
            v[i]=sc.nextInt();
        }
        System.out.println(f(0,rl));
        
    }
    //cur(0---num-1)代表物品編號, max代表從第一個物品到現在的物品揹包還剩下的空間
    public static int f(int cur,int max)
    {
        if(cur==num)//物品已經嘗試完
         return 0;
        if(max<=0)//揹包已經裝滿,沒有容量了
            return 0;
        if(max<w[cur])//揹包容量小於該商品的重量
            return f(cur+1,max);
        else
        {
            return Math.max(f(cur+1,max), f(cur+1,max-w[cur])+v[cur]);
        }
    }

}

注意:此處物品可以隨便輸入,不用排序,結果沒有影響,遞迴是拿著揹包容量向下推的,動態規劃已知前面的推出後面的,需要找狀態轉換公式,也就是根據遞迴公式來得到狀態轉換公式

擴充套件問題:

輸出哪些物品裝入揹包可使這些物品的費用總和不超過揹包容量,且價值總和最大???

動態規劃解決:

演算法基本思想:

利用動態規劃思想 ,子問題為:f[i][v]表示前i件物品恰放入一個容量為v的揹包可以獲得的最大價值。

其狀態轉移方程是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}    //這個方程非常重要,基本上所有跟揹包相關的問題的方程都是由它衍生出來的。

解釋一下上面的方程:“將前i件物品放入容量為v的揹包中”這個子問題,如果只考慮第i件物品放或者不放,那麼就可以轉化為只涉及前i-1件物品的問題,即1、如果不放第i件物品,則問題轉化為“前i-1件物品放入容量為v的揹包中”;2、如果放第i件物品,則問題轉化為“前i-1件物品放入剩下的容量為v-c[i]的揹包中”(此時能獲得的最大價值就是f [i-1][v-c[i]]再加上通過放入第i件物品獲得的價值w[i])。則f[i][v]的值就是1、2中最大的那個值。

(注意:f[i][v]有意義當且僅當存在一個前i件物品的子集,其費用總和為v。所以按照這個方程遞推完畢後,最終的答案並不一定是f[N] [V],而是f[N][0..V]的最大值。)

優化空間複雜度:

以上方法的時間和空間複雜度均為O(N*V),其中時間複雜度基本已經不能再優化了,但空間複雜度卻可以優化到O(V)。

上面f[i][v]使用二維陣列儲存的,可以優化為一維陣列f[v],將主迴圈改為:

for i=1..N

for v=V..0

f[v]=max{f[v],f[v-c[i]]+w[i]};

即將第二層迴圈改為從V..0,逆序。

解釋一下:

假設最大容量M=10,物品個數N=3,物品大小w{3,4,5},物品價值p{4,5,6}。

當進行第i次迴圈時,f[v]中儲存的是上次迴圈產生的結果,即第i-1次迴圈的結果(i>=1)。所以f[v]=max{f[v],f[v-c[i]]+w[i]}這個式子中,等號右邊的f[v]和f[v-c[i]]+w[i]都是前一次迴圈產生的值。

當i=1時,f[0..10]初始值都為0。所以

f[10]=max{f[10],f[10-c[1]]+w[1]}=max{0,f[7]+4}=max{0,0+4}=4;

f[9]=max{f[9],f[9-c[1]]+w[1]}=max{0,f[6]+4}=max{0,0+4}=4;

......

f[3]=max{f[3],f[3-c[1]]+w[1]}=max{0,f[3]+4}=max{0,0+4}=4;

f[2]=max{f[2],f[2-c[1]]+w[1]}=max{0,f[2-3]+4}=0;//陣列越界?

f[1]=0;

f[0]=0;

當i=2時,此時f[0..10]經過上次迴圈後,都已經被重新賦值,即f[0..2]=0,f[3..10]=4。利用f[v]=max{f[v],f[v-c[i]]+w[i]}這個公式計算i=2時的f[0..10]的值。

當i=3時同理。

具體的值如下表所示:

因此,利用逆序迴圈就可以保證在計算f[v]時,公式f[v]=max{f[v],f[v-c[i]]+w[i]}中等號右邊的f[v]和f[v-c[i]]+w[i]儲存的是f[i-1][v]和f[i -1][v-c[i]]的值

當i=N時,得到的f[V]即為要求的最優值。

初始化的細節問題:

 在求最優解的揹包問題中,一般有兩種不同的問法:1、要求恰好裝滿揹包時的最優解;2、求小於等於揹包容量的最優解,即不一定恰好裝滿揹包。

這兩種問法,在初始化的時候是不同的。

1、要求恰好裝滿揹包時的最優解:

在初始化時除了f[0]0其它f[1..V]均設為-∞,這樣就可以保證最終得到的f[N]是一種恰好裝滿揹包的最優解。如果不能恰好滿足揹包容量,即不能得到f[V]的最優值,則此時f[V]=-∞,這樣就能表示沒有找到恰好滿足揹包容量的最優值。

2、求小於等於揹包容量的最優解,即不一定恰好裝滿揹包:

如果並沒有要求必須把揹包裝滿,而是隻希望價值儘量大,初始化時應該將f[0..V]全部設為0。

總結

01揹包問題是最基本的揹包問題,它包含了揹包問題中設計狀態、方程的最基本思想,另外,別的型別的揹包問題往往也可以轉換成01揹包問題求解。故一定要仔細體會上面基本思路的得出方法,狀態轉移方程的意義,以及最後怎樣優化的空間複雜度。

0-1揹包問題程式碼:

程式碼1 複製程式碼
#include <iostream>
#include <vector>
using namespace std;
const int MIN=0x80000000;
const int N=3;   //物品數量const int V=5;  //揹包容量int f[N+1][V+1];

int Package(int *W,int *C,int N,int V);
void main(int argc,char *argv[])
{
 int W[4]={0,7,5,8};      //物品權重 int C[4]={0,2,3,4};      //物品大小 int result=Package(W,C,N,V);
 if(result>0)
 {
  cout<<endl;
  cout<<"the opt value:"<<result<<endl;
  int i=N,j=V;
  while(i)
  {
   if(f[i][j]==(f[i-1][j-C[i]]+W[i]))
   {
    cout<<i<<":"<<"w="<<W[i]<<",c="<<C[i]<<endl;
    j-=C[i];
   }
   i--;
  }
 }
 else
  cout<<"can not find the opt value"<<endl;
 return;
}

int Package(int *W,int *C,int N,int V)
{
 int i,j;
 memset(f,0,sizeof(f));  //初始化為0
 for(i=0;i<=N;i++)
 for(j=1;j<=V;j++)               //此步驟是解決是否恰好滿足揹包容量,  f[i][j]=MIN;                //若“恰好”滿足揹包容量,即正好裝滿揹包,則加上此步驟,若不需要“恰好”,則初始化為0    
 for(i=1;i<=N;i++)
  for(j=C[i];j<=V;j++)
  {
   f[i][j]=(f[i-1][j]>f[i-1][j-C[i]]+W[i])?f[i-1][j]:(f[i-1][j-C[i]]+W[i]);
   cout<<"f["<<i<<"]["<<j<<"]="<<f[i][j]<<endl;
  }
 return f[N][V];
}
複製程式碼 程式碼2 複製程式碼
#include <iostream>
#include <vector>
using namespace std;
const int MIN=0x80000000;
const int N=3;   //物品數量const int V=5;  //揹包容量int f[V+1];

int Package(int *W,int *C,int N,int V);
void main(int argc,char *argv[])
{
 int W[4]={0,7,5,8};      //物品權重 int C[4]={0,2,3,4};      //物品大小 int result=Package(W,C,N,V);
 if(result>0)
 {
  cout<<endl;
  cout<<"the opt value:"<<result<<endl;
 }
 else
  cout<<"can not find the opt value"<<endl;
 return;
}

int Package(int *W,int *C,int N,int V)
{
 int i,j;
 memset(f,0,sizeof(f));  //初始化為0
 for(i=1;i<=V;i++)               //此步驟是解決是否恰好滿足揹包容量,  f[i]=MIN;                //若“恰好”滿足揹包容量,即正好裝滿揹包,則加上此步驟,若不需要“恰好”,則初始化為0    
 for(i=1;i<=N;i++)
  for(j=V;j>=C[i];j--)    //注意此處與解法一是順序不同的,弄清原因  {
   f[j]=(f[j]>f[j-C[i]]+W[i])?f[j]:(f[j-C[i]]+W[i]);
   cout<<"f["<<i<<"]["<<j<<"]="<<f[j]<<endl;
  }
 return f[V];
}

參考網站:http://www.cnblogs.com/justinzhang/archive/2012/04/10/2441199.html