回溯法:
- 思想:走不通退回走别的路
在包含问题的所有解的空间树中,按照深度优先搜索策略,从根节点出发搜索解空间树。
活结点:自身已生成但其孩子结点没有全部生成的结点
扩展结点:指正在产生孩子结点的结点,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");
}