一、概念
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
二、基本思想
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
三、用回溯法解题的一般步骤:
(1)针对所给问题,确定问题的解空间:
首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。
(2)确定结点的扩展搜索规则
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
四、经典问题
1. 0-1背包问题
问题:给定n种物品和一背包。物品i的重量是wi,其价值为pi,背包的容量为C。问应如何选择装入背包的物品,使得装入背包中物品的总价值最大?
分析:问题是n个物品中选择部分物品,可知,问题的解空间是子集树。比如物品数目n=3时,其解空间树如下图,边为1代表选择该物品,边为0代表不选择该物品。使用x[i]表示物品i是否放入背包,x[i]=0表示不放,x[i]=1表示放入。回溯搜索过程,如果来到了叶子节点,表示一条搜索路径结束,如果该路径上存在更优的解,则保存下来。如果不是叶子节点,是中点的节点(如B),就遍历其子节点(D和E),如果子节点满足剪枝条件,就继续回溯搜索子节点。
#include <stdio.h>
#define N 3 //物品的数量
#define C 16 //背包的容量
int w[N] = { 10,8,5 }; //每个物品的重量
int v[N] = { 5,4,1 }; //每个物品的价值
int x[N] = { 0,0,0 }; //x[i]=1代表物品i放入背包,0代表不放入
int CurWeight = 0; //当前放入背包的物品总重量
int CurValue = 0; //当前放入背包的物品总价值
int BestValue = 0; //最优值;当前的最大价值,初始化为0
int BestX[N]; //最优解;BestX[i]=1代表物品i放入背包,0代表不放入
//t = 0 to N-1
void backtrack(int t)
{
//叶子节点,输出结果
if (t > N - 1)
{
//如果找到了一个更优的解
if (CurValue > BestValue)
{
//保存更优的值和解
BestValue = CurValue;
for (int i = 0; i < N; ++i) BestX[i] = x[i];
}
}
else
{
//遍历当前节点的子节点:0 不放入背包,1放入背包
for (int i = 0; i <= 1; ++i)
{
x[t] = i;
if (i == 0) //不放入背包
{
backtrack(t + 1);
}
else //放入背包
{
//约束条件:放的下
if ((CurWeight + w[t]) <= C)
{
CurWeight += w[t];
CurValue += v[t];
backtrack(t + 1);
CurWeight -= w[t];
CurValue -= v[t];
}
}
}
//PS:上述代码为了更符合递归回溯的范式,并不够简洁
}
}
int main(int argc, char* argv[])
{
backtrack(0);
printf("最优值:%d\n", BestValue);
for (int i = 0; i < N; i++)
{
printf("最优解:%-3d", BestX[i]);
}
return 0;
}
2.旅行售货员问题
某售货员要到若干城市去推销商品,已知各城市之间的路程(或旅费)。他要选定一条从驻地出发,经过每个城市一次,最后回到驻地的路线,使总的路程(或总旅费)最小。
如下图:1,2,3,4 四个城市及其路线费用图,任意两个城市之间不一定都有路可达。
/****************************************************************
*问 题:旅行售货员
*算 法:回溯法
*描 述:解空间为 排列树
****************************************************************/
#include <stdio.h>
#define N 4 //城市数目
#define NO_PATH -1 //没有通路
#define MAX_WEIGHT 4000
int City_Graph[N + 1][N + 1]; //保存图信息
int x[N + 1]; //x[i]保存第i步遍历的城市
int isIn[N + 1]; //保存 城市i是否已经加入路径
int bestw; //最优路径总权值
int cw; //当前路径总权值
int bestx[N + 1]; //最优路径
//-----------------------------------------------------------------
void Travel_Backtrack(int t) { //递归法
int i, j;
if (t > N) { //走完了,输出结果
for (i = 1; i <= N; i++) //输出当前的路径
printf("%d ", x[i]);
printf("/n");
if (cw < bestw) { //判断当前路径是否是更优解
for (i = 1; i <= N; i++) {
bestx[i] = x[i];
}
bestw = cw;
}
return;
}
else {
for (j = 1; j <= N; j++) { //找到第t步能走的城市
if (City_Graph[x[t - 1]][j] != NO_PATH && !isIn[j]) { //能到而且没有加入到路径中
isIn[j] = 1;
x[t] = j;
cw += City_Graph[x[t - 1]][j];
Travel_Backtrack(t + 1);
isIn[j] = 0;
x[t] = 0;
cw -= City_Graph[x[t - 1]][j];
}
}
}
}
void main() {
int i;
City_Graph[1][1] = NO_PATH;
City_Graph[1][2] = 30;
City_Graph[1][3] = 6;
City_Graph[1][4] = 4;
City_Graph[2][1] = 30;
City_Graph[2][2] = NO_PATH;
City_Graph[2][3] = 5;
City_Graph[2][4] = 10;
City_Graph[3][1] = 6;
City_Graph[3][2] = 5;
City_Graph[3][3] = NO_PATH;
City_Graph[3][4] = 20;
City_Graph[4][1] = 4;
City_Graph[4][2] = 10;
City_Graph[4][3] = 20;
City_Graph[4][4] = NO_PATH;
//测试递归法
for (i = 1; i <= N; i++) {
x[i] = 0; //表示第i步还没有解
bestx[i] = 0; //还没有最优解
isIn[i] = 0; //表示第i个城市还没有加入到路径中
}
x[1] = 1; //第一步 走城市1
isIn[1] = 1; //第一个城市 加入路径
bestw = MAX_WEIGHT;
cw = 0;
Travel_Backtrack(2); //从第二步开始选择城市
printf("最优值为%d/n", bestw);
printf("最优解为:/n");
for (i = 1; i <= N; i++) {
printf("%d ", bestx[i]);
}
printf("/n");
}