小Q的非素数和排列问题 | 豆包MarsCode AI刷题

106 阅读3分钟

小Q的非素数和排列问题 | 豆包MarsCode AI刷题

小Q的非素数和排列问题 - MarsCode

这题很有意思,我的做法是使用了埃拉托色尼筛法进行素数的快速判断,然后用剪枝的回溯法进行全排列,原理在文中有介绍

摘要

本文介绍了一种结合埃拉托色尼筛法和回溯法的算法,用于统计满足相邻元素之和都不是素数的排列数量。通过埃筛快速生成素数表,并在回溯生成排列的过程中利用剪枝优化,提升了算法效率。时间复杂度由素数筛选和排列生成的组合决定,适合中小规模问题的求解。


问题描述

小C对排列问题很感兴趣,她想知道有多少个长度为 nn 的排列满足任意两个相邻元素之和都不是素数。排列的定义如下:

  1. 长度为 nn 的数组,包含从 11nn 的所有整数。
  2. 每个数字恰好出现一次。

示例

输入

  • n = 5
  • n = 3
  • n = 6

输出

  • 输出:4
  • 输出:0
  • 输出:24

算法分析

1. 素数筛选——埃拉托色尼筛法

埃拉托色尼筛法是一种经典的求素数算法,适用于在范围 [2,limit][2, \text{limit}] 内快速生成素数表。算法步骤如下:

  1. 初始化一个布尔数组 isPrimeisPrime,其中 isPrime[i]isPrime[i] 表示数字 ii 是否为素数。
  2. isPrime[0]isPrime[0]isPrime[1]isPrime[1] 标记为 False,因为 0011 不是素数。
  3. 22 开始,遍历数组:
    • isPrime[i]isPrime[i]True,将其所有倍数标记为 False
  4. 遍历完成后,isPrimeisPrime 中所有值为 True 的位置即为素数。
复杂度分析

埃拉托色尼筛法的时间复杂度为:

O(nloglogn)O(n \log \log n)

在本问题中,我们需要筛选 [2,2n][2, 2n] 范围内的素数,因为相邻元素的最大可能和为 2n12n-1


2. 回溯法生成排列

回溯法概述

回溯法是一种用于构造和探索所有可能解的算法,适用于排列、组合、棋盘覆盖等问题。核心思想是:

  1. 从起始状态出发,逐步扩展解的路径。
  2. 若当前路径满足问题约束,继续探索;否则回溯并尝试其他路径。
  3. 通过递归实现,遍历所有可能的解空间。
剪枝优化

在生成排列的过程中,我们可以利用以下规则进行剪枝,减少无效的递归:

  1. 若当前排列路径 pathpath 中,最后一个元素与待添加元素之和为素数,则剪枝。
  2. 若某元素已被使用,则剪枝。
算法步骤
  1. 初始化路径数组 pathpath 和标记数组 usedused
  2. 11nn 尝试添加元素 ii
    • ii 已被使用或与路径末尾之和为素数,则跳过。
    • 否则,将 ii 加入路径,标记为已使用,递归调用下一步。
  3. 若路径长度等于 nn,则记录为有效排列。
  4. 递归返回后,移除路径中的最后一个元素,并取消使用标记(回溯)。

算法实现

Python实现
def generate_primes(limit):
    """
    使用埃拉托色尼筛法生成素数表。
    返回一个布尔列表,is_prime[i] 表示 i 是否为素数。
    """
    is_prime = [True] * (limit + 1)
    is_prime[0] = is_prime[1] = False  # 0 和 1 不是素数
    for i in range(2, int(limit ** 0.5) + 1):
        if is_prime[i]:
            for j in range(i * i, limit + 1, i):
                is_prime[j] = False
    return is_prime


def backtrack(n, path, used, is_prime, count):
    """
    使用回溯法生成排列并统计符合条件的数量。
    - n: 总长度
    - path: 当前排列路径
    - used: 当前使用过的数字标记
    - is_prime: 素数表
    - count: 计数器
    """
    if len(path) == n:
        # 如果成功构造长度为 n 的排列,计数加 1
        count[0] += 1
        return

    for i in range(1, n + 1):
        if used[i]:
            continue
        # 检查当前数字与前一个数字的和是否为素数
        if path and is_prime[path[-1] + i]:
            continue
        # 选择数字 i
        used[i] = True
        path.append(i)
        # 递归生成下一个数字
        backtrack(n, path, used, is_prime, count)
        # 回溯
        used[i] = False
        path.pop()


def solution(n):
    """
    主函数,计算符合条件的排列数量。
    """
    # 特殊情况
    if n == 1:
        return 1
    if n == 2:
        return 0
    # 生成素数表
    is_prime = generate_primes(2 * n)
    # 使用回溯法生成排列并计数
    used = [False] * (n + 1)  # 用于标记是否使用过某个数字
    count = [0]  # 记录符合条件的排列数量
    backtrack(n, [], used, is_prime, count)
    return count[0]


if __name__ == "__main__":
    # 测试用例
    print(solution(5))  # 输出:4
    print(solution(3))  # 输出:0
    print(solution(6))  # 输出:24

Go实现
package main

import (
	"fmt"
)

// 生成素数表(埃拉托色尼筛法)
func generatePrimes(limit int) []bool {
	isPrime := make([]bool, limit+1)
	for i := 2; i <= limit; i++ {
		isPrime[i] = true
	}
	for i := 2; i*i <= limit; i++ {
		if isPrime[i] {
			for j := i * i; j <= limit; j += i {
				isPrime[j] = false
			}
		}
	}
	return isPrime
}

// 回溯法生成排列并统计符合条件的数量
func backtrack(n int, path []int, used []bool, isPrime []bool, count *int) {
	if len(path) == n {
		(*count)++
		return
	}

	for i := 1; i <= n; i++ {
		if used[i] {
			continue
		}
		if len(path) > 0 && isPrime[path[len(path)-1]+i] {
			continue
		}
		used[i] = true
		path = append(path, i)
		backtrack(n, path, used, isPrime, count)
		used[i] = false
		path = path[:len(path)-1]
	}
}

func solution(n int) int {
	if n == 1 {
		return 1
	}
	if n == 2 {
		return 0
	}
	isPrime := generatePrimes(2 * n)
	used := make([]bool, n+1)
	count := 0
	backtrack(n, []int{}, used, isPrime, &count)
	return count
}

func main() {
	fmt.Println(solution(5)) // 输出:4
	fmt.Println(solution(3)) // 输出:0
	fmt.Println(solution(6)) // 输出:24
}