回溯法(1)
2015-07-02 15:48
363 查看
转自:http://www.cnblogs.com/chinazhangjie/archive/2010/10/22/1858410.html
回溯法
1、有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。
2、回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。
3、回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含(剪枝过程),则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。
问题的解空间
问题的解向量:回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…,xn)的形式。
显约束:对分量xi的取值限定。
隐约束:为满足问题的解而对不同分量之间施加的约束。
解空间:对于问题的一个实例,解向量满足显式约束条件的所有多元组,构成了该实例的一个解空间。
注意:同一个问题可以有多种表示,有些表示方法更简单,所需表示的状态空间更小(存储量少,搜索方法简单)。
下面是n=3时的0-1背包问题用完全二叉树表示的解空间:
![](http://cdn.acmerblog.com/img/1388063798343.jpg)
生成问题状态的基本方法
扩展结点:一个正在产生儿子的结点称为扩展结点
活结点:一个自身已生成但其儿子还没有全部生成的节点称做活结点
死结点:一个所有儿子已经产生的结点称做死结点
深度优先的问题状态生成法:如果对一个扩展结点R,一旦产生了它的一个儿子C,就把C当做新的扩展结点。在完成对子树C(以C为根的子树)的穷尽搜索之后,将R重新变成扩展结点,继续生成R的下一个儿子(如果存在)
宽度优先的问题状态生成法:在一个扩展结点变成死结点之前,它一直是扩展结点
回溯法:为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(bounding function)来处死(剪枝)那些实际上不可能产生所需解的活结点,以减少问题的计算量。具有限界函数的深度优先生成法称为回溯法。(回溯法 = 穷举 + 剪枝)
回溯法的基本思想
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
两个常用的剪枝函数:
(1)约束函数:在扩展结点处减去不满足约束的子数
(2)限界函数:减去得不到最优解的子树
用回溯法解题的一个显著特征是在搜索过程中动态产生问题的解空间。在任何时刻,算法只保存从根结点到当前扩展结点的路径。如果解空间树中从根结点到叶结点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n))。而显式地存储整个解空间则需要O(2^h(n))或O(h(n)!)内存空间。
递归回溯
回溯法对解空间作深度优先搜索,因此,在一般情况下用递归方法实现回溯法。
迭代回溯
采用树的非递归深度优先遍历算法,可将回溯法表示为一个非递归迭代过程。
回溯法一般依赖的两种数据结构:子集树和排列树
子集树(遍历子集树需O(2^n)计算时间)
![](http://cdn.acmerblog.com/img/1388063798479.jpg)
排列树(遍历排列树需要O(n!)计算时间)
![](http://cdn.acmerblog.com/img/1388063798592.jpg)
一、装载问题
问题表述:
有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且
![](http://cdn.acmerblog.com/img/1388063798706.jpg)
装载问题要求确定是否有一个合理的装载方案可将这个集装箱装上这2艘轮船。如果有,找出一种装载方案。
解决方案:
容易证明,如果一个给定装载问题有解,则采用下面的策略可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满;
(2)将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近。由此可知,装载问题等价于以下特殊的0-1背包问题。
![](http://cdn.acmerblog.com/img/1388063798818.jpg)
解空间:子集树
可行性约束函数(选择当前元素):
![](http://cdn.acmerblog.com/img/1388063798925.jpg)
上界函数(不选择当前元素):
当前载重量cw + 剩余集装箱的重量r<= 当前最优载重量bestw
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: 最优载重量
实现:
回溯法
1、有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。
2、回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。
3、回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含(剪枝过程),则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。
问题的解空间
问题的解向量:回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…,xn)的形式。
显约束:对分量xi的取值限定。
隐约束:为满足问题的解而对不同分量之间施加的约束。
解空间:对于问题的一个实例,解向量满足显式约束条件的所有多元组,构成了该实例的一个解空间。
注意:同一个问题可以有多种表示,有些表示方法更简单,所需表示的状态空间更小(存储量少,搜索方法简单)。
下面是n=3时的0-1背包问题用完全二叉树表示的解空间:
![](http://cdn.acmerblog.com/img/1388063798343.jpg)
生成问题状态的基本方法
扩展结点:一个正在产生儿子的结点称为扩展结点
活结点:一个自身已生成但其儿子还没有全部生成的节点称做活结点
死结点:一个所有儿子已经产生的结点称做死结点
深度优先的问题状态生成法:如果对一个扩展结点R,一旦产生了它的一个儿子C,就把C当做新的扩展结点。在完成对子树C(以C为根的子树)的穷尽搜索之后,将R重新变成扩展结点,继续生成R的下一个儿子(如果存在)
宽度优先的问题状态生成法:在一个扩展结点变成死结点之前,它一直是扩展结点
回溯法:为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(bounding function)来处死(剪枝)那些实际上不可能产生所需解的活结点,以减少问题的计算量。具有限界函数的深度优先生成法称为回溯法。(回溯法 = 穷举 + 剪枝)
回溯法的基本思想
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
两个常用的剪枝函数:
(1)约束函数:在扩展结点处减去不满足约束的子数
(2)限界函数:减去得不到最优解的子树
用回溯法解题的一个显著特征是在搜索过程中动态产生问题的解空间。在任何时刻,算法只保存从根结点到当前扩展结点的路径。如果解空间树中从根结点到叶结点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n))。而显式地存储整个解空间则需要O(2^h(n))或O(h(n)!)内存空间。
递归回溯
回溯法对解空间作深度优先搜索,因此,在一般情况下用递归方法实现回溯法。
01 | // 针对N叉树的递归回溯方法 |
02 | void backtrack ( int t) |
03 | { |
04 | if (t > n) { |
05 | // 到达叶子结点,将结果输出 |
06 | output (x); |
07 | } |
08 | else { |
09 | // 遍历结点t的所有子结点 |
10 |
11 | for ( int i = f(n,t); i <= g(n,t); i ++ ) { |
12 |
13 | x[t] = h[i]; |
14 | // 如果不满足剪枝条件,则继续遍历 |
15 | if (constraint (t) && bound (t)) |
16 | backtrack (t + 1); |
17 | } |
18 | } |
19 | } |
采用树的非递归深度优先遍历算法,可将回溯法表示为一个非递归迭代过程。
01 | // 针对N叉树的迭代回溯方法 |
02 | void iterativeBacktrack () |
03 | { |
04 | int t = 1; |
05 | while (t > 0) { |
06 | if (f(n,t) <= g(n,t)) { |
07 | // 遍历结点t的所有子结点 |
08 | for ( int i = f(n,t); i <= g(n,t); i ++) { |
09 | x[t] = h(i); |
10 | // 剪枝 |
11 |
12 | if (constraint(t) && bound(t)) { |
13 | // 找到问题的解,输出结果 |
14 | if (solution(t)) { |
15 |
16 | output(x); |
17 | } |
18 | else // 未找到,向更深层次遍历 |
19 | t ++; |
20 |
21 | } |
22 | } |
23 | } |
24 | else { |
25 | t--; |
26 |
27 | } |
28 | } |
29 |
30 | } |
子集树(遍历子集树需O(2^n)计算时间)
![](http://cdn.acmerblog.com/img/1388063798479.jpg)
01 | void backtrack ( int t) |
02 |
03 | { |
04 | if (t > n) |
05 | // 到达叶子结点 |
06 | output (x); |
07 | else |
08 | for ( int i = 0;i <= 1;i ++) { |
09 | x[t] = i; |
10 | // 约束函数 |
11 | if ( legal(t) ) |
12 | backtrack( t+1 ); |
13 | } |
14 |
15 | } |
![](http://cdn.acmerblog.com/img/1388063798592.jpg)
01 | void backtrack ( int t) |
02 | { |
03 | if (t > n) |
04 | output(x); |
05 | else |
06 |
07 | for ( int i = t;i <= n;i++) { |
08 | // 完成全排列 |
09 | swap(x[t], x[i]); |
10 |
11 | if (legal(t)) |
12 | backtrack(t+1); |
13 | swap(x[t], x[i]); |
14 |
15 | } |
16 | } |
问题表述:
有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且
![](http://cdn.acmerblog.com/img/1388063798706.jpg)
装载问题要求确定是否有一个合理的装载方案可将这个集装箱装上这2艘轮船。如果有,找出一种装载方案。
解决方案:
容易证明,如果一个给定装载问题有解,则采用下面的策略可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满;
(2)将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近。由此可知,装载问题等价于以下特殊的0-1背包问题。
![](http://cdn.acmerblog.com/img/1388063798818.jpg)
解空间:子集树
可行性约束函数(选择当前元素):
![](http://cdn.acmerblog.com/img/1388063798925.jpg)
上界函数(不选择当前元素):
当前载重量cw + 剩余集装箱的重量r<= 当前最优载重量bestw
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: 最优载重量
实现:
001 | /* 主题:装载问题 |
002 | * 作者:chinazhangjie |
003 | * 邮箱:chinajiezhang@gmail.com |
004 | * 开发语言:C++ |
005 | * 开发环境:Code::Blocks 10.05 |
006 | * 时间: 2010.10.22 |
007 | */ |
008 | #include <iostream> |
009 | #include <vector> |
010 | #include <iterator> |
011 | using namespace std; |
012 |
013 | /* 装载问题子函数 |
014 | * layers: 搜索到第layers层结点 |
015 | * layers_size: layers_size总层数 |
016 | * current_w: 当前承载量 |
017 | * best_w: 最优载重量 |
018 | * flag_x: 选取方案 |
019 | * best_x: 最佳选取方案 |
020 | * remainder_w:剩余重量 |
021 | * container_w:每个集装箱的重量 |
022 | * total_w: 总承载量 |
023 | */ |
024 | void __backtrack ( int layers, const int layers_size, |
025 | int current_w, int & best_w, |
026 | vector< int >& flag_x,vector< int >& |
027 |
028 | best_x, |
029 | int remainder_w, |
030 | const vector< int >& container_w, |
031 | int total_w) |
032 | { |
033 | if (layers > layers_size - 1) { |
034 | // 到达叶子结点,更新最优载重量 |
035 | if (current_w < best_w || best_w == -1) { |
036 | copy(flag_x.begin(),flag_x.end |
037 |
038 | (),best_x.begin()); |
039 | // copy(best_x.begin(),best_x.end |
040 |
041 | (),flag_x.begin()); |
042 | best_w = current_w; |
043 | } |
044 | return ; |
045 | } |
046 | remainder_w -= container_w[layers]; |
047 | if (current_w + container_w[layers] <= total_w) { |
048 | // 搜索左子树 |
049 | flag_x[layers] = 1; |
050 | current_w += container_w[layers]; |
051 | __backtrack(layers + 1,layers_size,current_w, |
052 |
053 | best_w,flag_x,best_x,remainder_w,container_w, |
054 | total_w); |
055 | current_w -= container_w[layers]; |
056 | } |
057 | if (current_w + remainder_w > best_w || best_w == - |
058 |
059 | 1) { |
060 | flag_x[layers] = 0; |
061 | __backtrack(layers + 1,layers_size,current_w, |
062 |
063 | best_w,flag_x,best_x,remainder_w,container_w, |
064 | total_w); |
065 | } |
066 | remainder_w += container_w[layers]; |
067 | } |
068 | /* 装载问题 |
069 | * container_w: 各个集装箱重量 |
070 | * total_w: 总承载量 |
071 | */ |
072 | void loading_backtrack ( int total_w, vector< int >& |
073 |
074 | container_w) |
075 | { |
076 | int layers_size = container_w.size(); // 层数 |
077 | int current_w // 当前装载重量 |
078 | int remainder_w = total_w; // 剩余重量 |
079 | int best_w = // 最优载重量 |
080 | vector< int > // 是否被选取标 |
081 |
082 | 志 |
083 | vector< int > // 最佳选取方案 |
084 | __backtrack(0,layers_size,current_w, |
085 |
086 | best_w,flag_x,best_x,remainder_w,container_w, |
087 | total_w); |
088 | cout << "path : " ; |
089 | copy(best_x.begin(),best_x.end |
090 |
091 | (),ostream_iterator< int >(cout, " " )); |
092 | cout <<endl; |
093 | cout << "best_w = " <<best_w |
094 | << "( " ; |
095 | // 将结果输出 |
096 | for ( size_t i = 0;i < best_x.size(); ++ i) { |
097 | if (best_x[i] == 1) { |
098 | cout <<container_w[i] << " " ; |
099 | } |
100 | } |
101 | cout << ")" << endl; |
102 | } |
103 |
104 | int main() |
105 | { |
106 | const int total_w = 30; |
107 | vector< int > container_w; |
108 | container_w.push_back(40); |
109 | container_w.push_back(1); |
110 | container_w.push_back(40); |
111 | container_w.push_back(9); |
112 | container_w.push_back(1); |
113 | container_w.push_back(8); |
114 | container_w.push_back(5); |
115 | container_w.push_back(50); |
116 | container_w.push_back(6); |
117 |
118 | loading_backtrack(total_w,container_w); |
119 | return 0; |
120 | } |
相关文章推荐
- 二叉查找树实现-双向链表
- getopt函数的使用——分析命令行参数
- IOS 错误:error: unknown type name 'UIImageView'
- crontab使用详解
- 正则表达式 Pattern
- python实现在控制台输入密码不显示的方法
- 如何使用Android Studio把自己的Android library分享到jCenter和Maven Central
- 浅谈如何推翻GRE阅读的固定思维
- c++正则表达式日期格式匹配
- Java学习笔记七(文件夹操作)
- selector资源的两种使用方式
- yum更新源
- 自适应网页设计(Responsive Web Design)
- textview字体的高度
- 回溯算法
- 判断浏览器 插件 jquery.ua.js
- 关于ffmpeg avcodec_open2函数失败的问题
- 本地数据库(SQL Server)远程连接服务器端服务器
- 修正iOS从照相机和相册中获取的图片方向(转)
- 黑马程序员——Java基础:枚举