6.3 排序、RMQ
排序
七夕祭
转换成一个环形均分纸牌的问题,以按行划分为例
每一行感兴趣的数量用a[N]a[N]a[N]表示
用x1x_1x1表示a1a_1a1挪给ana_nan的数量,如果x1x_1x1为负数,表示需要从ana_nan挪给a1a_1a1
最小操作数是∣x1∣+∣x2∣+...+∣xn∣|x_1|+|x_2|+...+|x_n|∣x1∣+∣x2∣+...+∣xn∣
调整后,每一行感兴趣摊位点的数量相同,都为平均值a=a1+a2+...+anna=\frac{a_1+a_2+...+a_n}{n}a=na1+a2+...+an,
要求满足
a1−x1+x2=aa_1-x_1+x_2=aa1−x1+x2=a
a2−x2+x3=aa_2-x_2+x_3=aa2−x2+x3=a
…
an−1−xn−1+xn=aa_{n-1}-x_{n-1}+x_n=aan−1−xn−1+xn=a
an−xn+x1=aa_{n}-x_{n}+x_1=aan−xn+x1=a
这是一个包含n个未知数、n个方程的方程组,但并不是有唯一解,因为把左边、右边全部加起来,会发现这是一个恒等式,所以上边实际上最多只有n-1个完全独立的方程。
转换上述方程组
x1−x2=a1−ax_1-x_2=a_1-ax1−x2=a1−a
x2−x3=a2−ax_2-x_3=a_2-ax2−x3=a2−a
…
xn−1−xn=an−1−ax_{n-1}-x_n=a_{n-1}-axn−1−xn=an−1−a
xn−x1=an−ax_{n}-x_1=a_{n}-axn−x1=an−a
进一步,用x1x_1x1表示所有变量,
x1=x1−0=x1−c1x_1=x_1-0=x_1-c_1x1=x1−0=x1−c1
x2=x1−(a1−a)=x1−c2x_2=x_1-(a_1-a)=x_1-c_2x2=x1−(a1−a)=x1−c2
x3=x2−(a2−a)=x1−(a1−a)−(a2−a)=x1−(a1+a2−2a)=x1−c3x_3=x_2-(a_2-a)=x_1-(a_1-a)-(a_2-a)=x_1-(a_1+a_2-2a)=x_1-c_3x3=x2−(a2−a)=x1−(a1−a)−(a2−a)=x1−(a1+a2−2a)=x1−c3
…
xn=x1−(a1+a2+...+an−1−(n−1)a)=xn−cnx_{n}=x_1-(a_1+a_2+...+a_{n-1}-(n-1)a)=x_n-c_nxn=x1−(a1+a2+...+an−1−(n−1)a)=xn−cn
那么
∣x1∣+∣x2∣+...+∣xn∣|x_1|+|x_2|+...+|x_n|∣x1∣+∣x2∣+...+∣xn∣=∣x1−c1∣+∣x1−c2∣+...+∣x1−cn∣|x_1-c_1|+|x_1-c_2|+...+|x_1-c_n|∣x1−c1∣+∣x1−c2∣+...+∣x1−cn∣
把c1、...、cnc_1、...、c_nc1、...、cn看作是数轴上的点,就转换成了求x1x_1x1到数轴上的点的距离之和的最小值,当为中点时,值最小。
#include <cstdio>
#include <cstring>
#include <algorithm>using namespace std;typedef long long LL;const int N = 100010;int row[N], col[N], s[N], c[N];LL work(int n, int a[])
{for (int i = 1; i <= n; i ++ ) s[i] = s[i - 1] + a[i];if (s[n] % n) return -1; // 不能整除int avg = s[n] / n;c[1] = 0;for (int i = 2; i <= n; i ++ ) c[i] = s[i - 1] - (i - 1) * avg;sort(c + 1, c + n);LL res = 0;for (int i = 1; i <= n; i ++ ) res += abs(c[i] - c[(n + 1) / 2]);return res;
}int main()
{int n, m, cnt;scanf("%d%d%d", &n, &m,& cnt);while (cnt -- ){int x, y;scanf("%d%d", &x, &y);row[x] ++ , col[y] ++ ;}LL r = work(n, row);LL c = work(m, col);if (r != -1 && c != -1) printf("both %lld\n", r + c);else if (r != -1) printf("row %lld\n", r);else if (c != -1) printf("column %lld\n", c);else printf("impossible\n");return 0;
}
动态中位数
我们只关心中位数,不需要对较小的数排序,也不需要对较大的数排序,所以可以用2个堆来做。对顶堆
当输入是偶数个数的时候,上下2个堆的数量一样多;当输入是奇数个数的时候,下边的堆数量比上边多一个。
核心是维护这个堆
1️⃣上边所有元素都≥下面的所有元素;
2️⃣下面的个数最多比上边多1;
算法复杂度O(nlogn)O(nlog^n)O(nlogn),思想很重要。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>using namespace std;int T, k, n;int main()
{scanf("%d", &T); // 多组测试数据while (T--){scanf("%d%d", &k, &n); // 编号、数据集个数printf("%d %d\n", k, (n + 1) / 2);priority_queue<int> down; // 默认大根堆priority_queue<int, vector<int>, greater<int>> up; // 小根堆int cnt = 0; // 记录当前已经输出了多少数for (int i = 0; i < n; i ++ ){int x;scanf("%d", &x);if (down.empty() || x <= down.top()) down.push(x);else up.push(x);if (down.size() < up.size()) {down.push(up.top());up.pop();} else if (down.size() > up.size() + 1) {up.push(down.top());down.pop();}if (i % 2 == 0){printf("%d ", down.top());if ( ++ cnt % 10 == 0) puts("");}}if (cnt % 10) puts("");}return 0;
}
超快速排序
有k个逆序对,则至少需要操作k次。
每一次交换,都是交换的前一个数>后一个数的场景(ai>ai+1,i<i+1a_i>a_{i+1},i<i+1ai>ai+1,i<i+1),只要这样操作k次,就能保证逆序对为0,所有数为有序。
最小值是k,且一定存在一个操作次数为k的方案。问题就转换成了求逆序对的数量。
#include <cstdio>using namespace std;using LL = long long;const int N = 500010;int n;
LL q[N], w[N];LL merge_sort(int l, int r)
{if (l == r) return 0;int mid = l + r >> 1;LL res = merge_sort(l, mid) + merge_sort(mid + 1, r);int i = l, j = mid + 1, k = 0;while (i <= mid && j <= r){if (q[i] <= q[j]) w[k ++ ] = q[i ++ ];else{res += mid - i + 1;w[k ++ ] = q[j ++ ];}}while (i <= mid) w[k ++ ] = q[i ++ ];while (j <= r) w[k ++ ] = q[j ++ ];for (i = l, j = 0; i <= r; i ++ , j ++ ) q[i] = w[j];return res;
}int main()
{while (scanf("%d", &n), n){for (int i = 0; i < n; i ++ ) scanf("%d", &q[i]);printf("%lld\n", merge_sort(0, n - 1));}
}
RMQ
天才的记忆
可以使用线段树做,而且只有查询没有修改
RMQ(Range Minimum/Maximum Query)区间最值算法
很常用、代码很短
也叫ST表,本质上是一个动态规划。
f(i,j)f(i, j)f(i,j)表示从iii开始,长度是2j2^j2j的区间中,最大值是多少。
f(i,j)=max{f(i,j−1),f(i+2j−1,j−1)}f(i, j)=max\{f(i, j-1), f(i+2^{j-1},j-1)\}f(i,j)=max{f(i,j−1),f(i+2j−1,j−1)}
状态数量,第一维是n个,第二维是lognlog^nlogn个
状态转移是O(1)O(1)O(1)的,所以总的时间复杂度是O(nlogn)O(nlog^n)O(nlogn)的。
当预处理完之后,怎么求呢?
比如要求区间[l,r][l,r][l,r]之间的最大值,区间长度为lenlenlen,先找到满足2k≤len2^k \leq len2k≤len的最大值,那么区间的最大值就是max{f(l,k),f(r−2k+1,k)}max \{ f(l, k), f(r-2^k+1, k) \}max{f(l,k),f(r−2k+1,k)},查询是O(1)O(1)O(1)的。
有什么缺点呢?
不支持修改,是一个静态算法,要求原来的数组不能变化。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <cmath>using namespace std;const int N = 200010, M = 18;int n, m;
int w[N];
int f[N][M];void init()
{for (int j = 0; j < M; j ++ ) // 枚举区间长度,2^jfor (int i = 1; i + (1 << j) - 1 <= n; i ++ )if (!j) f[i][j] = w[i];else f[i][j] = max(f[i][j - 1], f[i + (1 << j - 1)][j - 1]); // 当前区间的左右子区间的最大值
}int query(int l, int r)
{int len = r - l + 1;int k = log2(len);return max(f[l][k], f[r - (1 << k) + 1][k]);
}int main()
{scanf("%d", &n);for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);init();scanf("%d", &m);while (m -- ){int l, r;scanf("%d%d", &l, &r);printf("%d\n", query(l, r));}return 0;
}