详解点分治
本篇随笔讲解算法竞赛中的点分治淀粉质算法。
一、点分治概述及应用
点分治是一种在树上进行路径静态统计的算法。
所谓树上的静态统计,并不是像树剖一样维护路径最值、路径和之类的统计。那样的话,这个算法的存在就没什么意义了。
来上道小题体会一下点分治的解决问题。
给定一棵有n个节点的带边权无根树。求长度不超过k的路径有多少条。
看懂了么?就是这种有限制的路径统计问题,非常适合用点分治解决。
二、点分治的基本实现思路
点分治点分治,最终还是落回分治这个东西。分治是什么?其原理就是把原问题拆解成几个子问题,分别求解子问题之后再合并出原问题。那么点分治其实就是对一棵树进行拆开,分治。
那么用上面的问题作为引入。
显然,暴力的方式是枚举所有路径,然后判断是否合法。但是所有的路径都可以分两种情况:第一种,过根节点。第二种,不过根节点。
那么我们把根节点断掉,又形成了很多新子树,对于这些子树,仍然会对它们的路径分这两种情况......
可以看出,所有不过整棵树根节点的路径,必会在这种不断拆解形成新子树的过程中,过一遍根节点。
对其有感觉么?这就是递归的过程么。
于是点分治实现的大致思路就是:
1、任取一个点作为无根树根节点。
2、计算所有第一种路径。
3、将当前根节点断掉,递归计算下一层子树。步骤同上。
三、点分治的时空复杂度分析
点分治过程中,分治点的选择至关重要。
一个递归层的时间复杂度是(O(nlog n))
极端情况下,树会退化成链,那么对于链,我们最不划算的选择分治点方式是每次选择链头,那么就会变成递归n次,总复杂度是(O(n^2log n))。
那么我们每次选择链的中心呢?
很简单,递归变成了log次,总复杂度就是(O(nlog^2 n))。
那么,对于链是这样选择,对于树也是这样选择。选”中点“当然是最划算的。那么树的“中点”是什么呢?
即,树的重心。
重心有一个性质:那就是删除重心后,树被拆成若干个部分,这些部分中,不会有任何一个部分超过总点数的二分之一。否则就不符合重心的性质。
所以,每次选择重心作为分治点,就保证了整个点分治算法的时间是(O(nlog^2 n))
四、点分治的代码实现
根据第二部分的思路概述,我们应该很容易用递归方式写出其框架代码:
void dfz(int x)//大多数人叫divide,我就喜欢这样
{
v[x]=1;//避免重复选择绕死
calc();//注意!点分治的精华!
for(int i=head[x];i;i=nxt[i])
{
int y=to[i];
if(v[y])
continue;
sum=size[y],root=0;//找子树重心的前置信息
getroot(y,0);//这波是找子树重心
dfz(root);//套娃递归
}
}
其中的getroot函数,功能是找子树重心。根据重心的定义及之前学过的求法,应该这么写:
void getroot(int x,int f)
{
size[x]=1,mp[x]=0;
for(int i=head[x];i;i=nxt[i])
{
int y=to[i];
if(y==f||v[y])
continue;
getroot(y,x);
size[x]+=size[y];
mp[x]=max(mp[x],size[y]);
}
mp[x]=max(mp[x],sum-size[y]);
if(mp[x]<mp[root])
root=x;
}
只有这两个能算作模板化的东西。
然而点分治的精华,calc函数部分,需要按题目的意思来求。其功能就是维护题目想要维护的信息。
这个就需要自己通过例题和练习细细体会。