题解:洛谷 P9959 Sleepy Cow Sorting

【题目来源】

洛谷:P9959 [USACO19JAN] Sleepy Cow Sorting B - 洛谷

【题目描述】

Farmer John正在尝试将他的 \(N\) 头奶牛(\(1\le N\le 100\)),方便起见编号为 \(1\ldots N\),在她们前往牧草地吃早餐之前排好顺序。

当前,这些奶牛以 \(p_1,p_2,p_3,\ldots,p_N\) 的顺序排成一行,Farmer John 站在奶牛 \(p_1\) 前面。他想要重新排列这些奶牛,使得她们的顺序变为 \(1,2,3,\ldots,N\),奶牛 \(1\) 在 Farmer John 旁边。

今天奶牛们有些困倦,所以任何时刻都只有直接面向 Farmer John 的奶牛会注意听 Farmer John 的指令。每一次他可以命令这头奶牛沿着队伍向后移动 \(k\) 步,\(k\) 可以是范围 \(1\ldots N−1\) 中的任意数。她经过的 \(k\) 头奶牛会向前移动,腾出空间使得她能够插入到队伍中这些奶牛之后的位置。

例如,假设 \(N=4\),奶牛们开始时是这样的顺序:

FJ: \(4, 3, 2, 1\)

唯一注意 FJ 指令的奶牛是奶牛 \(4\)。当他命令她向队伍后移动 \(2\) 步之后,队伍的顺序会变成:

FJ: \(3, 2, 4, 1\)

现在唯一注意 FJ 指令的奶牛是奶牛 \(3\),所以第二次他可以给奶牛 \(3\) 下命令,如此进行直到奶牛们排好了顺序。

Farmer John 急欲完成排序,这样他就可以回到他的农舍里享用他自己的早餐了。请帮助他求出将奶牛们排好顺序所需要的最小操作次数。

【输入】

输入的第一行包含 \(N\)

第二行包含 \(N\) 个空格分隔的整数,\(p_1,p_2,p_3,\ldots,p_N\),表示奶牛们的起始顺序。

【输出】

输出一个整数,为 Farmer John 采用最佳策略可以将这 \(N\) 头奶牛排好顺序所需要的操作次数。

【输入样例】

4
1 2 4 3

【输出样例】

3

【核心思想】

  1. 问题分析:给定一个 \(1\)\(N\) 的排列 \(p\),每次操作只能移动当前队伍最前面的奶牛(即 \(p_1\)),将其向后移动任意 \(k\) 步(\(1 \le k \le N-1\)),插入到第 \(k+1\) 个位置之后。目标是将序列排成 \(1, 2, \dots, N\),求最小操作次数。由于只有队首奶牛能被移动,且移动后新队首变为原序列的第二头奶牛,因此操作本质是依次将前缀中的元素插入到后方合适位置

  2. 算法选择

    • 找最长已排序后缀:关键观察是,操作只会移动队首元素,因此队伍末尾的一段如果已经按递增顺序排列且是最终序列的后缀,则这些奶牛无需再移动。最小操作次数等于 \(N\) 减去从末尾开始的最长连续递增后缀的长度。因为所有不在这个后缀中的前缀元素都必须被依次移动到后方合适位置。
  3. 关键步骤

    • 读入:读取 \(N\) 和排列 \(p[1..N]\)
    • 从后向前扫描:初始化 ans = N - 1(最坏情况,只有最后一个元素在正确位置)。从 \(i = N-1\) 向下遍历到 \(1\)
      • \(p[i] < p[i+1]\),说明从 \(i\) 到末尾是连续递增的,则后缀长度增加,ans--
      • 否则,递增中断,说明 \(i\) 及之前的元素都必须被移动,此时剩余的前缀长度为 \(i\),答案为 \(i\),直接 break
    • 输出答案:若整个序列已经递增,则 ans = 0
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N)\),只需一次逆向扫描。
    • 空间复杂度:\(O(N)\),存储排列。
  5. 贪心 + 最长后缀的核心思想

    • 操作不可逆性:每次只能移动队首,意味着一旦某个元素被移到后方,它就不会再回到前面。因此,最终留在队伍前部的元素必须原本就在正确顺序中,且不能被移动。
    • 后缀稳定性:队伍末尾的某一段如果已经是递增的连续序列,且它们本身就是最终序列的最后几个数,那么它们无需移动。
    • 最优性:为了最小化操作次数,应保留尽可能长的末尾递增后缀不动,将前面所有元素依次移动到后缀中的正确位置。每次移动一个队首元素,恰好可以将其插入到后缀的适当位置,因此操作次数等于前缀长度。
    • 适用场景:适用于只能移动序列前端元素,且目标为有序序列的排序问题。核心是寻找从末尾开始的最长递增连续子序列。

【解题思路】

【算法标签】

普及- #其他排序

【代码详解】

#include <bits/stdc++.h>
using namespace std;

int n;
int a[105];
ifstream filein("sleepy.in");
ofstream fileout("sleepy.out");
int main()
{
    // cin >> n;
    filein >> n;  // 输入n
    for(int i=1;i<=n;i++)  // 遍历n个数
        filein >> a[i];  // 输入至a数组中
    int ans = n-1;  // 最多调整n-1次,如1 2 5 4 3
    for(int i=n-1;i>=1;i--)  // 从倒数第二个位置向前遍历
    {
        if(a[i] < a[i+1])  // 如果从后往前发现是连续递减,则每找到一个递减的数
            ans--;  // 结果减1
        else  // 满足该条件,说明前面i个数都要挪到最后,需要进行i次操作
            break;
    }
    fileout << ans << '\n';  // 输出结果
    return 0;
}

【运行结果】

4
1 2 4 3
3
posted @ 2026-08-30 09:18  团爸讲算法  阅读(7)  评论(0)    收藏  举报