最近我写了一题关于树的点分治题目的博客(^ - ^ 那篇博客的链接),于是现在就想写一篇关于树的点分治的博客作为本蒟蒻的又一篇博客。
Q:树的点分治通常用于解决哪一些问题呢?_?
A:有关于树的路径信息进行处理答案的问题(满足条件的路径/点对的数量,最小/大权值等等)通常可以使用树的点分治解决。
Q:那么树的点分治解决问题的框架是怎样的呢?…?
A:第一步 把树建好(统计每个节点的儿子数量,找到 树的重心)
第二步 选一个合适的点(树的重心)进入递归开始操作
第三步 对当前子树统计/维护答案
第四步 把现在的点删去(打个标记之类的)
第五步 在当前点的每个子树中再找一个合适的点(树的重心)建树,继续递归进行操作(返回第三步)
好的,那么接下来引入一个概念,这就是树的点分治的核心——树的重心!
树的重心也叫树的质心。对于一棵树n个节点的无根树,找到一个点,,其所有的子树中最大的子树节点数最少,那么这个点就是这棵树的重心,使得把树变成以该点为根的有根树时,最大子树的结点数最小。换句话说,删除这个点后最大连通块(一定是树)的结点数最小。(转载自百度百科)
这个树的重心它有一个很明显的性质:一棵树的重心最多只有两个,并且它的子树大小均不大于当前树大小的一半。
那么我们可以在递归操作选点时,每次选用树的重心,那么这样很显然,我们的递归层数优化到了 log2n。
接下来剩下的框架就可以用n解决(除了统计答案)。
至于统计答案,不同的题目有不同的统计方法,时间复杂度也有所不同(不过一般也就是 n或nlogn)
下面以一道例题为例,更好的说明树的点分治。
树中点对距离
Description
给出一棵带边权的树,问有多少对点的距离<=Len
Input
第一行两个整数N,Len(2<=n<=10000,len<=maxlongint)
接下来N-1行,每行3个整数,x,y,l,表示x和y有一条边长为l的边
Output
一行,一个整数ans,表示答案
Sample Input
5 4
1 2 3
1 3 1
1 4 2
3 5 1
Sample Output
8
首先第一步我们要把树先建好(code是pascal的有些丑~_ ~):
function build(x,la:longint):longint;
var
i,max,bd:longint;
begin
i:=l[x];
max:=0; //max表示当前节点最大子树大小
f[x]:=1; //f[x]表示当前节点的孩子个数
while i<>0 do
begin
if (bz[map[i]])and(map[i]<>la) then
begin
bd:=build(map[i],x);
if bd>max then max:=bd;
f[x]:=f[x]+bd;
end;
i:=last[i];
end;
if size-f[x]-1>max then max:=size-f[x]-1;
if max<minn then
begin
minn:=max; //寻找最小的节点最大的子树大小,记录一下节点编号,这个节点就是树的重心
mint:=x;
end;
f[x]:=f[x]+1;
exit(f[x]);
end;
然后接下来从树的重心开始递归进行操作
procedure dg(x:longint);
var
i:longint;
begin
ans:=ans+incs(x,0); //统计答案
bz[x]:=false; //打个标记,删除这个点
i:=l[x]; //链式前向星
while i<>0 do
begin
if bz[map[i]] then
begin
ans:=ans-incs(map[i],maps[i]); //容斥原理,减去重复的不合法的答案
mint:=0; minn:=maxlongint; size:=f[map[i]];
build(map[i],x);//以这个孩子建树,并且找到那个子树的树的重心
dg(mint); //递归这个子树的重心,进行下一轮操作
end;
i:=last[i];
end;
end;
接下来我们再来思考一下怎么统计答案(还有更优的方法)
function incs(now,st:longint):longint;
var
i,head,tail,s,l2,r:longint;
begin
head:=0;
tail:=1;
b[1]:=now; //以当前节点开始寻找路径
las[1]:=now;
dis[now]:=st; //统计路径长度(距离)
while head<tail do
begin
inc(head);
i:=l[b[head]];
while i<>0 do
begin
if (bz[map[i]])and(dis[map[i]]>dis[b[head]]+maps[i]) then //已经删去的点不管
begin
dis[map[i]]:=dis[b[head]]+maps[i];
inc(tail);
b[tail]:=map[i];
las[tail]:=b[head];
end;
i:=last[i];
end;
end;
s:=0;
for i:=1 to n do
if dis[i]<1000000000 then
begin
inc(s);
jl[s]:=dis[i];
dis[i]:=1000000000;
end;//找出能走到的点,记录长度
pai(1,s); //按照长度从小到大排序
l2:=1;
r:=s;
incs:=0;
while l2<r do
begin
while (l2<r)and(jl[l2]+jl[r]>len) do
dec(r);
if l2<r then
incs:=incs+r-l2;//满足jl[l2]+jl[r]<=len,说明这个区间任意匹配都满足,统计进答案
inc(l2);
end;
end;
把这道题完整的code贴出来:
var
bz:array[0..1000005]of boolean;
b,las,map,maps,jl,dis,l,last,f:array[0..1000005]of longint;
i,n,len,x,y,l2,size,minn,mint,ans,sum:longint;
function build(x,la:longint):longint;
var
i,max,bd:longint;
begin
i:=l[x];
max:=0;
f[x]:=1;
while i<>0 do
begin
if (bz[map[i]])and(map[i]<>la) then
begin
bd:=build(map[i],x);
if bd>max then max:=bd;
f[x]:=f[x]+bd;
end;
i:=last[i];
end;
if size-f[x]-1>max then max:=size-f[x]-1;
if max<minn then
begin
minn:=max;
mint:=x;
end;
f[x]:=f[x]+1;
exit(f[x]);
end;
procedure pai(i,j:longint);
var
l,r,mid,t:longint;
begin
l:=i;
r:=j;
mid:=jl[(i+j)div 2];
while i<j do
begin
while jl[i]<mid do inc(i);
while jl[j]>mid do dec(j);
if i<=j then
begin
t:=jl[i]; jl[i]:=jl[j]; jl[j]:=t;
inc(i);
dec(j);
end;
end;
if l<j then pai(l,j);
if i<r then pai(i,r);
end;
function incs(now,st:longint):longint;
var
i,head,tail,s,l2,r:longint;
begin
head:=0;
tail:=1;
b[1]:=now;
las[1]:=now;
dis[now]:=st;
while head<tail do
begin
inc(head);
i:=l[b[head]];
while i<>0 do
begin
if (bz[map[i]])and(dis[map[i]]>dis[b[head]]+maps[i]) then
begin
dis[map[i]]:=dis[b[head]]+maps[i];
inc(tail);
b[tail]:=map[i];
las[tail]:=b[head];
end;
i:=last[i];
end;
end;
s:=0;
for i:=1 to n do
if dis[i]<1000000000 then
begin
inc(s);
jl[s]:=dis[i];
dis[i]:=1000000000;
end;
pai(1,s);
l2:=1;
r:=s;
incs:=0;
while l2<r do
begin
while (l2<r)and(jl[l2]+jl[r]>len) do
dec(r);
if l2<r then
incs:=incs+r-l2;
inc(l2);
end;
end;
procedure dg(x:longint);
var
i:longint;
begin
ans:=ans+incs(x,0);
bz[x]:=false;
i:=l[x];
while i<>0 do
begin
if bz[map[i]] then
begin
ans:=ans-incs(map[i],maps[i]);
mint:=0; minn:=maxlongint; size:=f[map[i]];
build(map[i],x);
dg(mint);
end;
i:=last[i];
end;
end;
begin
readln(n,len);
for i:=1 to n do
begin
bz[i]:=true;
dis[i]:=1000000000;
end;
for i:=1 to n-1 do
begin
readln(x,y,l2);
inc(sum); map[sum]:=y; maps[sum]:=l2; last[sum]:=l[x]; l[x]:=sum;
inc(sum); map[sum]:=x; maps[sum]:=l2; last[sum]:=l[y]; l[y]:=sum;
end;
minn:=maxlongint;
mint:=0;
size:=n;
build(1,1);
dg(mint);
writeln(ans);
end.
好了那么树的点分治就告一段落了,这里有几道例题推荐一下,可以上网找找:
树的难题(这题以前我发过博客^ - ^)
阴阳
幻想乡的战略游戏
树上的路径