火龙信奥
  • 分享
  • 课程
  • 题库
  • 打卡
    • 代码对战
    • 快速对战
  • 题单
  • 团队
  • 荣誉墙
  • 商城
  • 登录 / 注册

数学概念:置换与不相交循环

作者: 作者的头像   huolong , 时间:2026-08-16 21:59:45 , 所有人可见, 阅读  51

这个问题在数学上有一个非常经典且优美的背景,它直接关联到抽象代数中的置换群(Permutation Group)以及置换的循环分解(Cycle Decomposition)。

下面将为您引出相关的数学概念、给出最少交换次数公式的数学证明,并提供一个基于 C++11 的高效算法实现。


一、 数学概念:置换与不相交循环

为了方便讨论,我们假设数组中的元素是互不相同的。如果存在重复元素,我们可以通过记录它们原本的相对顺序(如稳定排序)来将其转化为无重复元素的情况。

1. 置换(Permutation)

设 $S = {1, 2, \dots, n}$ 是一个有限集。从 $S$ 到 $S$ 的一个双射(一双一对应) $\sigma$ 称为 $S$ 上的一个置换。 在我们的问题中,将一个乱序数组通过调整位置变成有序数组,本质上就是寻找一个置换,将其恢复为恒等置换(Identity Permutation,即每个元素都在其正确位置上的状态)。

2. 循环(Cycle)与不相交循环分解

对于一个置换,如果我们从某个位置 $i$ 开始,查看它应该去的位置 $j$,再看 $j$ 应该去的位置……由于集合有限,最终一定会回到 $i$。这形成了一个封闭的环,称为循环(Cycle)。 例如,一个长度为 $k$ 的循环可以记为 $(a_1, a_2, \dots, a_k)$,表示 $a_1 \to a_2 \to \dots \to a_k \to a_1$。

重要定理:任何一个置换都可以唯一地分解为若干个互不相交的循环的乘积。

例子说明

假设数组为 [4, 3, 2, 1],排序后的目标数组为 [1, 2, 3, 4]。 我们将当前元素与它们应该在的目标位置进行映射(以 0 为起始索引): * 元素 4(当前在索引 0)的目标索引是 3 * 元素 1(当前在索引 3)的目标索引是 0 这构成了一个循环:$(0, 3)$ * 元素 3(当前在索引 1)的目标索引是 2 * 元素 2(当前在索引 2)的目标索引是 1 这构成了另一个循环:$(1, 2)$

整个数组的置换可以分解为 2 个不相交的循环:$(0, 3)$ 和 $(1, 2)$。


二、 数学证明:最少交换次数

定理:对于一个长度为 $n$ 的数组,如果它对应的置换可以分解为 $c$ 个不相交的循环(包括长度为 1 的循环,即已经在正确位置上的元素),那么将其排好序所需的最少交换次数为: $$\text{Min Swaps} = n - c$$

证明过程:

  1. 目标状态: 当数组完全排好序时,每个元素都在其对应的位置上。此时有 $n$ 个长度为 1 的循环。因此,目标状态下的循环总数 $c_{\text{target}} = n$。

  2. 单次交换(对换)对循环个数的影响: 单次任意位置的交换在代数上称为一个对换(Transposition)。我们来看对换如何影响循环的个数:

  3. 情况 A:交换同一个循环内的两个元素。 假设有一个循环 $C = (x_1, \dots, x_i, \dots, x_j, \dots, x_k)$。如果我们交换位置 $x_i$ 和 $x_j$ 的元素,这个循环会被分裂成两个独立的不相交循环: $$(x_1, \dots, x_i, x_{j+1}, \dots, x_k) \quad \text{和} \quad (x_{i+1}, \dots, x_j)$$ 这使得总循环数 $c$ 增加 1。
  4. 情况 B:交换两个不同循环中的元素。 如果我们选择处于不同循环中的两个元素进行交换,根据上述过程的逆过程,这两个循环会被合并成一个大循环。 这使得总循环数 $c$ 减少 1。

  5. 最少步骤推导: 我们的目标是让循环个数从初始的 $c$ 增加到最终的 $n$。 为了以最快的速度达到目标,我们每一次交换都应该尽可能多地增加循环的个数。 由上述分析可知,单词交换最多只能让循环个数增加 1(即每次都只在同一个循环内部进行交换)。 因此,要使循环数从 $c$ 增长到 $n$,至少需要进行的交换次数为: $$\text{次数} \ge n - c$$

同时,通过每次选择同一个循环内的两个元素进行交换,我们确实可以保证每次交换都使循环数恰好加 1。因此,这个下界是完全可以达到的。 证毕。


三、 C++11 代码实现

在实际编程中,我们不需要显式地去构建复杂的置换群结构。我们可以通过以下步骤高效求解: 1. 将元素及其原始索引绑定,然后对数组进行排序,从而确定每个元素排好序后应该去的“目标索引”。 2. 使用一个 visited 数组来标记哪些位置已经被处理过。 3. 遍历数组,对于未访问的位置,沿着它的目标索引一直追踪下去,直到回到起点,从而完整找出该位置所在的循环,并记录该循环的长度 $L$。 4. 一个长度为 $L$ 的循环需要 $L - 1$ 次交换来解开(这与 $n-c$ 的公式是一致的,因为对于这个子系统,元素数是 $L$,循环数是 1,交换次数即 $L-1$)。将所有循环的贡献累加即可。

以下是具体的 C++11 实现代码:

#include <iostream>
#include <vector>
#include <algorithm>

// 函数用于计算最少交换次数
int minSwaps(const std::vector<int>& arr) {
    int n = arr.size();

    // 存储元素值及其原始索引
    std::vector<std::pair<int, int>> arrPos(n);
    for (int i = 0; i < n; ++i) {
        arrPos[i] = {arr[i], i};
    }

    // 按照元素值进行升序排序,确定每个元素在有序状态下应该在的位置
    std::sort(arrPos.begin(), arrPos.end(), 
              [](const std::pair<int, int>& a, const std::pair<int, int>& b) {
                  return a.first < b.first;
              });

    // 用于记录该位置的元素是否已被归入某个循环中
    std::vector<bool> visited(n, false);
    int total_swaps = 0;

    for (int i = 0; i < n; ++i) {
        // 如果该元素已经被访问过,或者它本来就在正确的位置上(即排序后的原索引仍为i)
        if (visited[i] || arrPos[i].second == i) {
            continue;
        }

        // 寻找一个新循环的长度
        int cycle_size = 0;
        int curr = i;

        while (!visited[curr]) {
            visited[curr] = true;
            // 移向该元素原本应该去的位置(即它在排序数组中的正确位置)
            curr = arrPos[curr].second;
            cycle_size++;
        }

        // 一个长度为 L 的循环需要 L - 1 次交换
        if (cycle_size > 1) {
            total_swaps += (cycle_size - 1);
        }
    }

    return total_swaps;
}

int main() {
    // 示例 1: [4, 3, 2, 1] -> 理论上 c = 2, n = 4, 最少交换 4 - 2 = 2 次
    std::vector<int> arr1 = {4, 3, 2, 1};
    std::cout << "Array 1 min swaps: " << minSwaps(arr1) << " (Expected: 2)" << std::endl;

    // 示例 2: [2, 3, 1, 5, 4] -> 理论上最少交换 3 次
    std::vector<int> arr2 = {2, 3, 1, 5, 4};
    std::cout << "Array 2 min swaps: " << minSwaps(arr2) << " (Expected: 3)" << std::endl;

    return 0;
}

复杂度分析

  • 时间复杂度:$O(N \log N)$。主要开销在对辅助数组进行排序以确定目标位置。后续寻找循环的遍历中,每个节点最多被访问两次(一次在 for 循环,一次在 while 循环),因此寻找循环部分的时间复杂度为 $O(N)$。如果数组元素本身范围有限(例如是 $1$ 到 $N$ 的排列),我们可以用计数排序等方式将时间复杂度优化到 $O(N)$。
  • 空间复杂度:$O(N)$。需要额外的空间存储带索引的数组 arrPos 以及标记数组 visited。

—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com

关于火龙

  • 关于我们
  • 学员获奖
  • 预约试听
  • ACM课程
  • CSP课程
  • 学习指南

帮助中心

  • 用户协议
  • 打字练习
  • 在线画图
  • DevC++下载
  • CSP报名
  • GESP官网

推荐课程

  • C++零基础入门(可试看)
  • C++进阶提升
  • GESP考级辅导
  • GESP打卡
  • CSP-J/S打卡

公众号

火龙信奥公众号二维码

地址:义乌市北门街188号新天地商厦二楼2F 邮箱:wdlok305@126.com

© 2017-2026 义乌市睿码科技有限公司版权所有 浙ICP备2021013995号

火龙信奥
请输入登录信息


请完成安全验证
验证码底图 滑块
向右拖动滑块完成验证
请输入用户名 / 绑定的手机号码



请输入注册信息(手机号验证码注册)





验证码5分钟有效,60秒内不可重复获取,每日最多3次

微信登录

微信登录二维码

正在生成二维码...

账号已过期,请续期。
去续期

绑定手机号

📱

为了更好地保护您的账号安全,享受完整的平台服务

请您尽快绑定手机号码