题解:洛谷 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
【核心思想】
-
问题分析:给定长度为 \(N\) 的数组,模拟冒泡排序过程,求输出 "moo" 的次数。冒泡排序的
while循环每次遍历数组,将未归位的最大元素逐步后移,循环次数等于数组中最长"需要被冒泡"的链的长度。本质上是逆序对分析 + 最长下降子序列(LDS)问题:moo输出次数等于最长下降子序列长度 \(+ 1\)。 -
算法选择:
- 离散化 + 树状数组求 LDS:将数值离散化后,用树状数组维护前缀和,倒序或正序查询比当前元素大的已处理元素数量,求最长下降子序列长度
-
关键步骤:
- 读取数据:读入 \(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\))
-
时间/空间复杂度:
- 时间复杂度:\(O(N \log N)\),排序 + 树状数组操作
- 空间复杂度:\(O(N)\),树状数组和结构体数组
-
冒泡排序与 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
浙公网安备 33010602011771号