题目链接在此
题意是给定一个长度n, 再给一个[0,n-1]的排列, 可以循环地将第一个数放置序列末尾, 问这样循环出来的所有序列中, 最小的逆序数是多少?
思路:
- 先求得原序列串的逆序数
- 对于在当前序列串的第一个数字来说,贡献的逆序数是第一位后小于自己的数的数量即 a[1] - 1
- 将第一个数字放到末端, 原先在第一位贡献逆序数量被消除,在末尾贡献的逆序数是在末位前大于自己的数的数量, 即 n - a[1]
举个莉子, 序列3 1 4 5 2中, 3贡献的逆序数自然是3 - 1 = 2个(小于自己又在后面(必然在自己后面)的)
3若到末位, 成1 4 5 2 3, 贡献的则是5 - 3 = 2个(大于自己的又在前面(必然在自己前面)的)
综上所述, 每次循环,相当于当前原序列的总逆序数, 设为cur, 减去消失的逆序数 + 新添加的,得
cur = cur - (a[1] - 1) + (n - a[1])
至于如何求得原序列的逆序数,方法很多,常用的有归并排序/线段树/这里使用的树状数组
注意, 如果是树状数组,由于编号是[1,n] 所以对于a[i],值域也得是[1,n] 题目是[0,n-1],所以读取后得++a[i]调一下
#include <algorithm>
#include <cstdio>
#include <cstring>
using namespace std;
const int maxN = 5005, inf = 0x3f3f3f3f;
int A[maxN], bit[maxN + 1], N;
void add(int i, int x) { while (i <= N) { bit[i] += x; i += i & -i; } }
int sum(int i) { int ret = 0; while (i) { ret += bit[i]; i -= i & -i; } return ret; }
int main() {
// freopen("data.in", "r", stdin);
while (~scanf("%d", &N) && N) {
memset(bit, 0, sizeof(bit));
int inv = 0;
for (int i = 0; i < N; ++i) {
scanf("%d", &A[i]);
++A[i];
inv += i - sum(A[i]);
add(A[i], 1);
}
int ans = inv, u, v;
for (int i = 0; i < N; ++i) {
u = A[i] - 1;
v = N - A[i];
inv = inv - u + v;
ans = min(ans, inv);
}
printf("%d\n", ans);
}
return 0;
}
附加:
到最后..还是想稍微整理一下,为什么树状数组能求逆序数?
首先可能得先了解一下什么是树状数组,长什么样,节点怎么和数组下标对应起来,这个这里就不打算说了..树状数组的一般操作:
给定i, 计算a1 + a2 + a3 + ... an
给定i和x, 执行 ai += x
在知道这个数据结构的前提下:
求某个逆序数,即求在自己前面又大于自己的数量,那我们只需要:
在当前数字下标是j, 值是a[j], 在数组里做个小标记,比如 bit[a[j]] += 1, 假设所有数都这样处理过,那以后我们要统计当前数字下有多少个小于自己的数字呢? 求个前n项和就可以了, 那大于自己的数字呢?自然就是n - sum(a[j])啦..把每个数字的逆序数一加,得解
到这儿..其实思想可以再简单化一些: 你就想象你有一个标记数组vis[maxN] = {0} ,每当一个数字a[j], 出现了就vis[a[j]] = 1, 那你要统计前面有多少个小于自己的数字,相当于统计j前面出现了多少个1(求前n项和),设为sum,那么n - sum,就是当前数字贡献的逆序数了。 但是这种方法效率很低,只不过利用了树状数组来加速,思路是一样的。