Inverse pair
Inverse pair
逆序对
定义
设 A 为一个有 n 个数字的有序集 (n>1),其中所有数字各不相同。
如果存在正整数 i, j 使得 1 ≤ i < j ≤ n 而且 A[i] > A[j],则 <A[i], A[j]> 这个有序对称为 A 的一个逆序对,也称作逆序数。
对于一个包含N个非负整数的数组A[1…n],如果有i < j,且A[ i ]>A[ j ],则称(A[ i] ,A[ j] )为数组A中的一个逆序对。
例如,数组(3,1,4,5,2)的逆序对有(3,1),(3,2),(4,2),(5,2),共4个。
逆序对的计算
有序集中的逆序对数目是按照组合来计算的,对于大小为n的有序集,那么有$\frac{n(n+1)}{2} $种组合。
1. 朴素算法
枚举数组元素的所有组合,对于大小为n的数组,那么有$\frac{n(n+1)}{2} $种组合。
1 | int countInversions(int a[], int length){ |
2. 借助冒泡排序
冒泡排序的每一次交换就会消除一个逆序对, 交换一个相邻的逆序对,不会影响到其它的逆序对,所以可以计算冒泡排序在排序过程一共进行了多少次交换,由此得出数组的逆序对数。最普通的冒泡排序和枚举的循环次数一样多。但是枚举的过程没有元素交换,会比使用冒泡排序计算逆序对数快很多。
- 借助冒泡排序只需要在交换两个元素的同时逆序对计数加1即可。普通的冒泡算法会试遍所有的组合,而改进的冒泡算法则可以减少这些检查,逆序对数少时就比枚举法快很多,但是逆序对数多的话就不行,因为排序有很大一部分时间花在元素交换上。
- 缺点是计算的过程会消除逆序对,计算完成后,逆序对也被消除了,如果想保留数列,则需要复制数组,使用副本计算。
时间复杂度为
1 | int countInversions_bubbleSort(int a[], int length){ |
3. 借助插入排序
插入排序的每一次元素移动可以看做相邻元素互换, 移动一次就消除一个逆序。
所以可以用插入排序计算逆序对数,并且比用冒泡排序快。
时间复杂度为
1 | int countInversions_insertionSort(int a[], int length){ |
4. 借助归并排序
一个升序数组,逆序对为0。数组中一个连续段之间的逆序对交换,只会影响段内的逆序对数,而不会影响段外的逆序对,段外的元素与段内的元素之间的逆序对数也不受影响
将一个数组A从中间分成左右两部分, 分别是 . . . 和 $A [ m i d + 1 ] $. . . 。
假设两部分分别有序,在归并过程中,i, j 分别表示左右两部分归并到的元素下标, 必有i < j。如果有A[i] > A[j], 那么左边A[i]之后的元素也都会大于A[j], 所以A[j] 与 A[i] 到A[mid]的逆序对为 。
归并排序的两个有序数列归并,这两个有序数列在数组中是相邻的,所以交换这两个数列的元素,只会影响这两个数列元素之间的逆序对数。
由此修改归并排序来计算逆序对, 逆序对在归并时计算。为了简便,就使用最普通的递归归并排序来说明。
- 下面的临时数组是全局变量,是为了方便。如果想要把整个计算过程封装到函数内,可以参考递归归并排序的避免频繁开辟内存写法。
时间复杂度为
1 | int a[LEN], temp[LEN]; |
5. 借助树状数组
树状数组C, C[x] 中保存等于x的元素的个数,初始值都为0。
倒序扫描数列A, 每扫描到一个元素x,就在树状数组C[x]上加1,表示扫描到的x元素个数加1。然后在树状数组中查询前缀和S(x-1), 这个前缀和表示小于目前已经扫描到的元素中,比元素X小的有多少个,因为是从后面开始扫描的,所以先扫描的元素位置比元素x的位置靠后,又比元素x小,就是逆序了。树状数组的前缀和就是与元素X构成逆序的元素个数。
时间复杂度为
1 | ll a[M], b[M], c[M], n, m;//b[]为逆序对数组,a[]为辅助数组 |
模板题
火柴排队
[NOIP2013 提高组] 火柴排队
题目描述
涵涵有两盒火柴,每盒装有 根火柴,每根火柴都有一个高度。 现在将每盒中的火柴各自排成一列, 同一列火柴的高度互不相同, 两列火柴之间的距离定义为:$ \sum (a_i-b_i)^2$
其中 表示第一列火柴中第 个火柴的高度, 表示第二列火柴中第 个火柴的高度。
每列火柴中相邻两根火柴的位置都可以交换,请你通过交换使得两列火柴之间的距离最小。请问得到这个最小的距离,最少需要交换多少次?如果这个数字太大,请输出这个最小交换次数对 取模的结果。
输入格式
共三行,第一行包含一个整数 ,表示每盒中火柴的数目。
第二行有 个整数,每两个整数之间用一个空格隔开,表示第一列火柴的高度。
第三行有 个整数,每两个整数之间用一个空格隔开,表示第二列火柴的高度。
输出格式
一个整数,表示最少交换次数对 取模的结果。
样例 #1
样例输入 #1
1
2
3
4
2 3 1 4
3 2 1 4
样例输出 #1
1
1
样例 #2
样例输入 #2
1
2
3
4
1 3 4 2
1 7 2 4
样例输出 #2
1
2
提示
1 | 4 |
样例输出 #1
1
1
样例 #2
样例输入 #2
1
2
3
4
1 3 4 2
1 7 2 4
样例输出 #2
1
2
提示
1 | 1 |
样例输入 #2
1
2
3
4
1 3 4 2
1 7 2 4
样例输出 #2
1
2
提示
1 | 4 |
1 | 2 |
提示
【输入输出样例说明一】
最小距离是$ 0$,最少需要交换 次,比如:交换第 $1 2$ 根火柴或者交换第 列的前 $2 $根火柴。
【输入输出样例说明二】
最小距离是 ,最少需要交换 次,比如:交换第 列的中间 根火柴的位置,再交换第 列中后 根火柴的位置。
【数据范围】
对于 的数据, ;
对于 的数据,;
对于 的数据,;
对于 的数据,, 火柴高度 。
AC代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
using namespace std;
typedef struct node {
int val;
int id;
} Node;
Node a[M], b[M];
int n, res[M], p[M], ans;
bool cmp(Node a, Node b) {
return a.val < b.val;
}
void calc(int l, int r) {
if (l == r)
return;
int mid = (l + r) >> 1;
calc(l, mid);
calc(mid + 1, r);
int i = l, k = l, j = mid + 1;
while (i <= mid && j <= r) {
if (res[i] <= res[j]) {
p[k++] = res[i++];
} else {
p[k++] = res[j++];
ans += mid - i + 1;
ans %= mod;
}
}
while (i <= mid)
p[k++] = res[i++];
while (j <= r)
p[k++] = res[j++];
for (int i = l; i <= r; i++)
res[i] = p[i];
}
int main() {
ios::sync_with_stdio(false);
cout.tie(NULL);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i].val;
a[i].id = i;
}
for (int i = 1; i <= n; i++) {
cin >> b[i].val;
b[i].id = i;
}
sort(a + 1, a + n + 1, cmp);
sort(b + 1, b + n + 1, cmp);
for (int i = 1; i <= n; i++) {
res[b[i].id] = a[i].id;
}
calc(1, n);
cout << ans;
return 0;
}
1 |
|
- Title: Inverse pair
- Author: Charles
- Created at : 2023-01-11 18:56:14
- Updated at : 2026-05-11 20:11:16
- Link: https://charles2530.github.io/2023/01/11/inverse-pair/
- License: This work is licensed under CC BY-NC-SA 4.0.