计算将序列排序所需的最小交换次数

50
我正在处理一个没有相同数字的整数序列的排序问题(不失一般性,假设该序列是1,2,...,n的排列),将其按自然递增顺序(即1,2,...,n)排序。 我考虑直接交换元素(不考虑元素位置;换句话说,对于任意两个元素,交换都是有效的),并用最少的次数进行交换(以下可能是可行的解决方案):

交换两个元素,并约束其中一个或两个元素应被交换到正确的位置。 直到每个元素都放在正确的位置。

但我不知道如何数学证明上述解决方案是否最优。 有人可以帮忙吗?


高度相关/重复:将数组1更改为数组2所需的最小交换次数? - Bernhard Barker
20个回答

72

我可以用来证明这一点。你可能需要添加这个标签 :)

创建一个有n个顶点的图。如果在正确排序中,位置i上的元素应该在位置j上,则从节点n_in_j创建一条边。现在,你将拥有一个由若干不相交的环组成的图。我认为,正确排序所需的最少交换次数是

M = sum (c in cycles) size(c) - 1

花点时间自己说服自己...如果两个项目在一个循环中,一次交换就可以搞定它们。如果三个项目在一个循环中,您可以交换一对以将一个放在正确的位置,然后保留两个循环等等。如果有n个项目在一个循环中,则需要n-1次交换。(即使您不与直接邻居交换,这也始终是正确的。)
鉴于此,您现在可能能够看出为什么您的算法是最佳的了。如果您进行交换,并且至少有一个项目处于正确的位置,则它将始终将M的值减少1。对于长度为n的任何循环,请考虑将元素交换到由其邻居占据的正确位置。现在,您拥有一个正确排序的元素和一个长度为n-1的循环。
由于M是最小交换数量,而您的算法始终将M减少1个交换,因此它必须是最优的。

1
这个的时间复杂度会是多少? - puneet
3
时间复杂度: O(n*logn) 空间复杂度: O(n) @puneet - Rewanth Tammana
@AnT,我同意。具体来说,我可以构想一种算法,其中涉及交换,但是没有一个项目结束其预期位置,但实现了相同数量的移动。具体而言,可以进行交换以将任何循环减少到若干个二元循环(如果n为奇数,则可能以单个一元循环结束),然后将所有二元循环交换到正确的位置。这也涉及n-1次移动。虽然这不比提供的算法更快,但至少表明提供的算法的最优性显然并不明显。 - Him
@AnT 的“极简性”源于任何“n”元循环都是最小的“n-1”次置换积的事实。 - arax
3
为什么复杂度是n*log(n)?有人能够简单易懂地解释一下吗? - sherelock
显示剩余7条评论

24

所有的循环计数都很难记在脑子里,有一种更简单的方法可以记忆。

首先,手动演示一个样例。

  • 序列:[7, 1, 3, 2, 4, 5, 6]
  • 枚举它:[(0, 7), (1, 1), (2, 3), (3, 2), (4, 4), (5, 5), (6, 6)]
  • 按值排序枚举:[(1, 1), (3, 2), (2, 3), (4, 4), (5, 5), (6, 6), (0, 7)]
  • 从开头开始。当索引与枚举索引不同时,不断交换由索引和枚举索引定义的元素。请记住:swap(0,2);swap(0,3)swap(2,3);swap(0,2) 是相同的。
    • swap(0, 1) => [(3, 2), (1, 1), (2, 3), (4, 4), (5, 5), (6, 6), (0, 7)]
    • swap(0, 3) => [(4, 4), (1, 1), (2, 3), (3, 2), (5, 5), (6, 6), (0, 7)]
    • swap(0, 4) => [(5, 5), (1, 1), (2, 3), (3, 2), (4, 4), (6, 6), (0, 7)]
    • swap(0, 5) => [(6, 6), (1, 1), (2, 3), (3, 2), (4, 4), (5, 5), (0, 7)]
    • swap(0, 6) => [(0, 7), (1, 1), (2, 3), (3, 2), (4, 4), (5, 5), (6, 6)]

也就是说,语义上您将元素进行排序,然后通过交换最左侧不在正确位置的项来确定如何将它们放回初始状态。

Python算法就这么简单:

def swap(arr, i, j):
    arr[i], arr[j] = arr[j], arr[i]


def minimum_swaps(arr):
    annotated = [*enumerate(arr)]
    annotated.sort(key = lambda it: it[1])

    count = 0

    i = 0
    while i < len(arr):
        if annotated[i][0] == i:
            i += 1
            continue
        swap(annotated, i, annotated[i][0])
        count += 1

    return count
因此,您不需要记忆已访问的节点或计算一些循环长度。

这似乎无法针对具有重复值的数组返回最小数: [8, 8, 7, 9, 9, 9, 8, 9, 7] => 6,应该是4。 - Ryan Wood
6
已核对。之前写过。是的。不能处理重复项。但是,我的解决方案完全符合问题的规范:“我正在排序一个没有相同数字的整数序列”。它从来没有被设计用于处理有重复项的列表。因此,我会忽略@RyanWood的评论。 - Archibald
只是对@Archibald的解释进行补充:这种方法之所以有效,是因为从枚举和排序数组到原始数组的排序需要交换的次数与相反的操作是相同的。我觉得那个额外的排序有点多余。实际上,通过将 while 循环更改为以下内容也可以获得相同的结果(在 JS 中):while (i < enumeratedArr.length) { if (enumeratedArr[i][1] == i + 1) { i++ continue } else { swap(enumeratedArr, i, enumeratedArr[i][1] - 1) count++ } } - Dan Engel

6
我们不需要交换实际元素,只需要找到有多少个元素没有处于正确的索引位置(周期)。最小的交换次数将是Cycle - 1。以下是代码...
static int minimumSwaps(int[] arr) {
        int swap=0;
        boolean visited[]=new boolean[arr.length];

        for(int i=0;i<arr.length;i++){
            int j=i,cycle=0;

            while(!visited[j]){
                visited[j]=true;
                j=arr[j]-1;
                cycle++;
            }

            if(cycle!=0)
                swap+=cycle-1;
        }
        return swap;


    }

我无法理解while循环如何工作以找到循环次数,特别是while循环中的第二个语句j=arr[j]-1;。为什么j的值要减去1,而我们在开始时将其设置为i呢? - Ashish Santikari
最优解是直接找到最小交换次数,其他方法都是不必要的元素交换。 - Ajay Sharma
我认为j=arr[j]-1;的原因可以通过在已排序的数组上运行代码来看到。在这种情况下,填充visited数组按顺序进行,0是第一个索引,因此需要减去1。在这种情况下,每次while循环后终止1次循环。如果无序,则数组将是暂时稀疏的,循环计数器计算它以正确的顺序“查看”所需的时间,这相当于交换次数,如果您从基于0的索引中减去1。非常酷。 - John Wright

6

供您参考,这是我编写的一个算法,用于生成排序数组所需的最少交换次数。它按照@Andrew Mao描述的方式查找循环。

/**
 * Finds the minimum number of swaps to sort given array in increasing order.
 * @param ar array of <strong>non-negative distinct</strong> integers. 
 *           input array will be overwritten during the call!
 * @return min no of swaps
 */
public int findMinSwapsToSort(int[] ar) {
    int n = ar.length;
    Map<Integer, Integer> m = new HashMap<>();
    for (int i = 0; i < n; i++) {
        m.put(ar[i], i);
    }
    Arrays.sort(ar);
    for (int i = 0; i < n; i++) {
        ar[i] = m.get(ar[i]);
    }
    m = null;
    int swaps = 0;
    for (int i = 0; i < n; i++) {
        int val = ar[i];
        if (val < 0) continue;
        while (val != i) {
            int new_val = ar[val];
            ar[val] = -1;
            val = new_val;
            swaps++;
        }
        ar[i] = -1;
    }
    return swaps;
}

你能解释一下最后一个 while 循环中发生了什么吗? - GURMEET SINGH
@Spindoctor 是的,我已经弄明白了。 - GURMEET SINGH
@GURMEETSINGH,请问你能分享一下你的逻辑理解吗?我无法理解它。 - Spindoctor
1
@Spindoctor 在第一个循环中,它将实际值保留为键,原始数组中的位置作为值。然后使用Collections.sort()对数组进行排序。在第二个循环中,我们获取排序前的数组索引。在最后一个循环中,我们将循环元素设置为-1。 - GURMEET SINGH
@Spindoctor,理解这个最好的方法是取一个小数组,通过每一步并记录不同变量中的值。这将帮助您看到数组中的循环以及我们如何使这些元素为-1。 - GURMEET SINGH
显示剩余2条评论

3

@Archibald,我喜欢你的解决方案,我最初的假设也是将数组排序是最简单的解决方法,但是我认为没有必要进行所谓的反向遍历,即枚举数组,排序数组然后计算枚举的交换。

我发现将数组中的每个元素减1,然后计算排序所需的交换更简单。

这是我的改进/解决方案:

def swap(arr, i, j):
    tmp = arr[i]
    arr[i] = arr[j]
    arr[j] = tmp

def minimum_swaps(arr):

    a = [x - 1 for x in arr]

    swaps = 0
    i = 0
    while i < len(a):
        if a[i] == i:
            i += 1
            continue
        swap(a, i, a[i])
        swaps += 1

    return swaps

关于证明最优性,我认为@arax说得很有道理。


2

// 假设我们只处理以零开始的序列

function minimumSwaps(arr) {
    var len = arr.length
    var visitedarr = []
    var i, start, j, swap = 0
    for (i = 0; i < len; i++) {
        if (!visitedarr[i]) {
            start = j = i
            var cycleNode = 1
            while (arr[j] != start) {
                j = arr[j]
                visitedarr[j] = true
                cycleNode++
            }
            swap += cycleNode - 1
        }
    }
    return swap
}

2

我非常喜欢@Ieuan Uys在Python中提出的解决方案。

我对他的解决方案进行了改进;

  • 为了增加速度,while循环的迭代次数减少了一个:while i < len(a) - 1
  • 交换函数被解封装成一个单一的函数。
  • 添加了大量的代码注释以增加可读性。

我的Python代码。

def minimumSwaps(arr):
    #make array values starting from zero to match index values. 
    a = [x - 1 for x in arr] 

    #initialize number of swaps and iterator.
    swaps = 0
    i = 0

    while i < len(a)-1:
        if a[i] == i:
            i += 1
            continue

        #swap.
        tmp = a[i] #create temp variable assign it to a[i]
        a[i] = a[tmp] #assign value of a[i] with a[tmp]
        a[tmp] = tmp #assign value of a[tmp] with tmp (or initial a[i])

        #calculate number of swaps.
        swaps += 1

    return swaps

对长度为n的数组进行编码的详细说明;

我们逐一检查数组中除最后一个值外的每个值(n-1次迭代)。如果该值与数组索引不匹配,则将此值发送到其索引值等于其值的位置。例如,如果在a[0] = 3,则此值应与a[3]交换。a[0]和a[3]被交换。值3将位于其应在的a[3]处。一个值被发送到了它的位置。我们还剩下n-2次迭代。我不关心现在a[0]是什么。如果该位置上不是0,则稍后将通过另一个值进行交换。因为另一个值也存在错误的位置,这将在后面的while循环中被识别。

真实示例

a[4, 2, 1, 0, 3]
#iteration 0, check a[0]. 4 should be located at a[4] where the value is 3. Swap them. 
a[3, 2, 1, 0, 4] #we sent 4 to the right location now.  
#iteration 1, check a[1]. 2 should be located at a[2] where the value is 1. Swap them. 
a[3, 1, 2, 0, 4] #we sent 2 to the right location now.  
#iteration 2, check a[2]. 2 is already located at a[2]. Don't do anything, continue. 
a[3, 1, 2, 0, 4] 
#iteration 3, check a[3]. 0 should be located at a[0] where the value is 3. Swap them. 
a[0, 1, 2, 3, 4] #we sent 0 to the right location now.  
# There is no need to check final value of array. Since all swaps are done. 

1

Swift 4版本:

func minimumSwaps(arr: [Int]) -> Int {

      struct Pair {
         let index: Int
         let value: Int
      }

      var positions = arr.enumerated().map { Pair(index: $0, value: $1) }
      positions.sort { $0.value < $1.value }
      var indexes = positions.map { $0.index }

      var swaps = 0
      for i in 0 ..< indexes.count {
         var val = indexes[i]
         if val < 0 {
            continue // Already visited.
         }
         while val != i {
            let new_val = indexes[val]
            indexes[val] = -1
            val = new_val
            swaps += 1
         }
         indexes[i] = -1
      }
      return swaps
}

1
@bekce 提供的解决方案非常好。如果使用C#,设置修改后的数组ar的初始代码可以简洁地表达为:
var origIndexes = Enumerable.Range(0, n).ToArray();
Array.Sort(ar, origIndexes);

然后在代码的其余部分中使用origIndexes而不是ar


0

找到将1..N的排列放入顺序所需的最小交换次数。

我们可以利用已知的排序结果:1..N,这意味着我们实际上不必进行交换,只需计算它们。

1..N的洗牌称为排列,并由不相交的循环置换组成,例如,这个1..6的排列:

1 2 3 4 5 6
6 4 2 3 5 1

由循环置换(1,6)(2,4,3)(5)组成

1->6(->1) cycle: 1 swap
2->4->3(->2) cycle: 2 swaps
5(->5) cycle: 0 swaps

因此,一个由k个元素组成的循环需要k-1次交换才能排序。

由于我们知道每个元素“属于”哪个位置(即值k属于位置k-1),因此我们可以轻松地遍历该循环。从0开始,我们得到6,其属于5, 然后我们找到了1,它属于0,我们又回到了起点。

为了避免以后重新计算循环,我们跟踪哪些元素已经访问过—或者你可以执行交换,以便在以后访问时元素会出现在正确的位置。

最终代码:

def minimumSwaps(arr):
    visited = [False] * len(arr)
    numswaps = 0
    for i in range(len(arr)):
        if not visited[i]:
            visited[i] = True
            j = arr[i]-1
            while not visited[j]:
                numswaps += 1
                visited[j] = True
                j = arr[j]-1
    return numswaps

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接