在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点(开始结点)出发搜索解空间树。
回溯法搜索解空间时,通常采用两种策略避免无效搜索,提高回溯的搜索效率:
用约束函数在扩展结点处剪除不满足约束的子树;
用限界函数剪去得不到问题解或最优解的子树。
这两类函数统称为剪枝函数。
总结:
回溯法 = 深度优先搜索 + 剪枝
①确定问题的解空间树,问题的解空间树应至少包含问题的一个(最优)解。
②确定结点的扩展规则。
③以深度优先方式搜索解空间树,并在搜索过程中可以采用剪枝函数来避免无效搜索。
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--; //回溯:不存在子结点,返回上一层
}
}
int x[n]; //x存放解向量,全局变量
void backtrack(int i) //求解子集树的递归框架
{ if(i>n) //搜索到叶子结点,输出一个可行解
输出结果;
else
{ for (j=下界;j<=上界;j++) //用j枚举i所有可能的路径
{ x[i]=j; //产生一个可能的解分量
… //其他操作
if (constraint(i) && bound(i))
backtrack(i+1); //满足约束条件和限界函数,继续下一层
}
}
}
【例】有一个含n个整数的数组a,所有元素均不相同,设计一个算法求其所有子集(幂集)。
例如,a[]={1,2,3},所有子集是:{},{3},{2},{2,3},{1},{1,3},{1,2},{1,2,3}(输出顺序无关)。
分析:
显然本问题的解空间为子集树,每个元素只有两种扩展,要么选择,要么不选择。
采用深度优先搜索思路。解向量为**x[],x[i]=0表示不选择a[i],x[i]=1表示选择a[i]。
用i扫描数组a,也就是说问题的初始状态是(i=0,x的元素均为0),目标状态是(i=n,x为一个解)。从状态(i,x)可以扩展出两个状态:
不选择a[i]元素 下一个状态为(i+1,x[i]=0)。
选择a[i]元素 下一个状态为(i+1,x[i]=1)。
代码如下:
#include
#include
#define MAXN 100
void dispasolution(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[]) //回溯算法
{
if (i>=n)
dispasolution(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]
}
}
int 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");
}
int x[n]; //x存放解向量,并初始化
void backtrack(int i) //求解排列树的递归框架
{ if(i>n) //搜索到叶子结点,输出一个可行解
输出结果;
else
{ for (j=i;j<=n;j++) //用j枚举i所有可能的路径
{ … //第i层的结点选择x[j]的操作
swap(x[i],x[j]); //为保证排列中每个元素不同,通过交换来实现
if (constraint(i) && bound(i))
backtrack(i+1); //满足约束条件和限界函数,进入下一层
swap(x[i],x[j]); //恢复状态
… //第i层的结点选择x[j]的恢复操作
}
}
}
【例】有一个含n个整数的数组a,所有元素均不相同,求其所有元素的全排列。
例如,a[]={1,2,3},得到结果是(1,2,3)、(1,3,2)、(2,3,1)、(2,1,3)、(3,1,2)、(3,2,1)。
代码如下:
//例5.5的算法
#include
void swap(int &x,int &y) //交换x、y
{ int tmp=x;
x=y; y=tmp;
}
void dispasolution(int a[],int n) //输出一个解
{
printf(" (");
for (int i=0;i<n-1;i++)
printf("%d,",a[i]);
printf("%d)",a[n-1]);
}
void dfs(int a[],int n,int i) //求a[0..n-1]的全排列
{
if (i>=n) //递归出口
dispasolution(a,n);
else
{ for (int j=i;j<n;j++)
{ swap(a[i],a[j]); //交换a[i]与a[j]
dfs(a,n,i+1);
swap(a[i],a[j]); //交换a[i]与a[j]:恢复
}
}
}
int main()
{
int a[]={1,2,3,4};
int n=sizeof(a)/sizeof(a[0]);
printf("a的全排列\n");
dfs(a,n,0);
printf("\n");
}
1、两者的相同点:
回溯法在实现上也是遵循深度优先的,即一步一步往前探索,而不像广度优先遍历那样,由近及远一片一片地搜索。
2、两者的不同点:
(1)访问序不同:深度优先遍历目的是“遍历”,本质是无序的。而回溯法目的是“求解过程”,本质是有序的。
(2)访问次数的不同:深度优先遍历对已经访问过的顶点不再访问,所有顶点仅访问一次。而回溯法中已经访问过的顶点可能再次访问。
(3)剪枝的不同:深度优先遍历不含剪枝,而很多回溯算法采用剪枝条件剪除不必要的分枝以提高效能。
【问题描述】有n个集装箱要装上一艘载重量为W的轮船,其中集装箱i(1≤i≤n)的重量为wi。不考虑集装箱的体积限制,现要从这些集装箱中选出重量和小于等于W并且尽可能大的若干装上轮船。
例如,n=5,W=10,w={5,2,6,4,3}时,其最佳装载方案是(1,1,0,0,1)或者(0,0,1,1,0),maxw=10。
【问题求解】采用带剪枝的回溯法求解。问题的表示如下:
int w[]={0,5,2,6,4,3}; //各集装箱重量,不用下标0的元素
int n=5,W=10;
求解的结果表示如下:
int maxw=0; //存放最优解的总重量
int x[MAXN]; //存放最优解向量
将上述数据设计为全局变量。
求解算法如下:
void dfs(int i,int tw,int rw,int op[])
其中参数i表示考虑的集装箱i,tw表示选择的集装箱重量和,rw表示剩余集装箱的重量和(初始时为全部集装箱重量和),op表示一个解,即对应一个装载方案。
代码如下:
#include
#include
#define MAXN 20 //最多集装箱个数
int w[]={0,5,2,6,4,3}; //各集装箱重量,不用下标0的元素
int n=5,W=10;
int maxw; //存放最优解的总重量
int x[MAXN]; //存放最优解向量
int minnum=999999; //存放最优解的集装箱个数,初值为最大值
void dfs(int num,int tw,int rw,int op[],int i) //考虑第i个集装箱
{
if (i>n) //找到一个叶子结点
{
if (tw==W && num<minnum)
{ maxw=tw; //找到一个满足条件的更优解,保存它
minnum=num;
for (int j=1;j<=n;j++) //复制最优解
x[j]=op[j];
}
}
else //尚未找完所有集装箱
{ op[i]=1; //选取第i个集装箱
if (tw+w[i]<=W) //左孩子结点剪枝:装载满足条件的集装箱
dfs(num+1,tw+w[i],rw-w[i],op,i+1);
op[i]=0; //不选取第i个集装箱,回溯
if (tw+rw>W) //右孩子结点剪枝
dfs(num,tw,rw-w[i],op,i+1);
}
}
void dispasolution(int n) //输出一个解
{
for (int i=1;i<=n;i++)
if (x[i]==1)
printf(" 选取第%d个集装箱\n",i);
printf("总重量=%d\n",maxw);
}
int main()
{
int op[MAXN]; //存放临时解
memset(op,0,sizeof(op));
int rw=0;
for (int i=1;i<=n;i++)
rw+=w[i];
dfs(0,0,rw,op,1);
printf("最优方案\n");
dispasolution(n);
}
【问题描述】有一批共n个集装箱要装上两艘载重量分别为c1和c2的轮船,其中集装箱i的重量为wi,且w1+w2+…+wn≤c1+c2。
装载问题要求确定是否有一个合理的装载方案可将这些集装箱装上这两艘轮船。如果有,找出一种装载方案。
例如:
【问题求解】如果一个给定的复杂装载问题有解,则可以采用如下方式得到一个装载方案:
首先将第一艘轮船尽可能装满,然后将剩余的集装箱装在第二艘轮船上。
算法思路(输入为w1,w2,…,wn,c1,c2):
(1)将尽可能多的集装箱装到第一艘轮船上,得到解向量x。
(2)累计第一艘轮船装完后剩余的集装箱重量sum。
(3)若sum<=c2,表示第二艘轮船可以装完,返回true;否则表示第二艘轮船不能装完,返回false。
代码如下:
#include
#include
#define MAXN 20 //最多集装箱个数
int w[]={0,10,40,40}; //各集装箱重量,不用下标0的元素
int n=3;
int c1=50,c2=50;
int maxw=0; //存放第一艘轮船最优解的总重量
int x[MAXN]; //存放第一艘轮船最优解向量
void dfs(int tw,int rw,int op[],int i) //求第一艘轮船的最优解
{
if (i>n) //找到一个叶子结点
{
if (tw<=c1 && tw>maxw)
{
maxw=tw; //找到一个满足条件的更优解,保存它
for (int j=1;j<=n;j++) //复制最优解
x[j]=op[j];
}
}
else //尚未找完所有集装箱
{ op[i]=1; //选取第i个集装箱
if (tw+w[i]<=c1) //左孩子结点剪枝:装载满足条件的集装箱
dfs(tw+w[i],rw-w[i],op,i+1);
op[i]=0; //不选取第i个集装箱,回溯
if (tw+rw>c1) //右孩子结点剪枝
dfs(tw,rw-w[i],op,i+1);
}
}
void dispasolution(int n) //输出一个解
{
for (int j=1;j<=n;j++)
if (x[j]==1)
printf("\t将第%d个集装箱装上第一艘轮船\n",j);
else
printf("\t将第%d个集装箱装上第二艘轮船\n",j);
}
bool solve() //求解复杂装载问题
{
int sum=0; //累计第一艘轮船装完后剩余的集装箱重量
for (int j=1;j<=n;j++)
if (x[j]==0)
sum+=w[j];
if (sum<=c2) //第二艘轮船可以装完
return true;
else //第二艘轮船不能装完
return false;
}
int main()
{
int op[MAXN]; //存放临时解
memset(op,0,sizeof(op));
int rw=0;
for (int i=1;i<=n;i++)
rw+=w[i];
dfs(0,rw,op,1); //求第一艘轮船的最优解
printf("求解结果\n");
if (solve()) //输出结果
{
printf(" 最优方案\n");
dispasolution(n);
}
else
printf(" 没有合适的装载方案\n");
}
【问题描述】给定无向连通图G和m种不同的颜色。用这些颜色为图G的各顶点着色,每个顶点着一种颜色。如果有一种着色法使G中每条边的两个顶点着不同颜色,则称这个图是m可着色的。图的m着色问题是对于给定图G和m种颜色,找出所有不同的着色法。
【输入格式】第1行有3个正整数n、k和m,表示给定的图G有n个顶点和k条边,m种颜色。顶点编号为1,2,…,n。接下来的k行中,每行有两个正整数u、v,表示图G的一条边(u,v)。
【输出格式】程序运行结束时,将计算出的不同的着色方案数输出。如果不能着色,程序输出-1。
【输入样例】
5 8 4
1 2
1 3
1 4
2 3
2 4
2 5
3 4
4 5
【输出样例】
48
【问题求解】对于图G,采用邻接矩阵a存储,根据求解问题需要,这里a为一个二维数组(下标0不用),当顶点i与顶点j有边时,置ai=1,其他情况置ai=0。
图中的顶点编号为1~n,着色编号为1~m。对于图G中的每一个顶点,可能的着色为1~m,所以对应的解空间是一棵m叉树,高度为n,层次i从1开始。
代码如下:
#include
#include
#define MAXN 20 //图最多的顶点个数
int n,k,m;
int a[MAXN][MAXN];
int count=0; //全局变量,累计解个数
int x[MAXN]; //全局变量,x[i]表示顶点i的着色
bool Same(int i) //判断顶点i是否与相邻顶点存在相同的着色
{
for (int j=1;j<=n;j++)
if (a[i][j]==1 && x[i]==x[j])
return false;
return true;
}
void dfs(int i) //求解图的m着色问题
{
if (i>n) //达到叶子结点
count++; //着色方案数增1
else
{
for (int j=1;j<=m;j++) //试探每一种着色
{
x[i]=j;
if (Same(i)) //可以着色j,进入下一个顶点着色
dfs(i+1);
x[i]=0; //回溯
}
}
}
int main()
{
memset(a,0,sizeof(a)); //a初始化
memset(x,0,sizeof(x)); //x初始化
int x,y;
scanf("%d%d%d",&n,&k,&m); //输入n,k,m
for (int j=1;j<=k;j++)
{
scanf("%d%d",&x,&y); //输入一条边的两个顶点
a[x][y]=1; //无向图的边对称
a[y][x]=1;
}
dfs(1); //从顶点1开始搜索
if (count>0) //输出结果
printf("%d\n",count);
else
printf("-1\n");
return 0;
}
【问题描述】假设有一个需要使用某一资源的n个活动所组成的集合S,S={1,…,n}。该资源任何时刻只能被一个活动所占用,活动i有一个开始时间bi和结束时间ei(bi 一旦某个活动开始执行,中间不能被打断,直到其执行完毕。若活动i和活动j有bi≥ej或bj≥ei,则称这两个活动兼容。 设计算法求一种最优活动安排方案,使得所有安排的活动个数最多。 【问题求解】 调度方案(一种排列):x**[1],x[2],…,x[n]** 第1步选择活动x[1] … 第i步选择活动x[i] … 第n步选择活动x[n] 求解过程 代码如下:
活动编号i
1
2
3
4
开始时间bi
1
2
4
6
结束时间ei
3
5
8
10
#include