第五章——回溯法

本文详细介绍了回溯法的概念、状态空间树、解空间、剪枝函数、递归与非递归回溯框架,并通过实例展示了如何解决0/1背包问题、装载问题、子集和问题、n皇后问题等经典问题。回溯法是一种类似穷举的搜索策略,通过深度优先遍历在状态空间树中寻找解,并利用剪枝函数提高效率。文章最后总结了回溯法的核心思想及其在子集问题和排列问题中的应用。
摘要由CSDN通过智能技术生成

回溯法概述

对于回溯法的概念,我们需要先从回溯法在实际解决问题中的表现来理解。

书上称:回溯法实际上是一个类似穷举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现不满足求解条件时,就“回溯”(即回退),然后尝试别的可能。

就我个人的角度而言,我不认为回溯法是一个类似于穷举的方法,而是针对于一些通过穷举法求解的问题:

问题求解过程中的各个可能出现的状态作为结点,通过决策将两两结点连边构成一棵有向树,存在一个或多个我们想要求解的目标状态。

为了得到我们的目标状态(或者是初始状态到目标状态的路径),我们采取深度优先遍历的策略在状态结点构成的有向树中搜索我们想要的目标状态,深度优先遍历过程中无法向某个分支继续向下搜索而“回退”,这就是基于概念的,简化的,最基础版本的回溯方法。

深度优先遍历中“回退”的过程是最基本的回溯的方法,当无法向下搜索(即当前状态能够进行的决策都不能得到新的状态)之后进行回退。

而我们可以另设计一些函数,让结点能够提前预知到自己在这条分支下搜索无法得到我们想要的目标状态(解),然后提前进行“回退”的操作。

这就是(我个人觉得的)我们一般所说的回溯法的具体内容。

状态空间,状态空间树,可行解,解空间,最优解

各个可能出现的状态的集合构成了我们该问题的状态空间。

状态空间中的各个对应状态的结点通过决策将两两结点连边构成的有向树称为该问题的状态空间树(或者解空间树,我个人认为不能将解空间和状态空间混为一谈)。

状态空间中各个对应状态的结点既然存在于状态空间树中,那它一定满足问题背景的约束条件(反过来成立么?并不是所有满足问题背景约束调教的状态都能与初始状态相通,比如说数字电子技术中移位计数器的状态图)。

而满足我们需要最终需要的解的约束条件的状态称为可行解,例如我们求解8皇后问题,放置了3个棋子且不冲突的棋盘属于1个满足题设条件的状态,但它没有放满8个棋子,所以不属于我们需要的可行解。

在这里插入图片描述

上面的解空间树中,黄色结点和蓝色结点的并集为我们这个问题的状态空间,蓝色结点为我们这个问题的可行解。

(对于状态空间树,可行解一般而言就是状态空间树的叶子结点)。

可行解构成的集合称为该问题的解空间。对于一些问题例如迷宫问题,数独问题,我们只需要求得一个可行解;对于一些问题例如n皇后问题,我们往往被要求统计可行解的个数,或者求出所有的可行解;而对于另一些问题例如蛮力法求解0/1背包问题,我们想要得到所有可行解中在某一方面表现最为突出的解,这个解称为最优解,或者说目标状态(一般认为经典的八数码问题可行解唯一,而不是所有状态都是可行解,目标状态为最优解)。

子集树,排列树

解空间树根据需要解决的问题存在两种类型:

子集树:当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树称为子集树。

排列树:当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。

分别对应子集问题和排列问题,从回溯法解空间树的角度来看,我个人认为区别主要在于一个状态的各个决策和各个阶段对于决策的选择之间是否互相影响(子集树一般不影响,而排列树上一个阶段选择的决策,下一个阶段不能选择,显然是存在影响的)。

剪枝函数

前面提到:

“我们可以另设计一些函数,让结点能够提前预知到自己在这条分支下搜索无法得到我们想要的目标状态(解),然后提前进行“回退”的操作。”

当我们在某节点主动进行“回退”时,我们就不会对以该节点为根节点的子树上的结点进行搜索(即使他们确实都是可行解),相当于将一个子树的分枝进行了剪除。

判断是否进行“回退”的函数我们称为剪枝函数,“回退”不去访问子树结点的操作称为剪枝操作。

其中我们将判断是否满足约束条件而选择是否进行“回退”的剪枝函数称为约束函数,判断是否有机会得到最优解而选择是否进行“回退”的剪枝函数称为限界函数。

我个人认为:后者是回溯法与一般深度优先搜索的本质区别,约束函数在一般深度优先搜索是存在的,例如visited数组保证策略得到的状态满足约束。回溯是指算法进行过程中的回退过程,而回溯法=一般深度优先搜索+限界函数。

事实上限界函数进行的操作往往是在某个状态节点固定后面的决策,得到以该节点为根节点的子树的最左(右)叶子节点(这里的叶子节点不一定是可行解,例如n皇后问题中不能够通过决策向下扩展的非可行解)。

从而确定出该状态节点向下搜索能够搜索到状态节点的数据范围,根据数据范围与当前的最优解进行比较,判断是否有机会得到可行解或最优解,这也就是为什么称为“限界”的原因。

回溯法的算法框架

归纳起来,解题中使用回溯法的一般步骤如下:

1.针对所给问题,设计出该问题的状态空间,以及状态空间中状态结点构成的状态空间树。

2.以深度优先方式搜索解空间树,并在搜索过程中可以采用剪枝函数来避免无效搜索。

非递归回溯框架

这里的要求是得到所有的可行解:

int x[n];//全局变量,用于存放规模为n的可行解 
//非递归回溯框架(输出所有可行解)
void backtrack(int n){
    
	int i=1;
	while (i>=1){
   
		//当前结点存在可能的决策,课件上说这里是存在可能的子状态结点,这不准确
		//因为决策往往是固然存在的,但并不是每一种决策都能够得到一个满足约束条件的后继子状态节点
		if (ExistSubNode(t)){
   
			//选择可能的决策尝试扩展到可能满足约束条件的后继子节点 
			for (j=下界;j<=上界;j++){
   
				x[i]取一个可能的值;
				//满足约束函数的条件和限界函数的条件 
				if (constraint(i)&&bound(i)){
     
					//当扩展到的状态为可行解时,输出可行解 
					if (x是一个可行解) 输出x;
					//否则继续向状态空间树下一层的状态结点进行扩展 
					else i++;
				}
			}
		}
		//进行回溯,回溯的原因可能是不存在满足约束函数的条件和限界函数
		//或者分支上所有的结点访问完毕 
		else i--;			
   }
}

什么时候判断(在从前驱节点尝试扩展进行判断,还是扩展到该状态结点再进行判断)是否为可行解往往根据实际问题的要求而定。

例 5.2

用非递归的回溯框架设计求解上一章节“算式”问题的回溯算法。

这里没用用到限界函数在搜索的过程中剪除无法得到可行解的分支,只在叶子节点的位置判断是否为可行解,所以代码运行实际的过程和蛮力法其实是没有区别的(这也是我为什么一直认为回溯只是蛮力法进行中的一个过程和一个尝试优化的方向的原因)。

(这里并不是无法设计出限界函数,实际上是能够设计出限界函数的,不过实际意义并不大,所以不做赘述)

void fun(){
   	
	//dig[i]表示数字i是否选取,置初值0表示所有数字均没有使用
	bool dig[10]; memset(dig,0,sizeof(dig)); 
	int a,b,c,d,e,m,n,s;
	for (a=1;a<=9;a++){
   	dig[a]=1;//试探兵取值a
		for (b=0;b<=9;b++) if (!dig[b]){
    dig[b]=1;//试探炮取值b				
			for (c=0;c<=9;c++) if (!dig[c]){
    dig[c]=1;//试探马取值c
				for (d=0;d<=9;d++) if (!dig[d]){
    dig[d]=1;//试探卒取值d
					for (e=0;e<=9;e++) if (!dig[e]){
    dig[e]=1;//试探车取值e
						m=a*1000+b*100+c*10+d;
						n=a*1000+b*100+e*10+d;
						s=e*10000+d*1000+c*100+a*10+d;
						//判断是否为可行解,输出可行解 
						if (m+n==s) printf("兵:%d 炮:%d 马:%d 卒:%d 车:%d\n",a,b,c,d,e);
						dig[e]=0;//回溯车的取值
					}
					dig[d]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值