排序算法之:基于“最小交换次数排序”(Minimum Swaps to Sort)的贪心选择策略证明与正确性分析
题目描述:
我们已知一个“最小交换次数排序”的经典图论解法:对于一个由 n 个不同元素组成的数组,其任意排列可以通过交换任意两个元素来排序。最少的交换次数等于 n 减去排列中“循环”的数量。这里,我们将一个排列视为一个置换,每个元素指向它排序后应该去的位置,从而形成若干个环。每个长度为 L 的环,只需 L-1 次交换即可归位,因此最小交换次数是 Σ(L_i - 1) = n - 环数。
现在,我们考虑一种贪心策略:从左到右扫描数组。对于位置 i,如果当前元素 arr[i] 不等于其排序后的正确值,那么我们不是去寻找当前元素应该在的位置,而是将 arr[i] 与当前正位于位置 i 的正确元素进行交换。换句话说,我们查找那个“本应”在位置 i 的元素现在在哪里,然后将它换到 i 位置。
本题要求:
- 严格描述这个贪心策略的算法步骤。
- 证明这个贪心策略得到的结果,其交换次数恰好等于“最小交换次数”。
- 详细分析为什么这个策略是正确的,即它不会引入多余的交换,并且最终能够完成排序。
解题过程:
我们将问题分解,循序渐进地讲解。
第一步:问题与基本概念定义
- 输入:一个长度为 n 的数组
arr,包含 n 个互不相同的元素。为简化,我们假设这些元素是 1 到 n 的一个排列。如果不是,我们可以先记录元素与其排序后位置的映射关系,但本质相同。排序目标是升序排列[1, 2, 3, ..., n]。 - 操作:允许的交换操作是交换数组中任意两个位置的元素。每次交换成本为 1。目标是找到最少的交换次数,使数组完全有序。
- 置换的循环分解:
- 将当前排列看作一个置换 π。对于位置 i,π(i) 表示当前在位置 i 的元素。
- 由于是 1 到 n 的排列,每个元素的目标位置是确定的。例如,元素 x 的目标位置是 x(因为我们目标是升序
[1,2,...,n])。 - 我们从位置 i 开始,看当前元素 π(i) 应该去哪里(即位置 π(i)),然后看那个位置上现在是什么元素(即 π(π(i))),如此追踪,就会形成一个循环。例如,对于排列
[5, 1, 3, 4, 2],我们从位置 1(元素 5)开始:5 应该去位置 5,位置 5 目前是 2,2 应该去位置 2,位置 2 目前是 1,1 应该去位置 1,回到了起点。这就形成了一个循环 (1, 5, 2)。 - 整个排列可以分解为若干个不相交的循环。每个循环内的元素互相“占据”了彼此应该在的位置。
- 已知结论:对于一个长度为 L 的循环,将其所有元素归位的最小交换次数是 L-1。因此,整个排列的最小交换次数 = Σ(每个循环的长度 - 1) = n - 循环的个数。
第二步:贪心策略的算法描述
算法步骤(给定数组 arr[1..n],目标是 [1,2,...,n],索引从1开始):
- 初始化交换次数
swaps = 0。 - 从
i = 1到n遍历:
a. 如果arr[i]已经是其正确值(即arr[i] == i),则什么也不做,继续下一个 i。
b. 否则(arr[i] != i):
- 我们需要将正确的元素i放到位置 i 上。
- 找到元素i当前所在的位置j(即满足arr[j] == i的 j)。
- 交换arr[i]和arr[j]。
-swaps = swaps + 1。 - 循环结束后,数组有序,返回
swaps。
注意:这个策略是“贪心”的,因为每次当遇到位置 i 不正确时,我们立即采取行动将其修正,并且是选择将位置 i 一次性放上正确元素 i,而不是考虑更长远的影响。
第三步:将贪心策略映射到循环分解模型
为了证明其正确性,关键在于观察每一次交换如何影响排列的循环结构。
- 初始状态:排列 π 被分解为若干个不相交的循环。我们特别关注那些长度大于 1 的循环。长度等于 1 的循环(即
arr[i]==i)是已经就位的,算法会跳过。 - 一次交换操作分析:假设在位置 i,我们有
arr[i] != i。设j是元素i的当前位置(arr[j] = i)。交换arr[i]和arr[j]。- 情况分析:位置 i 和 j 在循环分解中是什么关系?
- 由于
arr[j] = i,这意味着在当前的置换中,从 j 出发,有π(j) = i。 - 又因为
arr[i]是某个值 x(x ≠ i),所以我们有π(i) = x。 - 现在,元素 i 的“目标位置”是 i。所以,在循环中,存在一条链路:
... → j → i → x → ...。即 i 和 j 在同一个循环中,并且 j 是 i 的前驱(在循环的指向关系上,j 指向 i)。
- 由于
- 交换的效果:交换
arr[i]和arr[j]后,我们让arr[i] = i(正确了),而arr[j]变成了原来的arr[i](即 x)。 - 对循环结构的影响:交换操作将这个包含 i 和 j 的原始循环拆分了。具体来说,原来形如
(..., a, j, i, x, b, ...)的循环(其中 a 是 j 的前驱,b 是 x 的后继),在交换后,元素 i 被固定了(形成了自环(i)),剩下的部分形成了一个新循环(..., a, j, x, b, ...)。核心结论:一次这样的交换,将一个长度 L > 1 的循环,分解为一个长度为 1 的自循环(已归位的元素)和一个长度为 L-1 的循环。
- 情况分析:位置 i 和 j 在循环分解中是什么关系?
- 计数:每次执行交换操作,都会使一个元素归位(循环数 +1,因为新增一个自环),同时原始循环的长度减少 1。从整体看,交换次数正好等于“将一个长度为 L 的循环分解为 L 个自环”所需的次数,即 L-1 次。
第四步:严格证明贪心策略得到最小交换次数
我们通过循环不变式来证明。
-
循环不变式:在算法主循环的每次迭代开始(处理位置 i 之前),所有位置
< i的元素都已经在其正确的位置上(即对于所有 k < i,有 arr[k] = k),并且已执行的交换次数等于 (i-1) - 当前(此时)前 i-1 个位置所形成的“独立循环”数量。更直观地说,已完成的交换是处理前 i-1 个位置所必需的最小交换。 -
初始化:当 i=1 时,前 0 个位置是空的,不变式自然成立。
-
保持:假设在处理位置 i 之前,前 i-1 个位置都已正确。现在检查位置 i:
- 如果
arr[i] == i,则位置 i 已经正确。这对应于循环分解中,i 自身构成一个自环。我们没有增加交换,循环数(对于前 i 个位置)增加 1(因为这个自环),但 (i) - 循环数 保持不变,与 (i-1) - 旧循环数 一致,不变式保持。 - 如果
arr[i] != i,根据算法,我们找到元素 i 的位置 j(arr[j]=i)。由于前 i-1 个位置已正确,且 i 不在其位,所以 j 一定 > i。交换arr[i]和arr[j]。- 这次交换将包含位置 i 和 j 的原始循环(长度 L>=2)拆分为:位置 i 变成自环(已归位),剩下的部分构成一个新循环,且新循环中所有元素的位置都 >= i+1(因为前 i 个位置现在都正确了)。
- 交换后,位置 i 正确。前 i 个位置都正确了。
- 在循环结构上,我们增加了一个自环(循环数+1),但原始循环长度减1(总元素数不变)。从“最小交换次数”公式看,对于这个刚被处理的循环,我们进行了一次交换,使得“已归位元素”增加1,恰好符合其最小交换逻辑。
- 因此,已执行的交换次数增加1,而前 i 个位置形成的“独立循环”数量也相应变化,不变式得以保持。
- 如果
-
终止:当 i = n+1 时,循环结束。此时所有位置 1 到 n 都已正确。根据不变式,总交换次数等于 n 减去最终循环的总数。而最终状态,每个位置都是自环,共有 n 个循环。因此,总交换次数 = n - n = 0?这里需要仔细推敲。
更准确的陈述是:在整个过程中,每次交换恰好将一个长度大于1的循环拆分为一个自环和一个长度减1的新循环。初始有 C 个循环(包括自环和非自环)。最终有 n 个自环(n 个循环)。每进行一次交换,循环数增加 1。设初始非自环(长度>1)的循环有 K 个,自环有 (C-K) 个。最终循环数为 n。那么交换次数 = 最终循环数 - 初始循环数 = n - C。
另一方面,初始最小交换次数公式为 n - C(因为总元素 n,初始循环数 C)。所以我们的交换次数恰好等于 n - C,即理论最小值。
第五步:总结与示例
我们通过一个例子验证:arr = [5, 1, 3, 4, 2]。
- 初始循环分解:(1,5,2) 和 (3) 和 (4)。循环数 C=3,理论最小交换次数 = 5-3=2。
- 贪心算法执行:
- i=1, arr[1]=5≠1,找到元素1在位置2,交换 arr[1]和arr[2] -> [1,5,3,4,2], swaps=1。
- i=2, arr[2]=5≠2,找到元素2在位置5,交换 arr[2]和arr[5] -> [1,2,3,4,5], swaps=2。
- i=3,4,5 都已正确,结束。
- 结果 swaps=2,等于理论最小值。
结论:所述贪心策略每一步都固定当前位置,且每次交换都恰好将一个循环拆出一个自环,因此总交换次数严格等于 n 减去初始循环数,即最小交换次数。该策略的正确性得到了循环分解模型和循环不变式证明的保证。