1. 程式人生 > >動態規劃之揹包問題(C語言)

動態規劃之揹包問題(C語言)

動態規劃

動態規劃(英語:Dynamic programming,簡稱DP)是一種通過把原問題分解為相對簡單的子問題的方式求解複雜問題的方法。
動態規劃常常適用於有重疊子問題和最優子結構性質的問題
動態規劃思想大致上為:若要解一個給定問題,我們需要解其不同部分(即子問題),再合併子問題的解以得出原問題的解。
由於通常許多子問題非常相似,為此動態規劃法試圖僅僅解決每個子問題一次,從而減少計算量:一旦某個給定子問題的解已經算出,則將其記憶化儲存,以便下次需要同一個子問題解之時直接查表。這種做法在重複子問題的數目關於輸入的規模呈指數增長時特別有用。

揹包問題

揹包問題可以描述為:給定一組物品,每種物品都有自己的重量和價格,在限定的總重量內,如何選擇才能使得物品的總價格最高。
揹包問題是典型的動態規劃問題。

而揹包問題還存在需要恰好裝滿揹包和不需要恰好裝滿兩種情況
這篇文章所探討的所有揹包問題都是建立在不需要恰好裝滿的情況下

由簡單揹包問題的基礎上又衍生出許多問題都可以採用動態規劃解決。
例如:
1. 01揹包問題(每種物品只有一件,放或者不放)
2. 完全揹包問題(每件物品有無限件可用)
3. 多重揹包問題(每件物品有n[i]件可用)

01揹包問題

題目:有N件物品和一個容量為V的揹包。第i件物品的費用是weight[i],價值是value[i]。求將哪些物品裝入揹包可使價值總和最大。

01揹包問題是最基礎的揹包問題,”01”的意思是:每種物品僅有一件,放為“1”,不放為“0”。
我們假定f[i][v]為將前i件物品前恰好放入一個容量為V的揹包中可獲得的最大價值


則其狀態轉移方程是:

f[i][V]=max{f[i-1][V],f[i-1][V-weight[i]]+value[i]}

狀態轉移方程分析
由上圖可知,揹包問題可以簡化為“將前i件物品放入容量為V的揹包中”的問題,而這個問題可以優化為,“不放第i件物品“和“放第i件物品“的問題。

如果不放第i件物品,問題為將前i-1件物品放入容量為V的揹包中,總價值為f[i-1][v]

如果放第i件物品,問題為前i-1件物品放入剩下的容量為V-weight[i]的揹包中,價值為f[i-1][V-weight[i]]+value[i]
程式碼部分較為簡單,主要學習動態規劃這種思想:

    for (int i
=1; i<=N; i++) for (int j=1; j<=M; j++) { if (weight[i]<=j) { f[i][j]=max(f[i-1][j],f[i-1][j-weight[i]]+value[i]); } else f[i][j]=f[i-1][j]; }

該演算法的時間複雜度一定降到最低,但是我們還可以通過壓縮空間複雜度的方式優化程式碼,方法就是將二維陣列f[i][j]轉化為一維陣列[j]只需將完整程式稍作修改即可,這裡只給出核心程式碼。

    for(int i = 1; i <= N; ++i)
        for(int j = V; j >= weight[i]; --j)
            f[j] = max(f[j], f[j - weight[i]] + value[i]);  

具體程式碼如下:

#include<stdio.h>   
#define  V 1500  
int f[10][V];//全域性變數,自動初始化為0  
int weight[10];  
int value[10];  
#define  max(x,y)   (x)>(y)?(x):(y)  
int main()  
{  

    //狀態轉移方程 f[i+1][j]=max{f[i][j],f[i][j-weight[i+1]+value[i+1]}
    int N,M; 
    freopen("1.txt","r",stdin); 
    scanf("%d%d",&N,&M);//N物品個數 M揹包容量  
    for (int i=1;i<=N; i++)  
    {  
        scanf("%d%d",&weight[i],&value[i]); 
    }  
    //動態規劃
    for (int i=1; i<=N; i++)  
        for (int j=1; j<=M; j++)  
        {  
            if (weight[i]<=j)  
            {  
                f[i][j]=max(f[i-1][j],f[i-1][j-weight[i]]+value[i]);  
            }  
            else  
                f[i][j]=f[i-1][j];  
        }  
      printf("%d\n",f[N][M]);//輸出最優解
 }

到這裡已經基本實現01揹包問題,但是該程式在輸出的時候,只能輸出最後的價值,不能知道選擇的物品是哪個。在這裡我們定義一個數組x[i],對於每一個物品,如果被選擇置為“1”,否則為“0”。通過這樣的方式來實現。
修改之後的程式碼如下:

#include<stdio.h>   
#define  V 100  
int f[10][V];//全域性變數,自動初始化為0  
int weight[10];  
int value[10];  
#define  max(x,y)   (x)>(y)?(x):(y)  
int main()  
{  
    //初始化
    int N,M; 
    freopen("1.txt","r",stdin); 
    scanf("%d%d",&N,&M);//N物品個數 M揹包容量  
    for (int i=1;i<=N; i++)  
    {  
        scanf("%d%d",&weight[i],&value[i]); 
    }  
    //動態規劃分析
    for (int i=1; i<=N; i++)  
        for (int j=1; j<=M; j++)  
        {  
            if (weight[i]<=j)  
            {  
                f[i][j]=max(f[i-1][j],f[i-1][j-weight[i]]+value[i]);  
            }  
            else  
                f[i][j]=f[i-1][j];  
        }  
      printf("%d\n",f[N][M]);//輸出最優解

     //輸出選擇的物品
    int j = M;
    int x[V];
    for(int i=N; i>0; --i)
    {
        if(f[i][j] > f[i-1][j])
        {
            x[i-1] = 1;
            j = j - weight[i-1];//裝入第i-1個寶石後背包能裝入的體積就只剩下j - V[i-1]
        }
    }
    for(int i=0; i<N; ++i)  
        printf("%d ", x[i]);  

}   

完全揹包問題

題目:有N件物品和一個容量為V的揹包。第i件物品的費用是weight[i],價值是value[i]。每件物品可以無限選用,求將哪些物品裝入揹包可使價值總和最大。
完全揹包問題不設定物品取用上限

對於演算法的優化我們可以這樣想:
在01揹包問題中,我們要保證第i次迴圈中的f[i][v]是由f[i-1][V-weight[i]]遞推而來,每一次都是“加選出一個(即一種)物品”而這種方式同時也保證了每件物品只選一次。
而完全揹包問題的特點剛好是每種物品可選無限件,所以在考慮“加選出一個(即一種)物品”時就是單純的考慮“加選出一個(可能為同一種)物品”,這樣我們就需要考慮選入的物品是已經選入的情況。相比來說,反而簡化了程式碼。

同樣,我們假定f[i][v]為將前i件物品前恰好放入一個容量為V的揹包中可獲得的最大價值
則其狀態轉移方程是:

f[i][V]=max{f[i-1][V],f[i-1][V-k*weight[i]]+k*value[i]}

0<=k*weight[i]<=v,其中0<=k<=V/weight[i+1]

該狀態方程等價於f[i][v]=max{f[i-1][v],f[i-1][V-weight[i]]+value[i]}
程式碼部分如下:

    for(int i = 1; i <= N; ++i)
        for(int j = 0; j <= M; ++j)
            for(int k = 0; k * weight[i] <= j; ++k)
            f[i][j] = max(f[i][j], f[i - 1][j - k * weight[i]] + k * value[i]);

一維陣列優化:

    for(int i = 1; i <= N; ++i)
        for(int j = weight[i]; j <= V; ++j)
            f[j] = max(f[j], f[j - weight[i]] + value[i]); 

具體程式碼如下:

#include<stdio.h>    
#define  V 1500  
int f[10][V];//全域性變數,自動初始化為0  
int weight[10];  
int value[10];  
#define  max(x,y)   (x)>(y)?(x):(y)  
int main()  
{  
    //f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v},其中0<=k<=V/weight[i+1]

    //f[j]=max(f[j],f[j-weight[i]]+value[i])    
    int N,M;  
    //初始化
    freopen("1.txt","r",stdin); 
    scanf("%d%d",&N,&M);//N物品個數 M揹包容量  
    for (int i=1;i<=N; i++)  
    {  
        scanf("%d%d",&weight[i],&value[i]); 
    }  
     //動態規劃
    for(int i = 1; i <= N; ++i)
        for(int j = 0; j <= M; ++j)
            for(int k = 0; k * weight[i] <= j; ++k)
            f[i][j] = max(f[i][j], f[i - 1][j - k * weight[i]] + k * value[i]);
     printf("%d",f[N][M]);//輸出最優解  

}

多重揹包問題

題目:有N件物品和一個容量為V的揹包。第i件物品最多有n[i]個,每個的費用是weight[i],價值是value[i]。每件物品最多可以選用相應的最大個數,求將哪些物品裝入揹包可使價值總和最大。
多重揹包問題設定物品選擇上限
程式碼部分如下:

    for(int i = 1; i <= N; ++i)
        for(int j = 0; j <= V; ++j)
            for(int k = 0; k <= num[i] && k * weight[i] <= j; ++k)
                f[i][j] = max(f[i-1][j], f[i - 1][j - k * weight[i]] + k * value[i]);

一維陣列優化:

    for(int i = 1; i <= N; ++i)
        for(int j = V; j >= 0; --j)
            for(int k = 1; k <= num[i] && k * weight[i] <= j; ++k)
                f[j] = max(f[j], f[j - k * weight[i]] + k * value[i]);  

具體程式碼如下:

#include<stdio.h>    
#define  V 1500  
int f[10][V];//全域性變數,自動初始化為0  
int weight[10];  
int value[10];   
int num[10];
#define  max(x,y)   (x)>(y)?(x):(y)  
int main()  
{  
    //f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v},其中0<=k<=V/weight[i+1]

    //f[j]=max(f[j],f[j-weight[i]]+value[i])    
    int N,M,cur;  
    freopen("2.txt","r",stdin); 
    scanf("%d%d",&N,&M);//N物品個數 M揹包容量  
    for (int i=1;i<=N; i++)  
    {  
        scanf("%d%d%d",&weight[i],&value[i],&num[i]); 
    }  

    for(int i = 1; i <= N; ++i)
        for(int j = 0; j <= V; ++j)
            for(int k = 0; k <= num[i] && k * weight[i] <= j; ++k)
                f[i][j] = max(f[i-1][j], f[i - 1][j - k * weight[i]] + k * value[i]);
      printf("%d\n",f[N][M]);//輸出最優解
}

注:由於測試輸入資料較多,程式可以採用檔案輸入
5 10
2 6
2 3
6 5
5 4
4 6
多重揹包需要輸入每件物品的可選用次數,所以它的輸入檔案有所不同
5 10
2 6 1
2 3 1
6 5 1
5 4 1
4 6 1
之所以將次數都定為1,是說明01揹包問題是多重揹包的一種特殊情況。