回溯法中子集树与排列树
2015-12-31 20:00
246 查看
回溯法
有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最优解时,往往使用回溯法。
回溯法的基本做法就是搜索,或者是一种能避免不必要搜索的暴力搜索法,这种方法适用于一些组合数相当大的问题。
回溯法在问题的解空间中,按深度优先策略,从根节点出发搜索解空间树,算法搜索到解空间中的任意一点时,首先进行判断该节点是不是包含问题的解,如果是不包含的,那么我们进行剪枝过程(就是跳过对该节点为根的子树的搜索,逐层向根节点进行回溯)否则,进行该子树,继续采用深度优先策略进行搜索。
问题的解空间
问题的解向量:回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…xn)
显约束:对分量xi的取值进行限定
隐约束:为满足问题的解对不同分量之间施加的约束。
解空间:对于一个问题的实例,解向量满足显约束条件的所有多元组,构成了该实例的一个解空间。
PS:同一问题可以有多种比奥斯,有些比表示方法更简单,所需表示的状态空间更小(存储量少,搜索方法简单)
下面结合一个例子,来分清楚这些概念:
n=3时的0-1背包问题用完全二叉树表示的解空间:
生成问题状态的基本方法
扩展节点:一个正在产生儿子的节点称为扩展节点
活节点:一个自身已生成但其儿子还没有全部生成的节点称为活节点
死节点:一个所有儿子已经产生的节点称做死节点
深度优先的问题状态生成法:如果对一个扩展节点R,一旦产生了它的一个儿子C,就把C当做新的扩展节点,在完成对子树C(以C为根的子树)的穷尽搜索之后,将R重新变成扩展节点,继续生成R的下一个儿子(如果存在)
广度优先的问题状态生成法:在一个扩展节点变成死节点之前,它一直是扩展节点
回溯法:为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(bounding function)来剪枝那些实际上不可能产生所需解的活节点,以减少问题的计算量。具有限界函数的深度优先法称为回溯法。(回溯法 = 穷举 +剪枝)
回溯法的基本思想
针对所给问题,定义问题的解空间
确定易于搜索的解空间结构
以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索
两个常用的剪枝函数:
1. 约束函数: 在扩展节点处减去不满足约束的子树
2. 限界函数: 减去得不到最优解子树
用回溯法解题的一个显著特征就是在搜索过程中动态产生问题的解空间。在任何死后,算法只保存了从根节点到当前扩展节点的路径。如果解空间树中从根节点到叶节点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n))。而显式的存储整个解空间则需要O(2^h(n))或O(h(n)!)内存空间。
递归回溯
回溯法对解空间做深度优先搜索,因此,在一般情况下用递归方法实现回溯法。
针对N叉树的递归回溯方法
迭代回溯
采用树的非递归深度优先遍历算法,可将回溯法表示成一个非递归迭代过程。
针对N叉树的迭代回溯方法
回溯法一般依赖的两种数据结构:子集树和排列树
子集树(遍历子集树需O(2^n)计算时间)
排列树(遍历排列树需要O(n!)计算时间)
下边结合具体问题:装载问题
有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且
装载问题要求确定是否有一个合理的装载方案可将这个集装箱装上这2艘轮船。如果有,找出一种装载方案。
解决方案:
容易证明,如果一个给定装载问题有解,则采用下面可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满;
(2)将剩余的集装箱装上第二艘轮船;
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱之和最接近。由此可知,装载问题等价于以下特殊的0-1背包问题。
解空间:子集树
可行性约束函数(选择当前元素):
上界函数(不选择当前元素):
当前载重量cw + 剩余集装箱的重量r<= 当前最优载重量bestw
变量解释:
r: 剩余重量
w: 各个集装箱重
cw:当前总重量
x: 每个集装箱是否被选取标志
bestx: 最佳选取方案
bestw: 最优载重量
有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最优解时,往往使用回溯法。
回溯法的基本做法就是搜索,或者是一种能避免不必要搜索的暴力搜索法,这种方法适用于一些组合数相当大的问题。
回溯法在问题的解空间中,按深度优先策略,从根节点出发搜索解空间树,算法搜索到解空间中的任意一点时,首先进行判断该节点是不是包含问题的解,如果是不包含的,那么我们进行剪枝过程(就是跳过对该节点为根的子树的搜索,逐层向根节点进行回溯)否则,进行该子树,继续采用深度优先策略进行搜索。
问题的解空间
问题的解向量:回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…xn)
显约束:对分量xi的取值进行限定
隐约束:为满足问题的解对不同分量之间施加的约束。
解空间:对于一个问题的实例,解向量满足显约束条件的所有多元组,构成了该实例的一个解空间。
PS:同一问题可以有多种比奥斯,有些比表示方法更简单,所需表示的状态空间更小(存储量少,搜索方法简单)
下面结合一个例子,来分清楚这些概念:
n=3时的0-1背包问题用完全二叉树表示的解空间:
生成问题状态的基本方法
扩展节点:一个正在产生儿子的节点称为扩展节点
活节点:一个自身已生成但其儿子还没有全部生成的节点称为活节点
死节点:一个所有儿子已经产生的节点称做死节点
深度优先的问题状态生成法:如果对一个扩展节点R,一旦产生了它的一个儿子C,就把C当做新的扩展节点,在完成对子树C(以C为根的子树)的穷尽搜索之后,将R重新变成扩展节点,继续生成R的下一个儿子(如果存在)
广度优先的问题状态生成法:在一个扩展节点变成死节点之前,它一直是扩展节点
回溯法:为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(bounding function)来剪枝那些实际上不可能产生所需解的活节点,以减少问题的计算量。具有限界函数的深度优先法称为回溯法。(回溯法 = 穷举 +剪枝)
回溯法的基本思想
针对所给问题,定义问题的解空间
确定易于搜索的解空间结构
以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索
两个常用的剪枝函数:
1. 约束函数: 在扩展节点处减去不满足约束的子树
2. 限界函数: 减去得不到最优解子树
用回溯法解题的一个显著特征就是在搜索过程中动态产生问题的解空间。在任何死后,算法只保存了从根节点到当前扩展节点的路径。如果解空间树中从根节点到叶节点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n))。而显式的存储整个解空间则需要O(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 --; } }
回溯法一般依赖的两种数据结构:子集树和排列树
子集树(遍历子集树需O(2^n)计算时间)
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背包问题。
解空间:子集树
可行性约束函数(选择当前元素):
上界函数(不选择当前元素):
当前载重量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: 最优载重量
#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; }
相关文章推荐
- mysqldump全备的shell脚本分享
- c++基础--类型别名
- Same Tree
- struct和typedef struct
- 聪明的kk
- git log 查看提交记录,参数:
- java源码分析:从System类中获取系统关键属性信息
- Ilya and Bank Account
- 1. 查看提交历史
- [Spring Boot 系列] 集成maven和Spring boot的profile功能
- 【笔记】Linux文件处理
- C语言中函数的参数列表为空和void的区别
- 解决Ubuntu14.04中VMware workstaion无法打开
- C语言写的工具箱
- 跨年快乐
- 一个测试人员的2015总结
- 代码规范文档
- JAVA Linux 排查CPU 过高的方法
- 代码规范文档
- nodejs 使用inspector 调试