回溯法 —— 算法框架及应用

回溯法:

  • 思想:走不通退回走别的路

在包含问题的所有解的空间树中,按照深度优先搜索策略,从根节点出发搜索解空间树。

活结点:自身已生成但其孩子结点没有全部生成的结点
扩展结点:指正在产生孩子结点的结点,E结点
死结点:指其所有结点均已产生的节点

首先根节点成为活结点,同时也成为当前的扩展结点

在当前的扩展结点处,搜索向纵深方向移至一个新结点。这个新结点就成为新的活结点,并成为当前扩展结点。如果在当前扩展结点处不能在向纵深方向移动,则当前扩展结点就成为死结点。此时应往回移动(回溯)至最近的一个活结点处,并使这个结点成为当前可扩展结点。

回溯法:以这种方式递归地在解空间中搜索,直到找到所有要求的解或解空间中已无活结点为止。

当从状态si搜索到s i+1后,s i+1变为死结点,则从状态s i+1回退到si,再从si找其他可能路径

在这里插入图片描述

若用回溯法求问题的所有解,需要回溯到根节点,且根节点的所有可行子树都已被搜索完才结束。若用回溯法求问题的任一解,只要搜索到问题的一个解就可以结束。

由于采用回溯法求解时存在退回到祖先结点的过程,所以需要保存搜索过的结点。通常有两种方法,
其一是用自定义栈保存祖先结点;
其二是采用递归法,因为递归调用会将祖先结点保存到系统栈中,在递归调用返回时自动回退到祖先结点。

另外,用回溯法搜索解空间时通常采用两种策略避免无效搜索,以提高回溯的搜索效率,
法一:用约束函数在扩展结点处减除不满足约束条件的路径;
法二:用限界函数减去得不到的问题解或最优解的路径

上述两种方法统称剪枝函数。

  • 回溯法解题步骤:

1.针对对给定的问题的解空间树,问题的解空间树应至少包含问题的一个解或者最优解。

2.确定结点的扩展搜索规则

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

回溯法的算法框架及其应用

设问题的解是一个n维向量(x1,x2,…,xn),约束条件是Xi满足某种条件,记为constraint(Xi);限界函数是Xi应满足某种条件,记为bound(Xi),回溯法的算法通常分为非递归回溯框架和递归回溯框架

  • 1.非递归回溯框架

i:对应解空间的第i层的某个结点

int x[n];    //x存放解向量,全局变量 
void backtrack(int n){
    
      //非递归框架 
	int i=1;           //根节点层次为1 
	while(i>=1)        //尚未回溯到头 
	{
    
                      
		if(ExistSubnode(t))  //当前结点存在子节点 
		{
    
    
			for(j=下界;j<=上界;j++){
    
      //对于子集数,j从0到1循环 
				x[i]取一个可能的值;
				if(constraint(i)&&bound(i))  //x[i]满足约束条件或界限函数 
				{
    
    
					if(x是一个可行解)
					输出x;
					else
					i++;   //进入下一层次 
				 } 
			}
		}
		else   //不存在子结点,返回上一层,即回溯 
		i--;
	}
}

  • 2.递归回溯框架

回溯法是对解空间的深度优先搜索,因为递归算法中的形参具有自动回退(回溯)的能力,所有许多回溯设计的算法都设计成递归算法,比同样的非递归算法设计起来更加简便。

其中,i为搜索的层次(深度),通常从0或1开始,分两种情况:

(1)解空间为子集树

一般地,解空间为子集树的递归回溯框架如下:

int x[n];    //x存放解向量,全局变量 
void backtrack(int n){
    
      //求解子集树递归框架 
  if(i>n)
    输出结果;
	else
	{
    
    
		for(j=下界;j<=上界;j++)  //用j枚举i的所有可能路径
		{
    
    
			x[i]=j;      //产生一个可能的解分量 
			...          //其他操作 
			if(constraint(i)&&bound(i))
			  backtrack(i+1);   //满足约束条件和限界函数,继续一下层 
		 } 
	   }
	 } 
	

采用上述算法框架需注意以下几点:

(1)i从1开始调用上述回溯算法框架,此时根结点为第1层,叶子结点为第n+1层,当然i也可以从0开始,这样根结点为第0层,叶子结点为第n层,所以需要将上述代码中的

if(i>n)
改为
if(i>=n)

(2)在上述框架中通过for循环使用j枚举i的所有可能路径,如果扩展路径只有两条,可以改为两次递归调用(例:求解0/1背包问题、子集和问题等)

(3)这里回溯框架只有 i 这 一个参数,在实际应用中可以根据情况设置多个参数。

例:在一个含有n个整数的数组a,所有元素均不相同,设计一个算法求其所有子集(幂集)
例如:a[ ]={1,2,3},所有子集是{},{1},{2},{1,2},{3},{1,3},{2,3},{1,2,3}

思路:采用回溯法

问题的解空间为子集树,每个元素只有两种扩展,要么选择,要么不选。

采用深度优先搜索思路,解向量为x[ ],x[i]=0表示不选择a[i]。用i扫描数组a,也就是说问题初始状态是(i=0,x的元素均为0),目标状态是(i=n,x为一个解)。从状态(i,x)可以扩展出两个状态。

(1)不选择a[i]元素——>下一个状态为(i+1,x[i]=0)
(2)选择a[i]元素——>下一个状态为(i+1,x[i]=i)

这里i总是递增的,所以不会出现状态重复的情况。

法一:标准解向量代码:

#include<stdio.h>
#include<string.h>
#define MAXN 100

void dispsolution(int a[],int n,int x[])   //输出一个解
{
    
    
	printf(" { ");
	for(int i=0;i<n;i++)
	if(x[i]==1)
	printf(" %d",a[i]);
	printf("}");
 } 
 void dfs(int a[],int n,int i,int x[])  //用回溯法求解解向量x
 {
    
    
 	if(i>=n)
 	dispsolution(a,n,x);
 	else
 	{
    
    
 		x[i]=0;
		dfs(a,n,i+1,x);    //不选择a[i]
		x[i]=1;
		dfs(a,n,i+1,x);    //选择a[i] 
	 }
  } 
  void main(){
    
    
  	int a[]={
    
    1,2,3};  //s[0 ... n-1]为给定的字符串,设置为全局变量
	  int n=sizeof(a)/sizeof(a[0]);
	  int x[MAXN];       //解向量 
	  memset(x,0,sizeof(x));   //解向量初始化 
	  printf("求解结果\n");
	  dfs(a,n,0,x);
	  printf("\n");
  }

在这里插入图片描述

法二:不采用标准解向量,直接求结果

#include<stdio.h>
#include<vector>
using namespace std;

void dispsolution(vector < int > path)   //输出一个解
{
    
    
	printf("{");
	for(int i=0;i<path.size();i++)
	printf(" %d",path[i]);
	printf("}");
 } 
 void dfs(int a[],int n,int i,vector < int > path)  //用回溯法求子集path
 {
    
    
 	if(i>=n)
 	dispsolution(path);
 	else
 	{
    
    
 		dfs(a,n,i+1,path);  //不选择a[i] 
 		path.push_back(a[i]);  //选择a[i],将a[i]加入path 
 		dfs(a,n,i+1,path);
	 }
  } 
  int main(){
    
    
  	int a[]={
    
    1,2,3};  //s[0 ... n-1]为给定的字符串,设置为全局变量
	int n=sizeof(a)/sizeof(a[0]);
	vector< int > path;
	 printf("求解结果\n");
	  dfs(a,n,0,path);
	  printf("\n");
  }

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/gl620321/article/details/108357191