算法设计与分析-回溯算法知识总结

目录

思维导图

回溯法的基本步骤

基本思想

递归回溯

 迭代回溯

子集树与 排列树

应用举例


思维导图

回溯法的基本步骤

 一般分为三步

(1)针对所给问题,定义问题的解空间

(2)确定易于搜索的解空间结构

(3)以深度优先搜索的方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索(界函数和约束条件)

基本思想

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

递归回溯

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

其中,形式参数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)即可完成整个回溯搜索过程。


 迭代回溯

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

 

子集树与 排列树

当所给的问题是从n个元素的集合S中找出满足某种性质的子集时,相应的解空间树称为子集树。例如,n个物品的0-1背包问题所相应的解空间树就是一棵子集树。这类子集树通常有2^n个叶结点,其结点总个数为2^(n+1)-1。遍历子集树的任何算法均需O(2^n)的计算时间。
当所给的问题是确定n个元素满足某种性质的排列时,相应的解空间树称为排列树。排列树通常有n!个叶结点。因此遍历排列树需要O(n!)的计算时间。例如旅行售货员问题的解空间树就是一棵排列树。

 

 

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

应用举例

这里列举几个例子理解一下回溯算法

1.n后问题

(1)问题描述

在n*n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于,在n*n格的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上。

(2)算法设计思想

需要借助全局变量记录此时第i个皇后在第几列,和一个记录解的个数。从第一个皇后(k=1)开始,如果k>n说明到叶节点即找到了一个可行解,输出,然后回退继续找。如果k<n,从第一列开始判断这个皇后放的位置合不合适,如果都不合适,剪枝,回退;如果合适,继续往下找。如果回退到根节点,即退出程序,算法结束。

回溯算法代码描述可如下:

bool check(int k){
	for(int i=1;i<k;i++){
		if((abs(k-i)==abs(p[k]-p[i]))||p[k]==p[i])
		return false;
	}
	return true;
}
void find(int k){
	if(k>n){
		sum++;
	   	printanswer();
	}
	else{
		for(int i=1;i<=n;i++){
			p[k]=i;
			if(check(k)){
				find(k+1);
			}
		}
	}
}

解决n后问题的非递归迭代回溯法如下:

bool check(int k){
	for(int i=1;i<k;i++){
		if((abs(k-i)==abs(p[k]-p[i]))||p[k]==p[i])
		return false;
	}
	return true;
}
void find(int k){
	p[k]=0;
	while(k>0){
		p[k]+=1;//从第一列开始找 
		while(p[k]<=n&&(!check(k)))//剪枝 
           p[k]+=1;
		 if(p[k]<=n){
		 	if(k==n){//找到可行解,输出 
		 		sum++;
		 		printanswer();
			 }
			 else{
			 	k++;
			 	p[k]=0;
			 }
		 }	
		 else
		 k--;	//不满足,返回上一层 
	}
}

2.0-1背包问题

(1)算法描述:

首先将输入的数据按性价比进行排序(从小到大,按单位重量),这里利用结构体(结构体里有序号,价值,重量,单位价值)在排序的时候比较好排。从第一个物品(a=1)开始放,如果a>n即找到一条分支的叶节点,取到最优价值,然后把此时记录的最优方案输出(用一个数组,装入背包此时对应的点为1)。如果a<n,则判断装入此时的物品背包是否在承重范围内,在的话标记为1,临时记录的价值和重量加上此物品的价值和重量,继续往后找。如果重量超背包承重,回退,考虑右子树,如果后面的预算比当前记录的最优价值还大,可以继续,如果小,剪枝,再往后找。

(2)回溯算法可描述如下:(核心代码—)

bool bound(int i){
	int w=bw;
	int v=bv;
	while(w+s[i].wi<=weight&&i<=n){
		w+=s[i].wi;
		v+=s[i].vi;
		i++;
	}
	if(i<=n){
		v+=(weight-w)*s[i].wi;
	}
	if(v>bestv){
		return true;
	}
	return false;
}
void find(int a){
   if(a>n){
   	bestv=bv;
   	for(int i=1;i<=n;i++){//找到最优方案,存入装入方案 
   		b[i]=x[i];
	   }
	   print();
	   return;
   }
   if(bw+s[a].wi<=weight){
   	x[s[a].id]=1;
   	bv+=s[a].vi;
   	bw+=s[a].wi;
   	find(a+1);
   	bv-=s[a].vi;
   	bw-=s[a].wi;
   	x[s[a].id]=0;
   }
   if(bound(a+1)){
   	x[s[a].id]=0;
   	find(a+1);
   }
}

3.图的m着色问题

(1)问题描述

给定无向连通图G和m种不同的颜色。用这些颜色为图G的各顶点着色,每个顶点着一种颜色。是否有一种着色法,使G中每条边的2个顶点着有不同颜色?这个问题是图的m可着色判定问题。若一个图最少需要m种颜色才能使图中每条边连接的2个顶点着不同颜色,则称这个数m为该图的色数。求一个图的色数m的问题称为图的m可着色优化问题。
(2)算法设计

算法设计思想:利用回溯法,如果当前节点k>n说明到了叶节点,即可以输出一种解决方案,如果不是,则在非叶节点。此时利用记录数组p[k]来记录k点着色,判断是否符合条件,即判断与此点相邻的边得点的着色是否和本点的着色相同,相同话剪枝,不相同则继续往后找。

关键代码如下

bool bound(int x){
	for(int i=1;i<=n;i++){
		if(graph[i][x]==1&&p[i]==p[x])
		//如果两个边相连并且颜色一样 
		return false;
	}
	return true;
}
void find(int x){
	if(x>n){
		sum++;
		print();
	}
	else{
		for(int i=1;i<=m;i++){
			p[x]=i;
			if(bound(x)){
				find(x+1);
			}
			p[x]=0;
		}
	}
}

4.旅行售货员问题(TSP)

(1)算法描述

算法设计思想:

从第一个城市出发,即直接从找第二个城市开始,利用回溯法的排列树。刚开始利用记录当前最优的数组记录每一个城市的编号,如果此时k==n,说明找到了叶子节点,如果此时记录的x[n]城市与1号城市相连并且成本比当前记录的最优值小,则更新最优解,如果k<n,从k开始往后找,如果此时x[k]与x[k-1]所记录的城市相连并且成本想加比记录的当前最优小,记录,往后找,否则剪枝并且在当前成本减去刚才加入的,x[k]恢复原值。

关键代码如下:

void find(int k){
	if(k==n){//走到最后一个城市 
		if(a[x[n]][1]!=0&&(now+a[x[n]][1]<bst)){
		//如果第n个城市和第一个城市想通并且成本小于此时存的最优成本 
			bst=now+a[x[n]][1];
			for(int i=1;i<=n;i++){
				b[i]=x[i];//记录最优 
			}
		}
	}
	else{
		for(int i=k;i<=n;i++){//从本城市开始 
			if(a[x[k-1]][x[i]]&&now+a[x[k-1]][x[i]]<bst){ 
			//如果和上一个记录的城市相连并且成本小于当前记录的最优成本 
				swap(x[k],x[i]);
				now+=a[x[k-1]][x[k]];
				find(k+1);
				now-=a[x[k-1]][x[k]];
				swap(x[k],x[i]);
			}
		}
	}
}



 

  • 0
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值