分治

【引入】

分(而)治(之)。

把一个问题分解成规模更小的子问题,从而降低复杂度的算法。

【归并排序】

我们用选择排序,复杂度是 \(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]\)

  1. 如果只有一个数,不用排序;

  2. 递归归并排序左右两边;

  3. 把左右两边的有序数组归并到一个临时数组;

  4. 把临时数组赋值回原数组。

【归并求逆序对】

当递归结束,开始归并的时候,如果前半段被放下去了,就诞生了逆序对。

双指针,因为两边已经有序了,所以移动 \(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;	
}

【用分治优化】

【使用场景】

  1. 可以把序列分开处理,分成两半之后,左右内部的答案不会相互干扰,而且各自求答案也快。

  2. 归并时,可以用双指针高效合并。

  3. 左右之间满足的性质在递归排序后是不影响的。
    (例如下一题按 v 排了之后 x 还是右边比较大)

【题目】

MooFest G 加强版

先按 x 排序,然后按 v 归并排序。

  1. 递归算出左左、右右的贡献,并把左、右按 v 排好序;

  2. 双指针,一遍排序一遍统计左右的贡献。

统计左右贡献的时候,因为已经归并排序过了,所以有序,所以可以用双指针,快速统计右边的 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】

一般是三维。

  1. 按照第一维排序,用 sort + cmp 就可以;

  2. 再按照第二维归并排序;

  3. 双指针(左指针指向第二维最大的且 \(\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;
}

posted @ 2024-02-15 11:09  FLY_lai  阅读(3)  评论(0编辑  收藏  举报