分治

分治法简介

主要作用

在计算机科学中,分治法是建基于多项分支递归的一种很重要的算法范型。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(归并排序、快速排序)、傅立叶变换(快速傅立叶变换)。

分治算法是一个解决复杂问题的好工具,它可以把问题分解成若干个子问题,把子问题逐个解决,再组合到一起形成大问题的答案。比如,汉诺塔问题如果采用分治算法,可以把高度为$n$的塔的问题转换成高度为n-1的塔来解决,如此重复,直至问题化简到可以很容易的处理为止。

实现

分治通过递归实现。在每一层递归上都有三个步骤:

  1. 分解:将原问题分解为若干个规模较小,相对独立,与原问题形式相同的子问题。
  2. 解决:若子问题规模较小且易于解决时,则直接解。否则,递归地解决各子问题。
  3. 合并:将各子问题的解合并为原问题的解。

递归中的问题

举例

假设当前需要求$x \times y$的值,如果我们一位一位地算,需要进行$n$次$1$位数乘$n$位数的乘法,需要进行$n$次加法。总的算起来,计算一次乘法的时间复杂度为$O(n^2)$。

现在我"学会了"分治,我设$m=\frac{1}{2}n$,那么就可以将$x,y$拆为$x=a \times 10^m + b$与$y=c \times 10^m + d $,那么$x \times y = ac \times 10^{2m} + (ad + bc) \times 10^m +bd$。

这个式子看起来很强,我们将$x,y$拆成了$a,b,c,d$,递归计算了$ac,ad,bc,bd$,最后加法合并起来。但是,这种方式的复杂度没有降低,仍是$O(n^2)$,为什么?它的复杂度计算如下:

$$T(n)=4 \times T(\frac{n}{2}) $$

将$n=1,2,4,8$带入可得最终复杂度为$n^2$。

正解

所以,这道题的正确解法为:

  • 计算$ac,(a+b)(c+d),bd$
  • $ac=bc$
  • $ad+bc=(a+b)(c+d)-ac-bd$
  • $bd=bd$

最终计算时间复杂度:

$$ T(n)=3 \times T(\frac{n}{2}) + O(n) $$

计算结果约为:

$$ O(n^{1.584}) $$

结论

分治是将问题变成小问题,每部分递归处理,最终合并得到答案。如果一个分法是有效的,意味着 递归合并比直接计算好。否则我们根本不需要分治!通常,我们可以使用“主定理”分析分治的复杂度。或者,可以展开几层看看是否符合直觉。

有时分治仅仅是把问题拆成了几个部分,而并没有降低复杂度,这种情况很容易骗过自己,请谨慎分析。

”主定理“是什么?在左侧目录中点击”主定理“选项跳转

简单分治应用实例

归并排序

将待排序的数分成两半,递归排序,最后合并两个有序数组。最主要的问题在于合并。

现在你有两个有序数组,你要将它们合并成一个有序数组。假设这两个有序数组$a,b$共有$n$个数。最小的数一定在$a,b$的开头,先删掉这个数,剩下的$n-1$个数仍然在开头。最后指针不断移动,直到所有的数进入这个有序序列。

合并过程中每次会比较两个序列中较小的数,至多进行$n-1$次比较,复杂度为$O(n)$。

将整个过程带入主定理,得出归并排序算法总复杂度为$ Θ(n \log n) $

逆序对

逆序对:如果$ia_j$,那么就称$(i,j)$为一组逆序对。现在给定一组数,求出逆序对的数量

对于这种问题,我们先想一种最简单的方式:进行冒泡排序,每次交换两个数,它们的逆序对数量就$-1$,所以一个复杂度为$O(n^2)$的冒泡排序可以解决这个问题。

现在考虑分治做法。每次将数组分为两半,递归数每部分的逆序对,最后数一数$i$在左边,$j$在右边的部分。如果直接暴力比较,就会出现上面提到的问题。但对于逆序对,我们可以通过归并排序来解决。归并排序会对两个有序序列中的最小数值一一比较,其中一部分我们看作为$i$,另一部分看作为$j$,因为$i$总是在$j$的左面,每当有$a_j$小于$a_i$的情况,就说明出现了一组逆序对。

二分法

二分法:对于在区间$[a,b]$上连续不断且$f(a)f(b)<0$的函数$y=f(x)$通过不断地把函数$f(x)$零点所在区间一分为二,使区间的两个端点逐步逼近零点,进而得到零点近似值的方法,叫做二分法。

直接运用二分法的有两种:二分查找、二分答案。

  • 二分查找:二分查找是一种在有序数组中查找某一特定元素的查找算法。查找过程从数组的中间元素开始:如果中间元素正好是要查找的元素,则查找过程结束;如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较。如果在某一步骤数组为空,则代表找不到。解通常是唯一的,或者不存在。

    使用STL来进行二分查找: lower_bound()返回有序表里第一个大于等于给定值的指针或迭代器; upper_bound()返回有序表里第一个大于给定值的指针或迭代器; binary_search()返回给定值是否在有序表里存在。

    • 对于以上的STL,普通数组可以直接使用下标寻找。如lower_bound(a+1,a+1+n,2)-a-1
    • vector用法:使用迭代器。lower_bound(a.begin(),a.end(),2)
    • set/map/…用法:使用专门的操作,他们的迭代器不可减。a.lower_bound(2)
  • 二分答案:二分答案,即通过题目中包含的单调性对答案进行二分。大多数二分答案可以看做是标准二分法套了一个奇奇怪怪的函数。解答需要使用二分答案的题目,最重要的是观察出单调性。

    通常来说,题面中有,或者暗含类似于如下的最优化问题:最大值最小/最小值最大、最靠近某个值、最小的能满足条件的代价……那么通常都可以往二分答案想。


进阶分治应用实例

CDQ分治

CDQ分治是我们处理各类问题的重要武器,它的优势在于可以顶替复杂的高级数据结构,而且常数比较小;缺点在于必须离线操作

它的具体形式多种多样,没有固定模板,其中一个典型应用如下:

三维偏序

三位偏序是CDQ分治的一个重要应用。首先说一下,什么是*维偏序:

形如$x_i

现在对于一维偏序:求$n$个$x_i$这样的点中,对于每个i求出满足$x_i

对于三维偏序,有过一句话:“一维排序,二维分治,三维数据结构”。在数逆序对的基础上,用数据结构维护$z$,按照$y$的顺序更新。时间复杂度为$O(n \log ^2 n)$,这种分治就被称为CDQ分治。

k维偏序

那么对于四维偏序,可以在数据结构上加$\log$,也可以分治套分治,但时间复杂度通常为$O(n \log ^3 n)$。所以在维护k维偏序($k \ge 4$)时通常不如bitset。

整体二分

对于需要回答$n$个位置上的相同类型的问题每次回答一个需要二分答案查询,且$check$需要构建一个数据结构,复杂度非常高的问题,可以采取整体二分。

思路:二分一个答案,构建数据结构并判断所有询问,递归处理二分的两边。典型复杂度是$O(n \log ^2 n)$。实现起来很像 CDQ 分治。

WQS二分

(待更新)


代码

平面内最近点对

传送门

三维偏序

来源:洛谷:P3810 【模板】三维偏序(陌上花开)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<set>
using namespace std;
#define LL long long
#define uLL unsigned long long
#define reg register
#define PI acos(-1.0)
#define pb(x) push_back(x)
#define mp(a,b) make_pair(a,b)
#define fi first
#define se second
#define pr(x) cerr<<#x<<"="<<(x)<<endl
#define pri(x,lo) {cerr<<#x<<"={";for (int ol=0;ol<=lo;ol++)cerr<<x[ol]<<",";cerr<<"}"<<endl;}
#define inf 100000000
#define N 1000
#define M 2000001
template<class T>inline void read(T &x)
{
    x=0;register char c=getchar();register bool f=0;
    while(!isdigit(c))f^=c=='-',c=getchar();
    while(isdigit(c))x=(x<<3)+(x<<1)+(c^48),c=getchar();
    if(f)x=-x;
}
template<class T>inline void print(T x)
{
    if(x<0)putchar('-'),x=-x;
    if(x>9)print(x/10);
    putchar('0'+x%10);
}
struct Node
{
	int a,b,c,ans,cnt;
}A[M],a[M];
int n,k,cnt1,cnt2,ANS[M],c[M];
bool cmp1(Node x,Node y)
{
	if(x.a==y.a)
		if(x.b==y.b)
			return x.c<y.c;
		else
			return x.b<y.b;
	else
		return x.a<y.a;
}
bool cmp2(Node x,Node y)
{
	if(x.b==y.b)
		return x.c<y.c;
	else
		return x.b<y.b;
}
int lowbit(int x)
{
	return x&-x;
}
void build(int x,int y)
{
	while(x<=k)
	{
		c[x]+=y;
		x+=lowbit(x);
	}
	return;
}
int search(int x)
{
	int ans=0;
	while(x)
	{
		ans+=c[x];
		x-=lowbit(x);
	}
	return ans;
}
void cdq(int l,int r)
{
	if(l==r)
		return;
	int mid=(l+r)>>1;
	cdq(l,mid);
	cdq(mid+1,r);
	sort(a+l,a+1+mid,cmp2);
	sort(a+1+mid,a+1+r,cmp2);
	int j=l;
	for(int i=mid+1;i<=r;i++)
	{
		while(a[i].b>=a[j].b&&j<=mid)
		{
			build(a[j].c,a[j].cnt);
			j++;
		}
		a[i].ans+=search(a[i].c);
	}
	for(int i=l;i<j;i++)
	{
		build(a[i].c,-a[i].cnt);
	}
	return;
}
int main()
{
	read(n),read(k);
	for(int i=1;i<=n;i++)
	{
		read(A[i].a),read(A[i].b),read(A[i].c);
	}
	sort(A+1,A+1+n,cmp1);
	for(int i=1;i<=n;i++)
	{
		cnt2++;
		if(A[i].a!=A[i+1].a||A[i].b!=A[i+1].b||A[i].c!=A[i+1].c)
		{
			a[++cnt1].a=A[i].a;
			a[cnt1].b=A[i].b;
			a[cnt1].c=A[i].c;
			a[cnt1].cnt=cnt2;
			cnt2=0;
		}
	}
	cdq(1,cnt1);
	for(int i=1;i<=cnt1;i++)
		ANS[a[i].ans+a[i].cnt-1]+=a[i].cnt;
	for(int i=0;i<n;i++)
		print(ANS[i]),putchar('\n');
	return 0;
}

主定理

主定理就是找出影响时间复杂度的最主要的部分。

以下是评估递归时间复杂度的主定理,例如有递归形式

$$T(n)=aT(n/b)+f(n)$$

其中, $a≥1$和$b≥1$, 均为常数,$ f(n)$是一个确定的正函数。 在$f(n)$的三类情况下, 我们有$T(n)$的渐近估计式:

  1. 若对于某常数$ε>0$, 有$f(n)=O(n \log_ba−ε)$, 则$T(n)=Θ(n^{\log_ba})$
  2. 若$f(n)=O(n^{\log_b a})$, 则$T(n)=O(n^{\log_ba}) \times \lg n)$
  3. 若对某常数$ε>0$, 有$f(n)=Ω(n^{\log_ba+ε})$, 且对常数$c<1$与所有足够大的$n$,有$af(\frac{n}{b})≤cf(n)$, 则$T(n)=Θ(f(n))$。
文章标题:分治
文章链接:https://www.laoguantx.top/%e5%88%86%e6%b2%bb.html
文章内容仅供参考,其中可能会有部分内容参考、引用其他网站,转发请务必标注作者为 “老官童鞋gogo” ,原文链接为 https://www.laoguantx.top/%e5%88%86%e6%b2%bb.html 。
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇