目录
1.回溯法基本思想
2.回溯法的算法框架
2.1问题的解空间
2.2剪枝函数的分类和设计
2.3回溯法的求解过程
2.4回溯法的时间复杂性
回溯法是一种用来寻找问题所有解的通用算法。其思想为:能进则进,进不了退,换条路再试。
回溯法步骤如下:
1)针对所给问题,定义问题的解空间。
2)确定易于搜索的解空间结构。
3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。(通常采用两种方式避免无效搜索:1.用约束条件减去得不到可行解的子树。2.用目标函数减去得不到最优解的子树。
解空间是进行穷举的搜索空间,所以解空间包含了所有的可能解。
确定正确的解空间很重要,如果没有确定正确的解空间就开始进行搜索,可能会造成增加很多重复解,或者根本搜索不到正确的解。
解空间的组织结构一般是由树或图形式表示,常用的典型解空间树是子集树和排列树。
子集树:当所给的问题是从n个元素的集合S中找到S满足某种性质的子集时,相对应的解空间树称为子集树。通常有个叶子结点。例如:n个物品的0-1背包问题所对应的解空间树是子集树
排列树:当所给的问题是确定n个元素满足某种性质的排列时,相对应的解空间树称为排列树。通常有n!个叶子结点。
确定好问题的解空间后,可用不同的剪枝函数和最优解表示方法来获得最终结果。
1.可行性剪枝。
作用:当前路径不满足问题的约束条件时,终止分支。
应用场景:约束满足问题(如N皇后,数独),组合优化问题。
示例:
N皇后问题:当前皇后的位置与之前皇后冲突(同行/同列/对角线)时,剪枝。
子集和问题:当前子集的和已超过目标值,剪枝。
2.最优性剪枝
作用:前路径无法得到比已知最优解更好的解时,终止分支。
应用场景:最优化问题(如TSP,0-1背包)
示例:
旅行商问题(TSP):当前路径长度已超过已知最短路径,剪枝。
0-1背包问题:当前物品总价值 + 剩余物品的最大可能价值 ≤ 已知最优价值,剪枝。
3.去重剪枝
作用:免生成重复解(尤其在解空间含重复元素时)。
应用场景:全排列II、组合总和II。
示例:
全排列II:排序后跳过 nums[i] == nums[i-1]
且 nums[i-1]
未被使用的分支。
回溯法对解空间做深度优先搜索,一般情况下可以用递归函数实现回溯法。
递归函数模板如下:
void BackTrace(int t)
{
if(t > n)
Output(x);
else
for(int i = f(n,t); i <= g(n,t); i++)
{
x[t] = h(i);
if(Constraint(t) && Bound(t))
BackTrace(t+1);
}
}
解释模板
1.函数定义和参数
void BackTrace(int t)
t 表示当前递归层级
例如在全排列问题中,t可能表示正在填充第t个位置的数字
2.终止条件
if(t > n)
Output(x);
n 代表问题规模(如数组长度,皇后数量等)
t > n 代表已经处理完所有层级,此时输出可行解。
Output(x):自定义函数,输出或保存当前解(如打印排列、记录路径等)
3.递归主体
for(int i = f(n,t); i <= g(n,t); i++)
f(n,t)
和 g(n,t)
:动态生成当前层级 t
的候选集范围。
作用:灵活控制每层的可选值,避免无效搜索。
4.赋值与约束检查
x[t] = h(i);
if(Constraint(t) && Bound(t))
BackTrace(t+1);
x[t] = h(i)
:将候选值 i
赋给解向量的第 t
个位置。
h(i):可能对 i
做转换(例如映射索引到实际值)。
Constraint(t) :检查当前解向量 x[1..t]
是否满足问题的 显式约束。
Bound(t) :检查当前解向量是否有潜力达到 最优解(用于剪枝)。
若约束和边界条件均满足,则递归进入下一层 t+1。
使用迭代法也可以实现回溯法。模板如下:
void IterativeBackTrace(void)
{
int t = 1;
while(t > 0)
{
if(f(n,t) <= g(n,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--;
}
}
Solution(t)判断在当前拓展结点处是否已得到问题的一个可行解。
1.子集树
在子集树中,每个结点都有相同数目的子树,通常为2,因此遍历子集树需要时间。
用回溯法搜索子集树的一般算法:
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);
}
}
2.排列树
通常第一层结点子树为n,逐层递减,最后一层为1,所以排列树中共有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))
backtrace(t+1);
swap(x[t],x[i]);
}
}