题解:洛谷 P1459 三值的排序

【题目来源】

洛谷:P1459 [USACO2.1] 三值的排序 Sorting a Three-Valued Sequence - 洛谷

【题目描述】

排序是一种很频繁的计算任务。现在考虑最多只有三值的排序问题。一个实际的例子是,当我们给某项竞赛的优胜者按金银铜牌排序的时候。在这个任务中可能的值只有三种 1,2,3。我们用交换的方法把他排成升序的。

写一个程序计算出,给定的一个 1,2,3 组成的数字序列,排成升序所需的最少交换次数

【输入】

第一行一个正整数 n,表示奖牌个数。接下来 n 行,每行一个 [1,3] 内的整数,表示奖牌。

【输出】

输出一行一个整数,表示排成升序所需的最少交换次数。

【输入样例】

9
2
2
1
3
3
3
2
3
1

【输出样例】

4

【核心思想】

  1. 问题分析:给定一个由 \(1, 2, 3\) 组成的序列,要求通过交换操作将其排成升序(所有 \(1\) 在前,\(2\) 在中,\(3\) 在后),求最少交换次数。排序后 \(1\) 的区间为 \([1, num[1]]\)\(2\) 的区间为 \([num[1]+1, num[1]+num[2]]\)\(3\) 的区间为 \([num[1]+num[2]+1, n]\)

  2. 算法选择

    • 贪心交换策略:优先处理"直接互换"(一次交换解决两个错位),再处理"三角互换"(两次交换解决三个错位)
    • 错位分类:将错位的元素按所在区间和目标区间分类,统计六种错位类型
  3. 关键步骤

    • 统计数量:遍历序列,统计 \(num[1], num[2], num[3]\)(各数字出现次数),确定排序后三个区间的边界
    • 第一轮:直接互换(一次交换修复两个位置):
      • \([1,2]\) 互换:在 \(1\) 的区间内找 \(2\),在 \(2\) 的区间内找 \(1\),配对交换
      • \([1,3]\) 互换:在 \(1\) 的区间内找 \(3\),在 \(3\) 的区间内找 \(1\),配对交换
      • \([2,3]\) 互换:在 \(2\) 的区间内找 \(3\),在 \(3\) 的区间内找 \(2\),配对交换
    • 第二轮:三角互换(一次交换修复一个位置,剩余错位形成三角循环):
      • \(1\) 的区间内找非 \(1\) 的元素,与后面区间中的 \(1\) 交换
      • \(2\) 的区间内找非 \(2\) 的元素,与后面区间中的 \(2\) 交换
    • 输出结果:总交换次数 \(ans\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n^2)\),最坏情况下需要双重循环查找配对元素
    • 空间复杂度:\(O(n)\),存储序列数组
  5. 贪心交换的核心思想

    • 直接互换优先\(a[i]=2, a[j]=1\)\(i\)\(1\) 区间、\(j\)\(2\) 区间时,一次交换同时修复两个位置,成本最低
    • 三角循环处理:直接互换后,剩余的错位元素形成三角循环(如 \(1\) 区间有 \(2\)\(2\) 区间有 \(3\)\(3\) 区间有 \(1\)),需要两次交换修复三个位置
    • 区间边界固定:排序后各数字的位置区间由数量唯一确定,无需实际排序即可知道每个元素的目标位置
    • 贪心最优性:优先执行直接互换不会破坏后续更优解,且每次直接互换减少两个错位,是局部最优选择
    • 适用于有限取值排序、最小交换次数、错位修复类问题

【解题思路】

【算法标签】

普及- #贪心

【代码详解】

#include <bits/stdc++.h>
using namespace std;
int n, num[5], a[1005], ans=0;
int main()
{
    cin >> n;  // 输入n
    for (int i=1; i<=n; i++) {  // 依次输入n个值
        cin >> a[i];
        num[a[i]]++;  // 同时记录每个数的数量
    }
    for (int i=1; i<=num[1]; i++) {  // 在排序后1的范围和2的范围内
        for (int j=num[1]+1; j<=num[1]+num[2]; j++) {
            if (a[i]==2 && a[j]==1) {  // 查找[2,1]和[1,2]的情况
                swap(a[i], a[j]);  // 进行对调
                ans++;
                break;
            }
        }
    }
    for (int i=1; i<=num[1]; i++) {  // 在排序后1的范围和3的范围内
        for (int j=num[1]+num[2]+1; j<=n; j++) {
            if (a[i]==3 && a[j]==1) {  // 查找[3,1]和[1,3]的情况
                swap(a[i], a[j]);  // 进行对调
                ans++;
                break;
            }
        }
    }
    for (int i=num[1]; i<=num[1]+num[2]; i++) {  // 在排序后2的范围和3的范围内
        for (int j=num[1]+num[2]+1; j<=n; j++) {
            if (a[i]==3 && a[j]==2) {  // 查找[3,2]和[2,3]的情况
                swap(a[i], a[j]);  // 进行对调
                ans++;
                break;
            }
        }
    }
    for (int i=1; i<=num[1]; i++) {  // 在排序后1的范围和2、3范围内
        for (int j=num[1]+1; j<=n; j++) {
            if (a[i]!=1 && a[j]==1) {  // 查找不等于1和等于1的
                swap(a[i], a[j]);  // 进行对调
                ans++;
                break;
            }
        }
    }
    for (int i=num[1]+1; i<=num[1]+num[2]; i++) {  // 在排序后2的范围和3的范围内
        for (int j=num[1]+num[2]+1; j<=n; j++) {
            if (a[i]!=2 && a[j]==2) {  // 查找不等于2和等于2的
                swap(a[i], a[j]);  // 进行对调
                ans++;
                break;
            }
        }
    }
    cout << ans << endl;  // 输出调换次数
    return 0;
}

【运行结果】

9
2
2
1
3
3
3
2
3
1
4
posted @ 2026-08-03 08:47  团爸讲算法  阅读(14)  评论(0)    收藏  举报