##[$POJ$ $2299$ $Ultra-QuickSort$](http://poj.org/problem?id=2299) ### 一、题目描述 给一个序列,我们使用冒泡排序法对它进行排序。请输出在排序过程中会进行多少次交换,没有重复数字。 ### 二、解题思路 记录元素的大小及坐标,因为要形成升序,前面大的元素要交换到后面,所以就是转换成了求一段序列的逆序数。 这道题很容易想到冒泡排序法暴力,复杂度$O(N^2)$ **本质:求序列中逆序对的数量** #### 方法1:【冒泡排序】 直接用冒泡排序,发现逆序就计数 冒泡排序是$O(N^2)$的,本题$N=5e5$,不能用冒泡模拟 结论:✘ #### 方法2:【归并排序】求逆序对速度最快 结论:✔ ```cpp {.line-numbers} #include using namespace std; typedef long long LL; const int N = 1e5 + 10; int q[N]; int t[N]; LL ans; void merge_sort(int q[], int l, int r) { if (l >= r) return; int mid = (l + r) >> 1; merge_sort(q, l, mid); merge_sort(q, mid + 1, r); int i = l; int j = mid + 1; int k = 0; while (i <= mid && j <= r) if (q[i] <= q[j]) t[k++] = q[i++]; else { t[k++] = q[j++]; ans += mid - i + 1; } while (i <= mid) t[k++] = q[i++]; while (j <= r) t[k++] = q[j++]; for (i = l, j = 0; i <= r; i++, j++) q[i] = t[j]; } int main() { //加快读入 ios::sync_with_stdio(false), cin.tie(0); int n; cin >> n; for (int i = 1; i <= n; i++) cin >> q[i]; merge_sort(q, 1, n); printf("%lld", ans); return 0; } ``` #### 方法3:树状数组 * 需要离散化【只要用$sort$就是不如归并排序了,图一乐】 * 不需要离散化,性能与归并排序一致$O(logN)$ 本题是我用来练手树状数组的,归并排序再优秀,也先放一边,研究一下树状数组的解法: 求逆序数,可以使用 **树状数组** 或 **线段树**,线段树的常数大,用树状数组,效率会快很多。 如果数据的最大值比较小($<=1e5$)时,可以直接树状数组,不用离散化,比如这样: ```cpp {.line-numbers} #include using namespace std; const int N = 1e6 + 10; int n, res; //树状数组模板 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; } /* /* 题目: 给定一个长度为 n 的整数数列,请你计算数列中的逆序对的数量。 逆序对的定义如下:对于数列的第 i 个和第 j 个元素,如果满足 ia[j],则其为一个逆序对;否则不是。 同时,本题有非常强烈的要求: 长度为n的整数数列,值的范围是[1~n],而且,不重复 测试用例: 6 2 3 4 5 6 1 答案:5 树状数组求逆序对: ① 开一个大小为 输入数据最大值 的数组tr ② 每当读入一个数x时,用桶排序的思想,将tr[x]加上1 ③ 统计 res = sum(tr[1]~tr[x]),这个sum是树状数组的内置方法,调用就可以快速返回区间和 ④ res - 1 :除掉这个数本身,就是在这个数前面已经录入的数字中,有多少个数比它小(因为是一个从左到右,从小到大的桶嘛,前缀和求的是小于等于a[i]的所有个数和) ⑤ i - res :前面有多少数比x大,也就是逆序对的数量 优点: 录入n个数字,每次执行效率为O(logN),总的效率为O(N*logN) */ int main() { scanf("%d", &n); for (int i = 1; i <= n; i++) { int x; scanf("%d", x); add(x, 1); res += i - sum(x); } printf("%d\n", res); return 0; } ``` 本题如果想用树状数组,那么问题就来了,$a[i]$ 的范围 $[0,999999999]$, 数据范围 **有点大**,而且通过样例可以发现,数据也不是连续的,所以按数据范围开数组这个太恶心了,我们不妨做一下 **离散化** 的处理,之后求逆序数就可以交给树状数组啦。 本题直接用$a[i]$的值来做,肯定是不行了,正如前面所说的$a[i]$的范围最大可以去到 $999999999$. 如果给一组的一组数据是 $999999999, 1, 0, 5,3$ . 就$5$个数, 但树状数组的范围就要开到$[0, 999999999]$, 造成了大量的内存浪费,而且题目也不允许,这笔买卖可不划算。所以我们要用把这五个数离散化一下。 **$Q$:如何离散化?** 记录他们的下标,用下标来搞事情。 既然要同时记录下标和数值,我们可以考虑映射 > 定义一个结构体 $v$用于记录数值(排序用), $no$用于记录下标 ```cpp {.line-numbers} struct Node{   int v, no; }; ``` 输入序列之后,按升序排序,这样我们就可以得到一个按元素升序排好序表示每个元素出现的顺序的序列 举个栗子: $999999999, 1, 0, 5, 3$
| $t.v$ | $9...9$ | $1$ | $0$ | $5$ | $3$ | | ---------------- | ------- | --- | --- | --- | ------- | | $t.no$ | $1$ | $2$ | $3$ | $4$ | $5$ | | $t.v$【排序后】 | $0$ | $1$ | $3$ | $5$ | $9...9$ | | $t.no$【排序后】 | $3$ | $2$ | $5$ | $4$ | $1$ |
很显然,我们就可以发现排序后的 $t.no$ 序列的逆序数就是需要交换的次数,例如$9...9$ 的 下标为 $1$ ,它前面 $t.no$ 比它大的数有四个, 说明在原序列(未排序)中 $9...9$的后面(因为下标也代表出现的顺序,越大越靠后)比 $9...9 小$(因为排序)的有四个。所以问题到这里就可以交给树状数组去解决啦! ```cpp {.line-numbers} #include #include #include #include #include #include #include #include #include using namespace std; typedef long long LL; const int N = 500010; int a[N], q[N], ql; int n; // 树状数组模板 #define lowbit(x) (x & -x) int c[N]; void add(int x, int v) { while (x < N) c[x] += v, x += lowbit(x); } int sum(int x) { int res = 0; while (x) res += c[x], x -= lowbit(x); return res; } // 二分模板 lower_bound (左闭右开) int find(int x) { return lower_bound(q + 1, q + 1 + ql, x) - q; } int main() { #ifndef ONLINE_JUDGE freopen("POJ2299.in", "r", stdin); #endif while (~scanf("%d", &n) && n) { // 清空树状数组 memset(c, 0, sizeof c); // 读入原始数组 for (int i = 1; i <= n; i++) scanf("%d", &a[i]), q[i] = a[i]; // 由小到大排序+离散化 sort(q + 1, q + 1 + n); ql = 1; for (int i = 2; i <= n; i++) if (q[ql] != q[i]) q[++ql] = q[i]; // 利用树状数组动态维护个数,动态获取前缀和(权值) LL ans = 0; for (int i = 1; i <= n; i++) { // 在我前面进来,比我大的有多少个? ans += sum(ql) - sum(find(a[i])); add(find(a[i]), 1); } printf("%lld\n", ans); } return 0; } ``` 结论:✔ #### 方法4:线段树 【只要用$sort$就是不如归并排序了,图一乐】 结论:✔