基于回溯策略的递归
基本思想:在按某种搜索策略的搜索过程中,在某种状态,继续往前搜索已经确定不会得到正确答案的情况下,我们可以返回上一搜索状态,去沿新的可能性继续搜索。要回溯到上一状态,则说明我们在前进中的状态必须保存下来,我们采用“栈”来存放。
它的求解过程实质上是一个先序遍历一棵“状态树”的过程,只不过这棵树不是预先建立的,而是隐含在遍历的过程中。
回溯法的特点
搜索策略:符合递归算法,问题解决可以化为子问题,算法类似,规模减小。
控制策略:当遇到失败的搜索状态,需要返回上一状态,沿另外的路径搜索。
数据结构:用数组保存搜索过程中的状态、路径。
下面介绍一些例子,分析基于回溯策略递归方法的求解方法。
例一、八皇后问题
在8×8的棋盘上,放置8个皇后(棋子),使两两之 间互不攻击。所谓互不攻击是说任何两个皇后都要 满足:
(1)不在棋盘的同一行; (2)不在棋盘的同一列; (3)不在棋盘的同一对角线上。
数据定义:
位置(i,j)对角线计算公式:从上到下Down[i - j + 7],从下到上Up[i + j - 2] 。
实现代码:
void TryQueen(int i, int Queen[], int Column[], int Down[], int Up[]);
void main()
{
int Queen[9] = { 0 }; // 第i行皇后所在的列
int Column[9] = { 0 }; // 第j列是否安全,{0, 1}
int Down[15] = { 0 }; // 记录每一条从上到下的对角线,是否安全{0,1}
int Up[15] = { 0 }; // 记录每一条从下到上的对角线,是否安全{0,1}
TryQueen(1, Queen, Column, Down, Up)
}
void TryQueen(int i, int Queen[], int Column[], int Down[], int Up[]) // 摆放第 i 行的皇后
{
int j, k;
for(j = 1; j <= 8; j++) // 尝试把该皇后放在每一列
{
if(Column[j] || Down[i-j+7] || Up[i+j-2])
continue; // 失败
Queen[i] = j; // 把该皇后放在第j列上
Column[j] = 1;
Down[i-j+7] = 1;
Up[i+j-2] = 1;
if(i == 8) // 已找到一种解决方案
{
for(k = 1; k <= 8; k++)
printf("%d ", Queen[k]);
printf("\n");
}
else
TryQueen(i + 1, Queen, Column, Down, Up); // 摆放第i+1行的皇后
Queen[i] = 0; // 回溯,把该皇后从第j列拿起
Column[j] = 0;
Down[i-j+7] = 0;
Up[i+j-2] = 0;
}
}
例二、爬楼梯问题
爬楼梯时可以1次走1个台阶,也可以1次走2个台阶。对于由n个台阶组成的楼梯,共有多少种不同的走法?在如何编写递归程序(分治法)中用分治法解决了这个问题,现在考虑用回溯法来解决。
#include
void TryStep(int n);
int steps[100];
static int s = 1;
void main()
{
int n;
printf("stairs:");
scanf("%d",&n);
TryStep(n);
}
void TryStep(int n) //爬n个台阶
{
int j, k;
for(j=1; j<=2; j++)//
{
if(n < j) //台阶数小于跨的步数
break;
steps[s++] = j; //一步走j个台阶
n -= j; //台阶数减j
if(n == 0) //一个方案
{
for(k = 1; k
例三、安排问题
如上图所示,A,B,C,D,E可以理解为五个主体对象(人,球队等),1,2,3,4,5可以理解五个应用对象(周一~周五,五种工作任务等),下面以分书来分析这个问题:A,B,C,D,E五个人对1,2,3,4,5五本书的阅读兴趣如上图,1表示喜欢,0表示不喜欢,编写一个程序,设计一个分书的方案,让所有人都能拿到喜欢的书。
定义:
void person(int i, int Like[][6], int BookFlag[], int BookTaken[]);
void main()
{
int Like[6][6] = {{0},
{0, 0, 0, 1, 1, 0},
{0, 1, 1, 0, 0, 1},
{0, 0, 1, 1, 0, 1},
{0, 0, 0, 0, 1, 0},
{0, 0, 1, 0, 0, 1}}; //阅读喜好
int BookFlag[6] = {0}; // 后五个元素记录书是否已分配
int BookTaken[6] = {0}; // 记录每一个人选用了哪一本书
person(1, Like, BookFlag, BookTaken);
}
/*尝试给第 i 个人分书*/
void person(int i, int Like[][6], int BookFlag[], int BookTaken[])
{
int j, k;
for(j = 1; j <= 5; j++) // 尝试把每本书分给第i个人
{
if((BookFlag[j] != 0) || (Like[i][j] == 0)) // 书已被分配或者不喜欢该书
continue;
BookTaken[i] = j; // 把第j本书分给第i个人
BookFlag[j] = 1;
if(i == 5) // 已找到一种分书方案
{
for(k = 1; k <= 5; k++)
printf("%d ", BookTaken[k]);
printf("\n");
}
else
{
person(i + 1, Like, BookFlag, BookTaken); // 给第i+1个人分书
}
BookTaken[i] = 0; // 回溯,把这一次分得的书退回
BookFlag[j] = 0;
}
}
例四、排列问题
n个对象的一个排列,就是把这 n 个不同的对象放在同一行上的一种安排。在如何编写递归程序(分治法)中就是求n!的过程,下面利用回溯法求解这个问题。
基本思路:每一个排列的长度为 N,对这N个不同的位置,按照顺序逐一地枚举所有 可能出现的数字。
定义:
#define N 3
void TryPlace(int i);
int NumFlag[N+1] = {0}; // 记录每一个数字是否已被使用
int PlaceTaken[N+1] = {0}; // 记录每一个位置上使用的是哪一个数字
void main( )
{
TryPlace(1);
}
void TryPlace(int i) // 位置i放置数字
{
int j, k;
for(j = 1; j <= N; j++)
{
if(NumFlag[j] != 0) // 数字j被使用
continue;
PlaceTaken[i] = j; // 位置i放数字 j
NumFlag[j] = 1;
if(i == N) // 找到一个方案
{
for(k = 1; k <= N; k++)
printf("%d ", NumTaken[k]);
printf("\n");
}
else
TryPlace(i + 1); // 位置i + 1放置数字
PlaceTaken[i] = 0; // 回溯
NumFlag[j] = 0;
}
}
总结
基于分治法的递归算法,需要总结出递归形式,通常其递归形式比较复杂;
基于回溯法的递算法,递归形式比较简单,其重点是定义合适的变量,用于标识各种状态,同时在一个循环结束之后要回溯,把当前状态转移到上一个状态。