回溯法讲解与实战训练

1、概念
回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回,尝试别的路径。
回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。
许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。
2、基本思想
在包含问题的所有解的解空间树中,按照深度优先搜索的策略,从根结点出发深度探索解空间树。当探索到某一结点时,要先判断该结点是否包含问题的解,如果包含,就从该结点出发继续探索下去,如果该结点不包含问题的解,则逐层向其祖先结点回溯。(其实回溯法就是对隐式图的深度优先搜索算法)。
若用回溯法求问题的所有解时,要回溯到根,且根结点的所有可行的子树都要已被搜索遍才结束。
而若使用回溯法求任一个解时,只要搜索到问题的一个解就可以结束。
3、用回溯法解题的一般步骤:
(1)针对所给问题,确定问题的解空间:
首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。
(2)确定结点的扩展搜索规则
(3)以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。
4、算法框架
(1)问题框架
设问题的解是一个n维向量(a1,a2,………,an),约束条件是ai(i=1,2,3,…..,n)之间满足某种条件,记为f(ai)。
(3)递归的算法框架
回溯法是对解空间的深度优先搜索,在一般情况下使用递归函数来实现回溯法比较简单,其中i为搜索的深度,框架如下:
1: int a[n];
2: try(int i)
{
if(i>n) 输出结果;
else
{ for(j = 下界; j <= 上界; j=j+1) // 枚举i所有可能的路径
{ if( fun(j) ) // 满足限界函数和约束条件
{ a[i] = j;
… // 其他操作
try(i+1);
回溯前的清理工作(如a[i]置空值等);
}
}
}
}

(2)非递归回溯框架

   1:int a[n],i;
 2: 初始化数组a[];
 3: i = 1;
 4: while (i>0(有路可走)   and  (未达到目标))  // 还未回溯到头
 5: {
 6:     if(i > n)                                              // 搜索到叶结点
 7:     {   
 8:           搜索到一个解,输出;
 9:     }
 10:     else                                                   // 处理第i个元素
 11:     { 
 12:           a[i]第一个可能的值;
 13:           while(a[i]在不满足约束条件且在搜索空间内)
 14:           {
 15:               a[i]下一个可能的值;
 16:           }
 17:           if(a[i]在搜索空间内)
 18:          {
 19:               标识占用的资源;
 20:               i = i+1; // 扩展下一个结点
 21:          }
 22:          else 
 23:         {
 24:               清理所占的状态空间;            // 回溯
 25:               i = i –1; 
 26:          }
 27: }
                   回溯法实战训练

一、递归复习
1、学生年龄
2、1*2*3*…*n
3、fibonacci数列的第n项。(1,1,2,3,5,8,11……)
4、汉诺塔问题

递归三要素: 自己调用自己,调用自已时问题规模会变小,有递归终止条件!!
递归关键技术: 找出递推公式。

【蟠桃记】
Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 14729 Accepted Submission(s): 11299
【Problem Description】
喜欢西游记的同学肯定都知道悟空偷吃蟠桃的故事,你们一定都觉得这猴子太闹腾了,其实你们是有所不知:悟空是在研究一个数学问题!
什么问题?他研究的问题是蟠桃一共有多少个!
不过,到最后,他还是没能解决这个难题,呵呵^-^
当时的情况是这样的:
第一天悟空吃掉桃子总数一半多一个,第二天又将剩下的桃子吃掉一半多一个,以后每天吃掉前一天剩下的一半多一个,到第n天准备吃的时候只剩下一个桃子。
聪明的你,请帮悟空算一下,他第一天开始吃的时候桃子一共有多少个呢?
【Input】
输入数据有多组,每组占一行,包含一个正整数n(1

/*
第一天悟空吃掉桃子总数一半多一个,第二天又将剩下的桃子吃掉一半多一个,
以后每天吃掉前一天剩下的一半多一个,到第n天准备吃的时候只剩下一个桃子。
聪明的你,请帮悟空算一下,他第一天开始吃的时候桃子一共有多少个呢?
【Input】
输入数据有多组,每组占一行,包含一个正整数n(1<n<30),表示只剩下一个桃子的时候是在第n天发生的。
【Output】
对于每组输入数据,输出第一天开始吃的时候桃子的总数,每个测试实例占一行。
【Sample Input】
2
4
【Sample Output】
4
22

处理递归一个非常重要的地方,
就是设置合理的退出递归的条件(递归终止条件),
明显最后一天是一个归触发条件,因为最后一天只有一个,
那么设最后一天: n=1 ,昨天,n=2,前天:n=3,以此类推。
则 f(n)=( f(n-1)+1 )*2 
*/

#include<stdio.h>
int peach(int n)
{
    if(n==1)   return 1;
    else return (peach(n-1)+1)*2;   
}

int main()
{
    int n;
    while(scanf("%d", &n)!=EOF)
        printf("%d\n", peach(n));
    return 0;
}


二、递归回溯

【全排列】
输入整数n, 生成整数1~n的全排列,按字典序输出.(假设n<=10)
【测试数据:】
输入:
3
输出:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

/* 生成整数1~n的全排列,按字典序输出.(假设n<=10) 测试数据: 输入: 3 输出: 1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1 */

#include<stdio.h>

void printf_permutation(int n, int a[], int cur)  //n个数的全排列,存在a中,正要写a[cur]在值 
{                                                  //a[0...cur-1]已经填好。 
    int i,j, ok=1;
    if(cur==n){   //a[]中数据填满,则输出. 
        for(i=0; i<n ;i++)  printf("%d ", a[i]);
        printf("\n");       
    }
    else
        for(i=1; i<=n; i++) //从1~n依次测试它是否能填到a[cur]中 
        {
            ok=1;  //标志变量,表达整数i是否已经在前缀序列中了 
            for(j=0; j<cur; j++)
                if(a[j]==i) ok=0;
            if(ok)    //整数i没有在前缀序列中 
            {
                a[cur]=i;   //把整数i加入前缀序列 
                printf_permutation(n, a, cur+1);    //递归,向后测试,去填a[cur+1]在值 
            }
        }
}
int main()
{
    int n,a[10]={0};  
    scanf("%d",&n);
    printf_permutation(n,a,0);   //递归初始状态, 准备填a[0]的值 
    return 0;
}
/* 生成整数1~n的全排列,按字典序输出.(假设n<=10) 测试数据: 输入: 3 输出: 1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1 */

#include<stdio.h>

int  a[10]={0};  //装答案的数组设置为全局变量

void P(int n,  int cur)  //n个数的全排列,存在a中,正要写a[cur]在值 
{                        //a[0...cur-1]已经填好。 
    int i,j, ok=1;
    if(cur==n){   //a[]中数据填满,则输出. 
        for(i=0; i<n ;i++)  printf("%d ", a[i]);
        printf("\n");       
    }
    else
        for(i=1; i<=n; i++) //从1~n依次测试它是否能填到a[cur]中 
        {
            ok=1;  //标志变量,表达整数i是否已经在前缀序列中了 
            for(j=0; j<cur; j++)
                if(a[j]==i) ok=0;
            if(ok)    //整数i没有在前缀序列中 
            {
                a[cur]=i;   //把整数i加入前缀序列 
                P(n, cur+1);    //递归,向后测试,去填a[cur+1]在值 
            }
        }
}

int main()
{
    int n; 
    scanf("%d",&n);
    P(n,0);   //递归初始状态, 准备填a[0]的值 
    return 0;
}

【求幂集】
求含有n个元素的集合的幂集。(数据结构书,第6.7节 回溯法与树的遍历)
如:A={1,2,3},则A的幂集是:
p(A)={{1,2,3},{1,2},{1,3},{1},{2,3},{2},{3},{空集}};
【测试数据:】
输入:
3
1 2 3
输出:
{ 1 2 3 }  // 左括号后面有一个空格,数据后面有一个空格
{ 1 2 }
{ 1 3}
{ 1 }
{ 2 3 }
{ 2 }
{ 3 }
{ }

/* 求含有n个元素的集合的幂集。(数据结构书,第6.7节 回溯法与树的遍历) 如:A={1,2,3},则A的幂集是: p(A)={{1,2,3},{1,2},{1,3},{1},{2,3},{2},{3},{空集}}; */ 

#include<stdio.h>

void Get_PowerSet( int n, int a[], int cur, int r[])
{   //集合A中n个元素存放在a[0...n-1]中,r[i]记录a[i]是否在子集中,
    // 目前已经确定a[0...cur-1]是否加入,正要决定a[cur]加入否 
    int i;
    if(cur==n){   //已经有n个数确定是否加入 ,则输出该子集
        printf("{ ");
        for(i=0; i<n; i++)
            if(r[i])    printf("%d ", a[i]);
        printf("}");
        printf("\n");
    }
    else{
        r[cur]=1;   //确定a[cur]加入 
            Get_PowerSet(n,a,cur+1,r);  // 确定下一个元素是否加入子集中 
        r[cur]=0;   //确定a[cur]不加入 
            Get_PowerSet(n,a,cur+1,r); 
    }
}

int main()
{
    int n, a[20],r[20]={0}, i;
    scanf("%d", &n);
    for(i=0; i<n; i++)
        scanf("%d", &a[i]);
    Get_PowerSet(n, a, 0, r);  // 递归初始,n个元素的集合a,准备确定第0个元素是否加入 

    return 0;   
}

【素数环】
输入正整数n,把整数1,2,3,…,n组成一个环,使得相邻两个整数之和均为素数。输出时从整数1开始排列。同一个环应恰好输出一次。n<=16 。
【测试数据】
输入:
6
输出:
1 4 3 2 5 6
1 6 5 2 3 4

【注意】如果用枚举法,n=16时,有16!=2*10(13)个排列,会超时。试试回溯法。

/* 【素数环】 输入正整数n,把整数1,2,3,…,n组成一个环,使得相邻两个整数之和均为素数。 输出时从整数1开始排列。同一个环应恰好输出一次。n<=16 。 【测试数据】 输入: 6 输出: 1 4 3 2 5 6 1 6 5 2 3 4 用回溯法解决 */


#include<stdio.h>
#include<math.h>
#include<memory.h>

int IsPrime(int x)   //若x为素数返回1,否则返回0 
{
    int i,k;
    k=sqrt(x);
    for(i=2; i<=k; i++)
        if(x%i==0)  return 0;
    return 1;
} 

void PrimeCircle(int n, int a[], int cur, int used[])
{ 
//a[]装答案数据, cur表示当前正要装a[cur]的数据, 
//used为标志数组, used[i]值为0或1, 表示整数i有没有被用过 
    int i;
    if( cur==n+1 && IsPrime(a[n]+a[0]) )
        {
            for(i=1; i<=n; i++) printf("%d ", a[i]);
            printf("\n");
        }   
    else 
        for(i=2; i<=n; i++)
            if( !used[i] && IsPrime(a[cur-1]+i) )  //整数i没有用过,且和为素数 
            {
                a[cur]=i;     //指定第cur个数值为i 
                used[i]=1;   //标记整数i已经用过 
                PrimeCircle(n, a, cur+1, used);   //继续向后探测 
                used[i]=0;  //准备测试另一个分支(第cur个数不为i为i+1的情况) 
            }
}

 int main()
{
    int a[20]={1,1}, used[20]={0};   //数组0号单元不用。 a[1]第1个数默认为1, 
    int n;
    scanf("%d", &n);
    PrimeCircle(n, a, 2, used); //从第2个位置开始填数 
    return 0;
}

【n皇后问题】 (数据结构书第6.7节 有4皇后问题的例子)
【问题描述:】
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯•贝瑟尔于1848年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。计算机发明后,有多种方法可以解决此问题。
这是来源于国际象棋的一个问题。皇后可以沿着纵横和两条斜线4个方向相互捕捉。求出在一个n×n的棋盘上,放置n个不能互相捕捉的国际象棋“皇后”的所有布局。

/* 八皇后 */
#include<stdio.h>

int A[8][8];
int  hy[8];
int liey[8]; 
int zudjy[2*8-1];
int fudjy[2*8-1];

void Output(int a[][8], int n)
{   
    int i,j;
    printf("\n--------------------------------\n");
    for(i=0; i<n ;i++)
    {   for(j=0; j<n; j++)
            printf("%4d", a[i][j]);
        printf("\n");
    }
}

int Peace(int a[][8], int n)
{   int i;
    for(i=0; i<n; i++)
        if(hy[i]>1||liey[i]>1)  return 0;
    for(i=0; i<2*n-1; i++)
        if(zudjy[i]>1||fudjy[i]>1)  return 0;
    return 1;   
}

void Queen(int i, int n)
{   int j;
    if(i>n)  Output(A,n);
    else{
        for(j=1; j<=n; j++)
        {   
            A[i-1][j-1]=1;  
            hy[i-1]++;   liey[j-1]++;   zudjy[i-j+n-1]++;   fudjy[i+j]++;
            if( Peace(A,n))   Queen(i+1, n);
            A[i-1][j-1]=0;
            hy[i-1]--;   liey[j-1]--;   zudjy[i-j+n-1]--;   fudjy[i+j]--;
        }
    }
}

int main()
{

    Queen(1,8);
    return 0;
}
    原文作者:回溯法
    原文地址: https://blog.csdn.net/privacy_googol/article/details/44961835
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞