回溯法之0-1背包問題


 問題描述:  

     給定n種物品和一背包。物品i的重量是wi,其價值為vi,背包的容量為C。問:應如何選擇裝入背包的物品,使得裝入背包中物品的總價值最大?例如

     形式化描述:給定c >0, wi >0, vi >0 , 1≤i≤n.要求找一n元向量(x1,x2,…,xn,), xi∈{0,1}, ∋ ∑ wi xi≤c,且∑ vi xi達最大.即一個特殊的整數規划問題。

      問題解析:0-1背包問題是子集選取問題。0-1 背包問題的解空間可以用子集樹表示。在搜索解空間樹時,只要其左兒子節點是一個可行節點,搜索就進入左子樹。當右子樹中有可能含有最優解時,才進入右子樹搜索。否則,將右子樹剪去。設r是當前剩余物品價值總和,cp是當前價值;bestp是當前最優價值。當cp+r<=bestp時,可剪去右子樹計算右子樹上界的更好的方法是將剩余物品依次按其單位價值排序,然后依次裝入物品,直至裝不下時,再裝入物品一部分而裝滿背包。

     例如:對於0-1背包問題的一個實例,n=4,c=7,p=[9,10,7,4],w=[3,5,2,1]。這4個物品的單位重量價值分別為[3,2,3,5,4]。以物品單位重量價值的遞減序裝入物品。先裝入物品4,然后裝入物品3和1.裝入這3個物品后,剩余的背包容量為1,只能裝0.2的物品2。由此得一個解為[1,0.2,1,1],其相應價值為22。盡管這不是一個可行解,但可以證明其價值是最優值的上界。因此,對於這個實例,最優值不超過22。

     在實現時,由Bound計算當前節點處的上界。類Knap的數據成員記錄解空間樹中的節點信息,以減少參數傳遞調用所需要的棧空間。在解空間樹的當前擴展節點處,僅要進入右子樹時才計算上界Bound,以判斷是否可將右子樹剪去。進入左子樹時不需要計算上界,因為上界預期父節點的上界相同。算法的具體實現如下:

代碼實現:

#include <iostream>
#include <stdio.h>
//#include <conio.h>
using namespace std;
int n;//物品數量
double c;//背包容量
double v[100];//各個物品的價值 value
double w[100];//各個物品的重量 weight
double cw = 0.0;//當前背包重量 current weight
double cp = 0.0;//當前背包中物品總價值 current value
double bestp = 0.0;//當前最優價值best price
double perp[100];//單位物品價值(排序后) per price
int order[100];//物品編號
int put[100];//設置是否裝入,為1的時候表示選擇該組數據裝入,為0的表示不選擇該組數據
 
 
//按單位價值排序
void knapsack()
{
    int i,j;
    int temporder = 0;
    double temp = 0.0;
 
    for(i=1;i<=n;i++)
        perp[i]=v[i]/w[i]; //計算單位價值(單位重量的物品價值)
    for(i=1;i<=n-1;i++)
    {
        for(j=i+1;j<=n;j++)
            if(perp[i]<perp[j])//冒泡排序perp[],order[],sortv[],sortw[]
        {
            temp = perp[i];  //冒泡對perp[]排序
            perp[i]=perp[i];
            perp[j]=temp;
 
            temporder=order[i];//冒泡對order[]排序
            order[i]=order[j];
            order[j]=temporder;
 
            temp = v[i];//冒泡對v[]排序
            v[i]=v[j];
            v[j]=temp;
 
            temp=w[i];//冒泡對w[]排序
            w[i]=w[j];
            w[j]=temp;
        }
    }
}
 
//回溯函數
void backtrack(int i)
{   //i用來指示到達的層數(第幾步,從0開始),同時也指示當前選擇玩了幾個物品
    double bound(int i);
    if(i>n) //遞歸結束的判定條件
    {
        bestp = cp;
        return;
    }
    //如若左子節點可行,則直接搜索左子樹;
    //對於右子樹,先計算上界函數,以判斷是否將其減去
    if(cw+w[i]<=c)//將物品i放入背包,搜索左子樹
    {
        cw+=w[i];//同步更新當前背包的重量
        cp+=v[i];//同步更新當前背包的總價值
        put[i]=1;
        backtrack(i+1);//深度搜索進入下一層
        cw-=w[i];//回溯復原
        cp-=v[i];//回溯復原
    }
    if(bound(i+1)>bestp)//如若符合條件則搜索右子樹
        backtrack(i+1);
}
 
//計算上界函數,功能為剪枝
double bound(int i)
{   //判斷當前背包的總價值cp+剩余容量可容納的最大價值<=當前最優價值
    double leftw= c-cw;//剩余背包容量
    double b = cp;//記錄當前背包的總價值cp,最后求上界
    //以物品單位重量價值遞減次序裝入物品
    while(i<=n && w[i]<=leftw)
    {
        leftw-=w[i];
        b+=v[i];
        i++;
    }
    //裝滿背包
    if(i<=n)
        b+=v[i]/w[i]*leftw;
    return b;//返回計算出的上界
 
}
 
 
 
int main()
{
    int i;
    printf("請輸入物品的數量和背包的容量:");
    scanf("%d %lf",&n,&c);
    /*printf("請輸入物品的重量和價值:\n");
    for(i=1;i<=n;i++)
    {
        printf("第%d個物品的重量:",i);
        scanf("%lf",&w[i]);
        printf("第%d個物品的價值是:",i);
        scanf("%lf",&v[i]);
        order[i]=i;
    }*/
    printf("請依次輸入%d個物品的重量:\n",n);
    for(i=1;i<=n;i++){
        scanf("%lf",&w[i]);
        order[i]=i;
    }
 
    printf("請依次輸入%d個物品的價值:\n",n);
    for(i=1;i<=n;i++){
        scanf("%lf",&v[i]);
    }
 
 
    knapsack();
    backtrack(1);
 
 
    printf("最優價值為:%lf\n",bestp);
    printf("需要裝入的物品編號是:");
    for(i=1;i<=n;i++)
    {
        if(put[i]==1)
            printf("%d ",order[i]);
    }
    return 0;
}
View Code

算法效率:

計算上界需要O(n)時間,在最壞情況下有O(2^n)個右兒子節點需要計算上界,故解0-1背包問題的回溯算法所需要的計算時間為O(n2^n)。

運行結果:

 參考文獻:王曉東《算法設計與分析》

                  https://blog.csdn.net/qian2213762498/article/details/79420269?depth_1-


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM