首页 > 其他分享 >分治

分治

时间:2024-02-15 11:11:51浏览次数:26  
标签:归并 int 分治 long ++ && 排序

【引入】

分(而)治(之)。

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

【归并排序】

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

标签:归并,int,分治,long,++,&&,排序
From: https://www.cnblogs.com/FLY-lai/p/18016063

相关文章

  • 根号分治学习笔记
    根号分治根号分治其实一般是广义上的阈值分治,当范围不超过\(B\)时用一种方法,超过\(B\)时用另一种做法。之所以叫根号分治主要是大部分的应用都是在和一定时,不超过\(\sqrt{n}\)的可以把所有\(1\sim\sqrt{n}\)的都处理,超过\(\sqrt{n}\)的最多只有\(O(\sqrt{n})\)个,也......
  • 点分治学习笔记
    点分治点分治是一种处理树上路径问题的常见方法。先引入例题。求树上有多少条路径的长度是3的倍数。点分治的过程是每次找到当前联通块的重心,然后处理所有跨过重心的路径,然后删去重心,递归每个子树再进行处理。根据重心的性质,重心的每个儿子的子树大小都不超过\(\dfrac{n}{......
  • CDQ分治学习笔记
    CDQ分治其实CDQ本质就类似线段树,\(i\)的贡献由\(1\)到\(i-1\)在线段树上拆出的log个节点组成,然后将可以被同一段区间做贡献的点一起计算从而保证复杂度。例题:P3810【模板】三维偏序(陌上花开)题意:对于\(k\in[1,n]\)求三维偏序数量为\(k\)的点的个数。思路:其实就是求每个点的......
  • 树分治
    目录树分治点分治入门1P4178TreeProblemSolutionP4149[IOI2011]RaceProblemSolutionP6626[省选联考2020B卷]消息传递ProblemSolutionP5351RuriLovesMascheraProblemSolutionCF293ECloseVerticesProblemSolution点分治入门2P5306[COCI2018-2019#5]TransportProblemS......
  • 线段树分治学习笔记
    线段树分治线段树分治是一种可以离线处理带撤销问题的常用手段。一般而言,题目中加入操作很好维护,但删除操作不好维护,这时可以对时间维建线段树,把每一个操作加入其存在时间段对应的线段树节点上,然后处理所有询问,进入一个节点时将这个节点里的操作加入,递归左右儿子,然后撤销这一次做......
  • [THUPC2024] 分治乘法
    [THUPC2024初赛]分治乘法题目描述小艾想要挑战分治乘法。TA将策略抽象成了如下问题:现在给定一个目标集合\(T\),该集合是\(\{1,\dots,n\}\)的一个子集(\(1\leqn\leq5\times10^5\))。你需要通过一系列操作构造一些集合最后得到\(T\),具体来说有以下三种操作:创造一个大小......
  • P4721 【模板】分治 FFT
    最具经济性的写法:\(\mathcalO(n^2)\)暴力拿下\(80\)分,遂跑路。一题多解了,分两部分:分治和多项式求逆。分治考虑cdq分治,每次把\(f_{l\dotsmid}\)和\(g_{1\dotsn-1}\)卷起来,贡献直接加到\(f_{mid+1\dotsr}\)里,要注意一下顺序,先递归左区间,再算当前区间,最......
  • 根号分治入门
    在vpcf的时候遇到的算法,当时看着就是题目很清楚,要不就是我不会的数据结果,要不就是算法,想了一会想不出直接去看题解了。现在补一下。根号分治虽然名字里面有“分治”但实际上和分治的关系并不大,根号分治更多的还是一种思想。根号分治的思想是将询问根据一个阈值设为\(S\)分为两......
  • 【算法】树分治
    1.算法简介树分治(Treedivision),是处理树上路径类问题的算法。树分治又可以分为点分治与边分治。考虑这样一个问题:给定一棵有\(n\)个点的树,询问树上距离为\(k\)的点对是否存在。暴力的做法就是枚举两个点然后计算距离,统计答案。这样显然\(O(n^2)\)的。我们发现瓶颈在于......
  • 点分治
    点分治定义树上的分治先求一个点的答案,然后求子树树上距离小于等于k的点对数量枚举一个点p求解经过p的点对贡献,然后递归解决子树为了降低分治复杂度,要求重心,求重心要限定子树范围内,添加vis防止上访,求dis也要ans要减去在同一个子树重心的子树小于\(n/2\),所以调用递归......