1. 程式人生 > >動態規劃、貪心、回溯、分支限界法解0-1揹包問題總結

動態規劃、貪心、回溯、分支限界法解0-1揹包問題總結

本文通過0-1揹包問題的不同解法,深入理解計算機常用演算法動態規劃、貪心、回溯、分支限界法的思想。

問題描述
0-1揹包問題:給定n種物品和一揹包。物品i的重量是wi,其價值是vi,揹包的容量為C。問:應該如何選擇裝入揹包的物品,使得裝入揹包中物品的總價值最大?
簡單n=3的例子:設w=[16,15,15],v=[45,25,25],c=30

1.動態規劃解0-1揹包問題

分析
(1)0-1揹包問題是求在以下條件下
1)∑wi*xi<=C, i from 1 to n
2)xi∈{0,1},1<=i<=n
總價值最大,
即∑vi*xi最大, i from 1 to n
(2)最優子結構性質
若(x1,x2…xn)是0-1揹包的最優解,則(x1,x2…xn-1)是下面相應子問題的最優解。
1)∑wi*xi<=C-wn*xn, i from 1 to n-1
2)xi∈{0,1},1<=i<=n-1
總價值最大,
即∑vi*xi最大, i from 1 to n-1
可用反證法證明,證明略。
(3)遞迴關係
設m[i][j]為選擇前i個物品,容量為j能裝入物品價值的最大值。
可得如下遞迴關係
1)當 0<=j<wi

時,m[i][j]=m[i-1][j];
2)當 j>=wi時,m[i][j]=max{m[i-1][j],m[i-1][j-wi]+vi};
遞推關係是這麼形成的:
通過選擇第i件物品放或不放來形成遞推關係,
1)如果不放第i件物品,問題就轉化為“前i-1件物品放入容量為c的揹包中”,價值為m[i -1][j];
2)如果放第i件物品,那麼問題就轉化為“前i -1件物品放入剩下的容量為v-Ci的揹包中“,價值為m[i-1][j-wi]+vi
而m[n][c]為選擇前n個物品,容量為c揹包能裝入物品價值的最大值。

程式碼如下

#include <stdio.h>
int m[100][100]; int dp_knapsack(int w[], int v[], int c, int m[][100],int n)//m[i][j]表示揹包可選物品為1,2,..i,容量為j時的最優解 { //初始化 int i, j; for (j = 1; j <= c; j++) { m[1][j] = 0; } for (j = w[0]; j <= c; j++) { m[1][j] = v[0]; } //迴圈直到求出m[n][c] for (i = 2; i <= n; i++)
{ for (j = 1; j <= c; j++) { if (j < w[i - 1]) { m[i][j] = m[i - 1][j]; }else { if (m[i - 1][j]>=(m[i - 1][j - w[i - 1]] + v[i - 1])) { m[i][j] = m[i - 1][j]; }else { m[i][j] = m[i - 1][j - w[i - 1]] + v[i - 1]; } } } } return 0; } int main() { int weight[3] = { 16, 15, 15 }; int value[3] = { 45, 25, 25 }; int c =30; dp_knapsack(weight, value, c,m,3); printf("%d", m[3][30]); }

2.貪心法求0-1揹包問題

貪心法的思路是先求每個物品單位重量的價值,按單位重量的價值從大到小排序。然後按這個順序往揹包裡面放物品。
注意,貪心法不能解這個n=3,weight[3] = { 16, 15, 15 };value[3] = { 45, 25, 25 }的0-1揹包問題。因為依照貪心選擇策略,首先將1物品裝入,得到的最大值為45。而實際上選擇2,3物品能得到最大價值為50。

3.回溯法求0-1揹包問題

分析
回溯法是一個帶有系統性和跳躍性的搜尋演算法。它在問題的解空間樹中,按深度優先策略,從根節點出發搜尋空間樹。
如以上揹包問題,當n=3時,解空間為:
{(0,0,0),(0,1,0),(0,0,1),(1,0,0),(0,1,1),(1,0,1),(1,1,0),(1,1,1)}
如圖為0-1揹包的解空間樹:
這裡寫圖片描述
按深度優先搜尋,A-B-K為一個可行路徑,此時maxValue=45,
繼續搜尋,A-C-F-M為一個可行路徑,此時maxValue=50;
遍歷所有路徑,得到maxValue=50;

#include <stdio.h>
#include <stdlib.h>
int bestValue=0,curWeight=0,curValue=0;
int backtrack_knapsack(int w[], int v[], int c, int n,int i)
{
    if (i > n)
    {
        if (curValue > bestValue)
        {
            bestValue = curValue;
        }
        return 0;
    }
    if (curWeight + w[i - 1]<=c)//搜尋左子樹
    {
        curWeight += w[i - 1];
        curValue += v[i - 1];
        backtrack_knapsack(w, v, c, n, i + 1);
        curWeight -= w[i - 1];
        curValue -=v[i - 1];
    }
    backtrack_knapsack(w, v, c, n, i + 1);//搜尋右子樹
    return 0;
}
int main()
{
    int weight[3] = { 16, 15, 15 };
    int value[3] = { 45, 25, 25 };
    int c =30;
    backtrack_knapsack(weight, value, c, 3, 1);
    printf("%d\n", bestValue);

}

4.分支限界法求0-1揹包問題

4.1分支限界法介紹

分支限界法類似於回溯法,是在解空間樹上搜索問題解的演算法。
分支限界法的搜尋策略是,在擴充套件結點處,先生成其所有的兒子結點(分支),然後再從當前活結點表中選出下一個擴充套件結點。為了有效的選擇下一擴充套件結點,加速搜尋過程,在每一活結點出,計算一個函式值(限界),並根據函式值,從當前結點表中選擇一個最有利的結點作為擴充套件結點。
分支限界常以廣度優先或以最小耗費(最大效益)優先的方式搜尋問題的解空間樹。
分析

4.2採用佇列式分支限界法解0-1揹包問題

佇列式分支限界法將活結點表組織成一個佇列,並按佇列的先進先出原則選取下一個結點為當前擴充套件結點。
分析
如0-1揹包解空間樹圖
每次選取佇列的最前面的結點為活結點。
1)演算法從根結點A開始,初始時活結點佇列為空,A入佇列。
2)A為活結點,A的兒子結點B、C為可行結點。將B、C加入佇列,捨棄A。此時佇列元素為C-B;
3)B為活結點,B的兒子結點D、E,而D為不可行結點。將E入佇列,捨棄B。此時佇列元素為E-C;
4)迴圈以上步驟
按照以上方式擴充套件到葉節點。
K為一個可行的葉節點,表示一個可行解,價值為45。
L為一個可行的葉節點,表示一個可行解,價值為50…
最後活結點佇列為空,演算法終止。
以下程式碼-1的作用主要有兩個,(a)用來標記樹的每一層。(b)保證佇列不為空,當為空時迴圈結束。
程式碼如下:

#include<iostream>
#include<queue>
using namespace std;
typedef struct treenode{
    int weight;
    int value;
    int level;
    int flag;
}treenode;
queue<struct treenode> que;
int enQueue(int w,int v,int level,int flag,int n,int* bestvalue)
{
    treenode node;
    node.weight = w;
    node.value = v;
    node.level = level;
    node.flag = flag;
    if (level == n)
    {
        if (node.value > *bestvalue)
        {
            *bestvalue = node.value;
        }
        return 0;
    }else
    {
        que.push(node);
    }
}
//w為重量陣列,v為價值陣列,n為物品個數,c為揹包容量,bestValue為全域性最大價值
int bbfifoknap(int w[],int v[],int n,int c,int* bestValue)
{
    //初始化結點
    int i=1;
    treenode tag, livenode;
    livenode.weight = 0;
    livenode.value = 0;
    livenode.level = 1;
    livenode.flag = 0;//初始為0
    tag.weight = -1;
    tag.value = 0;
    tag.level = 0;
    tag.flag = 0;//初始為0
    que.push(tag);
    while (1)
    {
        if (livenode.weight + w[i - 1] <= c)
        {
            enQueue(livenode.weight + w[i - 1], livenode.value + v[i - 1], i, 1,n,bestValue);
        }
        enQueue(livenode.weight,livenode.value, i, 0,n,bestValue);
        livenode = que.front();
        que.pop();
        if (livenode.weight == -1)
        {
            if (que.empty() == 1)
            {
                break;
            }
            livenode = que.front();
            que.pop();
            que.push(tag);
            i++;
        }

    }
    return 0;
}
int main()
{
    int w[] = { 16, 15, 15 };
    int v[] = { 45, 25, 25 };
    int n = 3;
    int c = 30;
    int bestValue=0;
    bbfifoknap(w, v,n,c,&bestValue);
    cout << bestValue<<endl;
    return 0;
}

4.3採用優先佇列式分支限界法解0-1揹包問題

優先佇列分支限界法將活結點表組織成優先佇列,並按優先佇列中規定的
結點優先順序選取最高的下一個結點成為當前擴充套件結點。
分析
如0-1揹包解空間樹圖
選取結點的價值為規定的優先順序。
每次選取優先順序最高的結點為活結點。
1)演算法從根結點A開始,初始時活結點佇列為空,設A為活結點。
2)A為活結點,A的兒子結點B、C為可行結點。將B、C加入優先順序佇列。此時優先順序佇列元素為C-B;
3)B為活結點,B的兒子結點D、E,而D為不可行結點。將E入優先順序佇列,捨棄B。此時佇列元素為C-E;
4)E為活結點,捨棄B。E的兒子結點J、K,J為不可行結點。由於到了樹的最後一層,K不用入佇列,K為一個可行的葉節點,表示一個可行解,價值為45。
5)C為活結點,捨棄C。F,G為兒子結點,為可行結點,入優先順序佇列。此時佇列元素為F,G。
6)迴圈以上步驟,直到優先佇列為空。

#include<iostream>
#include<queue>
using namespace std;
struct treenode{
    int weight;
    int value;
    int level;
    int flag;
    friend bool operator< (treenode a, treenode b)
    {
        return a.value < b.value;
    }
};
priority_queue<treenode> prique;
void enPriQueue(int weight,int value,int level,int flag,int n,int* bestValue)
{
    treenode node;
    node.weight = weight;
    node.value = value;
    node.level = level;
    node.flag = flag;
    if (level == n)
    {
        if (value > *bestValue)
        {
            *bestValue = value;
        }
        return;
    }else
    {
        prique.push(node);
    }
    return;
}
//
int prioritybbnap(int w[],int v[],int c,int n,int* bestValue)
{

    treenode liveNode;
    liveNode.weight = 0;
    liveNode.value = 0;
    liveNode.level = 0;
    liveNode.flag = 0;
    prique.push(liveNode);
    do
    {   
        if (liveNode.weight + w[liveNode.level] <= c)
        {
            enPriQueue(liveNode.weight + w[liveNode.level], liveNode.value + v[liveNode.level],
                liveNode.level + 1, 1,n,bestValue);
        }
        enPriQueue(liveNode.weight, liveNode.value, liveNode.level + 1, 0, n, bestValue);
        liveNode = prique.top();
        prique.pop();
    } while (!prique.empty());
    return 0;

}

int main()
{
    int w[] = { 16, 15, 15 };
    int v[] = { 45, 25, 25 };
    int c = 30;
    int n = 3;
    int bestValue=0;
    prioritybbnap(w, v, c,n,&bestValue);
    cout << bestValue << endl;
    return 0;
}