1.回溯法算法思想:
定義:ios
回溯法(探索與回溯法)是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步從新選擇,這種走不通就退回再走的技術爲回溯法,而知足回溯條件的某個狀態的點稱爲「回溯點」。算法
一、回溯法適用:有許多問題,當須要找出它的解集(所有解)或者要求回答什麼解是知足某些約束條件的最優解時,每每要使用回溯法。數組
二、有組織的窮舉式搜索:回溯法的基本作法是搜索或者有的組織窮盡搜索。它能避免搜索全部的可能性。即避免沒必要要的搜索。這種方法適用於解一些組合數至關大的問題。數據結構
三、搜索解空間樹:回溯法在問題的解空間樹中,按深度優先策略,從根結點出發搜索解空間樹。算法搜索至解空間樹的任意一點時,先判斷該結點是否包含問題的解。若是確定不包含(剪枝過程),則跳過對該結點爲根的子樹的搜索,逐層向其祖先結點回溯;不然,進入該子樹,繼續按深度優先策略搜索。app
爲了實現回溯,咱們先弄明白如下兩個問題:框架
1)首先應該明確問題的解空間。ide
2)其次是組織解空間以便它能用以被搜索到。函數
2. 問題的解空間 和空間樹
這個空間必須至少包含一個解(多是最優的)。 一個複雜問題的解決每每由多部分構成,即,一個大的解決方案能夠看做是由若干個小的決策組成。不少時候它們構成一個決策序列。解決一個問題的全部可能的決策序列構成該問題的解空間。解空間中知足約束條件的決策序列稱爲可行解。通常說來,解任何問題都有一個目標,在約束條件下使目標值達到最大(或最小)的可行解稱爲該問題的最優解。在解空間中,前k項決策已經取定的全部決策序列之集稱爲k定子解空間。0定子解空間便是該問題的解空間。 佈局
問題的解空間一般是在搜索問題的解的過程當中動態產生的,這是回溯算法的一個重要特性。post
解空間的肯定與咱們對問題的描述有關。如何組織解空間的結構會直接影響對問題的求解效率。這是由於回溯方法的基本思想是經過搜索解空間來找到問題所要求的解。通常地,能夠用一棵樹來描述解空間,稱爲解空間樹。
當所給的問題是從n個元素的集合S中找出知足某種性質的子集時,相應的解空間樹稱爲子集合樹。此時,解空間有個元素,遍歷子集樹的任何算法均需的計算時間。
如例:定和子集問題: 已知一個正實數的集合P= {W1,w2, ... Wn}和另外一個正實數M.試求P的全部子集S,使得S中的數之和等於M。這個問題的解能夠表
示成0/1數組{x1,x2,…,xn},依據W1是否屬於S, X1分別取值1或0。故解空間中共有個元素。它的樹結構是一棵完整二叉樹。
當所給的問題是肯定n個元素的知足某種性質的排列時,相應的解空間樹稱爲排列樹,此時,解空間有個元素。遍歷排列樹的任何算法均需的計算時間,均需的計算時間。
咱們把這個例子逐一解析:
問題的解向量:問題的解可以表示成一個n元式(x1,x2,…,xn)的形式。
顯約束:對份量xi的取值限定。
隱約束:爲知足問題的解而對不一樣份量之間施加的約束。
解空間:對於問題的一個實例,解向量知足顯式約束條件的全部多元組,構成了該實例的一個解空間。
注意:同一個問題能夠有多種表示,有些表示方法更簡單,所需表示的狀態空間更小(存儲量少,搜索方法簡單)。
下面是n=3時的0-1揹包問題用徹底二叉樹表示的解空間:
爲了敘述方便,引進一些關於解空間樹結構的術語。解空間樹上的每一個節點肯定求解問題的一個問題狀態,它由一條從根到該節點的路徑描述。由根到全部其它節點的路徑描述了這個問題的狀態空間。解狀態是這樣一些問題狀態S,對於這些問題狀態,由根到S的那條路徑肯定了解空間的一個元組。即答案狀態是這樣的一些解狀態S,對於這些解狀態而言,由根到S的這條路徑肯定了這個問題的一個解(便可行解),解空間的樹結構稱爲狀態空間樹。
肯定了解空間的組織結構後,回溯法就從初始節點(解空間樹的根節點)出發,以深度優先的方式搜索整個解空間。這個開始節點就成爲一個活節點,同時也成爲當前的擴展節點。在當前擴展節點處,搜索向縱深方向移至一個新節點。這個新節點就成爲一個新的活節點,而且成爲當前的擴展節點。若是在當前的擴展節點處不能再向縱深方向搜索,則當前的擴展節點就成爲死節點。此時應往回移動(回溯)至最近一個活節點處,並使這個活節點成爲當前擴展節點。如此繼續。回溯法就是以這種工做方式遞歸地在解空間中搜索,直至找到要求的解或解空間中已無活節點時爲止。
事實上,當咱們將問題的有關數據以必定的數據結構存儲好之後(例如,旅行商問題存儲賦權圖的鄰接矩陣、定和子集問題是存儲已知的n+1個數、4皇后問題用整數對(i,j)表示棋盤上各個位置,沒必要先創建一個解空間樹),就搜索生成解空間樹的一部分或所有,並尋找所須要的解。也就是說,對於實際問題沒必要生成整個狀態空間樹,而後在整個解空間中搜索,咱們只需有選擇地搜索。爲了使搜索更加有效,經常在搜索過程當中加一些判斷以決定搜索是否該終止或改變路線。一般採用兩種策略來避免無效的搜索,提升回溯法的搜索效率:
其一是使用約束函數,在擴展節點處剪去不知足約束的子樹;
其二是用限界函數, 「剪去」不能達到最優解的子樹。
這兩種函數統稱爲剪枝函數。
總結:
擴展結點:一個正在產生兒子的結點稱爲擴展結點
活結點:一個自身已生成但其兒子尚未所有生成的節點稱作活結點
死結點:一個全部兒子已經產生的結點稱作死結點
深度優先的問題狀態生成法:若是對一個擴展結點R,一旦產生了它的一個兒子C,就把C當作新的擴展結點。在完成對子樹C(以C爲根的子樹)的窮盡搜索以後,將R從新變成擴展結點,繼續生成R的下一個兒子(若是存在)
寬度優先的問題狀態生成法:在一個擴展結點變成死結點以前,它一直是擴展結點。
回溯法:爲了不生成那些不可能產生最佳解的問題狀態,要不斷地利用限界函數(bounding function)來處死(剪枝)那些實際上不可能產生所需解的活結點,以減小問題的計算量。具備限界函數的深度優先生成法稱爲回溯法。(回溯法 = 窮舉 + 剪枝)。
3.回溯法的思路
描述問題:
定義可用回溯法求解的問題P:對於已知的由n元組(x1,x2,…,xn)組成的一個狀態空間E={(x1,x2,…,xn)∣xi∈Si ,i=1,2,…,n},給定關於n元組中的一個份量的一個約束集D,要求E中知足D的所有約束條件的全部n元組。其中Si是份量xi的定義域,且 |Si| 有限,i=1,2,…,n。咱們稱E中知足D的所有約束條件的任一n元組爲問題P的一個解。
解問題P的最樸素的方法就是枚舉法,即對E中的全部n元組逐一地檢測其是否知足D的所有約束,若知足,則爲問題P的一個解。但顯然,其計算量是至關大的。
基本思路:
若已有知足約束條件的部分解,不妨設爲(x1,x2,x3,……xi),I<n,則添加x(i+1)屬於s(i+2),檢查 (x1,x2,……,xi,x(i+1))是否知足條件,知足了就繼續添加x(i+2)、s(i+2),若全部的x(i+1)屬於s(i+1)都不能獲得 部分解,就去掉xi,回溯到(xi,x2,……x(i- 1)),添加那些未考察過的x1屬於s1,看其是否知足約束條件,爲此反覆進行,直至獲得解或證實無解。
這個回溯法明顯提升算法效率。
4.回溯法的步驟
總結起來,運用回溯法解題一般包括如下三個步驟
1).肯定問題的解空間 :針對所給問題,定義問題的解空間;
子集樹問題:裝載問題、符號三角形問題、0-1揹包問題、最大團問題
排列樹問題:批處理做業調度、n後問題、旅行售貨員問題、圓排列問題、電路板排列問題
其餘:圖的m着色問題
2).肯定易於搜索的解空間結構:
找出適當的剪枝函數,約束函數和限界函數。
3).以深度優先的方式搜索解空間,而且在搜索過程當中用剪枝函數避免無效的搜索。
遞歸回溯
迭代回溯
4)利用限界函數避免移動到不可能產生解的子空間
三.
5.算法框架
1. 遞歸回溯:
回溯法對解空間做深度優先搜索,所以,在通常狀況下用遞歸方法實現回溯法。
void backtracking (int t)
{
if (t > n) {
// 到達葉子結點,將結果輸出
output (x);
}
else {
// 遍歷結點t的全部子結點,即枚舉t全部可能的路徑
// f(n,t)=下界;g(n,t)=上界;
for (int i = f(n,t); i <= g(n,t); i ++ ) {//
x[t] = h[i];//知足界限函數和約束函數
// 若是不知足剪枝條件,則繼續遍歷,進入下一層
if (constraint (t) && bound (t))
backtrack (t + 1);
}
}
}
t是遞歸深度;
n是深度控制,即解空間樹的的高度;
可行性判斷有兩方面的內容:不滿約束條件則剪去相應子樹;若限界函數越界,也剪去相應子樹;二者均知足則進入下一層;
2. 迭代回溯
採用樹的非遞歸深度優先遍歷算法,可將回溯法表示爲一個非遞歸迭代過程。
// 針對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--;
}
}
}
6. 回溯法依賴的兩種數據結構
回溯法一般在解空間樹上進行搜索,通常依賴的兩種數據結構:子集樹和排列樹
子集樹(遍歷子集樹需O(2^n)計算時間):
通常有裝載問題、符號三角形問題、0-1揹包問題、最大團問題
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!)計算時間):
通常有批處理做業調度、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]);
}
}
其中f(n,t),g(n,t)表示當前擴展結點處未搜索過的子樹的起始標號和終止標號, h(i)表示當前擴展節點處,x[t]第i個可選值constraint(t)和bound(t)是當前 擴展結點處的約束函數和限界函數。constraint(t)返回true時,在當前擴展結點 x[1:t]取值知足約束條件,不然不知足約束條件,可減去相應的子樹。bound(t)返 回的值爲true時,在當前擴展結點x[1:x]處取值未使目標函數越界,還須要由backtrack(t+1) 對其相應的子樹進一步搜索。
7.回溯法的應用
應用回溯法有:
- 1)裝載問題
- 2)批處理做業調度
- 3)符號三角形問題
- 4)n後問題
- 5)0-1揹包問題
- 6)最大團問題
- 7)圖的m着色問題
- 8)旅行售貨員問題
- 9)圓排列問題
- 10)電路板排列問題
- 11)連續郵資問題
n皇后問題:
1.問題表述:在n×n格的棋盤上放置彼此不受攻擊的n個皇后。按照國際象棋的規則,皇后能夠攻擊與之處在同一行或同一列或同一斜線上的棋子。n後問題等價於在n×n格的棋盤上放置n個皇后,任何2個皇后不放在同一行或同一列或同一斜線上。求不一樣的解的個數。
複雜問題從簡單問題入手,咱們先分析四皇后的問題,四叉樹展現了求解的過程:
2. 問題分析:
(1) 解空間:一組n元一維向量(x1, x2, x3, ... , xn),搜索空間是:1<=xi<=n, i=1,2,3,...,n
(2) 約束條件:
1)不一樣列:xi != xj
2)不處於同一正、反對角線:|i-j| != |x(i)-x(j)|
3. 代碼實現:
// stdafx.h : include file for standard system include files, // or project specific include files that are used frequently, but // are changed infrequently // #pragma once #include <stdio.h> #include "stdlib.h" #include <iostream> using namespace std; //宏定義 #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 typedef int Status; typedef int ElemType;
// Test.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <vector> 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 () { _backtracking (0); } private: /************************************************************************/ /* 判斷對應的位置是否存在當前的方案中 */ /************************************************************************/ bool _isCoordinate(int x, int y) { for (size_t i = 0;i < curr_solution.size(); ++ i) { if (curr_solution[i].x ==x && curr_solution[i].y == y) { return true; } } return false; } /************************************************************************/ /* 打印當前的位置 */ /************************************************************************/ void _printResult() { for (size_t i = 0;i < curr_solution.size(); ++ i) { for(size_t j = 0;j < curr_solution.size(); ++j) { if (_isCoordinate(i, j)) { cout<<"1 "; }else{ cout<<"0 "; } } cout<< endl; } cout << "sum_solution = " << sum_solution << endl; } /************************************************************************/ /* 如今從第i行算起繼續爲後續的棋子選擇合適的位置 */ /************************************************************************/ void _backtracking (int i) { if (i >= q_count) { //找到一個解決方案,將結果輸出 ++ sum_solution ; _printResult(); } else { for (int j = 0;j < q_count; ++ j) { //將第i行第j列放置一個棋子 curr_solution[i].x = j; curr_solution[i].y = i; if (isOk(i)) { //當前佈局合法 _backtracking (i + 1); } } } } /************************************************************************/ /* 判斷第k個皇后的位置是否與前面的皇后相沖突 */ /************************************************************************/ bool isOk(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; }
定和0/1揹包問題
問題表述:給定n種物品和一揹包。第i件物品的重量是wi,其價值爲pi,揹包的容量爲C。問應如何選擇裝入揹包的物品,使得裝入揹包中物品的總價值最大?
0-1揹包問題是一個數規劃問題:肯定一個向量:x=(x1,x2,...,xn)知足:
問題分析:
(1) 解空間:一組n元一維向量(x1, x2, x3, ... , xn),搜索空間是:1<=xi<=n, i=1,2,3,...,n
(2) 約束條件:
可行性約束函數:
上界函數:
考慮一個右子樹的時候,設
r:是當前未考慮的剩餘物品的總價值(remainder)
cp:是當前的價值(current price)
bestp:是當前獲得的最優價值(best price)
那麼,知足:
可是,上界r太鬆。
一個更加緊的上界:
將剩餘物品按照單位重量價值排序,而後依次裝入物品,直到裝不下,再將剩餘物品的一部分放入揹包。(r_n <= r)
c語言實現:
// TestWin32.cpp : Defines the entry point for the console application. // #include "stdafx.h" int curr_weight = 0; //當前重量 int curr_value = 0; //當前價值 int bestv = 0; //最優解 int x_length = 0; // /************************************************************************/ /* 將物品按單位價格降序排序 */ /************************************************************************/ void sortItem(itemGoods *item, int n){ itemGoods temp; for(int i = 0; i < n-1; ++i){ for(int j = i+1; j < n; ++j){ if((item[i].v/item[i].w) < (item[j].v/item[j].w)){ temp = item[i]; item[i] = item[j]; item[j] = temp; } } } } /************************************************************************/ /* 邊界函數 : 計算上界 @int C, 揹包容量 @int i 第i個物品 @int n 物品個數 */ /************************************************************************/ int bound(itemGoods *item, int capacity, int i, int n){ int capacity_left = capacity - curr_weight; int value_left = curr_value; // 按物品單位價值遞減序裝入物品 while(i <= n && item[i].w <= capacity_left){ capacity_left -= item[i].w; value_left += item[i].v; ++i; } //裝滿揹包 if(i <= n) value_left += item[i].v * capacity_left / item[i].w; return value_left; } /************************************************************************/ /* 遞歸回溯 @int capacity, 揹包容量 @int i 第i個物品 @int n 物品個數 */ /************************************************************************/ void backtrack(itemGoods *item, int capacity, int i, int n, int *bestX){ if(i >= n){ //到達葉子結點,更新最優價值 if(bestv < curr_value){ bestv = curr_value; x_length = 0; for(int i = 0; i < n; ++i) if(item[i].visited){ bestX[x_length] = item[i].id; ++x_length; } } return; } //搜索左子樹:左剪枝,能放的下的物品 if(curr_weight + item[i].w <= capacity){ curr_weight += item[i].w; curr_value += item[i].v; item[i].visited = true; backtrack(item,capacity,i+1,n,bestX); curr_weight -= item[i].w; curr_value -= item[i].v; } //搜索右子樹:放不下的物品 if(bound(item,capacity,i,n) > bestv) item[i].visited = false; backtrack(item,capacity,i+1,n,bestX); } int Knapsack(itemGoods *item, int n, int capacity, int *bestX){ sortItem(item,n); backtrack(item,capacity,0,n,bestX); return bestv; } void initGoods(itemGoods *item,int n){ cout << "物品信息:" << endl; for(int i = 0; i < n; ++i){ item[i].id = i; item[i].visited = false; cout << "物品" <<i<<"重量:"; cin >> item[i].w; cout << "物品" <<i<<"價值:"; cin >> item[i].v; } } void printKnapsack(int *bestX, int max_value){ cout << "揹包的物品id:" << endl; for(int i = 0; i < x_length; ++i) cout << bestX[i]+1 << "\t"; cout << endl; cout << "最大價值: " << max_value << endl; } int main(){ int n; cout << "物品數量:" << endl; cin >> n; int capacity; cout << "揹包容量:" << endl; cin >> capacity; itemGoods *item = new itemGoods[n]; initGoods(item, n); int *bestX = new int[n]; //當前最優解 int max_value = Knapsack(item,n,capacity, bestX); printKnapsack(bestX, max_value); return 0; }
C++
// stdafx.h : include file for standard system include files, // or project specific include files that are used frequently, but // are changed infrequently // #pragma once #include "targetver.h" #include <stdio.h> #include "stdlib.h" #include <iostream> using namespace std; //宏定義 #define TRUE 1 #define FALSE 0 #define OK 1 #define ERROR 0 #define INFEASIBLE -1 #define OVERFLOW -2 typedef int Status ; typedef int ElemType ; typedef struct itemGoods{ int id; bool visited; int w; int v; }itemGoods ; class knapsack{ private: itemGoods *item ; int capacity; //揹包容量 int n; //物品數 int curr_weight;//當前重量 int curr_value; //當前價值 Status bestV; //當前最優值 int *bestX; //當前最優解 int x_length; //最優解的數量 private: void _sortItem(); int _bound(int i); void _backtrack(int i); //遞歸回溯函數 public: knapsack (itemGoods *item, int c,int n) :capacity(c), n(n), curr_value(0), bestV (0), curr_weight(0),x_length(0),item(item) { bestX = new int[n]; bestX[0]=0; } int backtrack () ; void printKnapsack(); };
Test.cpp
// Test.cpp : Defines the entry point for the console application. // #include "stdafx.h" /************************************************************************/ /* 邊界函數 : 計算上界 */ /************************************************************************/ int knapsack::_bound(int i) { //計算上界 int cleft = capacity - curr_weight; int value_left = curr_value; //以物品單位重量價值遞減序裝入物品 while(i < n && item[i].w <= cleft) { cleft -= item[i].w; value_left += item[i].v; i++; } //裝滿揹包 if(i< n) value_left += item[i].v/item[i].w * cleft; return value_left; } /************************************************************************/ /* 遞歸回溯 */ /************************************************************************/ void knapsack::_backtrack(int i) { if(i>=n) { if(bestV < curr_value) { bestV = curr_value; x_length = 0; for(int j = 0;j < n;j++) if(item[j].visited) { bestX[j] = item[j].id; ++x_length; } } return; } if(curr_weight + item[i].w <= capacity) { //搜索左子樹 item[i].visited = TRUE; curr_weight += item[i].w; curr_value += item[i].v; _backtrack(i+1); curr_weight -= item[i].w; curr_value -= item[i].v; } if(_bound(i+1)>bestV) { //搜索右子樹 item[i].visited = FALSE; _backtrack(i+1); } } /************************************************************************/ /* 排序 :將物品按單位價格降序排序 */ /************************************************************************/ void knapsack::_sortItem(){ itemGoods temp; for(int i = 0; i < n-1; ++i){ for(int j = i+1; j < n; ++j){ if((item[i].v/item[i].w) < (item[j].v/item[j].w)){ temp = item[i]; item[i] = item[j]; item[j] = temp; } } } } int knapsack::backtrack () { _sortItem(); _backtrack(0); return bestV; } void knapsack::printKnapsack(){ cout << "揹包的物品id:" << endl; for(int i = 0; i < x_length; ++i) cout << bestX[ i] << "\t"; cout << endl; cout << "最大價值: " << bestV << endl; } int main(){ int n = 3; cout << "物品數量:" << endl; //cin >> n; int capacity = 5; cout << "揹包容量:" << endl; //cin >> capacity; itemGoods *item = new itemGoods[n]; //初始化物品 //cout << "物品信息:" << endl; //for(int i = 0; i < n; ++i){ // item[i].id = i; // item[i].visited = FALSE; // cout << "物品" <<i<<"重量:"; // cin >> item[i].w; // cout << "物品" <<i<<"價值:"; // cin >> item[i].v; //} item[0].id = 0; item[0].visited = FALSE; item[0].w =2; item[0].v = 2; item[1].id = 1; item[1].visited = FALSE; item[1].w = 2; item[1].v = 2; item[2].id = 2; item[2].visited = FALSE; item[2].w =4; item[2].v = 10; knapsack ks(item,capacity,n); int max_value = ks.backtrack(); ks.printKnapsack(); return 0; }