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 2 3 4 5 6 7 8 9 10 int countInversions (int a[], int length) { int count = 0 ; for (int i = 0 ; i < length - 1 ; i++) { for (int j = i + 1 ; j < length; j++) { if (a[i] > a[j]) count++; } } return count; }
2. 借助冒泡排序
冒泡排序的每一次交换就会消除一个逆序对, 交换一个相邻的逆序对,不会影响到其它的逆序对,所以可以计算冒泡排序在排序过程一共进行了多少次交换,由此得出数组的逆序对数。最普通的冒泡排序和枚举的循环次数一样多 。但是枚举的过程没有元素交换,会比使用冒泡排序计算逆序对数快很多。
借助冒泡排序只需要在交换两个元素的同时逆序对计数加1即可。普通的冒泡算法会试遍所有的组合,而改进的冒泡算法则可以减少这些检查,逆序对数少时就比枚举法快很多,但是逆序对数多的话就不行,因为排序有很大一部分时间花在元素交换上。
缺点是计算的过程会消除逆序对,计算完成后,逆序对也被消除了,如果想保留数列,则需要复制数组,使用副本计算。
时间复杂度为$O(n^2)$
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 int countInversions_bubbleSort (int a[], int length) { int count = 0 ; int left = 0 , right = length - 1 ; int leftBound, rightBound; while (left < right) { leftBound = right, rightBound = left; for (int i = left; i < right; i++) { if (a[i] > a[i + 1 ]) { count++; int temp = a[i]; a[i] = a[i + 1 ]; a[i + 1 ] = temp; rightBound = i; } } right = rightBound; for (int i = right; i > left; i--) { if (a[i - 1 ] > a[i]) { count++; int temp = a[i - 1 ]; a[i - 1 ] = a[i]; a[i] = temp; leftBound = i; } } left = leftBound; } return count; }
3. 借助插入排序
插入排序的每一次元素移动可以看做相邻元素互换, 移动一次就消除一个逆序。
所以可以用插入排序计算逆序对数,并且比用冒泡排序快。
时间复杂度为$O(n^2)$
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int countInversions_insertionSort (int a[], int length) { int count = 0 ; for (int i = 1 ; i < length; i++) { int key = a[i]; int j; for (j = i - 1 ; (j >= 0 ) && (a[j] > key); j--) { a[j + 1 ] = a[j]; count++; } a[j + 1 ] = key; } return count; }
4. 借助归并排序
一个升序数组,逆序对为0。数组中一个连续段之间的逆序对交换,只会影响段内的逆序对数,而不会影响段外的逆序对,段外的元素与段内的元素之间的逆序对数也不受影响
将一个数组A从中间分成左右两部分, 分别是 $A [ l e f t ]$ . . . $A [ m i d ]$ 和 $A [ m i d + 1 ] $. . . $A [ r i g h t ]$。
假设两部分分别有序,在归并过程中,i, j 分别表示左右两部分归并到的元素下标, 必有i < j。如果有A[i] > A[j], 那么左边A[i]之后的元素也都会大于A[j], 所以A[j] 与 A[i] 到A[mid]的逆序对为$m i d − i + 1$ 。
归并排序的两个有序数列归并,这两个有序数列在数组中是相邻的,所以交换这两个数列的元素,只会影响这两个数列元素之间的逆序对数。
由此修改归并排序来计算逆序对, 逆序对在归并时计算。为了简便,就使用最普通的递归归并排序来说明。
下面的临时数组是全局变量,是为了方便。如果想要把整个计算过程封装到函数内,可以参考递归归并排序的避免频繁开辟内存写法 。
时间复杂度为$O ( n log n )$
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 int a[LEN], temp[LEN];int mergeCountInversions (int a[], int left, int mid, int right) { int count = 0 ; int i = left, j = mid + 1 ; for (int k = left; k <= right; k++) { if ((j > right) || (i <= mid) && (a[i] <= a[j])) temp[k] = a[i++]; else { temp[k] = a[j++]; count += mid - i + 1 ; } } for (int i = left; i <= right; i++) a[i] = temp[i]; return count; } int countInversions_mergeSort (int a[], int left, int right) { if (left < right) { int mid = left + (right - left) / 2 ; return countInversions_mergeSort (a, left, mid) + countInversions_mergeSort (a, mid + 1 , right) + mergeCountInversions (a, left, mid, right); } else return 0 ; }
5. 借助树状数组
树状数组C, C[x] 中保存等于x的元素的个数,初始值都为0。
倒序扫描数列A, 每扫描到一个元素x,就在树状数组C[x]上加1,表示扫描到的x元素个数加1。然后在树状数组中查询前缀和S(x-1), 这个前缀和表示小于目前已经扫描到的元素中,比元素X小的有多少个,因为是从后面开始扫描的,所以先扫描的元素位置比元素x的位置靠后,又比元素x小,就是逆序了。树状数组的前缀和就是与元素X构成逆序的元素个数。
时间复杂度为$O ( n log n )$
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 ll a[M], b[M], c[M], n, m; ll lowbit (ll x) { return x & (-x); } ll getsum (ll x) { ll i, ans = 0 ; for (i = x; i > 0 ; i -= lowbit (i)) { ans += c[i]; } return ans; } void update (ll x, ll val) { while (x <= n) { c[x] += val; x += lowbit (x); } } bool cmp (ll num1, ll num2) { if (b[num1] == b[num2]){ return num1 > num2; } return b[num1] > b[num2]; } ll lowbit_inverse_pair (ll b[], int length) { ll cnt = 0 ; for (int i = 1 ; i <= length; i++) { a[i] = i; } sort (a + 1 , a + 1 + length, cmp); for (int i = 1 ; i <= n; i++) { update (a[i], 1 ); cnt += getsum (a[i] - 1 ); } return cnt; }
模板题
火柴排队
[NOIP2013 提高组] 火柴排队
题目描述
涵涵有两盒火柴,每盒装有 $n$ 根火柴,每根火柴都有一个高度。 现在将每盒中的火柴各自排成一列, 同一列火柴的高度互不相同, 两列火柴之间的距离定义为:$ \sum (a_i-b_i)^2$
其中 $a_i$ 表示第一列火柴中第 $i$ 个火柴的高度,$b_i$ 表示第二列火柴中第 $i$ 个火柴的高度。
每列火柴中相邻两根火柴的位置都可以交换,请你通过交换使得两列火柴之间的距离最小。请问得到这个最小的距离,最少需要交换多少次?如果这个数字太大,请输出这个最小交换次数对 $10^8-3$ 取模的结果。
输入格式共三行,第一行包含一个整数 $n$,表示每盒中火柴的数目。
第二行有 $n$ 个整数,每两个整数之间用一个空格隔开,表示第一列火柴的高度。
第三行有 $n$ 个整数,每两个整数之间用一个空格隔开,表示第二列火柴的高度。
输出格式一个整数,表示最少交换次数对 $10^8-3$ 取模的结果。
样例 #1 样例输入 #1
样例输出 #1 样例 #2 样例输入 #2 样例输出 #2 提示【输入输出样例说明一】
最小距离是$ 0$,最少需要交换 $1$ 次,比如:交换第 $1 $列的前$ 2$ 根火柴或者交换第 $2$ 列的前 $2 $根火柴。
【输入输出样例说明二】
最小距离是 $10$,最少需要交换 $2$ 次,比如:交换第 $1$ 列的中间 $2$ 根火柴的位置,再交换第 $2$ 列中后 $2$ 根火柴的位置。
【数据范围】
对于 $10%$ 的数据, $1 \leq n \leq 10$;
对于 $30%$ 的数据,$1 \leq n \leq 100$;
对于 $60%$ 的数据,$1 \leq n \leq 10^3$;
对于 $100%$ 的数据,$1 \leq n \leq 10^5$,$0 \leq$ 火柴高度 $< 2^{31}$。
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 #include <bits/stdc++.h> using namespace std;#define M 100007 #define N 1007 #define INF 0x3f3f3f3f #define mod 99999997 #define ll long long #define db double #define pii pair<int, int> 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 ; }