【算法分析】回溯法详解+范例+习题解答
🦄1.回溯法
1.1回溯法的设计思想
以深度优先方式搜索问题解的算法【回溯法是优化的暴力遍历,即一棵树在特定条件作为剪枝函数,树可以提前截掉,省去一些子节点。完全暴力遍历则是需要全部叶子节点都考虑】
回溯法:为了避免生成那些不可能产生最佳解的问题状态,要不断地利用限界函数(bounding function)来处死那些实际上不可能产生所需解的活结点,以减少问题的计算量。具有限界函数的深度优先生成法称为回溯法
- 有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。
- 回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。
- 回溯法在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先策略搜索。
1.2回溯法的基本思想
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索
常用剪枝函数:
用约束函数在扩展结点处剪去不满足约束的子树;
用限界函数剪去得不到最优解的子树。
1.3回溯法的空间复杂度
用回溯法解题的一个显著特征是在搜索过程中动态产生问题的解空间。在任何时刻,算法只保存从根结点到当前扩展结点的路径。如果解空间树中从根结点到叶结点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n))。而显式地存储整个解空间则需要O(2h(n))或O(h(n)!)内存空间。
🦄2.范例
2.1 0-1背包问题
2.2 装载问题
有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且
装载问题要求确定是否有一个合理的装载方案可将这些集装箱装上这2艘轮船。如果有,找出一种装载方案。
容易证明,如果一个给定装载问题有解,则采用下面的策略可得到最优装载方案。
(1)首先将第一艘轮船尽可能装满;
(2)将剩余的集装箱装上第二艘轮船。
将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集,使该子集中集装箱重量之和最接近。由此可知,装载问题等价于以下特殊的0-1背包问题
2.2.1 基本思想
- 解空间:子集树
- 可行性约束函数(选择当前元素)
- 上界函数(不选择当前元素)
- 当前载重量cw+剩余集装箱的重量r ≤ 当前最优载重量bestw
2.2.2 伪代码实现
void backtrack (int i){// 搜索第i层结点
if (i > n){//到达叶结点更新最优解bestx;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];
}
2.3 n后问题
在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上。
2.3.1基本思想1
-
解向量:(x1, x2, … , xn)
-
显约束:xi=1,2, … ,n
-
隐约束:
1)不同列:
2)不处于同一正、反对角线:
2.3.2 伪代码1【时间复杂度2n】
boolean place (int k) {
for (int j=1;j<k;j++)
if ((abs(k-j)==abs(x[j]-x[k]))||(x[j]==x[k])) return false;
return true;
}
void backtrack (int t) {
if (t>n) output(x);
else
for (int i=1;i<=n;i++) {
x[t]=i;
if (place(t)) backtrack(t+1);
}
}
2.3.3基本思想2【第2种比第1种时间复杂度低】
解向量:(x1, x2, … , xn)是1,2, … ,n的排列
约束:
不处于同一正、反对角线
2.3.4伪代码2【时间复杂度n!】
boolean place (int k) {
for (int j=1;j<k;j++)
if ((abs(k-j)==abs(x[j]-x[k]))) return false;
return true;
}
void backtrack (int t) {
if (t>n) output(x);
else
for (int i=t;i<=n;i++) {
swap(x[t],x[i]);
if (place(t)) backtrack(t+1);
swap(x[t],x[i]);
}
}
🦄3.习题
3.1 子集树【时间复杂度O(2n)空间复杂度O(n)】
- 已知集合S={a,b,c,d,e,f,g},请编程输出S的所有子集
#define n 7
char s[n] = {a,b,c,d,e,f,g};
int x[n];
void output(int x[]);
void all_subset ( ){
backtrack(0);
}
void backtrack (int t){
if (t>=n) output(x);
else
for (int i=0;i<=1;i++) {
x[t]=i;
backtrack(t+1);
}
}
- 找出{1,2,3}的所有子集
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);
}
}
3.2排列树【时间复杂度O(n!)空间复杂度O(n)】
- 已知集合S={1,2,3,4,5,6,7},请编程输出S的所有排列。
#define n 7
int s[n] = {1,2,3,4,5,6,7};
int x[n];
void output(int x[]);
void all_ permutation( ){
backtrack(0);
}
void backtrack (int t){
if (t>=n) output(x);
else
for (int i=t; i<n; i++) {
swap(x[t],x[i]);
backtrack(t+1);
swap(x[t],x[i]);
}
}
- 已知集合S={1,2,3,4,5,6,7,8},请编程输出S的所有满足下列条件的排列:奇偶数相间出现
#define n 7
int s[n] = {1,2,3,4,5,6,7};
int x[n];
void output(int x[]);
void all_subset ( ){
backtrack(0);
}
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]);
}
bool legal(int t){
bool bRet = true;
for (int i=0;i<t;i++) {
bRet &&= ((x[i]-x[i+1])%2==1);
}
return bRet;
}
剪枝剪剪【legal函数】
3.3子集以及排序
- 已知集合S={1,2,3,4,5,6,7},请编程输出S的满足特定约束的子集和排列。
#define n 7
int x[n], s[n] = {1,2,3,4,5,6,7};
void main ( ){
backtrack1(0);
backtrack2(0);
}
void backtrack1 (int t){
if (t>=n) output(x);
else
for (int i=0;i<=1;i++) {
x[t]=i;
if(legal(t)) backtrack1(t+1);
}
}
void backtrack2 (int t){
if (t>=n) output(x);
else
for (int i=t;i<n;i++) {
swap(x[t],x[i]);
if(legal(t)) backtrack2(t+1);
swap(x[t],x[i]);
}
}
🦄4.书后习题
5-1, 5-2, 5-3,5-4, 5-5