mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
2277 字
6 分钟
素数环问题(回溯法)
2026-06-08 06:49:15

1. 问题描述#

素数环问题,也叫 Prime Ring Problem

给定正整数 nn,要求将 11nnnn 个数字排成一个环,使得任意两个相邻数字之和都是素数。

例如,当 n=8n=8 时,一个合法的素数环为:

1, 2, 3, 8, 5, 6, 7, 41,\ 2,\ 3,\ 8,\ 5,\ 6,\ 7,\ 4

因为:

1+2=31+2=32+3=52+3=53+8=113+8=118+5=138+5=135+6=115+6=116+7=136+7=137+4=117+4=114+1=54+1=5

这些和全部都是素数。

注意,最后一个数和第一个数也是相邻的,因为它们组成的是一个环。


2. 算法策略#

素数环问题使用:

回溯法\boxed{\text{回溯法}}

回溯法可以理解为一种深度优先搜索,核心思想是:

能继续就继续往下搜索,走不通就退回来,换一个选择再试。

素数环问题是在 11nn 的所有排列中,寻找满足条件的排列。如果直接枚举所有排列,复杂度很高。例如 n=8n=8 时,排列总数为:

8!=403208! = 40320

但实际上,很多排列刚排到一半就已经不可能成为答案。例如:

1, 31,\ 3

因为:

1+3=41+3=4

44 不是素数,所以后面无论怎么排,都不可能形成合法素数环。

这时候就可以直接剪枝。


3. 数学性质:奇偶性约束#

素数环问题有一个很重要的性质:相邻数字必须一奇一偶。因为除了 22 之外,所有素数都是奇数。

两个整数相加时:

情况结果
奇数 + 奇数偶数
偶数 + 偶数偶数
奇数 + 偶数奇数

在素数环中,相邻两个数的和最小是 1+2=31+2=3,因此和不可能是 22。所以相邻两个数的和如果是素数,就只能是奇数,两个数字也就必须一奇一偶。因此,整个环必须满足:

, 偶, 奇, 偶, \text{奇},\ \text{偶},\ \text{奇},\ \text{偶},\ \ldots

交替排列。

这也说明:当 n>1n>1nn 为奇数时,素数环无解,因为 11nn 中奇数和偶数数量不相等,无法形成完整的奇偶交替闭环。


4. 固定第一个数为 1#

素数环是一个环,所以旋转后本质上还是同一个环。例如:

1, 2, 3, 8, 5, 6, 7, 41,\ 2,\ 3,\ 8,\ 5,\ 6,\ 7,\ 4

和:

2, 3, 8, 5, 6, 7, 4, 12,\ 3,\ 8,\ 5,\ 6,\ 7,\ 4,\ 1

其实表示的是同一个环,只是起点不同。

为了避免这种旋转重复,通常固定第一个位置为:

x[1]=1x[1] = 1

这样可以减少重复答案,也可以缩小搜索空间。


5. 解空间与排列树#

可以用数组表示当前排列:

x=(x1, x2, , xn)x = (x_1,\ x_2,\ \ldots,\ x_n)

其中:

x1=1x_1 = 1

其余位置从没有使用过的数字中选择,并要求相邻两数之和为素数,且最后一个数与第一个数之和也为素数。

因为每一层都要从剩余数字中选择一个数,所以解空间是一棵排列树

n=8n=8 为例:

第 1 层:固定 1
第 2 层:从 2 到 8 中选择一个数
第 3 层:继续从未使用的数字中选择一个数
……
第 8 层:形成完整排列

搜索过程中不是所有分支都会继续展开。如果新加入的数字和前一个数字之和不是素数,就直接剪枝。


6. 剪枝条件#

素数环问题主要有两个剪枝条件。

6.1 相邻和不是素数,直接剪枝#

每放入一个新数字 xix_i,都要检查:

xi1+xix_{i-1} + x_i

是否为素数。

如果不是素数,当前分支直接结束。例如:

1, 31,\ 3

因为:

1+3=41+3=4

不是素数,所以这一支不需要继续搜索。


6.2 填满后检查首尾#

由于这是一个环,所以当所有数字都放完后,还要检查最后一个数字和第一个数字的和:

xn+x1x_n + x_1

是否为素数。

只有首尾也满足条件,才是一个完整的素数环。


7. 算法步骤#

  1. 输入正整数 nn
  2. 如果 n>1n>1nn 为奇数,直接判断无解。
  3. 预处理 2n2n 范围内的素数表。
  4. 定义数组 path 保存当前排列。
  5. 定义数组 visited 标记数字是否已经使用。
  6. 固定 path[0] = 1
  7. 从第二个位置开始回溯搜索。
  8. 每次尝试放入一个未使用的数字。
  9. 如果它与前一个数字之和是素数,则继续递归。
  10. 如果不满足条件,则剪枝。
  11. 当所有位置填满时,检查最后一个数和第一个数之和是否为素数。
  12. 如果满足,输出当前排列。

8. 以 n = 8 为例#

固定第一个数为:

11

第二个数必须满足:

1+x1+x

是素数。

所以可以选择:

2, 4, 62,\ 4,\ 6

因为:

1+2=31+2=31+4=51+4=51+6=71+6=7

都是素数。

而:

1+3=41+3=41+5=61+5=61+7=81+7=81+8=91+8=9

都不是素数,所以这些分支直接剪掉。

n=8n=8 时,固定 11 在首位,可以得到 4 个素数环:

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

9. Java 实现#

import java.util.Arrays;
public class PrimeRing {
private static int n;
private static int[] path; // 存储当前排列
private static boolean[] visited; // 标记数字是否已经使用
private static boolean[] isPrime; // 素数表
public static void main(String[] args) {
int inputN = 8;
System.out.println("n = " + inputN + " 时的素数环结果:");
solvePrimeRing(inputN);
}
public static void solvePrimeRing(int inputN) {
n = inputN;
// 如果 n > 1 且 n 是奇数,则无法形成奇偶交替的闭环
if (n > 1 && n % 2 == 1) {
System.out.println("无解");
return;
}
path = new int[n];
// 下标直接对应数字,避免出现“差一错误”
visited = new boolean[n + 1];
// 相邻两数之和最大为 2n
precomputePrimes(2 * n);
// 固定第一个数为 1,避免旋转重复
path[0] = 1;
visited[1] = true;
// 从下标 1 开始填数
backtrack(1);
}
private static void backtrack(int index) {
// 已经填满 n 个数
if (index == n) {
// 检查最后一个数和第一个数之和是否为素数
if (isPrime[path[n - 1] + path[0]]) {
printPath();
}
return;
}
// 从 2 到 n 尝试放入未使用的数字
for (int i = 2; i <= n; i++) {
// 当前数字没有使用过,并且与前一个数字之和为素数
if (!visited[i] && isPrime[path[index - 1] + i]) {
path[index] = i;
visited[i] = true;
backtrack(index + 1);
// 回溯:撤销选择
visited[i] = false;
}
}
}
private static void precomputePrimes(int max) {
isPrime = new boolean[max + 1];
Arrays.fill(isPrime, true);
if (max >= 0) {
isPrime[0] = false;
}
if (max >= 1) {
isPrime[1] = false;
}
for (int p = 2; p * p <= max; p++) {
if (isPrime[p]) {
for (int i = p * p; i <= max; i += p) {
isPrime[i] = false;
}
}
}
}
private static void printPath() {
for (int i = 0; i < n; i++) {
System.out.print(path[i]);
if (i != n - 1) {
System.out.print(" ");
}
}
System.out.println();
}
}

运行结果:

n = 8 时的素数环结果:
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

10. 代码解释#

10.1 path 数组#

private static int[] path;

path 用来保存当前已经构造出的排列。

例如:

1 2 3

表示当前已经放好了前三个数字。


10.2 visited 数组#

private static boolean[] visited;

visited[i] 表示数字 i 是否已经被使用。

这里把 visited 的大小定义为 n + 1,是为了让数字和数组下标一一对应。比如数字 6 就直接用 visited[6] 表示,而不需要写成 visited[5]。这样虽然空出了 visited[0],但可以减少下标偏移带来的差一错误。

如果:

visited[3] = true;

表示数字 3 已经在当前排列中,后面不能重复使用。


10.3 isPrime 数组#

private static boolean[] isPrime;

isPrime[x] 表示数字 x 是否是素数。

例如:

isPrime[7] = true;
isPrime[8] = false;

因为 77 是素数,88 不是素数。


10.4 埃拉托斯特尼筛法#

代码中的 precomputePrimes 使用的是埃拉托斯特尼筛法,简称“埃氏筛”。

它的作用是:在回溯开始前,先把 00max 范围内的素数全部算出来,保存到 isPrime 数组中。之后判断相邻和是否为素数时,只需要查表:

isPrime[sum]

时间复杂度就是 O(1)O(1)

埃氏筛的思路是:先假设所有大于等于 22 的数都是素数,然后从小到大枚举每个数。如果当前数 pp 仍然是素数,就把它的倍数标记为非素数。

max = 10 为例:

步骤操作剩下仍可能是素数的数
初始化排除 00112,3,4,5,6,7,8,9,102, 3, 4, 5, 6, 7, 8, 9, 10
p=2p = 2筛掉 4,6,8,104, 6, 8, 102,3,5,7,92, 3, 5, 7, 9
p=3p = 3筛掉 992,3,5,72, 3, 5, 7

最终留下来的 2,3,5,72, 3, 5, 7,就是 1010 以内的素数。

代码中内层循环从:

int i = p * p;

开始,而不是从 2 * p 开始,是因为更小的倍数已经被前面的数筛过了。比如枚举到 33 时,6=2×36 = 2 \times 3 已经在 p=2p = 2 的时候被筛掉了,所以直接从 9=3×39 = 3 \times 3 开始即可。

外层循环条件是:

p * p <= max

原因是:如果一个数 NN 是合数,那么它一定可以写成 N=a×bN = a \times b,其中至少有一个因数不超过 N\sqrt{N}。所以只要筛到 max\sqrt{\text{max}},就足够把 max 以内的合数全部筛掉。

在素数环问题中,相邻两个数的最大和是:

n+(n1)<2nn + (n - 1) < 2n

为了写起来更方便,代码直接预处理到 2 * n,之后判断某个和是否为素数时,只需要访问:

isPrime[path[index - 1] + i]
空间换时间

埃氏筛会多用一个 isPrime 数组,但能把回溯过程中的素数判断变成查表。回溯会反复尝试很多组合,这种预处理通常很划算。


10.5 回溯函数#

private static void backtrack(int index)

index 表示当前要填写的位置。

如果:

index == n

说明所有位置都已经填完,需要检查首尾是否也满足素数条件。

如果还没有填完,就遍历所有还没使用过的数字,尝试放到当前位置。


10.6 回溯过程#

核心代码是:

if (!visited[i] && isPrime[path[index - 1] + i]) {
path[index] = i;
visited[i] = true;
backtrack(index + 1);
visited[i] = false;
}

这段代码可以分成四步:

1. 判断数字 i 是否可以放入当前位置。
2. 如果可以,就把 i 放入 path[index]。
3. 递归搜索下一个位置。
4. 搜索结束后撤销选择,尝试其他数字。

其中 visited[i] = false; 就是回溯的关键,它表示当前分支搜索完了,把这个数字拿出来,换别的数字试试。


11. 复杂度分析#

在最坏情况下,如果不考虑剪枝,需要枚举排列,复杂度接近:

O((n1)!)O((n-1)!)

因为第一个位置固定为 11,所以只需要排列剩下的 n1n-1 个数字。

但实际运行中,由于有“相邻和为素数”和“奇偶交替”两个强剪枝条件,搜索空间会大大减少。

空间复杂度主要来自:

  • path 数组:O(n)O(n)
  • visited 数组:O(n)O(n)
  • 递归调用栈:O(n)O(n)
  • isPrime 数组:O(n)O(n)

所以整体空间复杂度为:

O(n)O(n)

12. 总结#

素数环问题可以概括为:

全排列搜索 + 素数条件剪枝 + 首尾相连检查。

核心内容如下:

  • 使用回溯法解决。
  • 固定第一个数字为 11,避免旋转重复。
  • 使用 visited 数组避免重复使用数字。
  • 每次加入新数字时,检查它和前一个数字之和是否为素数。
  • 填满后,再检查最后一个数字和第一个数字之和是否为素数。
  • 如果 n>1n>1nn 为奇数,直接无解。
  • 预处理素数表,可以把素数判断变成 O(1)O(1)

口诀:

素数环问题就是在排列树上做回溯搜索,每一步用“相邻和为素数”进行剪枝。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

素数环问题(回溯法)
https://dawn114514.site/posts/algorithm/primeringbacktracking/
作者
黎明
发布于
2026-06-08 06:49:15
许可协议
MIT

部分信息可能已经过时

目录