参考书籍:算法设计与分析——C++语言描述(第二版)

算法设计策略-回溯法

回溯法是比贪心法和动态规划法更一般的方法,回溯法是一种通过搜索状态空间树来求问题的可行解或最优解的方法。回溯法使用约束函数和限界函数来压缩需要实际生成的状态空间树的结点数,从而大大节省问题的求解时间。通常情况下,回溯法是为了找出满足约束条件的所有可行解。

回溯法

基本概念

  • 显示约束和解空间

使用回溯法求解的问题通常需要给出某些必须满足的约束条件,这些约束条件可分为两类:显示约束和隐式约束。回溯法要求问题解的结构具有元组形式:(x0,x1,⋯,xn−1),每个xi从一个给定的集合Si取值。

这种用于规定每个xi取值的约束条件称为显示约束(explicit constraint)。对给定的问题实例,显示约束规定了所有可能的元素,他们组成问题的候选解集,被称为该问题实例的解空间

  • 隐式约束和判定函数

隐式约束给出了判定一个候选解是否为可行解的条件,一般需要从问题描述的隐式约束出发,设计一个判定函数(criterion function)p(x0,x1,⋯,xn−1),使得当且仅当p(x0,x1,⋯,xn−1)为真时,n-元组(x0,x1,⋯,xn−1)是问题实例的满足隐式约束(implicit constraint)的一个可行解。

  • 最优解和目标函数

目标函数,也称代价函数(cost function),用来衡量每个可行解的优劣。使目标函数取最大(或最小)值的可行解为问题的最优解。

  • 问题状态和状态空间树

状态空间树是描述问题解空间的树形结构。下图是n=3的排序问题的一种状态空间树。树中每一个结点称为一个问题状态。如果从根到树中某个状态的路径代表一个作为候选解的元组,则称该状态为解状态(solution state)。如果从根到某个解状态的路径代表一个作为可行解的元组,则称该解状态为答案状态(answer state).如果所求解的是最优化问题,还必须用目标函数衡量每个答案结点,从中找出使目标函数取最优值的最优答案结点。

算法分析回溯python 算法分析与设计回溯法_状态空间

剪枝函数和回溯法

如果问题的解空间可以用一颗状态空间树描述,那么通过搜索状态空间树可以找到答案状态。最简单的方法是:使用某种树搜索算法,检查树中的每个问题状态。如果是解状态,则用判定函数判定它是否是答案状态。对于最优化问题,在搜索过程中还需要对每一个答案结点计算其目标函数值,记录其中的最优者。深度优先搜索和广度优先搜索可以用于搜索状态空间树。

事实上,状态空间树并不需要事先生成,而只需在求解的过程中,随着搜索算法的进展,逐个生成状态空间树的问题状态结点。

为了提高搜索效率,在搜索过程中使用约束函数,可以避免无谓的搜索那些已知不含答案状态的子树。如果是最优化问题,还可以用限界函数(bound function)减去那些不可能包含最优答案结点的子树。约束函数和限界函数的目的相同,都是为了剪去不必要搜索的子树,减少问题求解所需实际生成的状态结点数,他们统称为剪枝函数(pruning function)

使用剪枝函数的深度优先生成状态空间树中结点的求解方法称为回溯法(backtracking)广度优先生成状态空间树中结点,并用剪枝函数的方法称为分枝限界法(branch-and-bound)

一个约束函数是关于部分向量的函数Bk(x0,x1,⋯,xk),它被定义为:如果可以断定Y的子树上不含任何答案状态,则Bk(x0,x1,⋯,xk)为false,否则为true。

回溯法本质上是一种以深度优先方式,逐一生成状态空间树中结点并检测答案结点的方法。与穷举法不同,回溯法使用约束函数,剪去那些可以断定不含答案状态的子树,从而提高算法效率

//递归回溯法框架
void RBcaktrack(int k)
  {
    //应以RBacktrack(0)调用本函数
    for(每个x[k],使得x[k]属于T(x[0],...,x[k-1]) && (B_k(x[0],...,x[k]))){
      // T(x0,x1,...,xk-1)表示沿路径(x0,x1,...,xk-1)从根到某个问题状态时,孩子结点xk可取值的集合。
      //Bk(x0,x1,...,xk)为约束函数。若子树上不含任何答案状态,则为false;否则,为true。
      if((x[0],x[1],...,x[k])是一个可行解){
        //判定是否是一个可行解
          输出(x[0],x[1],...,x[k]);//输出可行解
      }
      RBacktrack(k+1);//深度优先进入下一层
    }
  }
//迭代回溯法
void IBacktrack(int n)
  {
    int k = 0;
    while(k>=0){
      if(还剩下尚未检测的x[k],使得x[k]属于T(x[0],...,x[k-1]) && (B_k(x[0],...,x[k]))){
        if((x[0],x[1],...,x[k])是一个可行解){
          //考虑x[k]的下一个可取值
          输出(x[0],x[1],...,x[k]);
        }
        k++;//考虑下一层分量
      }
      else
        k--;//回溯到上一层
    }
  }

回溯法的本质

  • 是一种深度优先搜索方式,逐一动态生成状态空间树中结点并检测答案结点的方法。
  • 不同于穷举搜索,回溯法使用约束函数,剪去那些可以判定不含答案状态的子树,从而提高算法效率。

用回溯法求解的要求:

  1. 问题的解具有n-元组形式;
  2. 问题提供显式约束来确定状态空间树,并提供隐式约束来判断可行解;
  3. 应能设计有效的约束函数,缩小检索空间。

回溯法的效率分析

回溯法的时间通常取决于状态空间树上实际生成的那部分问题状态的数目。对于元组长度为n的问题实例,若其状态空间树中结点总数为n!(或2n或nn),则回溯法的最坏情况时间复杂度可达O(p(n)n!)(或O(p(n)2n)或O(p(n)nn)),这里p(n)是n的多项式,是生成一个结点所需的时间。

经验表明,在很多情况下,对于具有大n值的实例,回溯法的确可以在很短的时间内求得其解。

蒙特卡洛方法(Monte Carlo)是一种用于估算回溯法处理一个实例时,所实际生成的结点数的方法。这种估计方法的基本思想是在状态空间树中随机选择一条路径(x0,x1,⋯,xn−1)。设X是这条随机路径上,代表部分向量(x0,x1,⋯,xk−1)的结点,如果在X处不受限制的孩子数目是mk,则认为与X同层的其他结点不受限的孩子数目也是mk。也就是说,若不受限的x0有m0个,则第2层上有m0个结点(根是第一层);若不受限制的x1取值有m1个,则第3层上有m0m1个结点;以此类推,整个状态空间树上将实际生成的结点数估计为

m=1+m0+m0m1+m0m1m2+⋯

//蒙特卡洛算法估计m
//它从状态空间树的根结点出发,随机选择一条路径。
//集合S是未受限的x_k的取值,函数Size返回集合S的大小。
//函数Choose从集合S中未x_k随机选择一个值,生成一个随机路径。
int Estimate(SType *x)
{
  int k = 0,m = 1,r = 1;
  do{
    SetType S={x[k]|x[k]属于T(x[1],...,x[k-1]) && B_k(x[1],...,x[k])==true};
    if(!Size(S))//终止条件
      return m;
    r=r*Size(S);//r为第k层中未受限结点数的估计值
    m=m+r;//m为状态空间树中结点总数的估计值
    x[k]=Choose(S);//从集合S中为x[k]随机选择一个值,向下生成随机路径
    k++;
  }while(1);
}

回溯法注意:

  1. 在求解的过程中,以深度优先方式逐个生成状态空间树中结点,求问题的可行解或最优解;
  2. 为提高搜索效率,在搜索过程中用约束函数和限界函数(统称剪枝函数)来剪取不必要搜索的子树,减少问题求解所需实际生成的状态空间树结点数,避免无效搜索。(用约束函数剪去不含答案状态(可行解)的子树;用限界函数剪去不含最优答案结点(最优解)的子树)

n-皇后

问题描述

n-皇后问题要求在一个n×n的棋盘上放置n个皇后,使得它们彼此不受攻击。按照国际象棋的规则,一个皇后可以攻击与其处在同一行、同一列或同一斜线上的其他棋子,n-皇后问题要求寻找在棋盘上放置这n个皇后的方案,使得他们中任意两个不在同一行、同一列或同一斜线上。

回溯法求解

一个问题能够用回溯法求解,首先,它的解具有n-元祖形式;其次,问题提供显式约束来确定状态空间树,并提供隐式约束来判定可行解;最后,应能设计有效的约束函数,缩小检索空间。

首先考虑问题解的结构形式。由于每个皇后不应在同一行上,于是假定第i个皇后放在第i行上,这样,使用n-元祖(x0,x1,⋯,xn−1)表示n-皇后问题的解,其中xi表示第i行的皇后所处的列号(0≤xi<n)。

对于n-皇后问题,其显式约束的一种观点是:Si={0,1,⋯,n−1},0≤i<n。相应的隐式约束为:对任意0≤i,j<n,当i≠j时,xi≠xj且|i−j|≠|xi−xj|。此时的解空间大小为nn。另一种显式约束的观点是:Si={0,1,⋯,n−1},0≤i<n,且xi≠xj(0≤i,j<n,i≠j)。相应的隐式约束为:对任意0≤i,j<n,当i≠j时,|i−j|≠|xi−xj|。与此相应的解空间大小为n!。

最后来看约束函数的设计。约束函数从隐式约束中产生:对0≤i,j<n,当i≠j时,要求xi≤xj且|i−j|≤|xi−xj|(此处采用显式约束的第一种观点)。

一般称用于确定n个元素的排列满足某种性质的状态空间树为排列树(permutation tree)。

n-皇后算法

设已经生成了部分向量(x0,x1,⋯,xk−1),并且这前k个皇后已分配的列号相互不冲突。现在检查如果选择xk=i是否会引起冲突,即第k+1个皇后如果放在第i列,是否会因此而与前k个皇后在同一列或同一斜线上。

//n-皇后问题的回溯法
bool Place(int k, int i, int *x)
{
    //Place函数起着约束函数的作用
    //判定两个皇后是否在同一列或同一斜线上
    for(int j=0;j<k;j++){
        if((x[j]==i)||(abs(x[j]-i)==abs(j-k)))
            return false;
    }
    return true;
}

void NQueens(int k, int n, int *x)
{
    for(int i=0;i<n;i++){
        //显示约束的第一种观点,x[k]=0,1,...,n-1
        if(Place(k,i,x)){
            //约束函数
            x[k]=i;
            if(k==n-1){
                //输出一个可行解
                for(i=0;i<n;i++)
                    cout<<x[i]<<" ";
                cout <<endl;
            } else {
                NQueens(k+1,n,x);//深度优先进入下一层
            }
        }
    }
}

void NQueens(int n,int *x)
{
    NQueens(0,n,x);
}

以上程序能够求得n-皇后问题的全部解。

小结

  • 回溯法是比贪心法和动态规划法更一般的方法。
  • 解为n-元组(x0,x1,…,xn-1)形式。
  • 通过搜索状态空间树来求问题的可行解(满足约束条件)或最优解(使目标函数取最大或最小值)。
  • 回溯法使用约束函数和限界函数来压缩需要实际生成的状态空间树的结点数。
  • 通常情况下,回溯法是为了找出满足约束条件的所有可行解。

回溯法以深度优先次序生成状态空间树中的结点,并使用剪枝函数减少实际生成的结点树。回溯法是一种广泛适用的算法设计技术,只要问题的解是元组形式,可以用状态空间树描述,并采用判定函数识别答案结点,就能采用回溯法求解。回溯法使用约束函数剪去不含可行解的分枝。当时用回溯法求解最优化问题时,需设计限界函数,用于剪去不含最优解的分枝。约束函数和限界函数通常称为剪枝函数。回溯法的求解时间因实例而异,其计算时间可用蒙特卡洛方法估算。