开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第4天,点击查看活动详情
点分治,是一种针对可带权树上简单路径统计问题的算法,本质上是一种带优化的暴力。下面我们以一个经典问题为例对这个算法进行介绍。
题目链接
P3806 【模板】点分治1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
问题定义
给定一棵有 个节点的带权无根树,和 个询问,每个询问要求输出树上距离为 的点对是否存在。
。
问题分析
很容易想到可以枚举每个点作为根节点,对整棵树进行 DFS 求出每个节点到本次选择的根节点的距离,给所有合法解都打上标记。预处理后,对每个询问 查询输出。时间复杂度 ,会 TLE。
考虑优化,上述过程中时间浪费在什么地方呢?不同路径长度的种类是 级别,难以突破瓶颈。因为每个点都会被选择为根节点进行 DFS,每条边都会被访问左右两边点的数量之积次!而且每次 DFS 的结果都会被扔掉,这太浪费了。怎么才能更好的利用单次 DFS 求得的信息呢?
点分治
分治
因为 很小,我们尝试利用。对于每次一询问 :
让我们先随便找一个点作为根节点把整棵树拎起来,并求出每个点到该点的距离 ,和以每个点为根的子树的大小 。
我们可以把所有路径分为经过根节点和不经过两种。如果我们可以判断经过根节点的所有路径的长度是否存在 ,就可以把根节点删掉形成许多棵子树,对子树进行同样的处理求出不经过根节点的路径的答案。
下面我们讨论怎样求经过根节点的所有路径的长度是否存在 。
对于以 为端点的两条路径,如果它们的另一个端点 和 不在 的同一棵子树中,它们两个可以接在一起,形成一条长度为 的路径。
因为 不超过 ,我们可以开一个桶 记录以 为一个端点的路径长度,一开始桶为空。遍历 的每一个子树执行以下操作:
- 对于第 棵子树中的每一个节点 ,我们都去看看桶 里是否存在标记 。若存在,即可说明经过根节点的所有路径的长度存在 。
- 用子树 中所有节点的 值更新桶 。
对每个点执行该过程的时间复杂度为 。
上述过程因题而异。
重心
虽然上述求解好像比直接暴力优秀的一点点,但是总时间复杂度与树的形状关系很大,当树变成一条链的时候复杂度退化为 。随便估算一下发现时间复杂度约为点数与树高之积。
说到最小化树高容易想到可以求直径的中点,但是这还不是最优的。还记得我们刚才提到:
如果我们可以判断经过根节点的所有路径的长度是否存在 ,就可以把根节点删掉形成许多棵子树,对子树进行同样的处理求出不经过根节点的路径的答案。
当我们计算过经过根节点 的路径之后, 的各个子树之间的答案相互独立毫无关联,对 也没有贡献。可以视为将根节点直接删掉,形成的森林中的每棵树仍然是无根树。所以我们对节点 进行操作后,包含其子节点 的未被操作的连通块可以重新在其内部选择一个根,不必须以 为根。
所以如果每个根 被删除后其子节点形成的最大的连通块大小不超过 ,那么整棵树将被划分为 层。我们希望删除掉根节点后形成的最大的子树尽可能小,这样我们就可以用最少的层数计算整棵树的答案,这符合树的重心的定义。
求树的重心只需要 遍历一遍树即可,详见代码。
inline void getroot(int u,int fa,int num)
{
siz[u]=1;
mx[u]=0;
for (auto v:e[u])
{
if (v.to==fa||vis[v.to]) continue;
getroot(v.to,u,num);
siz[u]+=siz[v.to];
mx[u]=max(mx[u],siz[v.to]);
}
mx[u]=max(mx[u],num-siz[u]);
if (mx[rt]>mx[u]) rt=u;
}
所以总体复杂度 。
上述过程就是点分治,或称为树的重心剖分。即在大规模问题的答案与小规模问题的答案相互独立互不影响时,可以一直以没有被操作过的连通块的重心作为分治点(相当于改变树的形状以降低树高),来减小时间消耗。
代码
由于点分治常数较大,可以将查询都读进来在一次点分治内离线处理。
#include <stdio.h>
#include <algorithm>
#include <vector>
using namespace std;
const int N=10001;
struct asdf{
int to,val;
};
vector<asdf> e[N];
int n,m,mx[N],siz[N],rt,vis[N],q[N],ans[N];
int dis[N],c[10000001],tot,clr[N],cnt;
inline void getroot(int u,int fa,int num)
{
siz[u]=1;
mx[u]=0;
for (auto v:e[u])
{
if (v.to==fa||vis[v.to]) continue;
getroot(v.to,u,num);
siz[u]+=siz[v.to];
mx[u]=max(mx[u],siz[v.to]);
}
mx[u]=max(mx[u],num-siz[u]);
if (mx[rt]>mx[u]) rt=u;
}
inline void getdis(int u,int nw,int fa)
{
if (nw>1e7) return;
dis[++tot]=nw;
for (auto v:e[u])
{
if (vis[v.to]||v.to==fa) continue;
getdis(v.to,nw+v.val,u);
}
}
inline void divide(int u)
{
vis[u]=c[0]=1;
cnt=0;
for (auto v:e[u])
{
if (vis[v.to]) continue;
tot=0;
getdis(v.to,v.val,u);
for (int i=1;i<=m;++i)
for (int j=1;j<=tot&&!ans[i];++j)
if (q[i]>=dis[j]) ans[i]|=c[q[i]-dis[j]];
for (int i=1;i<=tot;++i)
{
c[dis[i]]=1;
clr[++cnt]=dis[i];
}
}
for (int i=1;i<=cnt;++i) c[clr[i]]=0;
for (auto v:e[u])
{
if (vis[v.to]) continue;
rt=0;
getroot(v.to,0,siz[v.to]);
getroot(rt,0,siz[v.to]);
divide(rt);
}
}
int main()
{
scanf("%d%d",&n,&m);
mx[0]=1e9;
for (int i=1,x,y,z;i<n;++i)
{
scanf("%d%d%d",&x,&y,&z);
e[x].push_back({y,z});
e[y].push_back({x,z});
}
for (int i=1;i<=m;++i) scanf("%d",&q[i]);
rt=0;
getroot(1,0,n);
getroot(rt,0,n);
divide(rt);
for (int i=1;i<=m;++i)
if (ans[i]) printf("AYE\n");
else printf("NAY\n");
return 0;
}