递归、回溯-算法框架

之前已经学习过回溯法的一些问题,从这篇文章开始,继续深入学习一下回溯法以及其他经典问题。

回溯法有通用的解题法之称。用它可以系统的搜索一个问题的所有解或任一解,回溯法是一个既带有系统性又带有跳跃性的搜索算法。

它的问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,先判断该结点是否包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的搜索,逐层向其祖先结点回溯。否则,进入该子树,继续按深度优先策略搜索。回溯法求问题的所有解时,要回溯到根,且根结点的所有子树都已被搜索遍才结束。回溯法求问题的一个解时,只要搜索到问题的一个解就可结束。

这种以深度优先方式搜索问题解的算法称为回溯法,它适用于解组合数较大的问题。

回溯法的算法框架:

1、问题的解空间

用回溯法解问题时,应明确定义问题的解空间。问题的解空间至少应包含问题的一个(最优解)。

2、回溯法的基本思想

确定了解空间的组织结构后,回溯法从开始结点出发,以深度优先方式搜索整个解空间。这个开始结点称为活结点,同时也称为当期那的扩展结点,如果在当前的扩展结点处不能再向纵深方向移动,则当前扩展结点就称为死结点。此时,应往回移动(回溯)至最近的一个或活结点处,并使这个活结点称为当前的扩展结点。回溯法以这种工作方式递归地在解空间中搜索,直至找到所要求的解或解空间中已无活结点时为止。

3、递归回溯 

回溯法对解空间作深度优先搜索,因此在一般情况下可用递归函数来实现回溯法如下:

void Backtrack(int t)
{
    if(t > n)                             //t>n时已搜索到一个叶结点,output(x)得到的可行解x进行记录或输出处理
        Output(x);
    else                                  //当前拓展结点是解空间树的内部结点
    {
        for(int i = f(n,t); i <= g(n, t); i++)   //函数f和g分别表示当前扩展结点处未搜索子树的起止编号
        {
            x[t] = h(i);                         //h(i)表示在当前扩展结点处x[t]的第i个可选值
            if(Constraint(t) && Bound(t))
                Backtrack(t+1);
        }                                        //循环结束时,已搜索遍当前扩展结点的所有未搜索子树
    }
}  

其中,形式参数t表示递归深度,即当前扩展结点在解空间树中的深度。n用来控制递归深度,当t>n时,算法已搜索到叶结点,此时,由Output(x)记录或输出得到的可行解x。算法BackTrack的for循环中f(n,t)和g(n,t)分别表示在当前扩展结点处未搜索过的子树的起始编号和终止编号。h(i)表示在当前扩展结点处x[t]的第i个可选值。Constraint(t)和Bound(t)表示在当前扩展结点处的约束函数和限界函数。Constraint(t)返回的值为true时,在当前扩展结点处x[1:t]的取值满足问题的约束条件,否则不满足问题的约束条件,可剪去相应的子树。

Bound(t)返回的值为true时,在当前扩展结点处x[1:t]的取值未使目标函数越界,还需由Backtrack(t+1)对其相应的子树做进一步搜索。

否则,当前扩展结点处x[1:t]的取值使目标函数越界,可剪去相应的子树。执行了算法的for循环后,已搜索遍当前扩展结点的所有未搜索过的子树。Backtrack(t)执行完毕,返回t-1层继续执行,对还没有测试过的x[t-1]的值继续搜索。当t=1时,若已测试完x[1]的所有可选值,外层调用就全部结束。显然,这一搜索过程按深度优先方式进行,调用一次Backtrack(1)即可完成整个回溯搜索过程。

4、迭代回溯

采用树的非递归深度优先遍历算法,也可将回溯法表示为一个非递归的迭代过程如下:

void IterativeBacktrack()
{
    int t;
 
    t = 1;                                       //当前扩展结点在解空间树中的深度,在这一层确定解向量的第t个分量x[t]的取值
    while(t > 0)
    {
        if(f(n,t) <= g(n,t))                    //f和g分别表示在当前扩展结点处未搜索子树的起止编号
        {
            for(int i = f(n,t); i <= g(n,t); i++)
            {
                x[t] = h(i);                    //h(i)表示在当前扩展结点处x[t]的第i个可选值
                if(Constraint(t) && Bound(t))
                {
                    if(Solution(t))             //solution(t)判断当前扩展结点处是否已得到问题的一个可行解
                        Output(x);
                    else
                        t++;                    //solution(t)为假,则仅得到一个部分解,需继续纵深搜索
                }
            }
        }
        else
            t--;                                //如果f(n,t)>g(n,t),已搜索遍当前扩展结点的所有未搜索子树,
    }                                           //返回t-1层继续执行,对未测试过的x[t-1]的值继续搜索
}       

上述迭代回溯算法中,用Solution(t)判断在当前扩展结点处是否已得到问题的可行解。它返回的值为true时,在当前扩展结点处x[1:t]是问题的可行解。此时,由Output(x)记录或输出得到的可行解。它返回的值为false时,在当前扩展结点处x[1:t]只是问题的部分解,还需向纵深方向继续搜索。

算法中f(n,t)和g(n,t)分别表示在当前扩展结点处未搜索过的子树的起始编号和终止编号。h(i)表示在当前扩展结点处x[t]的第i个可选值。Constraint(t)和Bound(t)是当前扩展结点处的约束函数和限界函数。Constraint(t)的返回的值为true时,在当前扩展结点处x[1:t]的取值满足问题的约束条件,否则不满足问题的约束条件,可剪去相应的子树。Bound(t)返回的值为true时,在当前扩展结点处x[1:t]的取值未使目标函数越界,还需对其相应的子树做进一步搜索。否则,当前扩展结点处x[1:t]的取值已使目标函数越界,可剪去相应的子树。算法的while循环结束后,完成整个回溯搜索过程。

5、字集树与排列树

当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间数称为子集树。这类子集树通常有个叶结点,其结点总个数为.遍历子集树的任何算法均需的计算时间。

void Backtrack(int t)
{
    if(t > n)
        Output(x);
    else
    {
        for(int i = 0; i <= 1; i++)
        {
            x[t] = i;
            if(Constraint(t) && Bound(t))
                Backtrack(t+1);
        }
    }
}

当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。排列树通常有n!个叶结点。因此遍历排列数需要\Omega (n!)的计算时间。

void Backtrack(int t)
{
    if(t > n)
        Output(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]);
        }
    }
}

在调用Backtrack(1)执行回溯搜索之前,先将变量数组x初始化为单位排列(1,2,....,n)

posted @ 2019-03-21 00:29  YFR718  阅读(1541)  评论(0编辑  收藏  举报