分治
别问我为什么咕咕咕了这么久
意义和本质
分治,顾名思义,即是分开后分别处理。对于一个问题直接求取较为困难时,我们可以将其分成多个子问题,每个子问题相对于原本问题数据规模会变小 / 处理起来简单,以至于我们可以在合法时间内求出我们的每一个子问题。在处理出各个子问题后将得到的结果合并以计算出我们的最终问题的答案。
典例1
先来一个经典问题:求取一个序列的最大子段和。
像下面这张图一样,我们将整个序列不断拆成两段(一般是平分),直到拆成的段中只有一个数为止。
显然,一个数的最大子段和就是他自己。
然后我们考虑如何向上合并。
对于一段区间
考虑子段的性质:子段是连续的,所以对于区间
- 全部在区间
[l,mid] 中 - 全部在区间
[mid+1,r] 中 - 一部分在区间
[l,mid] 中,另一部分在区间[mid+1,r] 中,且在区间[l,mid] 中的部分是区间[l,mid] 的一段后缀,在区间[mid+1,r] 中的部分是区间[mid+1,r] 的一段前缀。
现在,我们就知道在两个子区间中求什么了:这段区间中的最大子段和,这段区间中和最大的前缀,这段区间中和最大的后缀,这段区间中所有数的和(原因是你也要求出区间
定义一些东西:
考虑合并:你发现区间
考虑到区间
所以我们还需要两个式子:
这样我们一层层往上合并信息就能得出整个序列的最大子段和了。
这样的复杂度是多少呢?
考虑到我们最后一层有
所以复杂度是
典例2
一个经典的分治应用:平面最近点对,给你一个坐标系,在这个坐标系上选出
先将所有点按
然后开始分治,还是对于一个点区间
考虑到我们是按照点
code:
for(int i=0;i<k;i++)
for(int j=i+1;j<k&&a[tmp[j]].y-a[tmp[i]].y<d;j++)
if(minn>getdis(tmp[i],tmp[j])) d=getdis(tmp[i],tmp[j]);
更多的例题
先看看自己掌握了吗。
魔法GCD Magical GCD
给一个长度为
还是分开做,对于区间
相同的,我们还需要固定右端点,保证
code:
int p1=mid,p2=mid,g=a[mid];
ans=max(ans,g);
for(int i=p2;i>=l;i--)
{
g=gcd(g,a[i]);
while(p1<r&&g==gcd(g,a[p1+1])) p1++;
ans=max(ans,(p1-i+1)*g);
}
p1=p2=mid;
g=a[mid];
for(int i=p1;i<=r;i++)
{
g=gcd(g,a[i]);
while(p2>l&&g==gcd(g,a[p2-1])) p2--;
ans=max(ans,(i-p2+1)*g);
}
现在,我们看看分治还能干啥。
当然是:优化dp啦。
可以抽象的认为,如果是普通dp,则是对一个菊花图进行树上dp,而分治后,则是对于一个二叉树进行树上dp(可能不完全对)。
下面来一道题直观的感受一下:
CF1442D Sum
给定
- 选择一个数组,把
ans 加上数组的第一个元素,之后把它删除。
请求出
所有数组的元素总个数
你发现这题直接贪心是不可做的,因为我不知道这一轮是拿出哪个元素,可能是这一次能拿出的最大的,也有可能不是最大的,但是拿完这个不是最大的后可以拿这个数所在数组的下一个。
接着,我们想出此题的暴力dp:
转移:
其中
这是
然后我们手搓几组样例看看性质。
手搓过后,发现,最多只有一个数组被取过但是没有被取完,其他的要么没动过,要么都取完了。这不难理解,因为数组单调递增,如果我们选了一个数组中的一个,肯定再接着去这个数组中的下一个更优。
这样,我们就可以分治做了,例如我们现在处理到了区间
CF868F Yet Another Minimization Problem
首先,列出暴力方程:
定义
转移为:
然后,我们发现这个东西有决策单调性。
接着我们就可以分治优化转移了。
sol(p,l,r,lp,rp) 为现在所在区间是