问题描述
小C对排列很感兴趣,她想知道有多少个长度为的排列满足任意两个相邻元素之和都不是素数。排列定义为一个长度为的数组,其中包含从1到的所有整数,每个数字恰好出现一次。
测试样例
样例1:
输入:
n = 5
输出:4
样例2:
输入:
n = 3
输出:0
样例3:
输入:
n = 6
输出:24
题目解析:
1.明确素数的定义: 素数是大于1的正整数,且只能被1和它自身整除。
2.明确排列的定义:
长度为n的排列是从1到n的所有整数的一个排列组合。
但是题目要求在所有可能的排列中,过滤掉不满足“任意两个相邻元素之和都不是素数”的排列,统计剩下的排列数量。
思路分析:
- 判断素数:
编写一个函数is_prime判断一个数是否为素数 - 构建非素数图:
使用一个图结构存储每个数字i和其相邻元素的关系。
如果i+j不是素数,则认为i和j之间有一条边。 - 回溯搜索合法排列:
使用递归+回溯方法,从任意一个数字开始,逐步构建排列。
在每一步,检查当前数字与排列末尾数字的和是否为非素数。 - 剪枝优化 :
如果当前路径已不合法,立即停止搜索,减少不必要的计算。
解决方案:
- 素数判断函数:判断一个数字是否为素数。
- 构建非素数图:创建一个邻接表,记录从1到n每个数字可与哪些数字相邻。
- 回溯搜索排列:使用回溯算法生成所有可能的排列,并在过程中检查是否满足条件。
代码实现:
def is_prime(num):
"""判断一个数是否是素数"""
if num < 2:
return False
for i in range(2, int(num ** 0.5) + 1):
if num % i == 0:
return False
return True
def build_non_prime_graph(n):
"""构建图,找出哪些数字的和是非素数"""
non_prime_pairs = {}
for i in range(1, n + 1):
non_prime_pairs[i] = set()
for j in range(1, n + 1):
if not is_prime(i + j):
non_prime_pairs[i].add(j)
return non_prime_pairs
def solution(n: int) -> int:
"""使用回溯法计算满足条件的排列数量"""
graph = build_non_prime_graph(n)
visited = [False] * (n + 1) # 记录哪些数字已被使用
count = 0 # 统计满足条件的排列数量
def backtrack(path):
nonlocal count
if len(path) == n:
count += 1
return
for next_num in range(1, n + 1):
# 检查未访问过的数字,且与当前路径尾部数字相邻和非素数
if not visited[next_num] and (not path or next_num in graph[path[-1]]):
visited[next_num] = True # 标记为已访问
backtrack(path + [next_num]) # 递归搜索
visited[next_num] = False # 回溯,撤销标记
backtrack([])
return count
代码解释:
-
is_prime 函数:通过试除法判断一个数字是否为素数。
-
build_non_prime_graph 函数 :创建一个邻接表,记录从1到n的每个数字可以与哪些数字相邻。
-
回溯算法:
递归地尝试每个数字。
如果当前排列满足条件,则继续尝试下一个数字。
如果排列长度达到n,则计数器加一。 -
优化:每次只尝试合法的数字组合,减少不必要的排列生成。
示例测试:
-
样例1:
print(solution(n = 5)) # 输出 4 -
样例2:
print(solution(n = 3)) # 输出 0 -
样例3:
print(solution(n = 6)) # 输出 24
时间复杂度:
- 图构建复杂度:,需要枚举1到n的所有数字对。
- 回溯搜索复杂度:,在最坏情况下,需要生成所有排列。
因此,整体时间复杂度为。
空间复杂度:
- 存储非素数图:邻接表存储每个数字与其相邻的数字集合。
- 递归深度:最大递归深度为排列长度。
因此空间复杂度为 。
总结
-
本题通过图的构建将排列问题转化为图问题,简化了相邻数字和的判定。
-
使用回溯算法有效地枚举合法排列,并结合剪枝优化减少搜索空间。
-
虽然时间复杂度较高,但对于n≤10的问题,运行效率可以接受。