1. 程式人生 > >0-1揹包問題分析

0-1揹包問題分析

一、題目 :

分別用蠻力法、動態規劃法、回溯法和分支限界法求解0/1揹包問題。

注:0/1揹包問題:給定種物品和一個容量為的揹包,物品的重量是,其價值為,揹包問題是如何使選擇裝入揹包內的物品,使得裝入揹包中的物品的總價值最大。其中,每種物品只有全部裝入揹包或不裝入揹包兩種選擇。

二、所用演算法的基本思想及複雜度分析:

1.蠻力法求解0/1揹包問題:

1)基本思想:

對於有n種可選物品的0/1揹包問題,其解空間由長度為n的0-1向量組成,可用子集數表示。在搜尋解空間樹時,深度優先遍歷,搜尋每一個結點,無論是否可能產生最優解,都遍歷至葉子結點,記錄每次得到的裝入總價值,然後記錄遍歷過的最大價值。

2)程式碼:

#include <iostream>
#include<cstdio>

using namespace std;

#define N 100

struct goods{
    int sign;//物品序號
    int wight;//物品重量
    int value;//物品價值
};

int n,bestValue,cv,cw,C;//物品數量,價值最大,當前價值,當前重量,揹包容量
int X[N],cx[N];//最終儲存狀態,當前儲存狀態
struct goods goods[N];

int Force(int i){
if(i>n-1){ if(bestValue < cv && cw + goods[i].wight <= C){ for(int k=0;k<n;k++) X[k] = cx[k];//儲存最優路徑 bestValue = cv; } return bestValue; } cw = cw + goods[i].wight; cv = cv + goods[i].value; cx[i] = 1;//裝入揹包 Force(i+1); cw = cw-goods[i].wight; cv
= cv-goods[i].value; cx[i] = 0;//不裝入揹包 Force(i+1); return bestValue; } int main() { printf("物品種類n:"); scanf("%d",&n); printf("揹包容量C:"); scanf("%d",&C); for(int i=0;i<n;i++){ printf("物品%d的重量w[%d]及其價值v[%d]:",i+1,i+1,i+1); scanf("%d%d",&goods[i].wight,&goods[i].value); } int sum1 = Force(0); printf("蠻力法求解0/1揹包問題:\nX=["); for(int i=0;i<n;i++){ cout << X[i]<<" "; } printf("] 裝入總價值%d\n",sum1); return 0; }

P.S.蠻力法使用的是遞迴,遞迴的使用經常會寄幾個看不懂T_T,這裡再提一下遞迴的問題(以後不要再總是看不懂啦,這樣會顯得自己很辣雞的有木有!!!)

force(0),向下執行,到force(1),進入force(1),一直到force(n+1),i>n,return 結果,跳出force(n+1),在force(n)處從跳出的地方繼續向下走,就是進入減減減的環節了,然後繼續向下,還是一樣,加到n+1時就會跳出來當前的force,調到前一個force,繼續向下,迴圈進行。

 

3)複雜度分析:

蠻力法求解0/1揹包問題的時間複雜度為:2^n

2.動態規劃法求解0/1揹包問題:

1)基本思想:

令表示在前個物品中能夠裝入容量為的揹包中的物品的最大值,則可以得到如下動態函式:

            

 

   2)程式碼:

#include <iostream>
#include<cstdio>
#define N 100
#define MAX(a,b) a < b ? b : a
using namespace std;

struct goods{
    int sign;//物品序號
    int wight;//物品重量
    int value;//物品價值
};

int n,bestValue,cv,cw,C;//物品數量,價值最大,當前價值,當前重量,揹包容量
int X[N],cx[N];//最終儲存狀態,當前儲存狀態
struct goods goods[N];

int KnapSack(int n,struct goods a[],int C,int x[]){
    int V[N][10*N];
    for(int i = 0; i <= n; i++)//初始化第0列
        V[i][0] = 0;
    for(int j = 0; j <= C; j++)//初始化第0行
        V[0][j] = 0;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= C; j++)
        if(j < a[i-1].wight)
            V[i][j] = V[i-1][j];
        else
            V[i][j] = MAX(V[i-1][j],V[i-1][j-a[i-1].wight] + a[i-1].value);

    for(int i = n,j = C; i > 0; i--){
        if(V[i][j] > V[i-1][j]){
            x[i-1] = 1;
            j = j - a[i-1].wight;
        }
        else
            x[i-1] = 0;
    }
    return V[n][C];
}
int main()
{
    printf("物品種類n:");
    scanf("%d",&n);
    printf("揹包容量C:");
    scanf("%d",&C);
    for(int i = 0; i < n; i++){
        printf("物品%d的重量w[%d]及其價值v[%d]:",i+1,i+1,i+1);
        scanf("%d%d",&goods[i].wight,&goods[i].value);
    }
    int sum2 = KnapSack(n,goods,C,X);
     printf("動態規劃法求解0/1揹包問題:\nX=[");
     for(int i = 0; i < n; i++)
        cout<<X[i]<<" ";//輸出所求X[n]矩陣
     printf("]   裝入總價值%d\n", sum2);
     return 0;
}

3)複雜度分析:

動態規劃法求解0/1揹包問題的時間複雜度為:n*C

3.回溯法求解0/1揹包問題:

1)基本思想:

回溯法:為了避免生成那些不可能產生最佳解的問題狀態,要不斷地利用限界函式(bounding function)來處死那些實際上不可能產生所需解的活結點,以減少問題的計算量。這種具有限界函式的深度優先生成法稱為回溯法。

對於有n種可選物品的0/1揹包問題,其解空間由長度為n的0-1向量組成,可用子集數表示。在搜尋解空間樹時,只要其左兒子結點是一個可行結點,搜尋就進入左子樹。當右子樹中有可能包含最優解時就進入右子樹搜尋。

2)程式碼:

#include <iostream>
#include<cstdio>
#include<string.h>
#include<algorithm>

using namespace std;

#define N 100

struct goods{
int wight;//物品重量
int value;//物品價值
};

int n,bestValue,cv,cw,C;//物品數量,價值最大,當前價值,當前重量,揹包容量
int X[N],cx[N];//最終儲存狀態,當前儲存狀態
struct goods goods[N];

int BackTrack(int i){
    if(i > n-1){
        if(bestValue < cv){
            for(int k = 0; k < n; k++)
                X[k] = cx[k];//儲存最優路徑
            bestValue = cv;
        }
        return bestValue;
    }
    if(cw + goods[i].wight <= C){//進入左子樹
        cw += goods[i].wight;
        cv += goods[i].value;
        cx[i] = 1;//裝入揹包
        BackTrack(i+1);
        cw -= goods[i].wight;
        cv -= goods[i].value;//回溯,進入右子樹
    }
    cx[i] = 0;//不裝入揹包
    BackTrack(i+1);
    return bestValue;
}

bool m(struct goods a, struct goods b){
    return (a.value/a.wight) > (b.value/b.wight);
}

int KnapSack3(int n, struct goods a[], int C,int x[N]){
    memset(x,0,sizeof(x));
    sort(a,a+n,m);//將各物品按單位重量價值降序排列
    BackTrack(0);
    return bestValue;
}
int main()
{
    printf("物品種類n:");
    scanf("%d",&n);
    printf("揹包容量C:");
    scanf("%d",&C);
    for(int i = 0; i < n; i++){
        printf("物品%d的重量w[%d]及其價值v[%d]:",i+1,i+1,i+1);
        scanf("%d%d",&goods[i].wight,&goods[i].value);
    }
    int sum3 = KnapSack3(n,goods,C,X);
    printf("回溯法求解0/1揹包問題:\nX=[");
    for(int i = 0; i < n; i++)
        cout << X[i] <<" ";//輸出所求X[n]矩陣
    printf("]   裝入總價值%d\n",sum3);
    return 0;
}

4.分支限界法求解揹包問題:

1)基本思想:

分支限界法類似於回溯法,也是在問題的解空間上搜索問題解的演算法。一般情況下,分支限界法與回溯法的求解目標不同。回溯法的求解目標是找出解空間中滿足約束條件的所有解,而分支限界法的求解目標則是找出滿足約束條件的一個解,或是在滿足約束條件的解中找出使某一目標函式值達到極大或極小的解,即在某種意義下的最優解。

首先,要對輸入資料進行預處理,將各物品依其單位重量價值從大到小進行排列。

在下面描述的優先佇列分支限界法中,節點的優先順序由已裝袋的物品價值加上剩下的最大單位重量價值的物品裝滿剩餘容量的價值和。

演算法首先檢查當前擴充套件結點的左兒子結點的可行性。如果該左兒子結點是可行結點,則將它加入到子集樹和活結點優先佇列中。當前擴充套件結點的右兒子結點一定是可行結點,僅當右兒子結點滿足上界約束時才將它加入子集樹和活結點優先佇列。當擴充套件到葉節點時為問題的最優值。

2)程式碼:

#include<iostream>
#include<algorithm>
using namespace std;
#define N 100 //最多可能物體數
struct goods  //物品結構體
{
    int sign;  //物品序號
    int w; //物品重量
    int p; //物品價值
}a[N];

bool m(goods a,goods b)
{
    return (a.p/a.w)>(b.p/b.w);
}

int max(int a,int b)
{
    return a<b?b:a;
}

int n,C,bestP=0,cp=0,cw=0;

int X[N],cx[N];

struct KNAPNODE   //狀態結構體
{
    bool s1[N]; //當前放入物體
    int k;     //搜尋深度
    int b; //價值上界
    int w; //物體重量
    int p; //物體價值
};

struct HEAP   //堆元素結構體
{
    KNAPNODE *p;//結點資料
    int b;        //所指結點的上界
};

//交換兩個堆元素
void swap(HEAP &a, HEAP&b)
{
    HEAP temp = a;
    a = b;
    b = temp;
}

//堆中元素上移
void mov_up(HEAP H[], int i)
{
    bool done = false;
    if(i!=1){
       while(!done && i!=1){
           if(H[i].b>H[i/2].b){
              swap(H[i], H[i/2]);
           }else{
              done = true;
           }
           i = i/2;
       }
    }
}

//堆中元素下移
void mov_down(HEAP H[], int n, int i)
{
    bool done = false;
    if((2*i)<=n){
       while(!done && ((i = 2*i) <= n)){
           if(i+1<=n && H[i+1].b > H[i].b){
              i++;
           }

           if(H[i/2].b<H[i].b){
              swap(H[i/2], H[i]);
           }else{
              done = true;
           }
       }
    }
}

//往堆中插入結點
void insert(HEAP H[], HEAP x, int &n)
{
    n++;
    H[n] = x;
    mov_up(H,n);
}

//刪除堆中結點
void del(HEAP H[], int &n, int i)
{
    HEAP x, y;
    x = H[i]; y = H[n];
    n --;
    if(i<=n){
       H[i] = y;
       if(y.b>=x.b){
           mov_up(H,i);
       }else{
           mov_down(H, n, i);
       }
    }
}

//獲得堆頂元素並刪除
HEAP del_top(HEAP H[], int&n)
{
    HEAP x = H[1];
    del(H, n, 1);
    return x;
}

//計算分支節點的上界
void bound( KNAPNODE* node,int M, goods a[], int n)
{
    int i = node->k;
    float w = node->w;
    float p = node->p;
    if(node->w>M){   //  物體重量超過揹包載重量
       node->b = 0;   //  上界置為0
    }else{
       while((w+a[i].w<=M)&&(i<n)){  
           w += a[i].w;   // 計算揹包已裝入載重
           p += a[i++].p; //    計算揹包已裝入價值
       }
       if(i<n){
           node->b = p + (M - w)*a[i].p/a[i].w;
       }else{
           node -> b = p;
       }
    }
}

//用分支限界法實現0/1揹包問題
int KnapSack4(int n,goodsa[],int C, int X[])
{
    int i, k = 0;      // 堆中元素個數的計數器初始化為0
    int v;
    KNAPNODE *xnode, *ynode, *znode;
    HEAP x, y, z, *heap;
    heap = new HEAP[n*n];        // 分配堆的儲存空間
    for( i=0; i<n; i++){
       a[i].sign=i;        //記錄物體的初始編號
    }
    sort(a,a+n,m);             // 對物體按照價值重量比排序
    xnode = new KNAPNODE;        // 建立父親結點
    for( i=0; i<n; i++){           //  初始化結點
       xnode->s1[i] = false;
    }
    xnode->k = xnode->w = xnode->p = 0;
    while(xnode->k<n) {
       ynode = new KNAPNODE;     // 建立結點y
       *ynode = *xnode;         //結點x的資料複製到結點y
       ynode->s1[ynode->k] = true;     //  裝入第k個物體
       ynode->w += a[ynode->k].w;     //  揹包中物體重量累計
       ynode->p += a[ynode->k].p;     //  揹包中物體價值累計
       ynode->k ++;               //  搜尋深度++
       bound(ynode, C, a, n); //       計算結點y的上界
       y.b = ynode->b;
       y.p = ynode;
        insert(heap, y, k);        //結點y按上界的值插入堆中
       znode = new KNAPNODE;     // 建立結點z
       *znode = *xnode;          //結點x的資料複製到結點z
       znode->k++;                         //   搜尋深度++
       bound(znode, C, a, n); //計算節點z的上界
       z.b = znode->b;
       z.p = znode;
       insert(heap, z, k);     //結點z按上界的值插入堆中
       delete xnode;
       x = del_top(heap, k);   //獲得堆頂元素作為新的父親結點
       xnode = x.p;
    }
    v = xnode->p;
    for( i=0; i<n; i++){     //取裝入揹包中物體在排序前的序號
       if(xnode->s1[i]){
           X[a[i].sign] =1 ;
       }else{
           X[a[i].sign] = 0;
       }
    }
    delete xnode;
    delete heap;
    return v;              //返回揹包中物體的價值
}

/*測試以上演算法的主函式*/
int main()
{
    goods b[N];
    printf("物品種數n: ");
    scanf("%d",&n);   //輸入物品種數
    printf("揹包容量C: ");
    scanf("%d",&C);   //輸入揹包容量
    for (int i=0;i<n;i++)    //輸入物品i的重量w及其價值v
    {
       printf("物品%d的重量w[%d]及其價值v[%d]:  ",i+1,i+1,i+1);
       scanf("%d%d",&a[i].w,&a[i].p);
       b[i]=a[i];
    }

int sum4=KnapSack4(n,a,C,X);//呼叫分支限界法求0/1揹包問題
    printf("分支限界法求解0/1揹包問題:\nX=[ ");
    for(i=0;i<n;i++)
       cout<<X[i]<<" ";//輸出所求X[n]矩陣
    printf("]  裝入總價值%d\n",sum4);
    return 0;
}