回溯法是一种组织搜索的一般技术,有“通用的解题法”之称,用它可以系统地搜索一个问题地所有解或任意解。 它可以系统地搜索一个问题地所有解或任意解,既有系统性又有跳跃性。 回溯法的基本做法是搜索,它是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。这种以深度优先方式系统地搜索问题的解的方法称为回溯法。
6.1 回溯算法的理论基础
6.1.1 问题的解空间
应用回溯法求解时,需要明确定义问题的解空间。问题的解空间应**至少**包含问题的**一个最优解**。
定义了问题的解空间后,还应将解空间组织起来,通常组织成**树**或**图**的形式。
从树根到叶子结点的任一路径表示解空间中的一个元素。
6.1.2 回溯法的基本思想
定义以下概念:
-
活结点: 如果已生成一个结点而它的所有儿子结点还没生成,则这个结点叫做活结点。
-
扩展结点: 当前正在生成其儿子结点的活结点叫做扩展结点。(正扩展的结点)
-
死结点: 不再进一步扩展或者其儿子结点已全部生成的结点就是死结点。
例6-1 假设背包容量为C=30, w = {16, 15, 15}, v={45, 25, 25}, 其回溯搜索过程如图6-2所示
*图6-2 0-1背包的回溯过程*
例6-2 旅行商问题
旅行商从n个城市中的某一城市出发,经过每个城市一次且仅一次,最后回到原出发点,在所有可能的路径中求出路径长度最短的一条。
目的是要一条汉密尔顿回路。
回路总权值最小,即:
m i n { ∑ i = 1 n − 1 w ( v i , v i + 1 ) + w ( v n , v 1 ) } min\lbrace\sum_{i=1}^{n-1}w(v_i, v_{i+1}) + w(v_n, v_1)\rbrace min{i=1∑n−1w(vi,vi+1)+w(vn,v1)}
*图6-4 旅行商问题的解空间树*
在回溯搜索解空间树时,通常采用两种策略(剪枝函数)避免无效搜索以提高回溯算法的搜索效率:
-
用约束函数在扩展结点处剪去不满足约束条件的子树。
-
用限界函数剪去不能得到最优解的子树。
综上所述,使用回溯法解题,通常有以下三个步骤:
-
针对所给问题,定义问题的解空间。
-
确定易于搜索的解空间结构。
-
以深度优先的方式搜索解空间树,并且在搜索过程中用剪枝函数避免无效搜索。
6.1.3 子集树和排列树
遍历子集树的任何算法,其计算时间复杂性都是O(2^n)。
**0-1背包问题是子集树。**
回溯算法搜索子集树的一般算法描述,如算法6-1所示。
算法6.1 回溯算法搜索子集树的伪代码
// t为树的深度,根为1
void traceback(t){
if (t > n){
update(x);
} else {
for(int i=0; i<=1; i++){ // 每个结点只有两棵子树
x[t] = i; // 即 0 或 1
// 约束函数constraint()和限界函数bound()
if (constraint(t) && bound(t)) traceback(t+1);
}
}
}
遍历排列树时的时间复杂性是O(n!)。
**旅行商问题是排列树。**
算法6.2 回溯算法搜索排列树的伪代码
void traceback(t){
if(t > n){
update(x);
} else {
for(int i=0; i<n; i++){
// 用交换元素的方式实现全排列
swap(x[t], x[i]);
if (constraint(t) && bound(t)) traceback(t+1);
// 恢复状态
swap(x[t], x[i]);
}
}
}