0/1揹包問題(回溯法、分支限界法、動態規劃法、貪心法)(C++版)
此篇整理自李老師上課PPT --- On one way by myself
(1)問題描述
有n個重量分別為{w1,w2,…,wn}的物品,它們的價值分別為{v1,v2,…,vn},給定一個容量為W的揹包。設計從這些物品中選取一部分物品放入該揹包的方案,每個物品要麼選中要麼不選中,要求選中的物品不僅能夠放到揹包中,而且重量和為W具有最大的價值。
輸入:
3 // n個物品假設為3
16 45 // 第一個物品的重量和價值
15 25 // 第二個物品的重量和價值
15 25 // 第三個物品的重量和價值
30 // 揹包容量W
輸出:
0 1 1 // 第幾個物品選中則為1,不選中則為0
50 // 最大價值
(2)回溯法與分支限界法比較(先區別這兩個)
方法 | 解空間搜尋方式 | 儲存結點的資料結構 | 結點儲存特性 | 常用應用 |
---|---|---|---|---|
回溯法 | 深度優先 | 棧 | 活結點的所有可行子結點被 遍歷後才從棧中出棧 | 找出滿足條件的所有解 |
分枝限界法 | 廣度優先 | 隊列,優先佇列 | 每個結點只有一次成為活結點的機會 | 找出滿足條件一個解或者特定意義的最優解 |
首先宣告:此兩種都有解空間樹,問題的解空間樹是虛擬的,並不需要在演算法執行時構造一棵真正的樹結構,然後再在該解空間樹中搜索問題的解,而是隻儲存從根結點到當前結點的路徑。
例如:以下求集合{a,b,c}的冪集的解空間樹:
第一層為空,第二層為a的選擇(左子樹)與不選擇(右子樹),第三層為b的選擇(左子樹)與不選擇(右子樹),第四層為c的選擇(左子樹)與不選擇(右子樹);求解過程分為3步,分別對a、b、c元素做決策,該解空間的每個葉子結點都構成一個解(很多情況並非如此);在一個解空間中搜索解的過程構成搜尋空間,上圖中所有葉子結點都是解,所以該問題的解空間和搜尋空間相同。
再比如:下圖是四皇后問題的搜尋空間,圖中每個狀態由當前放置的皇后的行列號構成。它給出了四皇后問題的全部搜尋過程,只有18個結點,其中標有X號的結點無法繼續擴充套件。
(1,*,*,*)表示第一行第一列放個皇后,其他待搜尋;(2,4,1,3)表示第一行第二列、第二行第四列、第三行第一列和第四行第三列各放一個皇后,構成一個解;(3,1,4,2)表示第一行第三列、第二行第一列、第三行第四列和第四行第二列各放一個皇后,構成另一個解;
最後說明,活節點:指自身已生成但其孩子結點沒有全部生成的結點;擴充套件節點:是指正在產生孩子結點的結點。
死節點:指由根結點到該結點構成的部分解不滿足約束條件,或者其子結點已經搜尋完畢
(3)回溯法
就是回退,如四皇后問題,最左側,放完第一行第一列和第二行第三列後,無法再向下擴充套件,則向父節點回退,父節點如果還有向下擴充套件的其他節點則擴充套件,不能擴充套件則再向上回退;程式碼的世界也有哲學,前進的道路走不通時,是深邃駭人的斷崖?是飛流急湍的河流?於我何干,我瀟灑回退你耐我何?
回溯法搜尋解空間時,通常採用兩種策略避免無效搜尋,提高回溯的搜尋效率:
①用約束函式在擴充套件結點處剪除不滿足約束的子樹;②用限界函式剪去得不到問題解或最優解的子樹。用回溯法解題的一般步驟如下:
①針對所給問題,確定問題的解空間樹,問題的解空間樹應至少包含問題的一個(最優)解。②確定結點的擴充套件搜尋規則。③以深度優先方式搜尋解空間樹,並在搜尋過程中可以採用剪枝函式來避免無效搜尋非遞歸回溯框架:
int x[n]; //x存放解向量,全域性變數
void backtrack(int n) //非遞迴框架
{ int i=1; //根結點層次為1
while (i>=1) //尚未回溯到頭
{ if(ExistSubNode(t)) //當前結點存在子結點
{ for (j=下界;j<=上界;j++) //對於子集樹,j=0到1迴圈
{ x[i]取一個可能的值;
if (constraint(i) && bound(i))
//x[i]滿足約束條件或界限函式
{ if (x是一個可行解)
輸出x;
else i++; //進入下一層次
}
}
}
else i--; //回溯:不存在子結點,返回上一層
}
}
遞歸回溯框架:
int x[n]; //x存放解向量,全域性變數
void backtrack(int i) //求解子集樹的遞迴框架
{ if(i>n) //搜尋到葉子結點,輸出一個可行解
輸出結果;
else
{ for (j=下界;j<=上界;j++) //用j列舉i所有可能的路徑
{ x[i]=j; //產生一個可能的解分量
… //其他操作
if (constraint(i) && bound(i))
backtrack(i+1); //滿足約束條件和限界函式,繼續下一層
}
}
}
說回0/1揹包問題:
<1>問題表示及求解結果表示----全域性變數
//問題表示
int n=0,W=0;
vector<int> w;//={0,16,15,15}; //重量,下標0不用
vector<int> v;//={0,45,25,25}; //價值,下標0不用
//求解結果表示
int maxv=-9999; //存放最大價值,初始為最小值
int bestx[MAXN]; //存放最優解,全域性變數
int total=1; //解空間中結點數累計,全域性變數
<2> 主要函式
// main函式呼叫,用來初始化rw和op,以及dfs_back的入口
void bfs_back_main();
/*
* 採用遞迴式呼叫
* 由於陣列下表從1開始,則初始時i=1;tw與tv都為0;
* rw為輸入資料的總容量;op初始為全0,暫存解空間,然後賦值到bestx陣列
* 此函式的內部,首先是到達葉子節點,也即遞迴的跳出條件,如果價值更優則更新bestx;
* 然後是左剪枝和右剪枝操作了:擴充套件左孩子,需判定已擴充套件節點的容量+此節點的容量<=揹包容量,
不滿足則剪枝,然後回溯;擴充套件右孩子,需判定已擴充套件節點的容量+剩餘節點的總容量>揹包容量,
不然的話就沒有擴充套件的必要,直接剪枝。不管擴充套件左右孩子,都得遞迴呼叫dfs_back
*/
void dfs_back(int i,int tw,int tv,int rw,int op[]);
程式碼貼在最後(4)分支限界-優先佇列(STL)
顧名思義,樹有分枝,採用廣度優先的策略,依次搜尋活結點的所有分枝,也就是所有相鄰結點;採用一個限界函式,計算限界函式值,選擇一個最有利的子結點作為擴充套件結點,使搜尋朝著解空間樹上有最優解的分枝推進,以便儘快地找出一個最優解; 優先佇列式分枝限界的主要特點是將活結點表組組成一個優先隊列,並選取優先順序最高的活結點成為當前擴充套件結點。步驟如下:①計算起始結點(根結點)的優先順序並加入優先佇列(與特定問題相關的資訊的函式值決定優先順序)。②從優先佇列中取出優先順序最高的結點作為當前擴充套件結點,使搜尋朝著解空間樹上可能有最優解的分枝推進,以便儘快地找出一個最優解。③對當前擴充套件結點,先從左到右地產生它的所有孩子結點,然後用約束條件檢查,對所有滿足約束條件的孩子結點計算優先順序並加入優先佇列。④重複步驟②和③,直到找到一個解或優先佇列為空為止。採用分枝限界法求解的3個關鍵問題如下:
①如何確定合適的限界函式。②如何組織待處理結點的活結點表。③如何確定解向量的各個分量。
0/1揹包:
<1> 問題表示及求解結果表示同回溯法的// 使用n,W,w[],v[],maxv,bestv[]<2> 主要函式
// # 分支限界優先佇列法
// 佇列中的節點型別
struct NodeType
{// 分支限界節點
int no; // 節點編號
int i; // 當前節點在搜尋空間的層次
int w; // 當前節點的總重量
int v; // 當前節點的總價值
int x[MAXN]; // 當前節點包含的解向量
double ub; // 上界
bool operator<(const NodeType& node) const
{// 優先佇列按此方式排序
return ub < node.ub; // ub越大越優先出隊
}
};
/* 主幹
* ->初始化根節點
* ->計算根節點上界及進隊
* ->迴圈遍歷佇列,條件為非空:出一個節點,
計算左孩子節點剪枝條件,滿足的左孩子計算上界及進隊;
計算右孩子節點上界,符合上界條件的右孩子進隊;
(根據容量剪去左孩子,根據上界條件剪去右孩子)
*
*/
void bfs();
// 進隊----不是葉子節點就直接進隊,是葉子節點則判斷是否更優解,是的話則更新最優解
void EnQueue(NodeType e,priority_queue<NodeType> &qu);
// 計算邊界 就是根據剩餘容量的大小,計算剩下全部物品裝入的價值和裝入部分物品的價值
// (部分物品按照單位容量內價值高低的順序裝入---這有點貪心的思想了)
void bound(NodeType &e);
// !# 分支限界優先佇列法
程式碼最後一塊貼(5) 動態規劃法
從求解斐波那契數列看動態規劃法:int count=1; //累計呼叫的步驟
int Fib(int n) //演算法
{ printf("(%d)求解Fib(%d)\n",count++,n);
if (n==1 || n==2)
{ printf(" 計算出Fib(%d)=%d\n",n,1);
return 1;
}
else
{ int x=Fib(n-1);
int y=Fib(n-2);
printf(" 計算出Fib(%d)=Fib(%d)+Fib(%d)=%d\n",
n,n-1,n-2,x+y);
return x+y;
}
}
執行上面的程式碼來計算n=100時的斐波那契數列的值,吃飯前一直閃著屏在計算,吃過飯圍著操場走了兩圈後回來,螢幕還在閃著。執行太慢,可以拿空間換時間,或者拿金錢換時間。拿金錢換時間,比如買個太湖之光之類的玩玩,短時間之內也能執行出來;拿空間換時間,就是動態規劃,建一個動態規劃陣列,將之前已經計算的陣列放進陣列中,下次使用的時候直接從資料裡取,不用再計算一遍了。 int dp[MAX]; //所有元素初始化為0
int count=1; //累計呼叫的步驟
int Fib1(int n) //演算法1
{ dp[1]=dp[2]=1;
printf("(%d)計算出Fib(1)=1\n",count++);
printf("(%d)計算出Fib(2)=1\n",count++);
for (int i=3;i<=n;i++)
{ dp[i]=dp[i-1]+dp[i-2];
printf("(%d)計算出Fib(%d)=%d\n",count++,i,dp[i]);
}
return dp[n];
}
執行上面的程式碼來計算n=100時的斐波那契數列的值,兩三秒搞定,就是這麼神奇。動態規劃是一種解決多階段決策問題的優化方法,把多階段過程轉化為一系列單階段問題,利用各階段之間的關係,逐個求解。
能採用動態規劃求解的問題的一般要具有3個性質:
最優性原理:如果問題的最優解所包含的子問題的解也是最優的,就稱該問題具有最優子結構,即滿足最優性原理。
無後效性:即某階段狀態一旦確定,就不受這個狀態以後決策的影響。也就是說,某狀態以後的過程不會影響以前的狀態,只與當前狀態有關。有重疊子問題:即子問題之間是不獨立的,一個子問題在下一階段決策中可能被多次使用到。(該性質並不是動態規劃適用的必要條件,但是如果沒有這條性質,動態規劃演算法同其他演算法相比就不具備優勢)。實際應用中簡化的步驟:
① 分析最優解的性質,並刻畫其結構特徵。②遞迴的定義最優解。③以自底向上或自頂向下的記憶化方式計算出最優值。④根據計算最優值時得到的資訊,構造問題的最優解。實際上就是:① 定義合適的動態規劃陣列 ② 找到合適的狀態轉移方程 ③從動態規劃陣列中找合適的解
0/1揹包問題:
<1> 問題表示及求解結果表示同回溯法
// 使用n,W,w[],v[],maxv,bestv[]額外定義動態規劃陣列
int dp[MAXN][MAXW]; // dp[i][r]表示揹包剩餘容量為r(1≤r≤W)
<2> 對應的狀態轉移方程dp[i][0]=0(揹包不能裝入任何物品,總價值為0)邊界條件dp[i][0]=0(1≤i≤n)―邊界條件
dp[0][r]=0(沒有任何物品可裝入,總價值為0)邊界條件dp[0][r]=0(1≤r≤W)―邊界條件
dp[i][r]=dp[i-1][r] 當r<w[i]時,物品i放不下
dp[i][r]=MAX{dp[i-1][r],dp[i-1][r-w[i]]+v[i]} 否則在不放入和放入物品i之間選最優解
這樣,dp[n][W]便是0/1揹包問題的最優解。
例如,某0/1揹包問題為,n=5,w={2,2,6,5,4},v={6,3,5,4,6}(下標從1開始),W=10。求dp:
<3> 從動態規劃陣列中找合適的解
<4> 函式說明
/*
* 根據狀態轉移方程來構造動態
* 1>兩個邊界條件
* 2>由於動態規劃陣列為二維陣列,則兩層for迴圈裡判斷是否擴充套件活動節點
擴充套件則dp[i][r]=dp[i-1][r];
不擴充套件則二者求最大
*/
void dp_Knap();
/*
* 動態規劃陣列已經填充完畢,逆著推出最優解
根據狀態轉移方程中的條件,判斷每個物品是否選擇
*/
void buildx();
程式碼最後面。(6)貪心法
貪心法的基本思路是在對問題求解時總是做出在當前看來是最好的選擇,也就是說貪心法不從整體最優上加以考慮,所做出的僅是在某種意義上的區域性最優解。每一次貪心選擇都將所求問題簡化為規模更小的子問題,並期望通過每次所做的區域性最優選擇產生出一個全域性最優解。所謂貪心選擇性質是指所求問題的整體最優解可以通過一系列區域性最優的選擇,即貪心選擇來達到。也就是說,貪心法僅在當前狀態下做出最好選擇,即區域性最優選擇,然後再去求解做出這個選擇後產生的相應子問題的解。
貪心法求解問題的演算法框架如下:
SolutionType Greedy(SType a[],int n)//假設解向量(x0,x1,…,xn-1)型別為SolutionType,其分量為SType型別
{ SolutionType x={}; //初始時,解向量不包含任何分量
for (int i=0;i<n;i++) //執行n步操作
{ SType xi=Select(a); //從輸入a中選擇一個當前最好的分量
if (Feasiable(xi)) //判斷xi是否包含在當前解中
solution=Union(x,xi); //將xi分量合併形成x
}
return x; //返回生成的最優解
}
求解部分揹包問題:與0/1揹包問題的區別是,這裡的每個物品可以取一部分裝入揹包。
既然貪心是選擇當前最優(區域性最優),則需要對資料按照一定的規則(求解的目的有關方面)排序,先選擇最有利的一個,然後再擴充套件其他。
揹包問題,有重量有價值,求得是利益最大化的,則按照單位重量重含有的價值從大到小排序,依次選擇。
<1> 問題表示及求解結果表示同回溯法
// 使用n,W,w[],v[]又定義了
vector<NodeType_Knap> A; // 含有輸入的資料和排序後的資料
double V = 0; // 價值,之前是int型,在這裡為double
double x[MAXN]; // 最優解double型別,可以選擇部分,即一定的比例
<2> 函式說明
/*
* 求單位重量的價值->按照自定義的格式排序->呼叫 Knap
*/
void knap_m();
/*
* 排序後則貪心迴圈選擇,如果剩餘的容量還能容納當前的,則放進去,不能的話跳出迴圈,選擇部分放入
*/
void Knap();
(7)蠻力法
(8)說明及程式碼
此篇文章根據WHU李老師課件整理,圖片也是來之課件,為什麼有我部落格水印我也不清楚,程式碼也整理之課件;幾個方法都放在一個檔案中了,可能有點亂,不過在main函式之前都有宣告,每一種都有註釋
// # 回溯法 以此開頭
// ...
// !# 回溯法 以此結尾
main函式中也有,如果用動態規劃法則把其他的註釋掉就行,輸入輸出別註釋。程式碼如下:
#include <iostream>
//#include <algorithm>
#include <queue>
#include <algorithm>
using namespace std;
#define MAXN 50
//問題表示
int n=3,W=30;
vector<int> w;//={0,16,15,15}; //重量,下標0不用
vector<int> v;//={0,45,25,25}; //價值,下標0不用
//求解結果表示
int maxv=-9999; //存放最大價值,初始為最小值
int bestx[MAXN]; //存放最優解,全域性變數
int total=1; //解空間中結點數累計,全域性變數
// # 分支限界優先佇列法
// 使用n,W,w[],v[],maxv,bestv[]
// 佇列中的節點型別
struct NodeType
{// 分支限界節點
int no; // 節點編號
int i; // 當前節點在搜尋空間的層次
int w; // 當前節點的總重量
int v; // 當前節點的總價值
int x[MAXN]; // 當前節點包含的解向量
double ub; // 上界
bool operator<(const NodeType& node) const
{
return ub < node.ub; // ub越大越優先出隊
}
};
/* 主幹
* ->初始化根節點
* ->計算根節點上界及進隊
* ->迴圈遍歷佇列,條件為非空:出一個節點,
計算左孩子節點剪枝條件,滿足的左孩子計算上界及進隊;
計算右孩子節點上界,符合上界條件的右孩子進隊;
(根據容量剪去不滿足要求的左孩子,根據上界條件剪去不滿足要求的右孩子)
*
*/
void bfs();
// 進隊----不是葉子節點就直接進隊,是葉子節點則判斷是否更優解,是的話則更新最優解
void EnQueue(NodeType e,priority_queue<NodeType> &qu);
// 計算邊界 就是根據剩餘容量的大小,計算剩下全部物品裝入的價值和裝入部分物品的價值
// (部分物品按照單位容量內價值高低的順序裝入---這有點貪心的思想了)
void bound(NodeType &e);
// !# 分支限界優先佇列法
// # 回溯法
// 使用n,W,w[],v[],maxv,bestv[]
// main函式呼叫,用來初始化rw和op,以及dfs_back的入口
void bfs_back_main();
/*
* 採用遞迴式呼叫
* 由於陣列下表從1開始,則初始時i=1;tw與tv都為0;
* rw為輸入資料的總容量;op初始為全0,暫存解空間,然後賦值到bestx陣列
* 此函式的內部,首先是到達葉子節點,也即遞迴的跳出條件,如果價值更優則更新bestx;
* 然後是左剪枝和右剪枝操作了:擴充套件左孩子,需判定已擴充套件節點的容量+此節點的容量<=揹包容量,
不滿足則剪枝,然後回溯;擴充套件右孩子,需判定已擴充套件節點的容量+剩餘節點的總容量>揹包容量,
不然的話就沒有擴充套件的必要,直接剪枝。不管擴充套件左右孩子,都得遞迴呼叫dfs_back
*/
void dfs_back(int i,int tw,int tv,int rw,int op[]);
// !# 回溯法
// # 貪心法----非0/1揹包問題,而是部分揹包問題
// 使用n,W,w[],v[]
struct NodeType_Knap
{
double w;
double v;
double p; //p=v/w
bool operator<(const NodeType_Knap &s) const
{
return p>s.p; //按p遞減排序
}
};
vector<NodeType_Knap> A; // 含有輸入的資料和排序後的資料
double V = 0; // 價值,之前是int型,在這裡為double
double x[MAXN]; // 最優解double型別,可以選擇部分,即一定的比例
/*
* 求單位重量的價值->按照自定義的格式排序->呼叫 Knap
*/
void knap_m();
/*
* 排序後則貪心迴圈選擇,如果剩餘的容量還能容納當前的,則放進去,不能的話跳出迴圈,選擇部分放入
*/
void Knap();
// !# 貪心法
// # 斐波那契數列
int countf = 1;
int Fib(int n);
int dp_fib[MAXN]; //所有元素初始化為0
int Fib1(int n);
// !# 斐波那契數列
// # 動態規劃法
// 使用n,W,w[],v[],maxv,bestv[]
// 動態規劃陣列
int dp[MAXN][MAXN];
/*
* 根據狀態轉移方程來構造動態
* 1>兩個邊界條件
* 2>由於動態規劃陣列為二維陣列,則兩層for迴圈裡判斷是否擴充套件活動節點
擴充套件則dp[i][r]=dp[i-1][r];
不擴充套件則二者求最大
*/
void dp_Knap();
/*
* 動態規劃陣列已經填充完畢,逆著推出最優解
根據狀態轉移方程中的條件,判斷每個物品是否選擇
*/
void buildx();
// !# 動態規劃法
int main()
{
// 輸入格式
/*
3 n個物品假設為3
16 45 第一個物品的重量和價值
15 25 第二個物品的重量和價值
15 25 第三個物品的重量和價值
30 揹包容量W
*/
cin >> n;
int m,l;
// 下表0不用,填充0
w.push_back(0);
v.push_back(0);
for (int j = 1; j <= n;j++)
{
cin >> m >> l;
w.push_back(m);
v.push_back(l);
}
cin >> W;
// # 分支界限優先佇列法
//bfs();
// !# 分支界限優先佇列法
// # 回溯法
//bfs_back_main();
// !# 回溯法
// # 貪心法
//knap_m();
// !# 貪心法
// # 斐波那契數列
//Fib(W);
//Fib1(W);
// !# 斐波那契數列
// # 動態規劃法
dp_Knap();
buildx();
// !# 動態規劃法
cout << "最優解:";
for (int i = 1;i <= n;i++)
{
if (V > 0)
{// 貪心法 輸出的是double型別
cout << x[i] << " ";
}else
{// 分支限界和回溯法輸出的是int型
cout << bestx[i] << " ";
}
}
if (V > 0)
{// 貪心法 輸出的是double型別
cout << endl << "最大價值為:" << V << endl;
}else
{// 分支限界和回溯法輸出的是int型
cout << endl << "最大價值為:" << maxv << endl;
}
return 0;
}
//////////////////////////////////////////////////////////////////////////
// 分支限界優先佇列法
void bfs()
{
int j;
NodeType e,e1,e2; //定義3個結點
priority_queue<NodeType> qu; //定義一個優先佇列(大根堆)
e.i=0; //根結點置初值,其層次計為0
e.w=0; e.v=0;
e.no=total++;
for (j=1;j<=n;j++)
e.x[j]=0;
bound(e); //求根結點的上界
qu.push(e); //根結點進隊
while (!qu.empty()) //隊不空迴圈
{
e=qu.top(); qu.pop(); //出隊結點e
if (e.w+w[e.i+1]<=W) //剪枝:檢查左孩子結點
{
e1.no = total++;
e1.i = e.i + 1; //建立左孩子結點
e1.w = e.w + w[e1.i];
e1.v = e.v + v[e1.i];
for (j=1;j<=n;j++) e1.x[j]=e.x[j]; //複製解向量
e1.x[e1.i]=1;
bound(e1); //求左孩子結點的上界
EnQueue(e1,qu); //左孩子結點進隊操作
}
e2.no = total++; //建立右孩子結點
e2.i = e.i + 1;
e2.w = e.w;
e2.v = e.v;
for (j=1;j<=n;j++)
e2.x[j]=e.x[j]; //複製解向量
e2.x[e2.i]=0;
bound(e2); //求右孩子結點的上界
if (e2.ub>maxv) //若右孩子結點剪枝
EnQueue(e2,qu);
}
}
void EnQueue(NodeType e,priority_queue<NodeType> &qu)
{ //結點e進隊qu
if (e.i==n) //到達葉子結點
{
if (e.v>maxv) //找到更大價值的解
{
maxv=e.v;
for (int j=1;j<=n;j++)
bestx[j]=e.x[j];
}
}
else qu.push(e); //非葉子結點進隊
}
void bound(NodeType &e) //計算分枝結點e的上界
{
int i=e.i+1; //考慮結點e的餘下物品
int sumw=e.w; //求已裝入的總重量
double sumv=e.v; //求已裝入的總價值
while (i<=n && (sumw+w[i]<=W) )
{
sumw+=w[i]; //計算揹包已裝入載重
sumv+=v[i]; //計算揹包已裝入價值
i++;
}
if (i<=n) //餘下物品只能部分裝入
e.ub=sumv+(W-sumw)*v[i]/w[i];
else //餘下物品全部可以裝入
e.ub=sumv;
}
//////////////////////////////////////////////////////////////////////////
// 回溯法
void bfs_back_main()
{
int *op = new int[n];
for (int j = 0;j < n;j++)
{// 初始化為全0
op[j] = 0;
}
// 所有物品的總容量
int rw = 0;
for (int j = 0;j < n;j++)
{
rw += w[j];
}
dfs_back(1,0,0,rw,op);
}
//求解0/1揹包問題
void dfs_back(int i,int tw,int tv,int rw,int op[])
{ //初始呼叫時rw為所有物品重量和
int j;
if (i>n) //找到一個葉子結點
{
if (tw==W && tv>maxv) //找到一個滿足條件的更優解,儲存
{
maxv=tv;
for (j=1;j<=n;j++) //複製最優解
bestx[j]=op[j];
}
}else
{ //尚未找完所有物品
if (tw+w[i]<=W) //左孩子結點剪枝
{
op[i]=1; //選取第i個物品
dfs_back(i+1,tw+w[i],tv+v[i],rw-w[i],op);
}
op[i]=0; //不選取第i個物品,回溯
if (tw+rw>W) //右孩子結點剪枝
dfs_back(i+1,tw,tv,rw-w[i],op);
}
}
//////////////////////////////////////////////////////////////////////////
// 貪心法
void knap_m()
{
for (int i=0;i<=n;i++)
{
NodeType_Knap k;
k.w = w[i];
k.v = v[i];
A.push_back(k);
}
for (int i=1;i<=n;i++) //求v/w
A[i].p=A[i].v/A[i].w;
sort(++A.begin(),A.end()); //A[1..n]排序
Knap();
}
// 求解揹包問題並返回總價值
void Knap()
{
V=0; //V初始化為0
double weight=W; //揹包中能裝入的餘下重量
int i=1;
while (A[i].w < weight) //物品i能夠全部裝入時迴圈
{
x[i]=1; //裝入物品i
weight -= A[i].w; //減少揹包中能裝入的餘下重量
V += A[i].v; //累計總價值
i++; //繼續迴圈
}
if (weight > 0) //當餘下重量大於0
{
x[i] = weight / A[i].w; //將物品i的一部分裝入
V += x[i] * A[i].v; //累計總價值
}
}
//////////////////////////////////////////////////////////////////////////
// 斐波那契數列
int Fib(int n)
{
printf("(%d)求解Fib(%d)\n",countf++,n);
if (n==1 || n==2)
{
printf(" 計算出Fib(%d)=%d\n",n,1);
return 1;
}
else
{
int x = Fib(n-1);
int y = Fib(n-2);
printf(" 計算出Fib(%d)=Fib(%d)+Fib(%d)=%d\n",
n,n-1,n-2,x+y);
return x+y;
}
}
// 動態規劃後的斐波那契數列
int Fib1(int n) //演算法1
{
dp_fib[1]=dp_fib[2]=1;
printf("(%d)計算出Fib(1)=1\n",countf++);
printf("(%d)計算出Fib(2)=1\n",countf++);
for (int i=3;i<=n;i++)
{
dp_fib[i]=dp_fib[i-1]+dp_fib[i-2];
printf("(%d)計算出Fib(%d)=%d\n",countf++,i,dp_fib[i]);
}
return dp_fib[n];
}
//////////////////////////////////////////////////////////////////////////
// 動態規劃法
void dp_Knap()
{
int i,r;
for(i = 0;i <= n;i++) //置邊界條件dp[i][0]=0
dp[i][0] = 0;
for (r = 0;r <= W;r++) //置邊界條件dp[0][r]=0
dp[0][r] = 0;
for (i = 1;i <= n;i++)
{
for (r = 1;r <= W;r++)
if (r < w[i])
dp[i][r] = dp[i-1][r];
else
dp[i][r] = max(dp[i-1][r],dp[i-1][r-w[i]]+v[i]);
}
}
void buildx()
{
int i=n,r=W;
maxv=0;
while (i>=0) //判斷每個物品
{
if (dp[i][r] != dp[i-1][r])
{
bestx[i] = 1; //選取物品i
maxv += v[i]; //累計總價值
r = r - w[i];
}
else
bestx[i]=0; //不選取物品i
i--;
}
}