【转自《王道上机指南》】
题目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 为已经使用,而后又解除该标记的原因。