第三类算法换了一种思路, 它们不直接比较大小, 而是对被排序的数值采取按位划分、分类映射等处理方式, 其时间复杂度不仅与 n 有关, 还与数值的大小范围 m 有关。
离散化
排序算法的第一个应用是离散化。通俗地讲, “离散化” 就是把无穷大集合中的若干个元素映射为有限集合以便于统计的方法。例如在很多情况下, 问题的范围虽然定义在整数集合 Z, 但是只涉及其中 m 个有限数值, 并且与数值的绝对大小无关 (只把这些数值作为代表, 或只与它们的相对顺序有关)。此时, 我们就可以把整数集合 Z 中的这 m 个整数与 1∼m 建立映射关系。如果有一个时间、空间复杂度与数值范围 Z 的大小有关的算法, 在离散化后, 该算法的时间、空间复杂度就降低为与 m 相关。
具体地说, 假设问题涉及 int 范围内的 n 个整数 a[1]∼a[n], 这 n 个整数可能有重复, 去重以后共有 m 个整数。我们要把每个整数 a[i] 用一个 1∼m 之间的整数代替, 并且保持大小顺序不变, 即如果 a[i] 小于 (或等于、大于) a[j], 那么代替 a[i] 的整数也小于(或等于、大于) 代替 a[j] 的整数。
很简单, 我们可以把 a 数组排序并去掉重复的数值, 得到有序数组 b[1]∼b[m], 在 b 数组的下标 i 与数值 b[i] 之间建立映射关系。若要查询整数 i(1≤i≤m) 代替的数值, 只需直接返回 b[i]; 若要查询整数 a[j](1≤j≤n) 被哪个 1∼m 之间的整数代替, 只需在数组 b 中二分查找 a[j] 的位置即可。
1 2 3 4 5 6 7 8 9 10 11 12
// 离散化 voiddiscrete(){ sort(a + 1, a + n + 1); for (int i = 1; i <= n; i++) // 也可用STL中的unique函数 if (i == 1 || a[i] != a[i - 1]) b[++m] = a[i]; }
// 离散化后,查询x映射为哪个1~m之间的整数 voidquery(int x){ returnlower_bound(b + 1, b + m + 1, x) - b; }
//Author:XuHt #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> usingnamespace std; constint N = 200006; int n, m, a[N], x[N], y[N], cinema[N*3], tot = 0, k, ans[N*3];
把 A[1]∼A[N] 排序, 设货仓建在 X 坐标处, X 左侧的商店有 P 家, 右侧的商店有 Q 家。若 P<Q, 则每把货仓的选址向右移动 1 单位距离, 距离之和就会变小 Q−P 。同理, 若 P>Q, 则货仓的选址向左移动会使距离之和变小。当 P=Q 时为最优解。
因此货仓应该建在中位数处, 即把 A 排序后, 当 N 为奇数时, 货仓建在 A[(N+1)/2] 处最优;当 N 为偶数时,货仓建在 A[N/2]∼A[N/2+1] 之间的任何位置都是最优解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
#include<cstdio> #include<iostream> #include<algorithm> #define ll long long usingnamespace std; constint N = 100006; int n, a[N];
intmain(){ cin >> n; for (int i = 1; i <= n; i++) scanf("%d", &a[i]); sort(a + 1, a + n + 1); ll ans = 0; for (int i = 1; i <= n / 2; i++) ans += a[n-i+1] - a[i]; cout << ans << endl; return0; }
#include<cstdio> #include<cstring> #include<iostream> #include<algorithm> #define ll long long usingnamespace std; constint N = 100006; int n, m, t, x[N], y[N], a[N], s[N];
intmain(){ cin >> n >> m >> t; for (int i = 1; i <= t; i++) scanf("%d %d", &x[i], &y[i]); bool row = !(t % n), column = !(t % m); if (row) { if (column) cout << "both "; else cout << "row "; } else { if (column) cout << "column "; else { cout << "impossible" << endl; return0; } } ll ans = 0; if (row) { int num = t / n; memset(a, 0, sizeof(a)); for (int i = 1; i <= t; i++) a[x[i]]++; for (int i = 1; i <= n; i++) a[i] -= num; s[0] = 0; for (int i = 1; i <= n; i++) s[i] = s[i-1] + a[i]; sort(s + 1, s + n + 1); for (int i = 1; i <= n / 2; i++) ans += s[n-i+1] - s[i]; } if (column) { int num = t / m; memset(a, 0, sizeof(a)); for (int i = 1; i <= t; i++) a[y[i]]++; for (int i = 1; i <= m; i++) a[i] -= num; s[0] = 0; for (int i = 1; i <= m; i++) s[i] = s[i-1] + a[i]; sort(s + 1, s + m + 1); for (int i = 1; i <= m / 2; i++) ans += s[m-i+1] - s[i]; } cout << ans << endl; return0; }
voidRunning_Median(){ while (q1.size()) q1.pop(); while (q2.size()) q2.pop(); int num, n; cin >> num >> n; cout << num << " " << (n + 1) / 2 << endl; int a; cin >> a; cout << a << " "; q2.push(-a); int cnt = 1; for (int i = 2; i <= n; i++) { scanf("%d", &a); if (a < -q2.top()) q1.push(a); else q2.push(-a); int s = q1.size(); if (s > i / 2) { q2.push(-q1.top()); q1.pop(); } if (s < i / 2) { q1.push(-q2.top()); q2.pop(); } if (i % 2) { cout<< -q2.top() << " "; if (++cnt % 10 == 0) cout << endl; } } cout << endl; }
intmain(){ int t; cin >> t; while (t--) Running_Median(); return0; }
Solution
第 k 大数
给定 n 个整数, 如何求出第 k 大的数? 我们当然可以直接对这 n 个整数进行快速排序, 然后输出从大到小排在第 k 个的数, 时间复杂度为 O(nlogn) 。实际上利用类似于快速排序的思想, 只需要 O(n) 的时间即可求出第 k 大数。
0≤n<500000,
一个测试点中,所有 n 的和不超过 500000。 0≤ai≤999999999
输入样例:
1 2 3 4 5 6 7 8 9 10 11
5 9 1 0 5 4 3 1 2 3 0
输出样例:
1 2
6 0
算法分析
只通过比较和交换相邻两个数值的排序方法, 实际上就是冒泡排序。在排序过程中每找到一对大小颠倒的相邻数值, 把它们交换, 就会使整个序列的逆序对个数减少 1 。最终排好序后逆序对个数显然为 0 , 所以对 a 进行冒泡排序需要的最少交换次数就是序列 a 中逆序对的个数。我们直接使用归并排序求出 a 的逆序对数就是本题的答案。
//Author:XuHt #include<cstdio> #include<iostream> #include<algorithm> #define ll long long usingnamespace std; constint N = 500006; int n, a[N], b[N]; ll ans;
voidmerge(int l, int mid, int r){ if (l == r) return; if (l + 1 == r) { if (a[l] > a[r]) { ans++; swap(a[l], a[r]); } return; } merge(l, (l + mid) >> 1, mid); merge(mid + 1, (mid + 1 + r) >> 1, r); int i = l, j = mid + 1; for (int k = l; k <= r; k++) if (j > r || (i <= mid && a[i] <= a[j])) b[k] = a[i++]; else { b[k] = a[j++]; ans += mid - i + 1; } for (int k = l; k <= r; k++) a[k] = b[k]; }
voidUltra_QuickSort(){ for (int i = 1; i <= n; i++) scanf("%d", &a[i]); ans = 0; merge(1, (1 + n) >> 1, n); cout << ans << endl; }
intmain(){ while (cin >> n && n) Ultra_QuickSort(); return0; }
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<vector> usingnamespace std; int n; longlong ans; vector<int> a[2]; int c[250010];
voidmerge(int k, int l, int mid, int r) { int x = l, y = mid + 1; for (int i = l; i <= r; i++) { if (y>r || x <= mid&&a[k][x]<a[k][y]) c[i] = a[k][x++]; else ans += mid - x + 1, c[i] = a[k][y++]; } for (int i = l; i <= r; i++) a[k][i] = c[i]; }
voidmergesort(int k, int l, int r) { if (l == r) return; int mid = (l + r) / 2; mergesort(k, l, mid); mergesort(k, mid + 1, r); merge(k, l, mid, r); }