小C的非素数排列问题:用回溯解决排列问题 | 豆包MarsCode AI刷题

129 阅读4分钟

前言

排列问题一直是算法学习中的经典场景,尤其是涉及到排列中的特定限制条件时,更加考验我们的算法设计能力。小C对排列特别感兴趣,这次她的问题是:有多少个长度为 n 的排列满足任意两个相邻元素之和都不是素数

这个问题不仅包含了对素数的理解,还结合了排列生成与条件判断,非常具有挑战性。本文将带你一步步剖析问题,并使用回溯法高效解决。

问题描述

我们需要找到所有长度为 n 的排列,这些排列需满足以下条件:

  1. 排列定义: 长度为 n 的数组,包含从 1 到 n 的所有整数,且每个数字恰好出现一次。
  2. 限制条件: 任意两个相邻元素之和都不能是素数。

image.png

解题思路

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 的排列时,需要用到回溯法。回溯是一种逐步构造解的算法:

  1. 状态表示: 当前排列 current 和已经使用的数字集合 used
  2. 递归搜索: 在当前排列的基础上,尝试添加一个新的数字。
  3. 约束条件: 如果新添加的数字与排列的最后一个数字之和为素数,则跳过该数字。
  4. 终止条件: 当排列长度等于 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的非素数排列问题。这个问题考察了对素数性质的理解以及回溯算法的灵活应用。在刷题过程中,学会结合数学和算法思想,是解决复杂问题的关键。

思考问题:

  • 如果增加限制条件,比如任意两个相邻数字之和都不能是偶数,该如何调整算法?
  • 对于大规模输入,如何优化素数判断的效率?

希望大家在刷题时能举一反三,将这些算法思想应用到更多场景中!