分治学习笔记

算法思想

分治的主要思想就是分而治之,即把一个大问题分成若干个小问题,先去解决这些小问题,再去解决大问题。分治是一个思想,我们通过一些实际应用来感受一下。

  • 归并排序

归并排序是一种稳定的排序算法,最好和最坏时间复杂度均为 O(nlogn),是一种极其优秀的排序算法,它的原理如下:

假设我们要对一个 n 长度的数组排序,那么首先我们先把 [1,n+12)[n+12,n+1) 排好序,然后再把两段合到一起就可以整体排好序。而那段数的排序也重复以上过程,知道只剩一个数为止。

把两段已经排好序的数组合并是很简单的,只要维护两个指针就可以在 O(n) 的时间内排好序。这就是分治,不过它的时间复杂度为什么是 O(nlogn) 的呢?我们设 T(n) 表示对 n 长度数组归并排序的时间复杂度,则会有 T(n)=2T(n/2)+O(n),然后《算法导论》上讲了一个主方法,根据那个方法就可以知道 T(n)=O(nlogn)。不过本人比较菜,不会,于是我给一下章节:

《算法导论》第四章 4.5 用主方法求解递归式

这就是分治的其中一个应用之一,分治能解决很多问题,下面通过以下习题来一一讲述。

一些习题

P1177 【模板】快速排序

题目链接:P1177 【模板】快速排序

思路:

我们可以用归并排序来解决这一问题,归并排序上面已经说过,于是这里给一下代码。

代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int n, a[100005] = {0}, t[100005] = {0};
void slv(int l, int r) {
	if (l + 1 == r)
		return;
	int m = (l + r) / 2;
	slv(l, m), slv(m, r);
	for (int i = l, j = m, cur = l; i < m || j < r; )
		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];
}
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	slv(1, n + 1);
	for (int i = 1; i <= n; i++)
		printf("%d ", a[i]);
	return 0;
} 

P1908 逆序对

题目连接:P1908 逆序对

思路:

这里涉及归并排序一个用处——求逆序对,逆序对是一个序列中满足 1i<jnai>aj(i,j) 个数。

我们采用分治的思路,考虑对于一个 n 长度的序列,我们设 mid=n+12。我们先求出 [1,mid)[mid,n+1) 两个区间的逆序对个数,并且把这两个区间排好序,然后再求 [1,n+1) 这个区间的逆序对个数。而那两个区间也是继续一分为二。

我们可以用双指针来求出所有 i[1,mid),j[mid,n+1) 的逆序对个数。
我们枚举所有的 i,并且维护 j 指向 [mid,n+1) 中第一个满足 ajai 的位置,那么说明 [mid,j) 中的所有数都可以和 ai 组成逆序对,于是我们直接把答案加上 jmid 即可。
因为 j 做多移动 nmid+1 次,所以时间复杂度史 O(n) 的。

我们在求出了逆序对后还要对两端区间进行合并,使得整个区间是有序的,这样才能去求解更大的区间。

时间复杂度和归并排序一样,也是 O(nlogn)

代码:

#include <iostream>
#include <cstdio>
#include <map>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
int n, a[500005] = {0}, t[500005] = {0};
long long slv(int l, int r) {
	if (l + 1 == r)
		return 0ll;
	int m = (l + r) / 2;
	long long ans = slv(l, m) + slv(m, r);
	for (int i = l, j = m; i < m; i++) {
		while (j < r && a[j] < a[i])
			j++;
		ans += j - m;
	} 
	for (int i = l, j = m, cur = l; i < m || j < r; )
		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;
}
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%d", &a[i]);
	printf("%lld\n", slv(1, n + 1));
	return 0;
} 

String Reversal

题目链接:CF1430E String Reversal

思路:

这道题和 P1966 [NOIP2013 提高组] 火柴排队 几乎完全一样,所以这里只讲一下这道题的做法。

逆序对有一个作用,一个数组的逆序对个数就是这个数组冒泡排序的最少交换次数。我们首先把原字符串的每个字符记作一个二元组,以字符为第一关键字,编号为第二关键字,然后我们再把字符串反转,同样是每个元素记作一个二元组,字符为第一关键字,编号为第二关键字。

我们把两个数组进行从小到大的排序,这时编号相同的元素就是我们要再交换后放在一起的,于是我们新建一个数组 c,对于 i[1,n]cai.second=bi.second,然后我们只需要求出 c 的逆序对个数即可。时间复杂度 O(nlogn)

代码:提交记录


P5094 [USACO04OPEN] MooFest G 加强版

题目链接:P5094 [USACO04OPEN] MooFest G 加强版

思路:

直接暴力复杂度是 O(n2) 的,考虑如何优化。我们没办法去处理最大值和绝对值(dis(i,j)=|xixj|),于是我们考虑把听力和坐标变有序,可以用归并。

我们首先对坐标 x 从小到大排序,这样的话每次归并我们依然是先处理 [l,m),[m,r) 两个区间内任两头牛的总和(m 是中点),再求出每个区间各一只的总和。我们发现,由于我们最开始就按坐标拍好了序,所以对于所有 li<m,mj<r 都有 xixj,所以我们就不需要去管绝对值了。

于是我们归并时是按照听力的大小从小到大归并,因为外部顺序已经确定,所以内部是可以打乱的。

考虑中间如何求出答案。我们采用双指针的方法,先枚举 i[l,m),维护 j[m,r),再反过来枚举即可。我们找到最大的 j 满足 vi>vj,此时答案就加上 vi×(k=mjxkxi),因为 k[m,r),所以 xkxi,于是就变成了 vi×(k=mjxk(jm+1)×xi),这个 k=mj 可以递推,所以每次可以 0(1) 求出,而 j 最多移动 n2 次,于是这是 O(n) 的。

我们现在求出了所有情况中 vi(i[l,m)) 更大的,再求所有 vj 更大的即可。

时间复杂度是 O(nlogn)

代码:提交记录


P5502 [JSOI2015]最大公约数

题目链接:P5502 [JSOI2015]最大公约数

思路:

这道题直接枚举时间复杂度是 O(n2)(不过求 gcd 还要加上一个 log),我们考虑如何用分治去求解。

我们依然是先处理出 [l,m),[m,r) 区间内的答案,然后我们枚举区间的左端点再 [l,m) 内,右端点再 [m,r) 内的权值的最大值。最大公约数可以递推,我们用 O(n) 的复杂度先预处理出 si,i[l,m) 表示 [i,m) 的最大公约数,sj,j[m,r) 表示 [m,j] 的最大公约数。

最大公约数的个数其实不会超过 logn 个,为啥?因为数越多,最大公约数会单调不升,而每次减少至少一半,所以会有 sj 的最大公约数相同,且是连续的一段,我们枚举 i,然后找到 sj 中所有不同最大公约数中最后一个,去更新答案即可。

时间复杂度是 O(nlog2n)

代码:提交记录


P8600 [蓝桥杯 2013 省 B] 连号区间数

题目链接:P8600 [蓝桥杯 2013 省 B] 连号区间数

思路:

这题我们不难发现对于一个区间 [l,r]maxi=lrpimini=lrpi=rl+1 如果成立,则这个区间就是一个所谓的连号区间。我们依然是分治,在合并时我们分别处理最大值最小值在左右区间的情况,共四种。每一种都用双指针维护即可。

代码:提交记录


P7883 平面最近点对(加强加强版)

题目链接:P7883 平面最近点对(加强加强版)

思路:

我们首先把所有点按照 x 轴坐标从小到大排序,分治求出左右两个小区间的最近距离,然后考虑如何求出大区间的。

我们设两个小区间内部最近的两个点最小的距离为 d,同时对两个小区间内的点按 y 轴从小到大排序,然后再对大区间排序。我们选择 x 轴坐标排序后在中间的那个点,以它的 x 轴坐标 x1 话一条直线 x=x1。我们把所有距离这条直线距离小于 d 的所有点找出来。因为我们这时是按 y 轴从小到大排序的,所以选出来的每个点,往后找所有选出的点他们 y 轴坐标的差的绝对值小于 d 的,然后跟新答案即可。

这样合并的时间复杂度是 O(kn) 的,k 是一个常数,即每个选出的点往后最多有多少个选出的点,他们的 y 轴坐标差小于 d,一般来说 k6,所以可以看成是 O(n) 的,总的时间复杂度是 O(nlogn) 的。

代码:提交记录


P3810 【模板】三维偏序(陌上花开)

题目链接:P3810 【模板】三维偏序(陌上花开)

思路:

这题我们可以用 CDQ 分治来解决。我们首先按照 ai 来从小到大排序,然后再按照 b 从小到大归并排序,在归并的同时,我们来处理答案。

我们在处理完两个小区间的答案后,我们枚举有区间每个元素,维护一个指针指向左区间第一个比有区间当前元素的 b 大的元素,在移动指针的过程中,如过当前的 b 比较小,就指针移动,同时用树状数组来维护 c,在 c 上对应的值加一,这样每次合并就是 O(nlogn) 的了,总的时间复杂度为 O(nlog2n)

代码:提交记录

posted @   rlc202204  阅读(61)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
点击右上角即可分享
微信分享提示