稀土掘金AI刷题第213题 小R的排列挑战|豆包MarsCode AI刷题

172 阅读5分钟

标题: 通过有限交换次数使数组升序排列的算法分析

1. 问题描述

小R有一个长度为 n 的排列,排列中的数字是从 1n 的整数。她每次操作可以选择两个数 a_ia_j 进行交换,前提是这两个数的下标 ij 的奇偶性相同(即要么都为奇数,要么都为偶数)。小R希望通过最少的操作将数组变成升序排列。

要求

  • 计算最少需要多少次操作才能使得数组变成升序排列。
  • 如果无法通过这样的操作使数组有序,则输出 -1

2. 解题思路

本题的核心在于“限制交换”的问题。由于每次只能交换相同奇偶性下标的元素,问题变成了我们能否通过这些限制交换,将数组中的数字按升序排列。我们可以将解题思路分为以下几步:

1. 划分为两个子问题:

  • 将数组分为两个部分:

    • 奇数下标部分:包括下标为 1, 3, 5...的元素。
    • 偶数下标部分:包括下标为 2, 4, 6...的元素。

2. 分别排序奇偶部分:

  • 由于奇数下标部分的元素只能和其他奇数下标部分的元素交换,偶数下标部分的元素只能和其他偶数下标部分的元素交换,因此我们需要单独对奇数下标部分和偶数下标部分进行排序。

3. 检查是否能排序:

  • 排序完奇数和偶数部分后,重构回原数组并检查其是否为升序排列。如果合并后的数组不是升序,则返回 -1,否则继续进行最小交换次数计算。

4. 最小交换次数计算:

  • 对于每个部分(奇数下标部分和偶数下标部分),我们可以通过计算最小交换次数来得到最终的操作次数。最小交换次数的计算通过“排列环”的数量来实现。

3. 算法实现

分割和排序

  1. 分割:我们将数组分为奇数下标和偶数下标部分。
  2. 排序:分别对奇数下标部分和偶数下标部分进行排序。
  3. 重构:将排序后的部分重新放回到原数组的对应位置。
  4. 检查升序:如果重构后的数组是升序排列,计算最小交换次数,否则返回 -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. 代码详解

  1. min_swaps_to_sort(arr)

    • 目的:计算数组 arr 的最小交换次数。

    • 步骤

      • 首先,创建一个排序后的副本 arr_copy,并为每个元素记录它在排序后数组中的索引位置。
      • 然后,遍历每个元素,检查是否已经访问过或是否已经排到正确位置。如果没有,追踪它所在的环,并计算环的大小。
      • 最终,通过环的数量减 1 来计算最小交换次数。
  2. solution(n, a)

    • 目的:主函数用于解决问题。

    • 步骤

      • 首先将输入数组 a 按照奇数下标和偶数下标拆分成两个子数组 odd_elementseven_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,否则返回最小交换次数。该方法时间复杂度较低,适用于较大的输入。