树链剖分

简单来说树链剖分就是把树上问题转化为区间问题的一种方法,同时可以用时间复杂度来换空间复杂度。

\(LCA\)

首先来说一下如何求 \(LCA\)

其中我们把每个父亲的儿子中 \(size\) (子树个数) 最大的儿子称为重儿子。而其它则为 轻儿子,这样子就把树分为了很多条链。下图中的 \(H\) 都是重儿子。

我们再记录一下每一个节点的 \(top\) (链顶) 和 \(dep\) (深度)。(图的编号不同了,且只记录了链顶)

假如我们先求 \(7\) 号点和 \(5\) 号点的 \(LCA\)

我们发现 \(dep[top[5]] > dep[top[7]]\),所以从 \(5\) 节点跳到 \(father[top[5]]\)

然后现在两个点在同一个链里面了,我们取 \(dep\) 比较浅的那一个作为 \(LCA\)

前面需要两个 \(Dfs\) 预处理。

// luogu-judger-enable-o2
var
    cnt,size,dep,top,father,son:array[-1..510000] of longint;
    next,reach:array[-1..1050000] of longint;
    j,i,n,m,l,r,tot,root:longint;

procedure add(l,r:longint); // 链式前向星
begin
    inc(tot);
    reach[tot]:=r;
    next[tot]:=cnt[l];
    cnt[l]:=tot;
end;

procedure Dfs_1(x:longint); // 预处理 father 和 size
var i:longint;
begin
    size[x]:=1; i:=cnt[x]; size[0]:=-maxlongint div 843;
    while i<>-1 do
    begin
        if dep[reach[i]]=0 then
        begin
            dep[reach[i]]:=dep[x]+1;
            father[reach[i]]:=x;
            Dfs_1(reach[i]); inc(size[x],size[reach[i]]);
            if size[reach[i]]>size[son[x]] then son[x]:=reach[i];
        end; 
        i:=next[i];
    end;
end;

procedure Dfs_2(x,centre:longint); // 已知 father 和 size 求 top 和 son(重儿子)
var i:longint;
begin
    top[x]:=centre;
    if son[x]=0 then exit; Dfs_2(son[x],centre);
    i:=cnt[x];
    while i<>-1 do
    begin 
        if (reach[i]<>father[x])and(reach[i]<>son[x]) then Dfs_2(reach[i],reach[i]);
        i:=next[i];
    end;
end;

function Refer(x,y:longint):int64; // 查找 LCA
begin
    while top[x]<>top[y] do
    begin
        if dep[top[x]]<dep[top[y]] then y:=father[top[y]] else x:=father[top[x]]; // 看谁的 dep[top[]] 深,谁跳
    end;
    if dep[x]<dep[y] then exit(x) else exit(y); // 同一条链
end;

begin
    filldword(cnt,sizeof(cnt) div 4,maxlongint*2+1); // 初值为 -1
    read(n,m,root);
    for i:=1 to n-1 do begin read(l,r); add(l,r); add(r,l); end; // 双向边
    dep[root]:=1; father[root]:=1;
    Dfs_1(root); Dfs_2(root,root);
    for i:=1 to m do begin read(l,r); writeln(Refer(l,r)); end;
end.

树上两点最近距离之和

树上求值也很简单。不同的是我们要还要预处理以下信息:

  • \(dfn\) : \(Dfs\)序, \(Dfs\) 中重儿子先遍历
  • \(val\) : 每一个点的点权

其中 \(dfn\) 的用处就是把树变成一个区间。注 : \(val\) 是乱给的。

假设我们求 \(2\)\(8\) 的和。
第一步,先让 \(dep[top[]]\) 大的跳到 \(father[top[]]\),红色是到链顶,蓝色是到父亲 : (\(ans+3+1+10=14\))

然后我们会发现它们在同一条链里面了 (同一个链顶),就可以直接搞。(\(ans+2+1=17\))

树上修改和树上查询是同理的。查询的时候用线段树维护,打一个懒标记就好了。

下面说一下如何查子树的值。举个例子:

一眼就可以看出的规律。因为 \(Dfs\) 序的原因,所以只需要查询 \(dfn[x] -> dfn[x]+size[x]-1\) 这个区间就可以了。子树的顺序不用管它。修改也是同理。

// luogu-judger-enable-o2
var
    cnt,size,dfn,dep,top,num,father,son:array[-1..210000] of int64; // 分别为链式前向星的 head,子树大小,Dfs序,链顶,点权,父亲,重儿子
    next,reach:array[-1..450000] of longint; // 链式前向星
    left,right:array[-1..450000] of longint; // 线段树中的左右区间,我这里用数组记录
    tree,lazy:array[-1..450000] of int64; // 线段树和懒标记(tag)
    val:array[-1..210000] of int64; // 树上点权
    j,i,n,m,l,r,dfnum,tot,root,order:longint; // 后 4 个分别是 Dfs序,链式前向星的 tot,根和命令
    k,modn:int64; // 取模数

function remainder(var num:int64):longint; begin num:=num mod modn; exit(num); end; // 取模

procedure swap(var a,b:longint);var t:longint; begin t:=a; a:=b; b:=t; end; // 交换

procedure add(l,r:longint); // 连边
begin
    inc(tot);
    reach[tot]:=r;
    next[tot]:=cnt[l];
    cnt[l]:=tot;
end;

procedure Dfs_1(x:longint); // 求 size,father,dep
var i:longint;
begin
    size[x]:=1; i:=cnt[x]; size[0]:=-maxlongint div 843;
    while i<>-1 do
    begin
        if dep[reach[i]]=0 then
        begin
            dep[reach[i]]:=dep[x]+1;
            father[reach[i]]:=x;
            Dfs_1(reach[i]); inc(size[x],size[reach[i]]);
            if size[reach[i]]>size[son[x]] then son[x]:=reach[i];
        end;
        i:=next[i];
    end;
end;

procedure Dfs_2(x,centre:longint); // 求 top,dfn,son
var i:longint;
begin
    inc(dfnum); dfn[x]:=dfnum; val[dfnum]:=num[x]; top[x]:=centre;
    if son[x]=0 then exit; Dfs_2(son[x],centre);
    i:=cnt[x];
    while i<>-1 do
    begin
        if (reach[i]<>father[x])and(reach[i]<>son[x]) then Dfs_2(reach[i],reach[i]);
        i:=next[i];
    end;
end;

procedure SUC(k:longint); // 懒标记
begin
    inc(lazy[k << 1],lazy[k]);
    inc(lazy[k << 1+1],lazy[k]); 
    inc(tree[k << 1],lazy[k]*(right[k << 1]-left[k << 1]+1)); remainder(tree[k << 1]);
    inc(tree[k << 1+1],lazy[k]*(right[k << 1+1]-left[k << 1+1]+1)); remainder(tree[k << 1+1]);
    lazy[k]:=0;
end;

procedure Build(k,l,r:longint); // 建树
var mid:longint;
begin
    left[k]:=l; right[k]:=r;
    if (l=r) then begin tree[k]:=val[l]; exit; end; // 注意是 val[l]
    mid:=(l+r) >> 1;
    Build(k << 1,l,mid); Build(k << 1+1,mid+1,r);
    tree[k]:=tree[k << 1]+tree[k << 1+1]; remainder(tree[k]);
end;

procedure Change(k,x,y:longint;modify:int64); // 线段树修改
var mid:longint;
begin
    if (x<=left[k])and(right[k]<=y) then
    begin
        inc(tree[k],modify*(right[k]-left[k]+1)); remainder(tree[k]);
        inc(lazy[k],modify); exit;
    end;
    mid:=(left[k]+right[k]) >> 1;
    if lazy[k]>0 then SUC(k);
    if mid>=y then Change(k << 1,x,y,modify) else
    if mid<x then Change(k << 1+1,x,y,modify) else
    begin
        Change(k << 1,x,mid,modify);
        Change(k << 1+1,mid+1,y,modify);
    end;
    tree[k]:=tree[k << 1]+tree[k << 1+1]; remainder(tree[k]);
end;

function Query(k,x,y:longint):int64; // 线段树查询
var mid:longint;
begin
    Query:=0;
    if (x<=left[k])and(right[k]<=y) then exit(remainder(tree[k]));
    mid:=(left[k]+right[k]) >> 1;
    if lazy[k]>0 then SUC(k);
    if mid>=y then inc(Query,Query(k << 1,x,y)) else
    if mid<x then inc(Query,Query(k << 1+1,x,y)) else
    inc(Query,Query(k << 1,x,mid)+Query(k << 1+1,mid+1,y)); 
    exit(remainder(Query));
end;

function Refer(x,y:longint):int64; // 树上查询
begin
    Refer:=0;
    while top[x]<>top[y] do
    begin
        if dep[top[x]]<dep[top[y]] then swap(x,y);
        inc(Refer,Query(1,dfn[top[x]],dfn[x])); remainder(Refer);
        x:=father[top[x]];
    end;
    if dep[x]>dep[y] then swap(x,y);
    inc(Refer,Query(1,dfn[x],dfn[y])); remainder(Refer);
end;

procedure Revise(x,y:longint;modify:int64); // 树上修改
begin
    while top[x]<>top[y] do
    begin
        if dep[top[x]]<dep[top[y]] then swap(x,y);
        Change(1,dfn[top[x]],dfn[x],modify);
        x:=father[top[x]];
    end;
    if dep[x]>dep[y] then swap(x,y);
    Change(1,dfn[x],dfn[y],modify);
end;

begin
    filldword(cnt,sizeof(cnt) div 4,maxlongint*2+1);
    read(n,m,root,modn);
    for i:=1 to n do begin read(num[i]); remainder(num[i]); end;
    for i:=1 to n-1 do begin read(l,r); add(l,r); add(r,l); end;
    dep[root]:=1; father[root]:=1;
    Dfs_1(root); Dfs_2(root,root); Build(1,1,n);
    for i:=1 to m do
    begin
        read(order);
        if order=1 then begin read(l,r,k); Revise(l,r,remainder(k)); end;
        if order=2 then begin read(l,r); writeln(Refer(l,r)); end;
        if order=3 then begin read(l,k); Change(1,dfn[l],dfn[l]+size[l]-1,remainder(k)); end; // 自己可以试一下为什么可以这样子搞
        if order=4 then begin read(l); writeln(Query(1,dfn[l],dfn[l]+size[l]-1)); end;
    end;
end.

注意 :

  • 线段树维护的是 \(val[dfn[x]]\) !!!! (改了好几个钟)
  • 可以用其它数据结构维护
  • 要取模,空间开大。

总结 :

  • 把树上问题很好的转化成了区间问题
  • 很好的把握了空间复杂度与时间复杂度的 (相对) 平衡。
posted @ 2018-10-16 13:06  _ARFA  阅读(179)  评论(1编辑  收藏  举报