您的位置:首页 > 理论基础 > 数据结构算法

如何编写递归程序(回溯法)

2016-07-26 19:30 330 查看
基于回溯策略的递归

基本思想:在按某种搜索策略的搜索过程中,在某种状态,继续往前搜索已经确定不会得到正确答案的情况下,我们可以返回上一搜索状态,去沿新的可能性继续搜索。要回溯到上一状态,则说明我们在前进中的状态必须保存下来,我们采用“栈”来存放。

它的求解过程实质上是一个先序遍历一棵“状态树”的过程,只不过这棵树不是预先建立的,而是隐含在遍历的过程中。

回溯法的特点

搜索策略:符合递归算法,问题解决可以化为子问题,算法类似,规模减小。

控制策略:当遇到失败的搜索状态,需要返回上一状态,沿另外的路径搜索。

数据结构:用数组保存搜索过程中的状态、路径。

下面介绍一些例子,分析基于回溯策略递归方法的求解方法。

例一、八皇后问题

在8×8的棋盘上,放置8个皇后(棋子),使两两之 间互不攻击。所谓互不攻击是说任何两个皇后都要 满足:

(1)不在棋盘的同一行; (2)不在棋盘的同一列; (3)不在棋盘的同一对角线上。

数据定义:

Queen[i] —— 第i行皇后所在的列;
Column[j]—— 第j列是否安全,{0, 1};
Down[1..15 ]——记录每一条从上到下的对角线,是否安全,{0,1}
Up[1..15]——记录每一条从下到上的对角角线,是否安全,{0,1}



位置(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 <stdio.h>
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<s; k++)
printf("step%d:%d\t",k,steps[k]);
printf("\n");
}
else
TryStep(n); // 新的n个台阶走法
n += j; // 回溯
steps[s--] = 0;
}
}

例三、安排问题





如上图所示,A,B,C,D,E可以理解为五个主体对象(人,球队等),1,2,3,4,5可以理解五个应用对象(周一~周五,五种工作任务等),下面以分书来分析这个问题:A,B,C,D,E五个人对1,2,3,4,5五本书的阅读兴趣如上图,1表示喜欢,0表示不喜欢,编写一个程序,设计一个分书的方案,让所有人都能拿到喜欢的书。
定义:
BookFlag[6]----后五个元素记录五本书是否已分配
BookTaken[6]----后五个元素记录每一个人选用了哪一本书  
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个不同的位置,按照顺序逐一地枚举所有 可能出现的数字。

定义:

NumFlag[N+1]----用来记录1-N之间的每一个数字是否已被使用,1表示已使用,0表示尚未被使用;
NumTaken[N+1]----用来记录每一个位置上使用的是哪一个数字,0表示未放置数字。
#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;
}
}

总结

基于分治法的递归算法,需要总结出递归形式,通常其递归形式比较复杂;

基于回溯法的递算法,递归形式比较简单,其重点是定义合适的变量,用于标识各种状态,同时在一个循环结束之后要回溯,把当前状态转移到上一个状态。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息