问题理解
小R需要构造一个从 1 到 N 的整数排列 A,使得通过计算每个位置 i(从 0 开始)上的元素与其索引的和 A[i] + i,恰好生成 K 个不同的整数。题目要求返回字典顺序最小的满足条件的排列 A。已知在给定的约束下,至少存在一个符合要求的排列。
示例解释:
-
测试用例 1:
-
输入:
N = 4,K = 3 -
输出:
[1, 2, 4, 3] -
解释:
-
计算
A[i] + i:A[0] + 0 = 1 + 0 = 1A[1] + 1 = 2 + 1 = 3A[2] + 2 = 4 + 2 = 6A[3] + 3 = 3 + 3 = 6
-
生成的不同整数为
{1, 3, 6},共3个,满足K = 3。
-
-
-
测试用例 2:
-
输入:
N = 5,K = 2 -
输出:
[1, 5, 4, 3, 2] -
解释:
-
计算
A[i] + i:A[0] + 0 = 1 + 0 = 1A[1] + 1 = 5 + 1 = 6A[2] + 2 = 4 + 2 = 6A[3] + 3 = 3 + 3 = 6A[4] + 4 = 2 + 4 = 6
-
生成的不同整数为
{1, 6},共2个,满足K = 2。
-
-
-
测试用例 3:
-
输入:
N = 6,K = 4 -
输出:
[1, 2, 3, 6, 5, 4] -
解释:
-
计算
A[i] + i:A[0] + 0 = 1 + 0 = 1A[1] + 1 = 2 + 1 = 3A[2] + 2 = 3 + 2 = 5A[3] + 3 = 6 + 3 = 9A[4] + 4 = 5 + 4 = 9A[5] + 5 = 4 + 5 = 9
-
生成的不同整数为
{1, 3, 5, 9},共4个,满足K = 4。
-
-
数据结构选择
- 列表(List) :用于存储最终的排列
A。 - 集合(Set) :用于跟踪当前排列中生成的不同的
A[i] + i的值。 - 布尔数组(Boolean Array) :用于标记哪些数字已经被使用过,确保排列的唯一性。
- 优先队列(Priority Queue):直接使用递归和回溯方法:用于生成字典顺序最小的排列。
算法步骤
为了生成字典顺序最小的满足条件的排列 A,我们可以按照以下步骤进行:
-
初始化:
- 创建一个空的排列
A。 - 创建一个布尔数组
used长度为N + 1,用于标记数字1到N是否被使用过,初始时全部为False。 - 创建一个集合
current_sums来存储当前排列中生成的不同的A[i] + i的值。 - 初始化变量
unique_count为0,用于记录当前排列中不同的A[i] + i的数量。
- 创建一个空的排列
-
递归生成排列:
-
从位置
0开始,尝试将1到N的数字依次放入排列中,确保每次选择的数字是未被使用的,并尽可能小,以保持字典顺序最小。 -
对于每个位置
i,选择一个未被使用的数字x,并计算x + i。有两种情况:- 如果
x + i不在current_sums中,那么放入x后,unique_count增加1,并将x + i添加到current_sums。 - 如果
x + i已经存在于current_sums中,则直接放入x,不改变unique_count。
- 如果
-
继续递归下一位置,直到排列长度为
N。 -
在递归过程中,如果
unique_count超过K,则剪枝,停止当前路径的搜索。 -
如果排列完成时,
unique_count == K,则返回当前排列作为答案。
-
-
剪枝策略:
- 在递归过程中,如果剩余的位置不足以达到所需的
K,则停止当前路径的搜索。 - 优先选择较小的数字,以确保字典顺序最小。
- 在递归过程中,如果剩余的位置不足以达到所需的
-
终止条件:
- 成功生成长度为
N的排列,并且unique_count == K。 - 如果遍历完所有可能仍未找到满足条件的排列,则返回空列表(根据题目描述总存在至少一个解,因此不需要处理无解的情况)。
- 成功生成长度为
代码实现
def solution(n: int, k: int) -> list:
def backtrack(position, current_permutation, used, current_sums, unique_count):
# 如果已经完成排列
if position == n:
if unique_count == k:
return current_permutation.copy()
else:
return None
# 尝试从1到n的数字
for num in range(1, n + 1):
if not used[num]:
new_sum = num + position
increment = 0
# 判断是否是新的唯一和
if new_sum not in current_sums:
increment = 1
# 早期剪枝,如果当前唯一和数超过k,不继续深入
if unique_count + increment > k:
continue
# 标记数字为已使用
used[num] = True
current_permutation.append(num)
if increment:
current_sums.add(new_sum)
# 递归下一位置
result = backtrack(position + 1, current_permutation, used, current_sums, unique_count + increment)
if result is not None:
return result # 找到答案,返回
# 回溯
used[num] = False
current_permutation.pop()
if increment:
current_sums.remove(new_sum)
return None # 无法找到满足条件的排列
# 初始化
used = [False] * (n + 1)
current_permutation = []
current_sums = set()
unique_count = 0
# 开始回溯
result = backtrack(0, current_permutation, used, current_sums, unique_count)
if result:
return result
else:
return [] # 根据题目描述,总是存在至少一个解
详细解释
-
回溯函数
backtrack:-
参数:
position:当前正在填充的位置(从0开始)。current_permutation:当前已经填充的排列。used:布尔数组,标记数字是否已经被使用。current_sums:集合,存储当前排列中生成的A[i] + i的不同值。unique_count:当前排列中不同A[i] + i的数量。
-
步骤:
- 如果当前
position == n,表示排列已完成。检查unique_count是否等于k,如果是,则返回当前排列。 - 遍历数字
1到n,选择未被使用的数字num。 - 计算
new_sum = num + position。 - 如果
new_sum不在current_sums中,意味着选择num后,unique_count将增加1。如果此时unique_count + 1 > k,则剪枝,不继续选择该数字。 - 标记
num为已使用,添加到current_permutation中。如果new_sum是新的,则将其添加到current_sums中。 - 递归调用
backtrack填充下一个位置。 - 如果在递归过程中找到满足条件的排列,则直接返回该排列。
- 如果未找到,则进行回溯,撤销选择,继续尝试下一个数字。
- 如果当前
-
-
回溯策略:
- 字典顺序最小:在尝试数字时,从小到大遍历
1到n,确保尽早选择较小的数字,进而构造字典顺序最小的排列。 - 剪枝:如果当前的
unique_count加上即将增加的1超过k,则不选择该数字,避免不必要的递归,提升效率。
- 字典顺序最小:在尝试数字时,从小到大遍历
-
终止条件:
- 成功找到满足条件的排列,立即返回。
- 根据题目描述,至少存在一个解,因此不需要处理无解的情况。
-
复杂度分析:
- 时间复杂度:最坏情况下,时间复杂度为
O(n!),因为需要遍历所有可能的排列。然而,由于回溯和剪枝的存在,实际运行时间会显著减少,特别是对于较小的n。 - 空间复杂度:
O(n),用于存储当前排列、使用标记和当前生成的和。
- 时间复杂度:最坏情况下,时间复杂度为
为什么答案是这样的
以第一个测试用例为例:
- 输入:
n = 4,k = 3 - 期望输出:
[1, 2, 4, 3]
详细过程:
-
开始填充位置 0:
-
尝试
num = 1:A[0] + 0 = 1 + 0 = 1,unique_count从0增加到1。- 标记
1为已使用,current_sums = {1},current_permutation = [1]。
-
递归填充位置
1。
-
-
填充位置 1:
-
尝试
num = 2:A[1] + 1 = 2 + 1 = 3,unique_count从1增加到2。- 标记
2为已使用,current_sums = {1, 3},current_permutation = [1, 2]。
-
递归填充位置
2。
-
-
填充位置 2:
-
尝试
num = 3:A[2] + 2 = 3 + 2 = 5,unique_count从2增加到3。- 标记
3为已使用,current_sums = {1, 3, 5},current_permutation = [1, 2, 3]。 - 达到
unique_count = k = 3。
-
递归填充位置
3。
-
-
填充位置 3:
-
尝试
num = 4:A[3] + 3 = 4 + 3 = 7,unique_count将从3增加到4,超过k,剪枝。
-
尝试下一个未使用的数字
3(已使用,跳过)。 -
尝试
num = 4:- 同上,剪枝。
-
尝试下一个未使用的数字
4,剪枝。 -
回溯到位置
2,撤销选择3。
-
-
回溯到位置 2:
-
尝试下一个未使用的数字
4:A[2] + 2 = 4 + 2 = 6,unique_count从2增加到3。- 标记
4为已使用,current_sums = {1, 3, 6},current_permutation = [1, 2, 4]。
-
递归填充位置
3。
-
-
填充位置 3:
-
尝试
num = 3:A[3] + 3 = 3 + 3 = 6,unique_count保持3。- 标记
3为已使用,current_sums不变,current_permutation = [1, 2, 4, 3]。
-
达到排列长度
4,且unique_count = k = 3,成功找到符合条件的排列[1, 2, 4, 3]。
-
这个过程确保了每一步都选择最小的数字,同时控制了生成的不同和数不超过 K,最终得到字典顺序最小的满足条件的排列。