\(AcWing\) \(788\). 逆序对的数量
一、题目描述
给定一个长度为 \(n\)
逆序对的定义如下:对于数列的第 \(i\) 个和第 \(j\) 个元素,如果满足 \(i<j\)
且 \(a[i]>a[j]\),则其为一个逆序对;否则不是。
输入格式
第一行包含整数 \(n\),表示数列的长度。
第二行包含 \(n\)
输出格式
输出一个整数,表示逆序对的个数。
数据范围
\(1≤n≤100000\),数列中的元素的取值范围 \([1,10^9]\)
输入样例:
6
2 3 4 5 6 1
输出样例:
5
二、树状数组解法
#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
// 原数组 / 排序号数组 /树状数组
int a[N], d[N];
int n;
// 树状数组模板
int tr[N];
int lowbit(int x) {
return x & -x;
}
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int sum(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
// 两个位置的元素进行PK,先看位置上的值,如果不一样,值大的在前;如果值一样,位置编号大的在前
bool cmp(int x, int y) {
if (a[x] == a[y]) return x > y; // 值都一样,号大在前
return a[x] > a[y]; // 值不一样,值大在前
}
int ans;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i], d[i] = i;
sort(d + 1, d + n + 1, cmp); // d为a的排序号辅助数组
// 将最大,次大,第三大...依次加入树状数组
//(1)d[1]=4进入树状数组,4环顾四周,没有看到以前有其它号比自己小的,ans+=0
//(2)d[2]=5进入树状数组,5环顾四周,发现以前有过比自己大,而且号比自己小的d[1]=4~, ans+=1
//(3)d[3]=1进入树状数组,1环顾四周,发现以前有过比自己大,而且号比自己小的d[1]=4,d[2]=5,ans+=2
for (int i = 1; i <= n; i++) {
add(d[i], 1);
ans += sum(d[i] - 1);
}
printf("%lld", ans);
return 0;
}
三、\(d\)数组的作用
数组\(a[]\)中的每个元素\(a[i]\)有三个属性:
- ① 数值 \(value\)
- ② 整体排名 \(rank\)
- ③ 原始位置 \(pos\)
当我们求逆序对时,只关心 ② 和 ③。 ① 在这里是一个临时的概念,它的存在导致可以求出整体的排名 ②
\(d[]\)记录的就是 ② 整体的排名 \(rank\) + ③ 原来的位置 \(pos\)
比如:\(d[x]=y\) 表示排名第\(x\)位的数,原始位置是\(y\)
\(Q\): 排名数组\(d[]\)的构建步骤?
\(A\):
- 随着\(a[i]\)读入,记录\(d[i] = i\)
- 实现比较函数 \(cmp\), 如果值不一样,值大的在前;如果值一样,则输入序号大的在前
- \(sort(d+1,d+1+n,cmp)\) 调用\(STL\)+自定义\(cmp\) 对\(d\)数组进行排序,最终的排序结果如下面的栗子:
输入
\(a[]\) 值 4 2 3 5 1
\(d[]\) 序号 1 2 3 4 5
结果
\(a[]\) 值 4 2 3 5 1
\(d[]\) 序号 4 1 3 2 5
可以清楚看出:
- 原数组没有改变
- \(d[1]=4\),表示第\(1\)名,值最大的,在\(a[]\)中第\(4\)个位置上,最大值\(=a[d[1]]=a[4]=5\)
\(d[2]=1\),表示第\(2\)名,值次大的,在\(a[]\)中第\(1\)个位置上,次大值\(=a[d[2]]=a[1]=4\)
....
算法步骤:
1、排名由高到低,逐个让每个数字下场,也就是 大的数字先入场,占领它在原始数组中的位置
2、当某个数字下场时,检查它左侧(也就是号比自己小的),现在已经有多少个数字存在,这此数字都符合一个特点:
数比自己大,号比自己小,也就是 逆序对的数量。
#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
int n;
int a[N], d[N];
bool cmp(int x, int y) {
if (a[x] == a[y]) return x > y;
return a[x] > a[y];
}
int main() {
// 文件输入输出
#ifndef ONLINE_JUDGE
freopen("788.in", "r", stdin);
#endif
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i], d[i] = i;
sort(d + 1, d + n + 1, cmp);
// 从结果反推逻辑
for (int i = 1; i <= n; i++) printf("%d ", a[i]); // a数组是不动的
puts("");
for (int i = 1; i <= n; i++) printf("%d ", d[i]);
// 输出 4 1 3 2 5
// a数组原封不动
// d数组得到了一个值从大到小的顺序:a[4],a[1],a[3],a[2],a[5]
return 0;
}
788.in
5
4 2 3 5 1
结构体的版本似乎更容易理解一些
#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
typedef long long LL;
LL ans;
int n;
// 每个输入的数字,我们关心两个方面:1、数值 2、位置(序号)
struct Node {
int val;
int id;
} d[N];
bool cmp(Node a, Node b) {
if (a.val == b.val) return a.id > b.id; // 两个数一样大,序号大的靠前,这样统计出来的正序对才正确
return a.val > b.val; // 数不一样大,数大的靠前
}
// 树状数组模板
int tr[N];
int lowbit(int x) {
return x & -x;
}
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int sum(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main() {
cin >> n;
// 读入每个数,分别将数值、序号记录到结构体数组d中
for (int i = 1; i <= n; i++) {
cin >> d[i].val;
d[i].id = i;
}
// 对结构体数组进行排序,大的在前,小的在后;如果数值一样,序号大的在前,小的在后
sort(d + 1, d + 1 + n, cmp);
// 逆序对的定义:i<j && d[i]>d[j]
// d数组是由大到小排完序的,按由大到小的顺序动态维护树状数组,计算每次变化后出现的i<j 的个数。
for (int i = 1; i <= n; i++) {
// 将d[i].id放入树状数组,描述这个号的数字增加了1个
add(d[i].id, 1);
// 查询并累加所有比当前节点id小的数字个数
ans += sum(d[i].id - 1);
}
// 输出结果
printf("%lld\n", ans);
return 0;
}
四、树状数组的作用
就是可以维护一个 动态前缀和 ,这是我起的名字,不一定准确,但事就是这么个事。普通前缀和适合于静态数组,像这样一会加进来一个,就需要计算一下前缀和,一会又加进来一个,还想算前缀和的,树状数组的优势就来了,虽然达不到\(O(1)\),但\(O(logN)\)也是很不错的运算速度了。
五、疑问
是不是可以最大排序向左看,最小排序向右看呢?需要测试一下
答案是一样可以\(AC\)!原理是一样的,从本质上说应该没有大的在前快,因为需要多计算一下\(sum\),但实测效果两者基本一致。
普通版本
#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
int a[N], d[N];
int n;
int tr[N];
int lowbit(int x) {
return x & -x;
}
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int sum(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
bool cmp(int x, int y) {
if (a[x] == a[y]) return x < y;
return a[x] < a[y];
}
int ans;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i], d[i] = i;
sort(d + 1, d + n + 1, cmp);
for (int i = 1; i <= n; i++) {
add(d[i], 1);
ans += sum(n) - sum(d[i]);
}
printf("%lld", ans);
return 0;
}
结构体版本
#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
typedef long long LL;
LL ans;
int n;
struct Node {
int val;
int id;
} d[N];
bool cmp(Node a, Node b) {
if (a.val == b.val) return a.id < b.id; // 值相等时,号小的在前
return a.val < b.val; // 值小的在前
}
int tr[N];
int lowbit(int x) {
return x & -x;
}
void add(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int sum(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> d[i].val;
d[i].id = i;
}
sort(d + 1, d + 1 + n, cmp);
for (int i = 1; i <= n; i++) {
add(d[i].id, 1);
ans += sum(n) - sum(d[i].id); // 因为是值小的提前进场,如果值一样,则号小的先进场,那么发现有值比我小,号比我大的,就是逆序了
}
printf("%lld\n", ans);
return 0;
}
六、总结
- 能使用结构体还是用结构体吧,这样更直观
- 不用结构体也没啥复杂的,就是一个比较函数的套路,背下来,然后\(d[]\)的含义就是排名第几位的,在原始数组的哪个位置,也就是排名与原始位置的对应关系数组。这两者的关系是使用树状数组的关键信息
- 树状数组讲究的是逐个下场,动态统计前缀和