题解:洛谷 P4378 Out of Sorts

【题目来源】

洛谷:P4378 [USACO18OPEN] Out of Sorts S - 洛谷

【题目描述】

留意着农场之外的长期职业生涯的可能性,奶牛 Bessie 开始在不同的在线编程网站上学习算法。

她到目前为止最喜欢的算法是“冒泡排序”。这是 Bessie 的对长度为 \(N\) 的数组 \(A\) 进行排序的奶牛码实现。

sorted = false
while (not sorted):
   sorted = true
   moo
   for i = 0 to N-2:
      if A[i+1] < A[i]:
         swap A[i], A[i+1]
         sorted = false

显然,奶牛码中的“moo”指令的作用只是输出“moo”。奇怪的是,Bessie 看上去执着于在她的代码中的不同位置使用这个语句。

给定一个输入数组,请预测 Bessie 的代码会输出多少次“moo”。

【输入】

输入的第一行包含 \(N\)\(1 \leq N \leq 100,000\))。接下来 \(N\) 行描述了 \(A[0] \ldots A[N-1]\),每个数都是一个范围为 \(0 \ldots 10^9\) 的整数。输入数据不保证各不相同。

【输出】

输出“moo”被输出的次数。

【输入样例】

5
1
5
3
8
2

【输出样例】

4

【核心思想】

  1. 问题分析:给定长度为 \(N\) 的数组,模拟冒泡排序过程,求输出 "moo" 的次数。冒泡排序的 while 循环每次遍历数组,将未归位的最大元素逐步后移,循环次数等于数组中最长"需要被冒泡"的链的长度。本质上是逆序对分析 + 最长下降子序列(LDS)问题:moo 输出次数等于最长下降子序列长度 \(+ 1\)

  2. 算法选择

    • 离散化 + 树状数组求 LDS:将数值离散化后,用树状数组维护前缀和,倒序或正序查询比当前元素大的已处理元素数量,求最长下降子序列长度
  3. 关键步骤

    • 读取数据:读入 \(N\) 和数组 \(a[1..N]\)
    • 离散化
      • 按数值排序,相同数值映射到同一离散值
      • 按原始位置排序恢复顺序
    • 树状数组求 LDS\(i = 1\)\(N\)):
      • add(a[i].newval, 1):将当前元素加入树状数组
      • query(n) - query(a[i].newval):统计已处理中比当前大的元素数
      • \(ans = \max(ans, \text{上述值})\)
    • 输出\(ans + 1\)(LDS 长度 \(+ 1\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N \log N)\),排序 + 树状数组操作
    • 空间复杂度:\(O(N)\),树状数组和结构体数组
  5. 冒泡排序与 LDS 的核心思想

    • 冒泡排序的轮次:每轮 while 循环将至少一个未归位元素冒泡到正确位置,轮次等于最长"逆序链"长度
    • 最长下降子序列的等价性:若存在长度为 \(L\) 的下降子序列,则至少需要 \(L\) 轮冒泡才能将其完全排序,每轮处理一个逆序对
    • 树状数组优化:用树状数组 \(O(\log N)\) 维护前缀和,将 LDS 的 \(O(N^2)\) DP 优化到 \(O(N \log N)\)
    • \(+1\) 的修正:代码中 ans++ 是因为 query(n) - query(a[i].newval) 统计的是"已处理中比当前大的元素数",最长下降子序列长度需要 \(+1\)(包含当前元素自身)
    • 适用于排序分析、逆序对统计、最长子序列类问题

【算法标签】

普及 #树状数组

【代码详解】

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

#define int long long  // 使用长整型
const int N = 1000005;  // 最大数组大小

// 结构体:存储数值、原始位置和离散化后的新值
struct Node
{
    int val;     // 原始数值
    int pos;     // 原始位置(下标)
    int newval;  // 离散化后的新值
} a[N];

int tr[N];    // 树状数组,用于统计
int n;        // 数组长度
int ans;      // 最长下降子序列长度(LDS)

/**
 * 计算lowbit:获取x的最低位的1
 * @param x 输入数值
 * @return x的最低位的1所代表的值
 */
int lowbit(int x)
{
    return x & -x;  // 利用补码性质
}

/**
 * 树状数组单点更新操作
 * @param x 更新位置
 * @param c 增加的值
 */
void add(int x, int c)
{
    // 树状数组标准更新操作,向上更新所有相关节点
    for (int i = x; i <= n; i += lowbit(i))
        tr[i] += c;
}

/**
 * 树状数组前缀和查询操作
 * @param x 查询位置
 * @return 前x个位置的和
 */
int query(int x)
{
    int res = 0;
    // 树状数组标准查询操作,向下累加所有相关节点
    for (int i = x; i; i -= lowbit(i))
        res += tr[i];
    return res;
}

/**
 * 比较函数1:按数值升序排列
 * 用于离散化处理
 */
bool cmp(Node x, Node y)
{
    return x.val < y.val;  // 数值小的排在前面
}

/**
 * 比较函数2:按位置升序排列
 * 用于恢复原始顺序
 */
bool cmp2(Node x, Node y)
{
    return x.pos < y.pos;  // 位置小的排在前面
}

signed main()
{
    // 输入数组长度
    cin >> n;

    // 读入数组并记录原始位置
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i].val;  // 读入数值
        a[i].pos = i;     // 记录原始位置
    }

    // 第一步:按数值升序排序,准备离散化
    sort(a + 1, a + n + 1, cmp);

    int cnt = 0;  // 离散化计数器

    // 离散化处理:将数值映射为连续的整数
    for (int i = 1; i <= n; i++)
    {
        // 如果当前数值与前一个不同,计数器加1
        if (a[i].val != a[i - 1].val)
            cnt++;

        // 为当前数值分配新的离散化值
        a[i].newval = cnt;
    }

    // 第二步:按原始位置排序,恢复数组的原始顺序
    sort(a + 1, a + n + 1, cmp2);

    // 调试输出(注释状态)
    // for (int i = 1; i <= n; i++)
    //     cout << a[i].pos << " " << a[i].val << " " << a[i].newval << endl;

    // 第三步:计算最长下降子序列(LDS)长度
    for (int i = 1; i <= n; i++)
    {
        // 将当前数值的离散化值加入树状数组
        add(a[i].newval, 1);

        // 计算当前元素在最长下降子序列中的位置
        // query(n) - query(a[i].newval) 返回比当前数值大的已处理元素数量
        ans = max(ans, query(n) - query(a[i].newval));
    }

    // 因为从0开始计数,需要加1得到实际长度
    ans++;

    // 输出最长下降子序列长度
    cout << ans << endl;

    return 0;
}

【运行结果】

5
1
5
3
8
2
4
posted @ 2026-08-17 09:47  团爸讲算法  阅读(3)  评论(0)    收藏  举报