目录
1. 点分治
分治
分治算法的基本思想:
将一个规模较大的问题分解为若干个规模较小的子问题,这些子问题相互独立且与原问题性质相同,求出每个子问题后将解合并,就可以得到原问题的解。
点分治和边分治是在树形结构上进行分治的一种操作
点分治的题,边分治也可以做
主要思想就是去掉树中的某些结构(点或边),使原来的树被分解为多个不相交的部分,多用来解决 路经询问 之类的问题
经典题目
https://vjudge.net/problem/POJ-1741
给定一棵有n个点的树,树上的边有权值,问这棵树上总长度小于k的路径有多少个(1e5)
初步思路:暴力,即遍历所有的点,对于每一个点,dfs找到他和所有的点的路径长度,最后求出个数
可以考虑点分治
对于一个根结点来说,可以把树上的路径分为两种,一种是经过根节点的,一种是不经过的,(这种路径完全在一棵子树中)
(对于不经过这个根节点的子树来说,也可以看成是经过另一个根节点的路径)
基于这种分治的思想,大致的思路可以是:
- 找到树的根结点
- 计算经过根的路径
- 把根去掉,递归计算子树的答案(与原问题性质相同的子问题)
1. 找到树的根结点
此时可以计算一下时间复杂度,处理每一层的复杂度都会比上一层小,相加的多项式,所以只需要考虑第一层的情况,可以假设处理每一层的复杂度都是T(N)
总体复杂度=递归层数*T(N)
对于步骤一,考虑最复杂的情况
(来个图,一条链)这样的话跟暴力没有很大的区别
n次递归,降低层数
可以看到,根的选取对于时间复杂度有着重要的影响
选择根的目标:删除这个根节点之后,结点数最多的树的节点个数最小(平均)
这个点就是树的重心
性质:以树的重心为根时,所有子树的大小都不超过整棵树大小的一半(反证法证明)
假设当前树的重心为u,则整棵树的大小为size(u),对于u的任意儿子v有,size(v)<=size(u)/2.
这样每次分的时候,可以看成是分成了一半,所以递归深度就是log(n),在树是一条链的时候取到上界
(来个图,一条链)
//找重心的代码
void GetRoot(int x,int fa)
{
int tmp=0;
sz[x] = 1;
for (int i=head[x]; i; i=e[i].nxt)
{
int v=e[i].to;
if (v==fa || vis[v])
continue;
GetRoot(v,x);
sz[x]+=sz[v];
tmp = max(tmp, sz[v]);
}
tmp = max(tmp, SZ - sz[x]);//除了子树之外的部分**********
//更新root
if (tmp < mx)
mx = tmp, root = x;
}
2. 计算经过根的路径
可以用dis数组,保存每个点到根的路径长度,(得到所有过跟的链)
那么,经过根的路径就可以表示为两条链拼接而成的
但是可以注意到,会存在一些不合法的情况:在同一边,所以要删除
把得到的dis数组从小到大排序之后,统计dis[i]+dis[j]<=k的个数即可
//当前子树中,每个节点到根结点的距离
void GetDis(int x, int fa)
{
Q[++r] = dis[x]; //**********
for (int i = head[x]; i; i = e[i]