前言
排列问题一直是算法学习中的经典场景,尤其是涉及到排列中的特定限制条件时,更加考验我们的算法设计能力。小C对排列特别感兴趣,这次她的问题是:有多少个长度为 n 的排列满足任意两个相邻元素之和都不是素数?
这个问题不仅包含了对素数的理解,还结合了排列生成与条件判断,非常具有挑战性。本文将带你一步步剖析问题,并使用回溯法高效解决。
问题描述
我们需要找到所有长度为 n 的排列,这些排列需满足以下条件:
- 排列定义: 长度为 n 的数组,包含从 1 到 n 的所有整数,且每个数字恰好出现一次。
- 限制条件: 任意两个相邻元素之和都不能是素数。
解题思路
1. 素数的判断
首先,我们需要判断某两个数字的和是否为素数。这是问题的核心限制条件。
素数的定义:
- 一个大于 1 的正整数,除了 1 和自身外,不能被其他数整除。
优化判断方法:
- 遍历从 2 开始的所有可能因子,直到其平方大于当前数字。如果当前数字能被任何因子整除,则它不是素数。
public static boolean isPrime(int num) {
if (num <= 1) return false;
for (int i = 2; i * i <= num; i++) {
if (num % i == 0) return false;
}
return true;
}
2. 回溯法生成排列
生成所有长度为 n 的排列时,需要用到回溯法。回溯是一种逐步构造解的算法:
- 状态表示: 当前排列
current和已经使用的数字集合used。 - 递归搜索: 在当前排列的基础上,尝试添加一个新的数字。
- 约束条件: 如果新添加的数字与排列的最后一个数字之和为素数,则跳过该数字。
- 终止条件: 当排列长度等于 n 时,计数结果。
回溯算法实现:
public static void backtrack(int n, List<Integer> current, boolean[] used, int[] count) {
if (current.size() == n) {
count[0]++;
return;
}
for (int i = 1; i <= n; i++) {
if (!used[i]) {
// 检查当前数字与前一个数字之和是否为素数
if (!current.isEmpty() && isPrime(current.get(current.size() - 1) + i)) {
continue;
}
// 做出选择
used[i] = true;
current.add(i);
// 递归生成下一个数字
backtrack(n, current, used, count);
// 回溯,撤销选择
current.remove(current.size() - 1);
used[i] = false;
}
}
}
3. 主函数封装
主函数 solution 初始化必要的变量并调用回溯函数:
count[0]用于记录满足条件的排列数量。used数组跟踪哪些数字已经被选择。current列表记录当前排列。
public static int solution(int n) {
int[] count = new int[1]; // 记录满足条件的排列数量
boolean[] used = new boolean[n + 1]; // 记录数字是否已被使用
List<Integer> current = new ArrayList<>(); // 当前排列
backtrack(n, current, used, count);
return count[0];
}
Java完整代码
以下是完整代码实现:
import java.util.ArrayList;
import java.util.List;
public class Main {
// 判断是否为素数的辅助函数
public static boolean isPrime(int num) {
if (num <= 1) return false;
for (int i = 2; i * i <= num; i++) {
if (num % i == 0) return false;
}
return true;
}
// 回溯方法来生成排列并检查条件
public static void backtrack(int n, List<Integer> current, boolean[] used, int[] count) {
if (current.size() == n) {
count[0]++;
return;
}
for (int i = 1; i <= n; i++) {
if (!used[i]) {
// 如果当前排列不是空,并且当前数字与前一个数字之和为素数,则跳过
if (!current.isEmpty() && isPrime(current.get(current.size() - 1) + i)) {
continue;
}
// 选择当前数字
used[i] = true;
current.add(i);
// 递归生成下一个元素
backtrack(n, current, used, count);
// 回溯
current.remove(current.size() - 1);
used[i] = false;
}
}
}
public static int solution(int n) {
int[] count = new int[1]; // 用于计数满足条件的排列数量
boolean[] used = new boolean[n + 1]; // 用于跟踪哪些数字已被使用
List<Integer> current = new ArrayList<>(); // 当前排列
backtrack(n, current, used, count);
return count[0];
}
public static void main(String[] args) {
System.out.println(solution(5) == 4);
System.out.println(solution(3) == 0);
System.out.println(solution(6) == 24);
}
}
结果分析与复杂度
测试结果
- 输入
n = 5,输出4。 - 输入
n = 3,输出0。 - 输入
n = 6,输出24。
时间复杂度
由于需要生成所有排列,时间复杂度为 O(n!) ,其中包含检查素数的计算,实际运行时间可能稍高。
空间复杂度
空间复杂度为 O(n) ,因为需要存储当前排列和辅助数组。
总结
通过回溯法,我们高效解决了小C的非素数排列问题。这个问题考察了对素数性质的理解以及回溯算法的灵活应用。在刷题过程中,学会结合数学和算法思想,是解决复杂问题的关键。
思考问题:
- 如果增加限制条件,比如任意两个相邻数字之和都不能是偶数,该如何调整算法?
- 对于大规模输入,如何优化素数判断的效率?
希望大家在刷题时能举一反三,将这些算法思想应用到更多场景中!