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

基于回溯策略的递归

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

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

回溯法的特点

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

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

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

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

例一、八皇后问题

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

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

数据定义:

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

如何编写递归程序(回溯法)_第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 
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

例三、安排问题

如何编写递归程序(回溯法)_第2张图片

如上图所示,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;
    }
}

总结

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

基于回溯法的递算法,递归形式比较简单,其重点是定义合适的变量,用于标识各种状态,同时在一个循环结束之后要回溯,把当前状态转移到上一个状态。

你可能感兴趣的:(数据结构与算法)