回溯法 -数据结构与算法
1.回溯法算法思想:
定义:
回溯法(探索与回溯法)是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
1、回溯法适用:有许多问题,当需要找出它的解集(全部解)或者要求回答什么解是满足某些约束条件的最优解时,往往要使用回溯法。
2、有组织的穷举式搜索:回溯法的基本做法是搜索或者有的组织穷尽搜索。它能避免搜索所有的可能性。即避免不必要的搜索。这种方法适用于解一些组合数相当大的问题。
3、搜索解空间树:回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含(剪枝过程),则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。
为了实现回溯,我们先弄明白以下两个问题:
1)首先应该明确问题的解空间。
2)其次是组织解空间以便它能用以被搜索到。
2. 问题的解空间 和空间树
这个空间必须至少包含一个解(可能是最优的)。 一个复杂问题的解决往往由多部分构成,即,一个大的解决方案可以看作是由若干个小的决策组成。很多时候它们构成一个决策序列。解决一个问题的所有可能的决策序列构成该问题的解空间。解空间中满足约束条件的决策序列称为可行解。一般说来,解任何问题都有一个目标,在约束条件下使目标值达到最大(或最小)的可行解称为该问题的最优解。在解空间中,前k项决策已经取定的所有决策序列之集称为k定子解空间。0定子解空间即是该问题的解空间。
问题的解空间通常是在搜索问题的解的过程中动态产生的,这是回溯算法的一个重要特性。
解空间的确定与我们对问题的描述有关。如何组织解空间的结构会直接影响对问题的求解效率。这是因为回溯方法的基本思想是通过搜索解空间来找到问题所要求的解。一般地,可以用一棵树来描述解空间,称为解空间树。
当所给的问题是从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
- // 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 << " ";
- 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] << " ";
- 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;
- }