递归的应用-回溯

【转自《王道上机指南》】
题目1459:Prime ring problem
题目描述:
A ring is compose of n circles as shown in diagram. Put natural number 1, 2, …, n into each circle separately, and the sum of numbers in two adjacent circles should be a prime.
Note: the number of first circle should always be 1.
输入:
n (1 < n < 17).
输出:
The output format is shown as sample below. Each row represents a series of circle numbers in the ring beginning from 1 clockwisely and anticlockwisely. The order of numbers must satisfy the above requirements. Print solutions in lexicographical order.
You are to write a program that completes above process.
Print a blank line after each case.

样例输入:

6
8

样例输出:

 Case 1:
  1 4 3 2 5 6
  1 6 5 2 3 4
  Case 2:
  1 2 3 8 5 6 7 4
  1 2 5 8 3 4 7 6
  1 4 7 6 5 8 3 2
  1 6 7 4 3 8 5 2

题目大意为由给定的 1 到 n 数字中,将数字依次填入环中,使得环中任意两个相邻的数字间的和为素数。对于给定的 n,按字典序由小到大输出所有符合条件的解(第一个数恒定为 1)。这就是有名的素数环问题。
为了解决该问题,我们可以采用回溯法枚举每一个值。当第一个数位为 1确定时,我们尝试放入第二个数,使其和 1 的和为素数,放入后再尝试放入第三个数,使其与第二个数的和为素数,直到所有的数全部被放入环中,且最后一个数与 1 的和也是素数,那么这个方案即为答案,输出;若在尝试放数的过程中,发现当前位置无论放置任何之前未被使用的数均不可能满足条件,那么我们回溯改变其上一个数,直到产生我们所需要的答案,或者确实不再存在更多的解。为了实现这一回溯枚举的过程,我们采用递归的形式:

#include <stdio.h>
#include <string.h>
int ans[22];       /* 保存环中每一个被放入的数 */
bool    hash[22];      /* 标记之前已经被放入环中的数 */
int n;
int prime[] = { 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41 };   /*素数,若需判断一个数是否为素数则在其之中查找,因为输入不大于16,故两数和构成的素数必在该数组内 */
bool judge( int x )    /* 判断一个数是否为素数 */
{
    for ( int i = 0; i < 13; i++ )
    {
        if ( prime[i] == x )
            return(true);  /* 在素数数组中查找,若查找成功则该数为素数 */
    }
    return(false);                 /* 否则不是素数 */
}


void check()   /* 检查输出由回溯法枚举得到的解 */
{
    if ( judge( ans[n] + ans[1] ) == false )
        return; /* 判断最后一个数与第一个数的和是否为素数,若不是则直接返回 */
    for ( int i = 1; i <= n; i++ )/* 输出解,注意最后一个数字后没有空格 */
    {
        if ( i != 1 )
            printf( " " );
        printf( "%d", ans[i] );
    }
    printf( "\n" );
}


void DFS( int num )/* 递归枚举,num为当前已经放入环中的数字 */
{
    if ( num > 1 )/* 当放入的数字大于一个时 */
        if ( judge( ans[num] + ans[num - 1] ) == false )
            return;
 /* 判断最后两个数字的和是否为素数,若不是则返回继续枚举第num个数 */
    if ( num == n )    /* 若已经放入了n个数 */
    {
        check();    /* 检查输出 */
        return;   /* 返回,继续枚举下一组解 */
    }
    for ( int i = 2; i <= n; i++ )/* 放入一个数 */
    {
        if ( hash[i] == false )  /* 若i还没有被放入环中 */
        {
            hash[i]     = true; /* 标记i为已经使用 */
            ans[num + 1]    = i;    /* 将这个数字放入ans数组中 */
            DFS( num + 1 );         /* 继续尝试放入下一个数 */
            hash[i] = false;        /* 当回溯回枚举该位数字时,将i重新标记为未使用 */
        }
    }
}


int main()
{
    int cas = 0;                                    /* 记录Case数 */
    while ( scanf( "%d", &n ) != EOF )
    {
        cas++;                                  /* Case数递增 */
        for ( int i = 0; i < 22; i++ )
            hash[i] = false;                /* 初始化标记所有数字为未 */
        被 使 用
             ans[1] = 1;                        /* 第一个数字恒定为1 */
             printf( "Case %d:\n", cas );       /* 输出Case数 */
             hash[1] = true;                    /* 标记1被使用 */
             DFS( 1 );                          /* 继续尝试放入下一个数字 */
             printf( "\n" );                    /* 输出换行 */
             }
             return(0);
    }
}

读者在理解以下代码片段时可能有所困难,下面我们对其做补充说明。

hash[i] = true;
ans[num + 1] = i;
DFS(num + 1);
hash[i] = false;

在尝试放入第 num+1 个数字时,我们依次尝试放入所有在之前位置上未被使用的数字,假设当前 x 未被使用,我们将 x 放入第 num+1 个位置,标记 x 为已用,此时环中前 num+1 个数字全部确定,依次保存在 ans[1]到 ans[num+1]中,再进行下一个位置的枚举,即递归调用 DFS。当调用返回时,意味着当前 num+1个数字确定为 ans[1]到 ans[num+1]中的值时对应的所有可行的答案已经全部处理完毕(可能一个解也不存在),此时我们需要改变 ans[num + 1]的值,从而进行新的答案的搜索。所以,此时 ans[num + 1]的值将不再为已经被枚举过的 x,而是一个相异于 x,同时又未在之前被使用过的新数字。那么对于后序数字而言,x是未被使用的,是可以被放入后序的任意一个位置的,所以我们重新标记 x 为未使用,供后序数位选择。这就是为什么,我们先是标记 x 为已经使用,而后又解除该标记的原因。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Casionx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值