第五章 回溯法
回溯法在问题的解空间树中,按深度优先策略,从根节点出发搜索解空间树。
算法搜索至解空间树的任一结点时,先判断该节点是否包含问题的解。
• 如果肯定不包含,则跳过对以该结点为根的子树的搜索(剪枝),逐层向其祖先节点回溯;
• 否则,进入该子树,继续按深度优先策略搜索
回溯法的解空间
为了用回溯法求解一个具有n个输入的问题,一般情况下,将其可能的解表示为满足某个约束条件的等长向量
X=(x1,x2,...,xn)
其中分量xi(1 <= i <= n)的取值范围是某个有限集合Si={ai1,ai2,...,ain}, 所有可能的解向量构成了问题的解空间。
回溯法的思想
回溯法从根结点出发,按照深度优先策略遍历解空间树,搜索满足约束条件的解。在搜索至树中任一结点时,先判断该结点对应的部分解是否满足约束条件,或者是否超出目标函数的界,也就是判断该结点是否包含问题的(最优)解,如果肯定不包含,则跳过对以该结点为根的子树的搜索,即所谓剪枝 (Pruning);否则,进入以该结点为根的子树,继续按照深度优先策略搜索
回溯法的搜索过程涉及的结点(称为搜索空间)只是整个解空间树的一部分,在搜索过程中,通常采用两种策略避免无效搜索:
- 用约束条件剪去得不到可行解的子树;
- 用目标函数剪去得不到最优解的子树。
这两类函数统称为剪枝函数
回溯法的解题步骤
- 针对所给问题,定义问题的解空间
- 确定易于搜索的解空间结构
- 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
- 用回溯法解题的一个显著特征是在搜索过程中动态产生问题的解空间。
- 在任何时刻,算法只保存从根结点到当前扩展结点的路径。
- 如果解空间树中从根结点到叶结点的最长路径的长度为h(n),则回溯法所需的计算空间通常为O(h(n))。而显式地存储整个解空间则需要O(2h(n))或O(h(n)!)内存 空间。
回溯法:子集树和排列树
n皇后问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| bool judge(int map[],int line,int pos) { for(int i = 0;i < line;i++) { if(map[i] == pos || abs(map[i] - pos) == abs(i - line)) { return false; } } return true; }
int queen(int map[],int line) { if(line == map.size()) { return 1; } int sum = 0; for(int i = 0;i < map.size();i++) { if(judge(map,line,i)) { map[line] = i; sum += queen(map,line + 1); map[line] = -1; } } return sum; }
|
经过剪枝的时间复杂度为O(N!),未剪枝的复杂度为O(NN)
当然,可以设置三个数组保存列、左对角线、右对角线的棋子摆放情况,此时会减少一部分的遍历判断时间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| bool column[N]; bool left[2*N-1]; bool right[2*N-1];
int queen(int map[],int line) { if(line == map.size()) { return 1; } int sum = 0; for(int i = 0;i < map.size();i++) { if(column[i] && left[line + i] && right[line - i + N - 1]) { map[line] = i; column[i] = false; left[line + i] = false; right[line - i + N - 1] = false; sum += queen(map,line + 1); map[line] = -1; column[i] = true; left[line + i] = true; right[line - i + N - 1] = true; } } return sum; }
|
TSP旅行售货员
旅行商从驻地出发,经过每个需要访问的城市一次且只有一次,并最终返回出发点。
如何安排路线,使旅行总路程最短?
剪枝条件
1
如果当前正在考虑的顶点j与当前路径中的末端结点i没有边相连,即w[i, j]= ∞,则不必搜索j所在分支
5
如果B(i) ≥ bestw, 则停止搜索**x[i]**分支及其下面的层
其中,bestw代表到目前为止,在前面的搜索中,从其它已经搜索过的路径中,找到的最佳完整回路的权和(总长度)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| select[0] = 1; bestSelect[0] = 1; currentSum = 0; bestSum = 0;
void TSP(int select[],int i) { if (i=map.size()) { if (map(select[n-1], select[n]) != -1 and map(select[n],select[0]) != -1) { if (currentSum + map(select[n-1], select[n]) + map(select[n],select[0]) < bestSum) { bestSum = currentSum + map(select[n-1], select[n]) + map(select[n],select[0]); for (int j = 0;j < n;j++) { bestSelect[j]=select[j]; } } } } else { for(int j = i;j < n;j++) { if (map(select[i-1], select[j]) != -1 and currentSum + map[select[i-1],select[j]] < bestSum) { swap(select[i],select[j]); currentSum += map[select[i-1],select[i]]; TSP(select,i+1); swap(select[i],select[j]) currentSum -= map[select[i-1],select[i]]; } } } }
|
时间复杂度为O((n−1)!)
装载问题
可以发现,为1的左子树表示装上第一艘船,为0的右子树表示不装上第一艘船
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| int n = U; int currentSum = 0; int bestSum = 0; int weight[n]; int capacity; void backtrack(int i) { if(i > n) { if(currentSum > bestSum) { bestSum = currentSum; } } else { if(currentSum + weight[i] <= capacity) { currentSum += weight[i]; backtrack(i+1); currentSum -= weight[i]; } backtrack(i+1); } }
|
当然,对右子树还可以使用剪枝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| int n = U; int currentSum = 0; int bestSum = 0; int remainSum = sum(weight); int weight[n]; int capacity; void backtrack(int i) { if(i > n) { bestSum = currentSum } else { remainSum -= weight[i]; if(currentSum + weight[i] <= capacity) { currentSum += weight[i]; backtrack(i+1); currentSum -= weight[i]; } if(currentSum + remainSum > bestSum) { backtrack(i+1); } remainSum += weight[i]; } }
|
复杂度分析
由于bestSum可能被更新O(2n)次,改进后算法的计算时间复杂性为O(n2n)
一般0-1背包问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| int n = U; int currentSum = 0; int currentValue = 0; int bestValue = 0; int weight[n]; int value[n]; int capacity; int bound(int i) { int remainSum = capacity - currentSum; int tempValue = currentValue; while(i <= n && w[i] <= remainSum) { remainSum -= w[i]; tempValue += v[i]; i++; } if(i<=n) { tempValue += remainSum * (v[i]/w[i]); } return tempValue; }
void bucket(int i) { if(i > n) { bestValue = currentValue; } if(currentSum + w[i] <= capacity) { currentSum += w[i]; currentValue += v[i]; bucket(i+1); currentSum -= w[i]; currentValue -= v[i]; } if(bound(i+1) > bestValue) { bucket(i+1); } }
|
M图着色问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| bool judge(int i) { for(int j = 0;j < n;j++) { if(map[i][j] == 1 && select[i] == select[j]) { return false; } } return true } void color(int i) { if(i > n) { sum++; } else { for(int j = 1;j < colorNum;j++) { select[j] = i; if(judge(i)) { color(i + 1); } select[j] = -1; } } }
|
其实这是n皇后的一个改版.
第六章 分支限界
ub=v+(W−w)∗(vi/wi)
lb=(Σi=1k−12c[ri][ri−1]+Σri∈Uri行不在路径上的最小元素+Σrj∈/Urj行最小两个元素)/2
自求多福吧,,,