算法设计与分析(超详解!) 第四节 回溯法

1.回溯法基础

1.回溯法的基本思想

先定义问题的解空间,然后在问题的解空间树中,按深度优先策略,从根结点出发搜索解空间树。算法搜索至解空间树的任一结点时,总是先判断该结点是否肯定包含问题的解。如果肯定不包含,则跳过对以该结点为根的子树的搜索,逐层向其祖先结点回溯;否则,进入该子树,继续按深度优先的策略进行搜索。这种以深度优先的方式系统地搜索问题解的算法为回溯法,它适用于求解解空间较大的问题。整个试探搜索的过程是由计算机完成的,所以对于搜索试探要避免重复循环,即要对搜索过的结点做标记。

可见,回溯法就是"试探着走"。如果尝试不成功则退回一步,再换一个办法试试。反复进行这种试探性选择与返回纠错过程,直到求出问题的解为止。

2.回溯法的解空间

1.问题的解空间

(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);
   }
}

(3)解空间树---排列树

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)) backtrack(t+1);
        swap(x[t], x[i]);
      }
}

2.生成问题解空间的基本状态

扩展结点,活结点,死结点。

深度优先的问题状态生成法:如果对一个扩展结点R,一旦产生了它的一个儿子C,就把C当做新的扩展结点。在完成对子树C(以C为根的子树)的穷尽搜索之后,将R重新变成扩展结点,继续生成R的下一个儿子(如果存在)。

3.回溯法搜索

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

剪枝函数,一是用约束函数,是用限界法。

例:

0-1背包问题的解空间树及其搜索过程。 问题描述:物品种数n=3,背包容量C=20,物品价值(p1,p2,p3)=(20,15,25),物体重量(w1,w2,w3)=(10,5,15),求X=(x1,x2,x3)使背包价值最大?

3.回溯算法实现

1.实现回溯法的算法

回溯法是对解空间树的深度优先搜索法,通常有两种实现的算法。

递归回溯:采用递归的方法对解空间树进行深度优先遍历来实现回溯。

迭代回溯:采用非递归迭代过程对解空间树进行深度优先遍历来实现回溯。

2.递归回溯

void backtrack (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)) backtrack(t+1);
          }
}

3.迭代回溯

void iterativeBacktrack ()
{
    	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--;
             }
}

4.回溯法的基本步骤

(1)对所给定的问题,定义问题的解空间:子集树问题,排列树问题和其他因素;

(2)确定状态空间树的结构;

(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。其中深度优先方式可以选为递归回溯或者迭代回溯。

5.回溯法实例------运动员最佳配对问题

设一个羽毛球队有男女运动员各n人,给定2个n*n 矩阵P 和Q, 其中P [i][j]表示男运动员i 和女运动员j 配对组成混合双打时的竞赛优势,Q[i][j]则是女运动员i 和男运动员j 配对组成混合双打时的竞赛优势。由于技术的配合和心理状态等各种因素的影响,一般P[i][j]不一定与Q[j][i]相等。设计一个算法, 计算出男女运动员的最佳配对方法,使各组男女双方竞赛优势乘积的总和达到最大。

运动员最佳配对问题:

最优搭配为: 男1---女1, 男2---女2, 男3---女3. 最优搭配下的最大优势乘积的总和是56

1.定义问题的解空间

运动员最佳配对问题,当男女运动员各n 人时,假设固定男运动员的顺序,那么该问题的求解就是对n个女运动员的全排列问题,所以其解空间可以组织成一棵n+1 层的排列树,其中最后一层的叶子标志着一种配对方案的形成,无实际意义。

2.确定解空间树的结构

从根结点出发到该排列树的任一叶结点对应了一个运动员的配对方案, 其中第i层结点表示第i个男运动员, 从第i 层结点到第i+1层接点的连线表示与第i 个男运动员相配对的女运动员j(j 为连线上的标号) 。

3.搜索解空间树

在解空间树中,若当前的层数i>n 时,则说明已经找到了一个运动员配对方案, 此时只需判断其是否是最优解,设用变量 cc 存放各组男女运动员双方竞赛优势乘积的总和,用变量bestc最优值,即存放竞赛优势乘积总和的最大值,在搜索过程中,cc 和bestc 中存放相应的当前值与当前最优值,此时若cc>bestc,则说明当前的最优方案已不再最优, 此时就用找到的方案来更新当前的最优方案;否则仍然保持以前的最优值。

若i小于等于n且cc<bestc,则按照深度优先的策略继续往下搜索,否则,回溯到该结点的父结点。上述过程一直地进行下去,直到所有路径均被检查过,就可以得到一个最优方案。

4.算法的设计与实现

void backtrack(int i) 
{
      int j;
     if (i> n)
      {   if (cc>bestc) {  
               for (j=0;j<=n;j++)
                  bestx[j]=x[j];
                 bestc=cc;
             }
       }
    else
    {
        for(j=i;j<=n;j++)
      {
         swap(&x[i],&x[j]);
         cc+=F[i][x[i]];
         backtrack(i+1);
         cc- =F[i][x[i]];
         swap(&x[i],&x[j]);
       } 
     }
}

2.子集和问题

给定n个不同的正数集W={w(i)|1≤i≤n}和正数M,子集和问题是要求找出正数集W子集S,使该子集中所有元素的和为M,即

例如当n=4,(w1,w2,w3,w4)= (11,13,24,7),M=31,则满足要求的子集(11,13,7)和(24,7)。

1.定义问题的解空间

子集和问题是从n个元素的集合中找出满足某种性质的子集,其相应的解空间树为子集树。该问题的另一种表示是,每个解的子集由这样一个n元组(x1,x2,...,xn)表示,其中xi∈{0,1},1≤i≤n。如果解中含有wi,则直xi=1,否则xi=0。例如前面实例的解可以表示为(1,1,0,1)和(0,0,1,1)。

2.确定解空间树的结构

n=4的子集和数的问题的一种解空间结构

3.搜索解空间树

为解决该问题可以使用递归回溯的方法来构造最优解,设cs为当前子集和,在解空间树中进行搜索时,若当前层i>n时,算法搜索至叶节点,其相应的子集和为cs。当cs=c,则找到了符合条件的解。

当i小于等于n时,当前扩展结点Z是子集树的内部结点。该结点有x[i]=1和x[i]=0两个儿子结点。其左儿子结点表示x[i]=1的情形。

剪枝函数

(1)约束函数

(2)限界函数

4.算法的设计与实现

void Subsum::backtrack(int i) 
{		
                 if (i>n) 
 	{				  
                       if (cs ==c) 	                                     
                     {
                        for(int j=1;j<=len;j++)                    
                              if(x[j] ==1) 
     cout<< w [j]<<" ";//输出结果         
                                cout<<endl;
                           }
                return ;
                }
                r -= w[i];	
if (cs+w[i]<=c)  //检测左子树
              {			
                        x[i] = 1;			 
                        cs += w[i];		                               
                         backtrack(i+1)) cs -= w[i];		                }
               if (cs+r>=c)    //检测右子树
              {
                x[i] = 0;	
                 backtrack(i+1); //深度优先遍历 ,递归处理右子树 
               }
              r+= w[i];	//递归退层时:将该段加入剩余路径r中
               return ;
} 

3.n皇后问题

N 皇后问题,是一个古老而著名的问题,是回溯算法的典型例题,可以简单的描述为 :在N*N格的棋盘上摆放N个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法?

1.定义问题的解空间

以8皇后为例,可以用一棵树表示8皇后问题的解空间。假设皇后i放在第i行上,8皇后问题可以表示成8元组(X1,X2,... ,X8),其中Xi(i=1,2,...,8)表示皇后i所放位置的列号,此时该问题的解空间为8 个8元组。加上隐式约束条件:没有两个Xi相同,且不存在两个皇后在同一条对角线上,因此问题的解空间进一步减小为8!。由于8皇后问题的解空间为8!种排列,因此我们将要构造一棵排列树。

2.确定解空间树的结构

n=4时问题的一种空间树结构。

确定解空间树的结构

皇后问题的解示例

3.搜索解空间树

解n后问题的回溯算法可描述如下:求解过程从空配置开始。在第1列~的m列为合理配置的基础上,再配置第m+1列,直至第n列也是合理时,就找到了一个解。在每列上,顺次从第一行到第n行配置,当第n行也找不到一个合理的配置时,就要回溯,去改变前一列的配置。

用n元组x[1:n]表示n皇后问题的解,x[i]表示皇后i放在第i 行的第x[i]列上,用完全n叉树表示解空间。

剪枝函数设计:对于两个皇后A(i,j)、B(k,i)

两个皇后不同行:i不等于k;

两个皇后不同列:j不等于i;

两个皇后不同一条斜线:|i-k|≠|j-i|,即两个皇后不处于同一条y=x+a或y=-x+a的直线上

4.算法的设计与实现

int Queen::queen(int t)   
{   
    if(t>n && n>0) //当放置的皇后超过n时,可行解个数加1,此时n必须大于0   
      sum++;  
    else  
      for(int i=1;i<=n;i++)   
      {   
          x[t] = i; //标明第t个皇后放在第i列   
          if(place(t)) //如果可以放在某一位置,则继续放下一皇后   
           queen(t+1);    
      }   
    return sum;   
} 
void Queen::queen(void)
 {   x[1] = 0;
	int t = 1;
	while(t>0)
	{        x[t] += 1;
		while((x[t]<=n)&&!(Place(t)))  x[t] += 1;
		if(x[t]<=n)
		        If(t==n)  sum++;
                      else{ t++;     x[t] = 0;}
	           else
		     t--;
	}
}

4.连续邮资问题

假设某国家发行了n种不同面值的邮票,并且规定每张信封上最多只允许贴m张邮票。对于给定的n和m的值,给出邮票面值的最佳设计,在1张信封上贴出从邮资1开始,增量为1的最大连续邮资区间。

一张信封上贴出从邮资为1开始,增量为1的连续邮资区间。 (所以第一个邮票面值必为1) e.g.:n=2, m=3时,如果面值分别为1和2,则可得到的邮资值为 1~6,连续邮资区间为[1,6],6即为可得到的连续邮资最大值;如果 面值分别为1和3时,可得到的邮资为1~7和9,连续邮资区间为 [1,7],7即为连续邮资最大值。

1.定义问题的解空间

用n元组stampsvalue[1:n]表示n种不同的邮票面值,并约定它们从小到大排列,该问题需要找出最大连续邮资区间,和组成最大成邮资值的各种邮票面值,即从包含n个元素的集合中找出满足某个条件的子集,所以构造的解空间树是一个子集树

2.确定解空间树的结构

以n=3,m=3为例:

3.搜索解空间树

在解空间树中, 若当前的层数i>n 时,表示已搜索至一个叶结点,得到一个新的邮票面值设计方案stampsValue [1:n]。如果该方案能贴出的最大连续邮资区间大于当前已找到的最大连续邮资区间maxValue,则更新当前最优值maxValue和相应的最优解bestValue。

当i<=n时,当前扩展结点Z是解空间中的一个内部结点。在该结点处stampsValue [1:i-1]能贴出的最大连续邮资区间为r-1。因此,在结点Z处,stampsValue[i]的可取值范围是[stampsValue [i-1]+1:r],从而,结点Z有r-stampsValue [i-1]个儿子结点。算法对当前扩展结点Z的每一个儿子结点,以深度优先方式递归的对相应的子树进行搜索。

4.算法的设计与实现

void Stamp:: conStamps (int i,int r)
{
       for(int j=0;j<=stampsValue [i-2]*(m-1);j++)
          if(stampsNum [j]<m)
     	       for(int k=1;k<=m-stampsNum [j];k++)
                if(stampsNum[j]+k<stampNum[j+stampsValue [i-1]*k])
                 stampsNum [j+stampsValue[i-1]*k]= stampNum [j]+k;
                 while(stampsNum [r]<maxInt)   r++;
       	if(i>n)
       	{
                     if(r-1>maxValue)
              	  {
	                 maxValue=r-1;
	                 for(int j=1;j<=n;j++)
	                 bestValue [j]=stampsValue[j]; 		
                     }
	          return;
                 }

int *z=new int [maxl+1];
	for(int k=1;k<=maxl;k++)
	z[k]= stampsNum [k];
	for(int h=stampsValue[i-1]+1;h<=r;h++)
	   if(stampsNum [r-h]<m)
	  {
		 stampsValue[i]=h;
                conStamps (i+1,r+1);  /*递归回溯第i+1层结点*/
	            for(int k=1;k<=maxl;k++)
		   stampsNum [k]=z[k];
		}
	delete []z;
}

5.哈密顿回路

设G=(V,E)是一个有n个节点的有向图或无向图,一条哈密顿回路是通过图中每个节点仅且一次的回路 问:给定一个图,求其是否存在一条哈密顿回路。如果是,输出每一条回路;否则给出提示。

1.定义问题的解空间

所给问题是从n个元素的集合中找出满足某种性质的排列,因此,解空间可以组成一颗排列树,节点总数为n!,遍历它至多需要O(n!)的计算时间。

2.确定解空间树的结构

用数组x[n]表示该问题的一组解,x[i]表示在一个可能回路上第 i 此访问的节点号。规定x[1]=1,假定已经选完了x[1]到x[k-1],1<k<n. 那么x[k]可以取不同于x[i](i可取从1到k-1)且有一条边与x[k-1]相连的任意节点之一,而x[n]必须是与x[n-1]和x[1]都相连的节点,若这样的x[k]找不到则回溯到x[k-2],重找下一个节点x[k-1].

3.搜索解空间树

在遍历解空间树时,当i=n,当前扩展节点是排列树的叶节点的父节点。此时需要检测无向图G是否存在一条从顶点x[n-1]到顶点x[n]的边和一条从顶点x[n]到顶点1的边,如果存在,则找到一条哈密顿回路。

当i<n时,当前扩展结点位于排列树的第i-1层,图G中存在从顶点x[i-1]到顶点x[i]的路径时,x[1:n]就构成了图G的一条路径。

4.算法的设计与实现

(1)递归回溯
void hamilton(int k,bool g[][]){
	if(k>n&&g[x[k-1]][1]==1)
          output(x);
	   else for(int i=k;i<=n;i++)
	{
       swap(x[k],x[i]);
	   if(g[x[k-1]][x[k]]==1)
		   hamilton(k+1,g);
        swap(x[i],x[k]);
	}
	}

(2)迭代回溯法
void backtrack(int i)
{if(i==n+1)
{  //当前扩展结点是排列树叶结点的父结点,图G存在一条从顶点x[n-1]到顶点x[n]的边和一条从顶点x[n]到顶点1的边,即找到一条旅行售货员回路
if(a[x[n-1]][x[n]!=NoEdge&&a[x[n]][1]!=NoEdge
&&(bestc==NoEdge||(cc+a[x[n-1]][x[n]]+a[x[n]][1])<bestc))
    {
     for(int j=1;j<=n;j++)
      bestx[j]=x[j];//最优解
     bestc=cc+a[x[n-1]][x[n]]+a[x[n]][1];//当前最优值(最小费用)
       }
     }
else
   {
    for(int j=i;j<=n;j++)
     //是否可进入x[j]子树   
 if(a[x[i-1]][x[j ]!=NoEdge &&(bestc== NoEdge ||(cc+a[x[i-1]][x[j]])<bestc))
     {//搜索子树
      swap(x[i] ,x[j]); //交换使得可以从当前结点选择其他的子节点
      cc+=a[x[i-1]][x[i]];
      backtrack(i+1);     //递归回溯
      cc-=a[x[i-1]][x[i]];
      swap(x[i], x[j]);     
     }
   }
}
相关推荐
xiaoshiguang33 小时前
LeetCode:222.完全二叉树节点的数量
算法·leetcode
爱吃西瓜的小菜鸡3 小时前
【C语言】判断回文
c语言·学习·算法
别NULL3 小时前
机试题——疯长的草
数据结构·c++·算法
TT哇4 小时前
*【每日一题 提高题】[蓝桥杯 2022 国 A] 选素数
java·算法·蓝桥杯
yuanbenshidiaos5 小时前
C++----------函数的调用机制
java·c++·算法
唐叔在学习5 小时前
【唐叔学算法】第21天:超越比较-计数排序、桶排序与基数排序的Java实践及性能剖析
数据结构·算法·排序算法
ALISHENGYA5 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(switch语句)
数据结构·算法
chengooooooo5 小时前
代码随想录训练营第二十七天| 贪心理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
算法·leetcode·职场和发展
jackiendsc5 小时前
Java的垃圾回收机制介绍、工作原理、算法及分析调优
java·开发语言·算法
游是水里的游7 小时前
【算法day20】回溯:子集与全排列问题
算法