如何编写递归程序(回溯法)
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;
}
}
总结
基于分治法的递归算法,需要总结出递归形式,通常其递归形式比较复杂;
基于回溯法的递算法,递归形式比较简单,其重点是定义合适的变量,用于标识各种状态,同时在一个循环结束之后要回溯,把当前状态转移到上一个状态。
基本思想:在按某种搜索策略的搜索过程中,在某种状态,继续往前搜索已经确定不会得到正确答案的情况下,我们可以返回上一搜索状态,去沿新的可能性继续搜索。要回溯到上一状态,则说明我们在前进中的状态必须保存下来,我们采用“栈”来存放。
它的求解过程实质上是一个先序遍历一棵“状态树”的过程,只不过这棵树不是预先建立的,而是隐含在遍历的过程中。
回溯法的特点
搜索策略:符合递归算法,问题解决可以化为子问题,算法类似,规模减小。
控制策略:当遇到失败的搜索状态,需要返回上一状态,沿另外的路径搜索。
数据结构:用数组保存搜索过程中的状态、路径。
下面介绍一些例子,分析基于回溯策略递归方法的求解方法。
例一、八皇后问题
在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;
}
}
总结
基于分治法的递归算法,需要总结出递归形式,通常其递归形式比较复杂;
基于回溯法的递算法,递归形式比较简单,其重点是定义合适的变量,用于标识各种状态,同时在一个循环结束之后要回溯,把当前状态转移到上一个状态。
相关文章推荐
- 如何组织构建多文件 C 语言程序(二)
- 如何写好 C main 函数
- C#数据结构之顺序表(SeqList)实例详解
- Lua和C语言的交互详解
- Lua教程(七):数据结构详解
- 解析从源码分析常见的基于Array的数据结构动态扩容机制的详解
- C#使用回溯法解决背包问题实例分析
- C#数据结构之队列(Quene)实例详解
- C#数据结构揭秘一
- C#数据结构之单链表(LinkList)实例详解
- C#递归算法之快速排序
- 关于C语言中参数的传值问题
- 简要对比C语言中三个用于退出进程的函数
- 深入C++中API的问题详解
- 基于C语言string函数的详解
- C语言中fchdir()函数和rewinddir()函数的使用详解
- C语言内存对齐实例详解
- C语言编程中统计输入的行数以及单词个数的方法
- C 语言简单加减乘除运算
- C语言自动生成enum值和名字映射代码