分治
【引入】
分(而)治(之)。
把一个问题分解成规模更小的子问题,从而降低复杂度的算法。
【归并排序】
我们用选择排序,复杂度是 \(O(\frac{n^2}{2})\)。
但是如果我们把数组分成两半,分别选择排序,再归并起来,复杂度就降低为 \(O(\frac{n^2}{4}+n)\),几乎快了一半。
那么,如果我们一直不停分下去,复杂度 \(f(n)\) 就会不断降低。
\(f(n)=2f(\frac{n}{2})+n\),则 \(f(n)=O(n\log n)\)
流程:
当前处理区间 \([l,r]\)。
-
如果只有一个数,不用排序;
-
递归归并排序左右两边;
-
把左右两边的有序数组归并到一个临时数组;
-
把临时数组赋值回原数组。
【归并求逆序对】
当递归结束,开始归并的时候,如果前半段被放下去了,就诞生了逆序对。
双指针,因为两边已经有序了,所以移动 \(i\) 时,调整 \(j\):\([m,j)\) 能与 \(i\) 构成逆序对,调整之后累计答案即可。
long long slv(int l, int r) {
if (l + 1 == r)//只有一个元素,不用排
return 0;
//分为[l, m) [m, r)先各自排序,再归并到t,再放回a
int m = (l + r) / 2;
//先记录两段各自内部的个数
long long ans = slv(l, m) + slv(m, r);
//本次求与i组成逆序对的j的个数,移动j使得[m, j)恰好能与i组成逆序对
for (int i = l, j = m; i < m; i++) {
while (j < r && a[j] < a[i])//只要有下一个a[j]能与i组合,就移动j
j++;
ans += j - m;
}
for (int i = l, j = m, cur = l; i < m || j < r; )//左半边当前i,右半边j
if (i < m && (j == r || a[i] < a[j]))
t[cur++] = a[i++];
else
t[cur++] = a[j++];
for (int i = l; i < r; i++)
a[i] = t[i];
return ans;
}
【用分治优化】
【使用场景】
-
可以把序列分开处理,分成两半之后,左右内部的答案不会相互干扰,而且各自求答案也快。
-
归并时,可以用双指针高效合并。
-
左右之间满足的性质在递归排序后是不影响的。
(例如下一题按 v 排了之后 x 还是右边比较大)
【题目】
先按 x 排序,然后按 v 归并排序。
-
递归算出左左、右右的贡献,并把左、右按 v 排好序;
-
双指针,一遍排序一遍统计左右的贡献。
统计左右贡献的时候,因为已经归并排序过了,所以有序,所以可以用双指针,快速统计右边的 v 比左边大的情况。
做两边双指针就能求出左边大于右边和右边大于左边的情况。
记得无论何时,一定是右边的 x 减去左边的 x。
注:左边等于右边的情况只能放在一边。
注意:按照 v 排序的时候,可能会打乱 x 的顺序,但是在归并的时候,左边的任何一个 x 一定小于右边的 x。
归并排序优化:归并排序递归计算了左左右右,我们就只需要算左右了,而且因为有序,可以用双指针高效处理。
#include <bits/stdc++.h>
using namespace std;
struct Node {
long long x, v;
} a[50005], t[50005];//a是牛,t为归并的额外空间
int n;
//把a[]的[l, r)的元素,按v排好序,归并前计算[l, r)内部的数对求和并返回
long long slv(int l, int r) {
if (l + 1 == r)//只有一个元素,不用算
return 0;
//分为[l, m) [m, r)先各自排序,再归并到t,再放回a
int m = (l + r) / 2;
//先计算两段各自内部的数对求和,并各自内部按v排序,左侧x仍小于右侧
long long ans = slv(l, m) + slv(m, r);
//此时,左侧x小于右侧,左右各自按v排号序,此时双指针计算左、右匹配
//这里计算出的,是v值 右侧 >= 左侧 的数对求和
for (long long i = l - 1, j = m, sum = 0; j < r; j++) {//枚举j,调整i为最后一个使a[i].v <= a[j].v
while (i + 1 < m && a[i + 1].v <= a[j].v)//有下一个且也<=,则换下一个
sum += a[++i].x;//sum为a[l~i].x的和
ans += a[j].v * ((i - l + 1) * a[j].x - sum);
}
//再计算v值 左侧 > 右侧 的数对求和
for (long long i = l, j = m - 1, sum = 0; i < m; i++) {//枚举i,调整j为最后一个使a[i].v > a[j].v
while (j + 1 < r && a[j + 1].v < a[i].v)//有下一个且也<,则换下一个
sum += a[++j].x;//sum为a[m~j].x的和
ans += a[i].v * (sum - (j - m + 1) * a[i].x);
}
//按v归并排序
for (int i = l, j = m, cur = l; i < m || j < r; )//左半边当前i,右半边j
if (i < m && (j == r || a[i].v < a[j].v))
t[cur++] = a[i++];
else
t[cur++] = a[j++];
for (int i = l; i < r; i++)
a[i] = t[i];
return ans;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i].v >> a[i].x;
//先按x排序
sort(a + 1, a + n + 1, [](Node p, Node q){return p.x < q.x;});
cout << slv(1, n + 1) << endl;
return 0;
}
【CDQ】
一般是三维。
-
按照第一维排序,用 sort + cmp 就可以;
-
再按照第二维归并排序;
-
双指针(左指针指向第二维最大的且 \(\leq\) 右指针指向的) + 树状数组(第二维作下标,加一,因为是统计个数就只加一)计算左右之间的贡献。
注:sort 排序的时候,先按第一维,再按第二维,最后按第三维,可以保证左边 \(\leq\) 右边。
树状数组使用完一次要归零,但是用 memset 肯定超时,于是我们只把使用过的清零即可(1 ~ 左指针)。
还要记得把相等的算两次。
#include <bits/stdc++.h>
using namespace std;
//t提供归并的额外空间
struct Node {
int idx, x, y, z;
} a[200005], t[200005];
int n, k, cnt[200005] = {0};//cnt[i]为初始i号的个数统计;
inline int lowbit(int x) {
return x - (x & (x - 1));
}
int tre[200005] = {0};//初始所有字段全是0
void mdf(int x, int val) { //a[x] 增加val
while (x <= k)
tre[x] += val, x += lowbit(x);
}
int prx(int x) { //求a[1~x]的和
int ans = 0;
while (x > 0)
ans += tre[x], x -= lowbit(x);
return ans;
}
void slv(int l, int r) {
if (l + 1 == r)//只有一个元素,不用排
return ;
//先统计两边三维都相等的,对左侧元素的贡献
int m = (l + r) / 2;
int L = m - 1, R = m;//a[m, R)与a[m]相同,a[L, m) 与a[m]相同
while (R < r && a[R].x == a[m].x && a[R].y == a[m].y && a[R].z == a[m].z)
R++;
while (L >= l && a[L].x == a[m].x && a[L].y == a[m].y && a[L].z == a[m].z)
cnt[a[L--].idx] += (R - m);
//先统计内部点对,并归并按y排序
slv(l, m), slv(m, r);
//对[m, r)之间的j,扫过[l, m)中所有的 y 也有序的,并对z值打点树状数组统计
for (int i = l, j = m; j < r; j++) {//i为下一个要检查的位置
while (i < m && a[i].y <= a[j].y)
mdf(a[i++].z, 1);//扫过i并打点
cnt[a[j].idx] += prx(a[j].z);//对j统计此时的左右个数
}
//把此次用过的元素改回去,从而树状数组清零
//i为扫过的位置,需要不超过右侧j的最大y值
for (int i = l; i < m && a[i].y <= a[r - 1].y; i++)
mdf(a[i].z, -1);
//结束前按y归并
for (int i = l, j = m, cur = l; i < m || j < r; )
if (i < m && (j == r || a[i].y < a[j].y))
t[cur++] = a[i++];
else
t[cur++] = a[j++];
for (int i = l; i < r; i++)
a[i] = t[i];
}
bool cmp(Node A, Node B) {
if (A.x != B.x)
return A.x < B.x;
if (A.y != B.y)
return A.y < B.y;
return A.z < B.z;
}
int d[200005] = {0};
int main()
{
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i].x >> a[i].y >> a[i].z;
a[i].idx = i;
}
sort(a + 1, a + n + 1, cmp);
slv(1, n + 1);
for (int i = 1; i <= n; i++)
d[cnt[i]]++;
for (int i = 0; i < n; i++)
cout << d[i] << endl;
return 0;
}