先找出本来就不必移动的最长严格递增后缀,再用树状数组统计每头前缀奶牛插入有序部分时应后移的步数。
OJ: luogu
题目 ID: P5200
难度:普及+/提高
标签:树状数组排序思维模拟
日期: 2026-06-21 01:01
题意
给出一个
每次操作只能把当前最前面的那头奶牛拿出来,向后移动若干步,再插入到后面某个位置。
要求用最少操作次数把整个序列变成升序,并输出每次需要后移多少步。
思路
先看一个可以直接验证想法的朴素解:
cpp
#include <bits/stdc++.h>
using namespace std;
// brute.cpp:小数据直接模拟。
// 每次把队首奶牛插入到当前序列中合适的位置,直到整个序列有序。
bool is_sorted_vec(const vector<int> &a) {
int m = (int) a.size();
for (int i = 0; i + 1 < m; i++) {
if (a[i] > a[i + 1]) {
return false;
}
}
return true;
}
int suffix_start(const vector<int> &a) {
int n = (int) a.size();
int start = n - 1;
while (start > 0 && a[start - 1] < a[start]) {
start--;
}
return start;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
vector<int> answer;
while (!is_sorted_vec(a)) {
int start = suffix_start(a);
int x = a[0];
a.erase(a.begin());
// 删除队首后,前面仍然保留 start-1 头未处理奶牛。
// 再把 x 插到后缀有序段里第一个 >= x 的位置前面。
int pos = start - 1;
while (pos < (int) a.size() && a[pos] < x) {
pos++;
}
answer.push_back(pos);
a.insert(a.begin() + pos, x);
}
cout << answer.size() << '\n';
for (int i = 0; i < (int) answer.size(); i++) {
if (i > 0) {
cout << ' ';
}
cout << answer[i];
}
cout << '\n';
return 0;
}brute.cpp 直接按题意模拟:只要序列还没排好,就拿出队首奶牛,把它插入到当前序列里第一个不小于它的位置前面。
这个过程很好理解,但如果真的用数组做插入,复杂度会到
关键观察是:从后往前看,原序列中总会有一段最长严格递增后缀,这一段奶牛的相对顺序已经正确,不需要主动操作。
设这段后缀从
- 最少操作次数就是
- 前面的奶牛必须按顺序一个个拿出来插入到这段有序部分中
接下来只剩一个问题:第
它被拿出来以后:
- 前面还会保留
头尚未处理的前缀奶牛 - 后面是一个已经有序的集合
所以它应插入的位置,就是:
未处理前缀数量- 加上
有序集合里比它小的元素数量
第二部分可以用树状数组维护值域出现次数来统计。
因此做法是:
- 找最长严格递增后缀
- 先把这个后缀里的值加入树状数组
- 对每个前缀元素
,计算
- 然后再把
加入树状数组
代码
cpp
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100005;
int n;
int a[MAXN];
int bit[MAXN];
int ans[MAXN];
int lowbit(int x) {
return x & -x;
}
// 树状数组单点加一,表示某个数已经进入了“后缀有序段”。
void add(int pos, int val) {
for (int i = pos; i <= n; i += lowbit(i)) {
bit[i] += val;
}
}
// 查询当前已经放进去的数里,有多少个值 <= pos。
int sum(int pos) {
int ret = 0;
for (int i = pos; i > 0; i -= lowbit(i)) {
ret += bit[i];
}
return ret;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
int start = n;
// 从后往前找最长严格递增后缀。
while (start > 1 && a[start - 1] < a[start]) {
start--;
}
cout << start - 1 << '\n';
// 先把已经有序的后缀放入树状数组。
for (int i = start; i <= n; i++) {
add(a[i], 1);
}
for (int i = 1; i < start; i++) {
// 把当前队首拿走后,前面还会保留 start-i-1 头“尚未处理”的奶牛。
// 之后再接上已经有序的后缀里所有比它小的奶牛,这就是本次需要后移的步数。
ans[i] = (start - i - 1) + sum(a[i]);
add(a[i], 1);
}
if (start > 1) {
for (int i = 1; i < start; i++) {
if (i > 1) {
cout << ' ';
}
cout << ans[i];
}
}
cout << '\n';
return 0;
}复杂度
- 找后缀:
- 每次树状数组查询与修改:
总时间复杂度:
空间复杂度:
总结
这题的难点不在树状数组本身,而在于先看出:
- 后缀里已经有一部分天然有序
- 只有前缀需要依次插入
树状数组只是把“有序集合里有多少个数比当前值小”这个统计,降成了