回溯法是一种组织搜索的一般技术,有“通用的解题法”之称,用它可以系统的搜索一个问题的所有解或任一解。
有许多问题,当需要找出它的解集或者要求回答什么解是满足某些约束条件的最佳解时,往往要使用回溯法。
可以系统地搜索一个问题的所有解或任意解,既有系统性又有跳跃性。
回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。
这种以深度优先的方式系统地搜索问题的解的方法称为回溯法。
应用回溯法求解时,需要明确定义问题的解空间。问题的解空间应至少包含问题的一个(最优)解。
在生成解空间树时,定义以下几个相关概念:
活结点:如果已生成一个结点而它的所有儿子结点还没有全部生成,则这个结点叫做活结点。
扩展结点:当前正在生成其儿子结点的活结点叫扩展结点(正扩展的结点)。
死结点:不再进一步扩展或者其儿子结点已全部生成的结点就是死结点。
在确定了解空间的组织结构后,回溯从开始结点(根结点)出发,以深度优先的方式搜索整个解空间。
这个开始结点成为一个活结点,同时成为当前的扩展结点。在当前的扩展结点,搜索向深度方向进入一个新的结点。这个新结点成为一个新的活结点,并成为当前的扩展结点。
若在当前扩展结点处不能再向深度方向移动,则当前的扩展结点成为死结点,即该活结点成为死结点。此时回溯到最近的一个活结点处,并使得这个活结点成为当前的扩展结点。
回溯法以这样的方式递归搜索整个解空间(树),直至满足中止条件。
在回溯法搜索解空间树时,通常采用两种策略(剪枝函数)避免无效搜索以提高回溯法的搜索效率:用约束函数在扩展结点处减去不满足约束条件的子树;用限界函数减去不能得到最优解的子树。
有时问题是要从一个集合的所有子集中搜索一个集合,作为问题的解。或者从一个集合的排列中搜索一个排列,作为问题的解。回溯算法可以很方便地遍历一个集合的所有子集或者所有排列。
当问题是要计算n个元素的子集,以便达到某种优化目标时,可以把这个解空间组织成一棵子集树。
例如,n个物品的0-1背包问题相应的解空间树就是一棵子集树。
这类子集树通常有2n个叶结点,结点总数为2n +1-1。
遍历子集树的任何算法,其计算时间复杂度都是Ω(2n)。
//形参t为树的深度,根为1
void backtrack (int t)
{
if (t>n) update(x);
else
for (int i=0; i<=1; i++) //每个结点只有两个子树
{
x[t]=i; //即0/1
if (constraint(t) && bound(t)) backtrack(t+1);
}
}
当所给的问题是确定n个元素满足某种性质的排列时,可以把这个解空间组织成一棵排列树。
排列树通常有n!个叶子结点。因此遍历排列树时,其计算时间复杂度是Ω(n!) 。
//形参t为树的深度,根为1
void backtrack (int t)
{
if (t>n) update(x);
else
for (int i=t; i<=n; i++)
{
//为了保证排列中每个元素不同,通过交换 来实现
swap(x[t], x[i]);
if (constraint(t) && bound(t)) backtrack(t+1);
swap(x[t], x[i]); //恢复状态
}
}