回溯法
1、有許多問題,當需要找出它的解集或者要求回答什麼解是滿足某些約束條件的最佳解時,往往要使用回溯法。
2、回溯法的基本做法是搜索,或是一種組織得井井有條的,能避免不必要搜索的窮舉式搜索法。這種方法適用於解一些組合數相當大的問題。
3、回溯法在問題的解空間樹中,按深度優先策略,從根結點出發搜索解空間樹。算法搜索至解空間樹的任意一點時,先判斷該結點是否包含問題的解。如果肯定不包含(剪枝過程),則跳過對該結點爲根的子樹的搜索,逐層向其祖先結點回溯;否則,進入該子樹,繼續按深度優先策略搜索。
問題的解空間
問題的解向量:回溯法希望一個問題的解能夠表示成一個n元式(x1,x2,…,xn)的形式。
顯約束:對分量xi的取值限定。
隱約束:爲滿足問題的解而對不同分量之間施加的約束。
解空間:對於問題的一個實例,解向量滿足顯式約束條件的所有多元組,構成了該實例的一個解空間。
注意:同一個問題可以有多種表示,有些表示方法更簡單,所需表示的狀態空間更小(存儲量少,搜索方法簡單)。
下面是n=3時的0-1揹包問題用完全二叉樹表示的解空間:
生成問題狀態的基本方法
擴展結點:一個正在產生兒子的結點稱爲擴展結點
活結點:一個自身已生成但其兒子還沒有全部生成的節點稱做活結點
死結點:一個所有兒子已經產生的結點稱做死結點
深度優先的問題狀態生成法:如果對一個擴展結點R,一旦產生了它的一個兒子C,就把C當做新的擴展結點。在完成對子樹C(以C爲根的子樹)的窮盡搜索之後,將R重新變成擴展結點,繼續生成R的下一個兒子(如果存在)
寬度優先的問題狀態生成法:在一個擴展結點變成死結點之前,它一直是擴展結點
回溯法:爲了避免生成那些不可能產生最佳解的問題狀態,要不斷地利用限界函數(bounding function)來處死(剪枝)那些實際上不可能產生所需解的活結點,以減少問題的計算量。具有限界函數的深度優先生成法稱爲回溯法。(回溯法 = 窮舉 + 剪枝)
回溯法的基本思想
(1)針對所給問題,定義問題的解空間;
(2)確定易於搜索的解空間結構;
(3)以深度優先方式搜索解空間,並在搜索過程中用剪枝函數避免無效搜索。
兩個常用的剪枝函數:
- (1)約束函數:在擴展結點處減去不滿足約束的子數
- (2)限界函數:減去得不到最優解的子樹
用回溯法解題的一個顯著特徵是在搜索過程中動態產生問題的解空間。在任何時刻,算法只保存從根結點到當前擴展結點的路徑。如果解空間樹中從根結點到葉結點的最長路徑的長度爲h(n),則回溯法所需的計算空間通常爲O(h(n))。而顯式地存儲整個解空間則需要
回溯算法的設計步驟
回溯算法的遞歸實現和迭代實現
遞歸回溯
回溯法對解空間作深度優先搜索,因此,在一般情況下用遞歸方法實現回溯法。
// 針對N叉樹的遞歸回溯方法
void backtrack (int t)
{
if (t > n) {
// 到達葉子結點,將結果輸出
output (x);
}
else {
// 遍歷結點t的所有子結點
for (int i = f(n,t); i <= g(n,t); i ++ ) {
x[t] = h[i];
// 如果不滿足剪枝條件,則繼續遍歷
if (constraint (t) && bound (t))
backtrack (t + 1);
}
}
}
迭代回溯
採用樹的非遞歸深度優先遍歷算法,可將回溯法表示爲一個非遞歸迭代過程。
// 針對N叉樹的迭代回溯方法
void iterativeBacktrack ()
{
int t = 1;
while (t > 0) {
if (f(n,t) <= g(n,t)) {
// 遍歷結點t的所有子結點
for (int i = f(n,t); i <= g(n,t); i ++) {
x[t] = h(i);
// 剪枝
if (constraint(t) && bound(t)) {
// 找到問題的解,輸出結果
if (solution(t)) {
output(x);
}
else // 未找到,向更深層次遍歷
t ++;
}
}
}
else {
t--;
}
}
}
回溯法一般依賴的兩種數據結構:子集樹和排列樹
子集樹(遍歷子集樹需
void backtrack (int t)
{
if (t > n)
// 到達葉子結點
output (x);
else
for (int i = 0;i <= 1;i ++) {
x[t] = i;
// 約束函數
if ( legal(t) )
backtrack( t+1 );
}
}
排列樹(遍歷排列樹需要O(n!)計算時間)
void backtrack (int t)
{
if (t > n)
output(x);
else
for (int i = t;i <= n;i++) {
// 完成全排列
swap(x[t], x[i]);
if (legal(t))
backtrack(t+1);
swap(x[t], x[i]);
}
}
幾個典型的例子
裝載問題
問題表述:有一批共n個集裝箱要裝上2艘載重量分別爲c1和c2的輪船,其中集裝箱i的重量爲wi,且
裝載問題要求確定是否有一個合理的裝載方案可將這個集裝箱裝上這2艘輪船。如果有,找出一種裝載方案。
解決方案:
容易證明,如果一個給定裝載問題有解,則採用下面的策略可得到最優裝載方案。
(1)首先將第一艘輪船儘可能裝滿;
(2)將剩餘的集裝箱裝上第二艘輪船。
將第一艘輪船儘可能裝滿等價於選取全體集裝箱的一個子集,使該子集中集裝箱重量之和最接近。由此可知,裝載問題等價於以下特殊的0-1揹包問題。
解空間:
子集樹可行性約束函數(選擇當前元素):
上界函數(不選擇當前元素):
void backtrack (int i)
{
// 搜索第i層結點
if (i > n) // 到達葉結點
更新最優解bestx,bestw;return;
r -= w[i];
if (cw + w[i] <= c) {
// 搜索左子樹
x[i] = 1;
cw += w[i];
backtrack (i + 1);
cw -= w[i];
}
if (cw + r > bestw) {
x[i] = 0; // 搜索右子樹
backtrack(i + 1);
}
r += w[i];
}
變量解釋:
r: 剩餘重量
w: 各個集裝箱重
cw:當前總重量
x: 每個集裝箱是否被選取標誌
bestx: 最佳選取方案
bestw: 最優載重量
實現:
#include <iostream>
#include <vector>
#include <iterator>
using namespace std;
/* 裝載問題子函數
* layers: 搜索到第layers層結點
* layers_size: layers_size總層數
* current_w: 當前承載量
* best_w: 最優載重量
* flag_x: 選取方案
* best_x: 最佳選取方案
* remainder_w:剩餘重量
* container_w:每個集裝箱的重量
* total_w: 總承載量
*/
void __backtrack (int layers,const int layers_size,
int current_w,int& best_w,
vector<int>& flag_x,vector<int>&
best_x,
int remainder_w,
const vector<int>& container_w,
int total_w)
{
if (layers > layers_size - 1) {
// 到達葉子結點,更新最優載重量
if (current_w < best_w || best_w == -1) {
copy(flag_x.begin(),flag_x.end
(),best_x.begin());
// copy(best_x.begin(),best_x.end
(),flag_x.begin());
best_w = current_w;
}
return;
}
remainder_w -= container_w[layers];
if (current_w + container_w[layers] <= total_w) {
// 搜索左子樹
flag_x[layers] = 1;
current_w += container_w[layers];
__backtrack(layers + 1,layers_size,current_w,
best_w,flag_x,best_x,remainder_w,container_w,
total_w);
current_w -= container_w[layers];
}
if (current_w + remainder_w > best_w || best_w == -
1) {
flag_x[layers] = 0;
__backtrack(layers + 1,layers_size,current_w,
best_w,flag_x,best_x,remainder_w,container_w,
total_w);
}
remainder_w += container_w[layers];
}
/* 裝載問題
* container_w: 各個集裝箱重量
* total_w: 總承載量
*/
void loading_backtrack (int total_w, vector<int>&
container_w)
{
int layers_size = container_w.size(); // 層數
int current_w = 0; // 當前裝載重量
int remainder_w = total_w; // 剩餘重量
int best_w = -1; // 最優載重量
vector<int> flag_x(layers_size); // 是否被選取標
志
vector<int> best_x(layers_size); // 最佳選取方案
__backtrack(0,layers_size,current_w,
best_w,flag_x,best_x,remainder_w,container_w,
total_w);
cout << "path : " ;
copy(best_x.begin(),best_x.end
(),ostream_iterator<int>(cout," "));
cout << endl;
cout << "best_w = " << best_w
<< "( ";
// 將結果輸出
for (size_t i = 0;i < best_x.size(); ++ i) {
if (best_x[i] == 1) {
cout << container_w[i] << " ";
}
}
cout << ")" << endl;
}
int main()
{
const int total_w = 30;
vector<int> container_w;
container_w.push_back(40);
container_w.push_back(1);
container_w.push_back(40);
container_w.push_back(9);
container_w.push_back(1);
container_w.push_back(8);
container_w.push_back(5);
container_w.push_back(50);
container_w.push_back(6);
loading_backtrack(total_w,container_w);
return 0;
}
批處理作業調度
問題表述:給定n個作業的集合
批處理作業調度問題要求對於給定的n個作業,制定最佳作業調度方案,使其完成時間和達到最小。
顯然,1,3,2是最佳調度方案。
解空間:排列樹(將作業順序進行全排列,分別算出各種情況的完成時間和,取最佳調度方案)
實現:
#include <iostream>
#include <vector>
using namespace std;
class flowshop
{
public:
flowshop(vector<vector<int> >& rhs) {
task_count = rhs.size() ;
each_t = rhs ;
best_t.resize (task_count) ;
machine2_t.resize (task_count,0) ;
machine1_t = 0 ;
total_t = 0 ;
best_total_t = 0 ;
current_t.resize (task_count,0) ;
for (int i = 0 ;i < task_count; ++ i) {
current_t[i] = i; // 爲了實現全排列
}
}
void backtrack () {
__backtrack (0);
// 顯示最佳調度方案和最優完成時間和
cout << "the best flowshop scheme is : ";
copy (best_t.begin(),best_t.end(),ostream_iterator<int> (cout, " "));
cout << endl;
cout << "the best total time is : " << best_total_t << endl;
}
private:
void __backtrack (int i) {
if (i >= task_count) {
if (total_t < best_total_t || best_total_t == 0) {
// 存儲當前最優調度方式
copy (current_t.begin(),current_t.end(),best_t.begin()) ;
best_total_t = total_t;
}
return ;
}
for (int j = i; j < task_count; ++ j) {
// 機器1上結束的時間
machine1_t += each_t[current_t[j]][0] ;
if (i == 0) {
machine2_t[i] = machine1_t + each_t[current_t[j]][1] ;
}
else {
// 機器2上結束的時間
machine2_t[i] =
((machine2_t[i - 1] > machine1_t) ? machine2_t[i - 1] : machine1_t)
+ each_t[current_t[j]][1] ;
}
total_t += machine2_t[i];
// 剪枝
if (total_t < best_total_t || best_total_t == 0) {
// 全排列
swap (current_t[i],current_t[j]) ;
__backtrack (i + 1) ;
swap (current_t[i],current_t[j]) ;
}
machine1_t -= each_t[current_t[j]][0] ;
total_t -= machine2_t[i] ;
}
}
public :
int task_count ; // 作業數
vector<vector<int> > each_t ; // 各作業所需的處理時間
vector<int> current_t ; // 當前作業調度
vector<int> best_t ; // 當前最優時間調度
vector<int> machine2_t ; // 機器2完成處理的時間
int machine1_t ; // 機器1完成處理的時間
int total_t ; // 完成時間和
int best_total_t ; // 當前最優完成時間和
};
int main()
{
// const int task_count = 4;
const int task_count = 3 ;
vector<vector<int> > each_t(task_count) ;
for (int i = 0;i < task_count; ++ i) {
each_t[i].resize (2) ;
}
each_t[0][0] = 2 ;
each_t[0][1] = 1 ;
each_t[1][0] = 3 ;
each_t[1][1] = 1 ;
each_t[2][0] = 2 ;
each_t[2][1] = 3 ;
// each_t[3][0] = 1 ;
// each_t[3][1] = 1 ;
flowshop fs(each_t) ;
fs.backtrack () ;
}
N後問題
問題表述:在
解向量:(x1, x2, … , xn)
顯約束:xi = 1,2, … ,n
隱約束:
1)不同列:xi != xj
2)不處於同一正、反對角線:|i-j| != |x(i)-x(j)|
解空間:滿N叉樹
實現:
#include <iostream>
#include <vector>
using namespace std;
class queen
{
// 皇后在棋盤上的位置
struct q_place {
int x;
int y;
q_place ()
: x(0),y(0)
{}
};
public:
queen(int qc)
: q_count (qc), sum_solution (0) {
curr_solution.resize (q_count);
}
void backtrack () {
__backtrack (0);
}
private:
void __backtrack (int t) {
if (t >= q_count) {
// 找到一個解決方案
++ sum_solution ;
for (size_t i = 0;i < curr_solution.size(); ++ i) {
cout << "x = " << curr_solution[i].x
<< " y = " << curr_solution[i].y << endl;
}
cout << "sum_solution = " << sum_solution << endl;
}
else {
for (int i = 0;i < q_count; ++ i) {
curr_solution[t].x = i;
curr_solution[t].y = t;
if (__place(t)) {
__backtrack (t + 1);
}
}
}
}
// 判斷第k個皇后的位置是否與前面的皇后相沖突
bool __place (int k) {
for (int i = 0; i < k; ++ i) {
if ((abs(curr_solution[i].x - curr_solution[k].x)
== abs(curr_solution[i].y - curr_solution[k].y))
|| curr_solution[i].x == curr_solution[k].x) {
return false;
}
}
return true;
}
private:
vector<q_place> curr_solution; // 當前解決方案
const int q_count; // 皇后個數
int sum_solution; // 當前找到的解決方案的個數
};
int main()
{
queen q(5);
q.backtrack ();
return 0;
}
旅行售貨員問題
問題表述:在圖中找到一個權最小的周遊路線
解空間:排列樹
剪枝策略:
當前路徑的權重+下一個路徑的權重 < 當前的最小權重,則搜索該路徑
實現:
#include <iostream>
#include <vector>
#include <iterator>
#include <algorithm>
using namespace std;
class traveling
{
public:
static const int NOEDGE = -1 ;
public:
traveling (const vector<vector<int> >& ug)
: curr_cost (0), best_cost (-1) {
node_count = ug.size ();
undigraph = ug;
curr_solution.resize (node_count);
for (int i = 0; i < node_count; ++ i) {
curr_solution[i] = i;
}
best_solution.resize (node_count);
}
void backtrack () {
__backtrack (1);
cout << best_cost << endl;
}
private:
void __backtrack (int layers) {
if (layers >= node_count) {
if (undigraph[curr_solution[node_count - 1]][curr_solution[0]] == NOEDGE){
return ;
}
int total_cost = curr_cost +
undigraph[curr_solution[node_count - 1]][curr_solution[0]] ;
if (total_cost < best_cost || best_cost == -1) {
// 更新最優費用和最優路徑
best_cost = total_cost;
copy (curr_solution.begin(),
curr_solution.end(),
best_solution.begin());
}
return ;
}
for (int i = layers; i < node_count; ++ i) {
// 剪枝
if (undigraph[curr_solution[layers - 1]][curr_solution[i]] != NOEDGE &&
( curr_cost + undigraph[curr_solution[layers - 1]][curr_solution[i]]
< best_cost || best_cost == -1 )) {
// 搜索子樹
swap (curr_solution[layers],curr_solution[i]);
curr_cost +=
undigraph[curr_solution[layers - 1]][curr_solution[layers]];
__backtrack (layers + 1);
curr_cost -=
undigraph[curr_solution[layers - 1]][curr_solution[layers]];
swap (curr_solution[layers],curr_solution[i]);
}
}
}
int node_count; // 結點個數
int curr_cost; // 當前費用
int best_cost; // 當前
vector<int> curr_solution; // 當前解決方案
vector<int> best_solution; // 最優解決方案
vector<vector<int> > undigraph; // 無向圖(採用矩陣存儲)
};
int main()
{
int size = 4;
vector<vector<int> > ug(size);
for (int i = 0;i < size; ++ i) {
ug[i].resize (size);
}
ug[0][0] = -1;
ug[0][1] = 30;
ug[0][2] = 6;
ug[0][3] = 4;
ug[1][0] = 30;
ug[1][1] = -1;
ug[1][2] = 5;
ug[1][3] = 10;
ug[2][0] = 6;
ug[2][1] = 5;
ug[2][2] = -1;
ug[2][3] = 20;
ug[3][0] = 4;
ug[3][1] = 10;
ug[3][2] = 20;
ug[3][3] = -1;
traveling t(ug);
t.backtrack();
return 0;
}
0-1揹包問題
問題表述:給定n種物品和一揹包。物品i的重量是wi,其價值爲pi,揹包的容量爲C。問應如何選擇裝入揹包的物品,使得裝入揹包中物品的總價值最大?
0-1揹包問題是一個特殊的整數規劃問題。
解空間:
可行性約束函數:
上界函數:
考慮一個右子樹的時候,設
r:是當前未考慮的剩餘物品的總價值(remainder)
cp:是當前的價值(current price)
bestp:是當前得到的最優價值(best price)
那麼,滿足:
但是,上界r太鬆。
一個更加緊的上界:
將剩餘物品按照單位重量價值排序,然後依次裝入物品,直到裝不下,再將剩餘物品的一部分放入揹包。(r_n <= r)
實現
#include <iostream>
#include <vector>
#include <algorithm>
#include <functional>
using namespace std;
class goods {
public:
int weight; // 重量
int price; // 價格
goods() : weight(0),price(0)
{}
};
bool goods_greater(const goods& lhs,const goods& rhs)
{
return (lhs.price / lhs.weight) > (rhs.price / rhs.weight);
}
class knapsack
{
public:
knapsack (int c,const vector<goods>& gl)
: capacity (c), curr_price(0), best_price (0), curr_weight(0){
goods_list = gl;
total_layers = gl.size();
curr_path.resize (total_layers);
best_path.resize (total_layers);
}
void backtrack () {
__backtrack (0);
cout << "path: " ;
copy (best_path.begin(),best_path.end(),ostream_iterator<int> (cout, " "));
cout << endl;
cout << "best_price: " << best_price << endl;
}
private:
// 計算上界
int __bound (int layers) {
int cleft = capacity - curr_weight;
int result = curr_price;
// 將layer之後的物品進行按單位價格降序排序
vector<goods> tmp = goods_list;
sort (tmp.begin() + layers, tmp.end(),goods_greater);
// 以物品單位重量價值遞減序裝入物品
while (layers < total_layers && tmp[layers].weight <= cleft) {
cleft -= tmp[layers].weight;
result += tmp[layers].price;
++ layers;
}
// 裝滿揹包
if (layers < total_layers) {
result += (tmp[layers].price / tmp[layers].weight) * cleft;
}
return result;
}
void __backtrack (int layers) {
// 到達葉子結點,更新最優價值
if (layers >= total_layers) {
if (curr_price > best_price || best_price == 0) {
best_price = curr_price;
copy (curr_path.begin(), curr_path.end(), best_path.begin());
}
return ;
}
// 左剪枝(能放的下)
if (curr_weight + goods_list[layers].weight <= capacity) {
curr_path[layers] = 1;
curr_weight += goods_list[layers].weight;
curr_price += goods_list[layers].price;
__backtrack (layers + 1);
curr_weight -= goods_list[layers].weight;
curr_price -= goods_list[layers].price;
}
// 右剪枝
if (__bound (layers + 1) > best_price || best_price == 0 ) {
curr_path[layers] = 0;
__backtrack (layers + 1);
}
/*curr_path[layers] = 0;
__backtrack (layers + 1);*/
}
private:
vector<goods> goods_list; // 貨物信息列表
int capacity; // 揹包承載量
int curr_price; // 當前價格
int curr_weight; // 當前重量
int best_price; // 當前得到的最優價值
int total_layers; // 總層數
vector<int> curr_path; // 當前路徑
vector<int> best_path; // 最優價值下的路徑
};
int main()
{
const int size = 3;
vector<goods> gl(size);
gl[0].weight = 10;
gl[0].price = 1;
gl[1].weight = 8;
gl[1].price = 4;
gl[2].weight = 5;
gl[2].price = 5;
knapsack ks(16, gl);
ks.backtrack ();
return 0;
}
分支限界法
分支限界法與回溯法
(1)求解目標:回溯法的求解目標是找出解空間樹中滿足約束條件的所有解,而分支限界法的求解目標則是找出滿足約束條件的一個解,或是在滿足約束條件的解中找出在某種意義下的最優解。
(2)搜索方式的不同:回溯法以深度優先的方式搜索解空間樹,而分支限界法則以廣度優先或以最小耗費優先的方式搜索解空間樹。
分支限界法的基本思想
分支限界法常以廣度優先或以最小耗費(最大效益)優先的方式搜索問題的解空間樹。
在分支限界法中,每一個活結點只有一次機會成爲擴展結點。活結點一旦成爲擴展結點,就一次性產生其所有兒子結點。在這些兒子結點中,導致不可行解或導致非最優解的兒子結點被捨棄,其餘兒子結點被加入活結點表中。
此後,從活結點表中取下一結點成爲當前擴展結點,並重覆上述結點擴展過程。這個過程一直持續到找到所需的解或活結點表爲空時爲止。
常見的兩種分支限界法
(1)隊列式(FIFO)分支限界法
按照隊列先進先出(FIFO)原則選取下一個結點爲擴展結點。
(2)優先隊列式分支限界法
按照優先隊列中規定的優先級選取優先級最高的結點成爲當前擴展結點。
單源最短路徑問題
問題描述 :在下圖所給的有向圖G中,每一邊都有一個非負邊權。要求圖G的從源頂點s到目標頂點t之間的最短路徑。
下圖是用優先隊列式分支限界法解有向圖G的單源最短路徑問題產生的解空間樹。其中,每一個結點旁邊的數字表示該結點所對應的當前路長。
找到一條路徑:
目前的最短路徑是8,一旦發現某個結點的下界不小於這個最短路進,則剪枝:
同一個結點選擇最短的到達路徑:
2.剪枝策略
在算法擴展結點的過程中,一旦發現一個結點的下界不小於當前找到的最短路長,則算法剪去以該結點爲根的子樹。
在算法中,利用結點間的控制關係進行剪枝。從源頂點s出發,2條不同路徑到達圖G的同一頂點。由於兩條路徑的路長不同,因此可以將路長長的路徑所對應的樹中的結點爲根的子樹剪去。
3.算法思想
解單源最短路徑問題的優先隊列式分支限界法用一極小堆來存儲活結點表。其優先級是結點所對應的當前路長。
算法從圖G的源頂點s和空優先隊列開始。結點s被擴展後,它的兒子結點被依次插入堆中。此後,算法從堆中取出具有最小當前路長的結點作爲當前擴展結點,並依次檢查與當前擴展結點相鄰的所有頂點。如果從當前擴展結點i到頂點j有邊可達,且從源出發,途經頂點i再到頂點j的所相應的路徑的長度小於當前最優路徑長度,則將該頂點作爲活結點插入到活結點優先隊列中。這個結點的擴展過程一直繼續到活結點優先隊列爲空時爲止。
實現
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
using namespace std;
struct node_info
{
public:
node_info (int i,int w)
: index (i), weight (w) {}
node_info ()
: index(0),weight(0) {}
node_info (const node_info & ni)
: index (ni.index), weight (ni.weight) {}
friend
bool operator < (const node_info& lth,const node_info& rth) {
return lth.weight > rth.weight ; // 爲了實現從小到大的順序
}
public:
int index; // 結點位置
int weight; // 權值
};
struct path_info
{
public:
path_info ()
: front_index(0), weight (numeric_limits<int>::max()) {}
public:
int front_index;
int weight;
};
// single source shortest paths
class ss_shortest_paths
{
public:
ss_shortest_paths (const vector<vector<int> >& g,int end_location)
:no_edge (-1), end_node (end_location), node_count (g.size()) , graph (g)
{}
// 打印最短路徑
void print_spaths () const {
cout << "min weight : " << shortest_path << endl;
cout << "path: " ;
copy (s_path_index.rbegin(),s_path_index.rend(),
ostream_iterator<int> (cout, " "));
cout << endl;
}
// 求最短路徑
void shortest_paths () {
vector<path_info> path(node_count);
priority_queue<node_info,vector<node_info> > min_heap;
min_heap.push (node_info(0,0)); // 將起始結點入隊
while (true) {
node_info top = min_heap.top (); // 取出最大值
min_heap.pop ();
// 已到達目的結點
if (top.index == end_node) {
break ;
}
// 未到達則遍歷
for (int i = 0; i < node_count; ++ i) {
// 頂點top.index和i間有邊,且此路徑長小於原先從原點到i的路徑長
if (graph[top.index][i] != no_edge &&
(top.weight + graph[top.index][i]) < path[i].weight) {
min_heap.push (node_info (i,top.weight + graph[top.index][i]));
path[i].front_index = top.index;
path[i].weight = top.weight + graph[top.index][i];
}
}
if (min_heap.empty()) {
break ;
}
}
shortest_path = path[end_node].weight;
int index = end_node;
s_path_index.push_back(index) ;
while (true) {
index = path[index].front_index ;
s_path_index.push_back(index);
if (index == 0) {
break;
}
}
}
private:
vector<vector<int> > graph ; // 圖的數組表示
int node_count; // 結點個數
const int no_edge; // 無通路
const int end_node; // 目的結點
vector<int> s_path_index; // 最短路徑
int shortest_path; // 最短路徑
};
int main()
{
const int size = 11;
vector<vector<int> > graph (size);
for (int i = 0;i < size; ++ i) {
graph[i].resize (size);
}
for (int i = 0;i < size; ++ i) {
for (int j = 0;j < size; ++ j) {
graph[i][j] = -1;
}
}
graph[0][1] = 2;
graph[0][2] = 3;
graph[0][3] = 4;
graph[1][2] = 3;
graph[1][5] = 2;
graph[1][4] = 7;
graph[2][5] = 9;
graph[2][6] = 2;
graph[3][6] = 2;
graph[4][7] = 3;
graph[4][8] = 3;
graph[5][6] = 1;
graph[5][8] = 3;
graph[6][9] = 1;
graph[6][8] = 5;
graph[7][10] = 3;
graph[8][10] = 2;
graph[9][8] = 2;
graph[9][10] = 2;
ss_shortest_paths ssp (graph, 10);
ssp.shortest_paths ();
ssp.print_spaths ();
return 0;
}
測試數據(圖)
測試結果
min weight : 8
path: 0 2 6 9 10
裝載問題
問題描述
有一批共個集裝箱要裝上2艘載重量分別爲
裝載問題要求確定是否有一個合理的裝載方案可將這個集裝箱裝上這2艘輪船。如果有,找出一種裝載方案。
容易證明:如果一個給定裝載問題有解,則採用下面的策略可得到最優裝載方案。
(1)首先將第一艘輪船儘可能裝滿;
(2)將剩餘的集裝箱裝上第二艘輪船。
將第一艘輪船儘可能裝滿等價於選取全體集裝箱的一個子集,使該子集中集裝箱重量之和最接近。由此可知,裝載問題等價於以下特殊的0-1揹包問題。
例如:W = <10,8,5> , C = 16
- 隊列式分支限界法
在算法的while循環中,首先檢測當前擴展結點的左兒子結點是否爲可行結點。如果是則將其加入到活結點隊列中。然後將其右兒子結點加入到活結點隊列中(右兒子結點一定是可行結點)。2個兒子結點都產生後,當前擴展結點被捨棄。
活結點隊列中的隊首元素被取出作爲當前擴展結點,由於隊列中每一層結點之後都有一個尾部標記-1,故在取隊首元素時,活結點隊列一定不空。當取出的元素是-1時,再判斷當前隊列是否爲空。如果隊列非空,則將尾部標記-1加入活結點隊列,算法開始處理下一層的活結點。
while (true) {
// 檢查左兒子結點
if (Ew + w[i] <= c) //x[i] = 1
EnQueue(Q, Ew + w[i], bestw, i, n);
// 右兒子結點總是可行的
EnQueue(Q, Ew, bestw, i, n); //x[i] = 0
Q.Delete(Ew); // 取下一擴展結點
if (Ew == -1) { // 同層結點尾部
if (Q.IsEmpty())
return bestw;
Q.Add(-1); // 同層結點尾部標誌
Q.Delete(Ew); // 取下一擴展結點
i++; // 進入下一層
}
}
變量含義:
Ew: 擴展節點的載重量
W: 重量數組
Q: 活節點隊列
bestw: 當前最優載重量
i: 當前處理到的層數
n: 總貨物數
- 算法的改進
節點的左子樹表示將此集裝箱裝上船,右子樹表示不將此集裝箱裝上船。設bestw是當前最優解;ew是當前擴展結點所相應的重量;r是剩餘集裝箱的重量。則當ew + r £ bestw時,可將其右子樹剪去,因爲此時若要船裝最多集裝箱,就應該把此箱裝上船。
另外,爲了確保右子樹成功剪枝,應該在算法每一次進入左子樹的時候更新bestw的值。
// 檢查左兒子結點
Type wt = Ew +w[i]; //左兒子結點的重量
if (wt <= c) { //可行結點
if (wt > bestw)
bestw = wt;
// 加入活結點隊列
if (i < n)
Q.Add(wt);
}
// 檢查右兒子結點
if (Ew + r > bestw&& i < n)
Q.Add(Ew); // 可能含最優解
Q.Delete(Ew); //取下一擴展結點
構造最優解
爲了在算法結束後能方便地構造出與最優值相應的最優解,算法必須存儲相應子集樹中從活結點到根結點的路徑。爲此目的,可在每個結點處設置指向其父結點的指針,並設置左、右兒子標誌。
class QNode
{
QNode *parent ; // 指向父結點的指針
bool LChild ; // 左兒子標誌
Type weight ; // 結點所相應的載重量
}
找到最優值後,可以根據parent回溯到根節點,找到最優解。
// 構造當前最優解
for (int j = n – 1; j> 0; j–) {
bestx[j] = bestE->LChild;
bestE = bestE->parent;
}
LChild是左子樹標誌,1表示左子樹,0表示右子樹;
bestx[i]取值爲0/1,表示是否取該貨物。
備註:下面僅是我個人的理解:
以W = <10,8,5> , C = 16 問題爲例,
最後遍歷路徑隊列找出路徑(office學的不好,別見怪啊)。從分析可知如果集裝箱數量爲n,那麼需要的存儲空間爲(2^n-1),無疑是很費內存空間的,而且代碼要複雜的多,所以在我的代碼中沒有實現。
優先隊列式分支限界法
解裝載問題的優先隊列式分支限界法用最大優先隊列存儲活結點表。活結點x在優先隊列中的優先級定義爲從根結點到結點x的路徑所相應的載重量再加上剩餘集裝箱的重量之和。
優先隊列中優先級最大的活結點成爲下一個擴展結點。以結點x爲根的子樹中所有結點相應的路徑的載重量不超過它的優先級。子集樹中葉結點所相應的載重量與其優先級相同。
在優先隊列式分支限界法中,一旦有一個葉結點成爲當前擴展結點,則可以斷言該葉結點所相應的解即爲最優解。此時可終止算法。
兩種實現方式:
(1) 在結點優先隊列的每一個活結點中保存從解空間樹的根節點到該活結點的路徑。 算法確定了達到最優值的葉結點時,在該葉結點處同時得到相同的最優解。
(2) 在算法的搜索進程中保存當前以構造出的部分解空間樹。這樣在算法確定了達到最優值的葉結點時,就可以在解空間樹種該葉結點開始向根結點回溯,構造出相應的最優解。
實現:
#include <iostream>
#include <vector>
#include <queue>
#include <numeric>
using namespace std;
// BAB for "branch and bound method"
// FIFO隊列式分支限界法
class load_BAB
{
public:
load_BAB (const vector<int>& w, int c)
: weight (w), capacity (c), c_count ((int)w.size()), best_w(0) {
}
int get_best_w () const {
return best_w ;
}
// 隊列式分支限界法
int queue_BAB () {
live_node_q.push (-1); // 同層節點尾部標識
int i = 0;
int cw = 0;
while (true) {
// 檢查左子結點
if (cw + weight[i] <= capacity) {
__en_queue (cw + weight[i], i) ;
/*if ((cw + weight[i]) > best_w) {
best_w = cw + weight[i];
}*/
}
// 檢查右子節點(可能產生最優解)
int best_rest = accumulate (weight.begin() + i + 1, weight.end(), 0) ;
if (best_rest > best_w) {
__en_queue (cw, i) ;
}
// 取下一個結點
cw = live_node_q.front ();
live_node_q.pop ();
if (cw == -1) {
if (live_node_q.empty ()) {
return best_w ;
}
live_node_q.push (-1);
cw = live_node_q.front ();
live_node_q.pop ();
++ i ;
}
}
}
private:
void __en_queue (int cw, int i) {
// 將活結點加入到活結點隊列Q中
if (i >= c_count - 1) {
if (cw > best_w) {
best_w = cw ;
}
}
else {
live_node_q.push (cw) ;
}
}
private:
vector<int> weight; // 集裝箱重量
queue<int> live_node_q; // 活結點隊列
int c_count; // 集裝箱 (container) 個數
int capacity; // 輪船承載量
int best_w; // 最優載重量
};
// 子集空間樹中結點
class BB_node
{
public:
BB_node (BB_node* par, bool lc) {
parent = par ;
left_child = lc ;
}
public:
BB_node* parent ; // 父結點
bool left_child ; // 左兒子結點標誌
} ;
// 優先級隊列結點
class heap_node
{
public:
heap_node (BB_node* node, int uw, int lev) {
live_node = node ;
upper_weight = uw ;
level = lev ;
}
friend
bool operator < (const heap_node& lth, const heap_node& rth) {
return lth.upper_weight < rth.upper_weight ;
}
friend
bool operator > (const heap_node& lth, const heap_node& rhs) {
return lth.upper_weight > rhs.upper_weight ;
}
public:
BB_node* live_node ; //
int upper_weight ; // 活結點優先級(上界)
int level ; // 活結點在子集樹中所處的層序號
};
// 優先權隊列式分支限界法
class load_PQBAB
{
public:
load_PQBAB (const vector<int>& w, int c)
: weight (w), capacity (c), c_count (static_cast<int>(w.size())) {
}
~load_PQBAB () {
}
void max_loading () {
BB_node* pbn = NULL ; // 當前擴展結點
int i = 0 ; // 當前擴展結點所處的層
int ew = 0 ; // 擴展結點所相應的載重量
vector<int> r (c_count, 0);// 剩餘重量數組
for (int j = c_count - 2; j >= 0; -- j) {
r[j] = r[j + 1] + weight[j + 1] ;
}
/*copy (r.begin(), r.end(), ostream_iterator<int>(cout, " ")) ;
cout << endl; */
// 搜索子集空間樹
while (i != c_count) {
// 非葉結點,檢查當前擴展結點的兒子結點
if (ew + weight[i] <= capacity) {
// 左兒子爲可行結點
__add_live_node (ew + weight[i] + r[i], i + 1, pbn, true) ;
}
// 右兒子結點爲可行結點
__add_live_node (ew + r[i], i + 1, pbn, false) ;
// 釋放內存
while (pbn != NULL) {
BB_node *p = pbn ;
pbn = pbn->parent ;
delete p ;
}
// 取下一擴展結點
heap_node node = pri_queue.top () ;
pri_queue.pop ();
// cout << node.upper_weight <<endl;
i = node.level ;
pbn = node.live_node ;
ew = node.upper_weight - r[i - 1] ;
}
// 釋放內存
while (pri_queue.size() != 0) {
heap_node node = pri_queue.top () ;
pri_queue.pop () ;
while (node.live_node != NULL) {
BB_node* temp = node.live_node ;
node.live_node = node.live_node->parent ;
delete temp ;
}
}
// 構造最優解
cout << "best capacity: " << ew << endl ;
cout << "path: " ;
vector<bool> temp_path ;
while (pbn != NULL) {
temp_path.push_back (pbn->left_child) ;
BB_node *temp = pbn ;
pbn = pbn->parent ;
delete temp ;
}
copy (temp_path.rbegin(), temp_path.rend(), ostream_iterator<bool> (cout, " "));
cout << endl ;
}
private:
// 產生新的活結點,加入到子集樹中
void __add_live_node (int uw, int lev, BB_node* par, bool lc) {
// 深拷貝
BB_node *first = NULL;
BB_node *end = NULL ;
while (par != NULL) {
BB_node* p = new BB_node(NULL, par->left_child) ;
if (first == NULL) {
first = p ;
end = p ;
}
else {
end->parent = p ;
end = end->parent ;
}
par = par->parent ;
}
BB_node* p = new BB_node (first, lc) ;
pri_queue.push (heap_node (p, uw, lev)) ;
}
private :
vector<int> weight; // 集裝箱重量
int c_count; // 集裝箱 (container) 個數
int capacity; // 輪船承載量
priority_queue<heap_node> pri_queue ; // 活結點優先級隊列
} ;
int main()
{
const int capacity = 20 ;
vector<int> weight ;
weight.push_back (10);
weight.push_back (8);
weight.push_back (5);
weight.push_back (1);
weight.push_back (3);
load_PQBAB l (weight, capacity) ;
l.max_loading ();
/*load_BAB lb (weight, capacity) ;
lb.queue_BAB () ;
cout << lb.get_best_w() << endl ;*/
return 0;
}
旅行售貨員問題
- 問題描述
某售貨員要到若干城市去推銷商品,已知各城市之間的路程(或旅費)。他要選定一條從駐地出發,經過每個城市一次,最後回到駐地的路線,使總的路程(或總旅費)最小。
路線是一個帶權圖。圖中各邊的費用(權)爲正數。圖的一條周遊路線是包括V中的每個頂點在內的一條迴路。周遊路線的費用是這條路線上所有邊的費用之和。
旅行售貨員問題的解空間可以組織成一棵樹,從樹的根結點到任一葉結點的路徑定義了圖的一條周遊路線。旅行售貨員問題要在圖G中找出費用最小的周遊路線(解空間:排列樹)。
- 算法描述
算法開始時創建一個最小堆,用於表示活結點優先隊列。堆中每個結點的子樹費用的下界lcost值是優先隊列的優先級。接着算法計算出圖中每個頂點的最小費用出邊並用minout記錄。
如果所給的有向圖中某個頂點沒有出邊,則該圖不可能有迴路,算法即告結束。如果每個頂點都有出邊,則根據計算出的minout作算法初始化。
使用最小堆:
對樹中的每個節點,定義以下成員變量:
優先級:lcost
當前節點的路徑長度:cc
剩餘節點的最小出邊和:rcost
節點在樹中的深度:s
長度爲n的數組x[0:n-1],用來存放從起點開始的路徑。
我們定義:
對第n-2層以上的節點:lcost = cc + rcost
對第n-1,n-2層的節點:lcost = 該回路的長度
算法的while循環體完成對排列樹內部結點的擴展。對於當前擴展結點,算法分2種情況進行處理:
1、首先考慮s = n-2的情形,此時當前擴展結點是排列樹中某個葉結點的父結點。如果該葉結點相應一條可行迴路且費用小於當前最小費用,即:lcost < bestc,則將該葉結點插入到優先隊列中,否則捨去該葉結點。
2、當s < n-2時,算法依次產生當前擴展結點的所有兒子結點。由於當前擴展結點所相應的路徑是x[0:s],其可行兒子結點是從剩餘頂點x[s+1:n-1]中選取的頂點x[i],且(x[s],x[i])是所給有向圖G中的一條邊。對於當前擴展結點的每一個可行兒子結點,計算出其前綴(x[0:s],x[i])的費用cc和相應的下界lcost。當lcost
#include <iostream>
#include <vector>
#include <queue>
#include <limits>
using namespace std ;
class heap_node
{
public:
heap_node (float lc, float cc, float rc, int s, const vector<int>& p)
: lower_cost (lc), current_cost (cc), remainder_cost (rc), size(s)
{
path = p ;
}
friend
bool operator < (const heap_node& rhs, const heap_node& lhs) {
return rhs.lower_cost > lhs.lower_cost ;
}
public:
float lower_cost ; // 子樹費用的下界
float current_cost ; // 當前費用
float remainder_cost ;// 剩餘頂點的最小出邊費用和
int size ; // 根節點到當前結點的路徑爲path [0 : s]
vector<int> path ; // 需要進一步搜索的頂點是path [s+1 : n-1]
} ;
class BBTSP
{
public:
static float MAX_VALUE;
static float NO_EDGE_VALUE;
typedef priority_queue<heap_node> min_heap ;
public:
// 構造函數
BBTSP (const vector<vector<float> >& g) {
graph = g ;
node_count = (int)g.size ();
best_p.resize (node_count) ;
}
void bb_TSP () {
int n = node_count;
min_heap mh ; // 最小堆
// min_out[i] = 頂點i最小出邊費用
vector<float> min_out(node_count) ;
float min_sum = 0.0f ; // 最小出邊費用和
for (int i = 0; i < node_count ; ++ i) {
float min = MAX_VALUE ;
for (int j = 0; j < node_count ; ++ j) {
if (graph[i][j] != NO_EDGE_VALUE && graph[i][j] < min) {
min = graph[i][j] ;
}
}
if (min == MAX_VALUE) {
cerr << " No cycle !" << endl;
return ;
}
min_out[i] = min ;
min_sum += min ;
}
for (int i = 0; i < node_count ; ++ i) {
cout << "結點" << i << "的最小出邊費用和爲: " << min_out[i] << endl ;
}
cout << "總出邊費用爲: " << min_sum << endl << endl ;
// 初始化
vector<int> path(n) ;
for (int i = 0; i < n; ++ i) {
path[i] = i;
}
heap_node hn(0, 0, min_sum, 0, path);
float best_c = MAX_VALUE ;
// 搜索排列空間樹
while (hn.size < n - 1) {
// 非葉結點
path = hn.path ;
cout << "path: " ;
copy (path.begin(), path.end(), ostream_iterator<int>(cout,"")) ;
cout << endl ;
if (hn.size == n - 2) {
// 當前擴展結點是葉結點的父結點
// 再加條邊構成迴路
// 所構成的迴路是否優於當前最優解
if (graph[path[n-2]][path[n-1]] != NO_EDGE_VALUE &&
graph[path[n-1]][1] != NO_EDGE_VALUE &&
hn.current_cost + graph[path[n-2]][path[n-1]] +
graph[path[n-1]][1] < best_c ) {
// 找到費用更小的迴路
best_c = hn.current_cost + graph[path[n-2]][path[n-1]] +
graph[path[n-1]][1] ;
hn.current_cost = best_c ;
hn.lower_cost = best_c ;
hn.size ++ ;
mh.push (hn) ;
}
}
else {
// 產生當前擴展結點的兒子結點
for (int i = hn.size + 1; i < n; ++ i) {
if (graph[path[hn.size]][path[i]] != NO_EDGE_VALUE) {
// 可行的兒子結點
float cc = hn.current_cost + graph[path[hn.size]][path[i]] ;
float rcost = hn.remainder_cost - min_out[path[hn.size]] ;
// 優先級= 當前費用+ 剩餘結點的最小費用和- 當前節點的最小費用
float b = cc + rcost ; // 下界
if (b < best_c) {
// 子樹可能含最優解,結點插入最小堆
vector<int> p(n) ;
for (int j = 0; j < n; ++ j) {
p[j] = path[j] ;
}
//copy (p.begin(), p.end(), ostream_iterator<int> (cout, " ")) ;
//cout << ", " ;
p[hn.size + 1] = path[i] ;
p[i] = path[hn.size + 1] ;
//copy (p.begin(), p.end(), ostream_iterator<int> (cout, " ")) ;
//cout << endl;
heap_node in(b, cc, rcost, hn.size + 1, p) ;
mh.push (in) ;
}
}
}
}
// 取下一擴展結點
hn = mh.top () ;
mh.pop () ;
}
best_cost = best_c ;
for (int i = 0; i < node_count; ++ i) {
best_p[i] = path[i] ;
}
copy (best_p.begin(), best_p.end(), ostream_iterator<int> (cout, "")) ;
cout << endl ;
cout << "best cost : " << best_cost << endl ;
}
private:
vector<vector<float> > graph ; // 圖的數組表示
int node_count ;// 結點個數
vector<int> best_p ; // 產生最優解的路徑
float best_cost ; // 最優解
} ;
float BBTSP::MAX_VALUE = numeric_limits<float>::max() ;
float BBTSP::NO_EDGE_VALUE = -1.0f ;
int main()
{
// 圖的初始化
const int size = 6 ;
vector<vector<float> > g(size) ;
for (int i = 0; i < size; ++ i) {
g[i].resize (size) ;
}
for (int i = 0;i < size; ++ i) {
g[i][i] = BBTSP::NO_EDGE_VALUE ;
}
g[0][1] = 30 ;
g[0][2] = 6 ;
g[0][3] = 4 ;
g[0][4] = 5 ;
g[0][5] = 6 ;
g[1][0] = 30 ;
g[1][2] = 4 ;
g[1][3] = 5 ;
g[1][4] = 2 ;
g[1][5] = 1 ;
g[2][0] = 6 ;
g[2][1] = 4 ;
g[2][3] = 7 ;
g[2][4] = 8 ;
g[2][5] = 9 ;
g[3][0] = 4 ;
g[3][1] = 5 ;
g[3][2] = 7 ;
g[3][4] = 10 ;
g[3][5] = 20 ;
g[4][0] = 5 ;
g[4][1] = 2 ;
g[4][2] = 8 ;
g[4][3] = 10 ;
g[4][5] = 3 ;
g[5][0] = 6 ;
g[5][1] = 1 ;
g[5][2] = 9 ;
g[5][3] = 20 ;
g[5][4] = 3 ;
BBTSP bt(g) ;
bt.bb_TSP () ;
return 0 ;
}
運行結果:
結點 0的最小出邊費用和爲: 4
結點 1的最小出邊費用和爲: 1
結點 2的最小出邊費用和爲: 4
結點 3的最小出邊費用和爲: 4
結點 4的最小出邊費用和爲: 2
結點 5的最小出邊費用和爲: 1
總出邊費用爲: 16
path: 0 1 2 3 4 5
path: 0 3 2 1 4 5
path: 0 4 2 3 1 5
path: 0 3 1 2 4 5
path: 0 4 1 3 2 5
path: 0 3 1 5 4 2
path: 0 4 1 5 2 3
path: 0 2 1 3 4 5
path: 0 3 1 4 2 5
path: 0 5 2 3 4 1
path: 0 4 5 3 1 2
path: 0 5 1 3 4 2
path: 0 4 5 1 3 2
path: 0 2 1 3 4 5
path: 0 2 1 5 4 3
path: 0 3 1 5 4 2
path: 0 5 1 4 3 2
path: 0 2 1 4 3 5
path: 0 3 2 1 4 5
path: 0 3 1 4 5 2
path: 0 3 2 1 4 5
path: 0 3 2 1 5 4
path: 0 2 1 5 4 3
path: 0 2 1 4 5 3
path: 0 4 1 2 3 5
path: 0 5 4 3 2 1
path: 0 3 1 2 4 5
path: 0 5 4 1 2 3
path: 0 3 2 1 4 5
path: 0 5 1 2 4 3
0 5 1 2 4 3
best cost : 21