動態規劃、貪心、回溯、分支限界法解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
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;
}