分治学习笔记

算法思想

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

  • 归并排序

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

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

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

《算法导论》第四章 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 逆序对

思路:

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

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

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

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

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

代码:

#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 \in [1,n]\)\(c_{a_i.second}=b_i.second\),然后我们只需要求出 \(c\) 的逆序对个数即可。时间复杂度 \(O(n \log n)\)

代码:提交记录


P5094 [USACO04OPEN] MooFest G 加强版

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

思路:

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

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

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

考虑中间如何求出答案。我们采用双指针的方法,先枚举 \(i \in[l,m)\),维护 \(j \in [m,r)\),再反过来枚举即可。我们找到最大的 \(j\) 满足 \(v_i > v_j\),此时答案就加上 \(v_i \times(\sum_{k=m}^{j}x_k-x_i)\),因为 \(k \in [m, r)\),所以 \(x_k \le x_i\),于是就变成了 \(v_i \times(\sum_{k=m}^{j}x_k -(j - m+1) \times x_i)\),这个 \(\sum_{k=m}^j\) 可以递推,所以每次可以 \(0(1)\) 求出,而 \(j\) 最多移动 \(\frac{n}{2}\) 次,于是这是 \(O(n)\) 的。

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

时间复杂度是 \(O(n \log n)\)

代码:提交记录


P5502 [JSOI2015]最大公约数

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

思路:

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

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

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

时间复杂度是 \(O(n \log^2 n)\)

代码:提交记录


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

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

思路:

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

代码:提交记录


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

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

思路:

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

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

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

代码:提交记录


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

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

思路:

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

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

代码:提交记录

posted @ 2022-12-28 09:33  rlc202204  阅读(49)  评论(0编辑  收藏  举报