标题: 通过有限交换次数使数组升序排列的算法分析
1. 问题描述
小R有一个长度为 n 的排列,排列中的数字是从 1 到 n 的整数。她每次操作可以选择两个数 a_i 和 a_j 进行交换,前提是这两个数的下标 i 和 j 的奇偶性相同(即要么都为奇数,要么都为偶数)。小R希望通过最少的操作将数组变成升序排列。
要求:
- 计算最少需要多少次操作才能使得数组变成升序排列。
- 如果无法通过这样的操作使数组有序,则输出
-1。
2. 解题思路
本题的核心在于“限制交换”的问题。由于每次只能交换相同奇偶性下标的元素,问题变成了我们能否通过这些限制交换,将数组中的数字按升序排列。我们可以将解题思路分为以下几步:
1. 划分为两个子问题:
-
将数组分为两个部分:
- 奇数下标部分:包括下标为 1, 3, 5...的元素。
- 偶数下标部分:包括下标为 2, 4, 6...的元素。
2. 分别排序奇偶部分:
- 由于奇数下标部分的元素只能和其他奇数下标部分的元素交换,偶数下标部分的元素只能和其他偶数下标部分的元素交换,因此我们需要单独对奇数下标部分和偶数下标部分进行排序。
3. 检查是否能排序:
- 排序完奇数和偶数部分后,重构回原数组并检查其是否为升序排列。如果合并后的数组不是升序,则返回
-1,否则继续进行最小交换次数计算。
4. 最小交换次数计算:
- 对于每个部分(奇数下标部分和偶数下标部分),我们可以通过计算最小交换次数来得到最终的操作次数。最小交换次数的计算通过“排列环”的数量来实现。
3. 算法实现
分割和排序
- 分割:我们将数组分为奇数下标和偶数下标部分。
- 排序:分别对奇数下标部分和偶数下标部分进行排序。
- 重构:将排序后的部分重新放回到原数组的对应位置。
- 检查升序:如果重构后的数组是升序排列,计算最小交换次数,否则返回
-1。
最小交换次数
最小交换次数可以通过“环”的概念来计算。一个环代表一组可以通过交换排序的元素,环的大小减 1 就是交换的次数。
代码实现
import math
# 计算区间内的最小交换次数
def min_swaps_to_sort(arr):
n = len(arr)
arr_copy = arr[:]
arr_copy.sort()
index_map = {value: i for i, value in enumerate(arr_copy)}
visited = [False] * n
swaps = 0
# 计算环的数量,环的大小减1即为交换次数
for i in range(n):
if visited[i] or arr[i] == arr_copy[i]:
continue
cycle_size = 0
x = i
while not visited[x]:
visited[x] = True
x = index_map[arr[x]]
cycle_size += 1
if cycle_size > 1:
swaps += cycle_size - 1
return swaps
# 主解法
def solution(n, a):
# 分离奇数下标和偶数下标的元素
odd_elements = a[::2]
even_elements = a[1::2]
# 排序奇数下标和偶数下标部分
sorted_odd = sorted(odd_elements)
sorted_even = sorted(even_elements)
# 检查是否能通过交换来形成升序排列
sorted_a = sorted(a)
a_copy = a[:]
for i in range(0, n, 2):
a_copy[i] = sorted_odd[i // 2] # 重新放入奇数下标元素
for i in range(1, n, 2):
a_copy[i] = sorted_even[i // 2] # 重新放入偶数下标元素
if a_copy != sorted_a:
return -1
# 计算最小交换次数
odd_swaps = min_swaps_to_sort(odd_elements)
even_swaps = min_swaps_to_sort(even_elements)
return odd_swaps + even_swaps
# 测试样例
print(solution(5, [1, 4, 5, 2, 3])) # 输出: 2
print(solution(4, [4, 3, 2, 1])) # 输出: -1
print(solution(6, [2, 4, 6, 1, 3, 5])) # 输出: -1
4. 代码详解
-
min_swaps_to_sort(arr):-
目的:计算数组
arr的最小交换次数。 -
步骤:
- 首先,创建一个排序后的副本
arr_copy,并为每个元素记录它在排序后数组中的索引位置。 - 然后,遍历每个元素,检查是否已经访问过或是否已经排到正确位置。如果没有,追踪它所在的环,并计算环的大小。
- 最终,通过环的数量减 1 来计算最小交换次数。
- 首先,创建一个排序后的副本
-
-
solution(n, a):-
目的:主函数用于解决问题。
-
步骤:
- 首先将输入数组
a按照奇数下标和偶数下标拆分成两个子数组odd_elements和even_elements。 - 对这两个数组分别排序并插回原数组的对应位置。
- 检查插回后的数组是否与升序数组相同。如果相同,则计算每部分的最小交换次数,并返回总交换次数。如果不同,则返回
-1。
- 首先将输入数组
-
5. 时间复杂度分析
-
min_swaps_to_sort:排序的时间复杂度是O(n log n),遍历一遍数组计算最小交换次数的时间复杂度是O(n),所以总体复杂度是O(n log n)。 -
solution:- 分割数组和排序数组的时间复杂度是
O(n log n)。 - 计算最小交换次数的时间复杂度是
O(n log n)。 - 总体的时间复杂度为
O(n log n),这是因为排序操作是最耗时的。
- 分割数组和排序数组的时间复杂度是
6. 图解
为了帮助读者理解,可以通过 Venn 图 或 数组示意图 来解释:
- 数组拆分:将原数组
a分为奇数下标部分和偶数下标部分。 - 排序:对奇数下标部分和偶数下标部分分别进行排序。
- 合并后检查:检查合并后的数组是否为升序排列。
7. 总结
本题通过限制条件“只能交换相同奇偶性下标的元素”,将问题转化为对两个子数组(奇数下标部分和偶数下标部分)分别排序的问题。通过排序后的重构检查和环的最小交换次数计算,我们有效地解决了这个问题。如果两个部分不能独立排序使得数组升序,则返回 -1,否则返回最小交换次数。该方法时间复杂度较低,适用于较大的输入。