对LCA、树上倍增、树链剖分(重链剖分&长链剖分)和LCT(Link-Cut Tree)的学习

LCA


what is LCA & what can LCA do

LCA(Lowest Common Ancestors),即最近公共祖先
在一棵树上,两个节点的深度最浅的公共祖先就是 L C A LCA (本身能够是本身的祖先)
很简单,咱们能够快速地进入正题了html

下图中的树, 4 4 5 5 L C A LCA 2 1 1 3 3 L C A LCA 1(1也算1的祖先)node

除了是直接求两点的 L C A LCA 模板题,没有题目会叫你打个模板c++

那么LCA能干吗?web

其实 L C A LCA 用途很广,感受用 L C A LCA 最多的像是这样子的题算法

  • 给你一棵树 (或者是一个图求完MST,这很常见) 和树上的两点数组

  • 而后找出这两点之间的路径极值权值和或其余东东数据结构

这时咱们能够借助这两点的LCA做为中转点来轻松地解决这类题目app


how to discover LCA

仍是这棵树svg

在这里插入图片描述

4 4 3 3 L C A LCA 1,这两点间的路径是下图标红的路径学习

怎么求LCA呢?


method one

暴力dfs一遍,时间复杂度 O ( n ) O(n)
侮辱智商
(逃)


method two

模拟,从两点开始一块儿向上跳一步,跳过的点标记一下,时间复杂度 O ( n ) O(n)
侮辱智商*2
(逃)


method three

dfs记录每一个点的深度,求 L C A LCA 时先调至统一深度,再一块儿向上跳
若是这棵树是一个恰好分红两叉的树,时间复杂度 O ( n ) O(n)
侮辱智商*3
(逃)


上面的都是小学生都会的东西,没必要讲了
想真正用有意义 L C A LCA ,下面才是重点


tarjan算法(离线)求LCA

L C A LCA 有一种很是容易理解的方法就是tarjan算法预处理离线解决,时间复杂度 O ( n + q ) O(n+q) 飞速

仍感受离线算法不适用于全部题目
(好比某些良心出题人丧病地要你强制在线,GG)

反正在线算法能够解决离线能作出来全部的题

因此,我不讲 t a r j a n tarjan L C A LCA 了,推荐一篇写的很棒的blog
接下来的几个在线算法才是搞 L C A LCA 的门槛


ST(RMQ)算法(在线)求LCA

这种方法的思想,就是将LCA问题转化成RMQ问题

  • 若是不会 R M Q RMQ 问题—— S T ST 算法的就戳这里

能够转化成RMQ问题?

这里有棵树

如何预处理呢?

咱们用dfs遍历一次,获得一个 d f s dfs 序(儿子节点回到父亲节点还要再算一遍
d f s dfs 序就是这样的1->2->4->7->4->8->4->2->5->2->6->9->6->10->6->2->1->3->1
(一开始在 r o o t root 向儿子节点走,到了叶子节点就向另外一个儿子走,最后回到 r o o t root

d f s dfs 预处理的时间复杂度 O ( n ) O(n)


dfs序妙啊

r [ x ] r[x] 表示 x x d f s dfs 序当中第一次出现的位置, d e p t h [ x ] depth[x] 表示 x x 的深度
若是求 x x y y L C A LCA r[x]~r[y]这一段区间内必定有 L C A ( x , y ) LCA(x,y) ,并且必定是区间中深度最小的那个点

(好比上面的dfs序中,第一个 7 7 和第一个 5 5 之间的序列里的深度最小的点是 2 2 ,而 2 2 正是 7 7 5 5 L C A LCA !)

  • 遍历以 L C A ( x , y ) LCA(x,y) 为根的树时,不遍历完全部以 L C A ( x , y ) LCA(x,y) 为根的树的节点是不会回到 L C A ( x , y ) LCA(x,y)

  • 还有就是明显地,想到达x再到y,必须上溯通过它们的LCA(两个点之间有且只有一条路径

  • 因此 L C A LCA 的深度必定最小

直接用RMQ——ST表维护这个东西,求出来的最小值的点即为 L C A LCA


matters need attention

  • d f s dfs 序的长度是 2 n 1 2n-1 ,用 d f s O ( n ) dfsO(n) 处理出 r r d e p t h depth d f s dfs 序以后直接套上裸的 R M Q RMQ

  • f [ i ] [ j ] f[i][j] 表示dfs序中j~j+2^i-1的点当中,depth值最小的是哪一个点 便可

  • 那么单次询问时间复杂度 O ( 1 ) O(1)

  • 完美


code

这段代码是我co过来的,由于我也是看别人博客学的
但这代码是真的丑

#include <cstdio>
#include <cstring>
#include <cmath>

using namespace std;

int n,_n,m,s;//_n是用来放元素进dfs序里,最终_n=2n-1

struct EDGE
{
    int to;
    EDGE* las;
}e[1000001];//前向星存边

EDGE* last[500001];
int sx[1000001];//顺序,为dfs序
int f[21][1000001];//用于ST算法
int deep[500001];//深度
int r[500001];//第一次出现的位置

int min(int x,int y)
{
	return deep[x]<deep[y]?x:y;
}

void dfs(int t,int fa,int de)
{
    sx[++_n]=t;
    r[t]=_n;
    deep[t]=de;
    EDGE*ei;
    for (ei=last[t];ei;ei=ei->las)
        if (ei->to!=fa)
        {
            dfs(ei->to,t,de+1);
            sx[++_n]=t;
        }
}

int query(int l,int r)
{
    if (l>r)
    {
    	//交换 
        l^=r;
        r^=l;
        l^=r;
    }
    int k=int(log2(r-l+1));
    return min(f[k][l],f[k][r-(1<<k)+1]);
}

int main()
{
    scanf("%d%d%d",&n,&m,&s);
    int j=0,x,y;
    for (int i=1;i<n;++i)
    {
        scanf("%d%d",&x,&y);
        e[++j]={y,last[x]};
        last[x]=e+j;
        e[++j]={x,last[y]};
        last[y]=e+j;
    }
    dfs(s,0,0);
    //如下是ST算法
    for (int i=1;i<=_n;++i)f[0][i]=sx[i];
    int ni=int(log2(_n)),nj,tmp;
    for (int i=1;i<=ni;++i)
    {
        nj=_n+1-(1<<i);
        tmp=1<<i-1;
        for (j=1;j<=nj;++j)
            f[i][j]=min(f[i-1][j],f[i-1][j+tmp]);
    }
    //如下是询问,对于每次询问,能够O(1)回答
    while (m--)
    {
        scanf("%d%d",&x,&y);
        printf("%d\n",query(r[x],r[y]));
    }
}

小结

基础的 L C A LCA 知识你应该已经会了吧
L C A LCA 的运用挺广吧,感受和线段树的考频有的一比,不掌握是会吃亏的
上面的是比较普通的方法求 L C A LCA ,其实树上倍增也能求 L C A LCA
接下来到丧心病狂 (其实很简单) 的树上倍增了


树上倍增


又是什么东东?

倍增这个东东严格来讲就是种思想,只可意会不可言传
倍增,是根据已经获得了的信息,将考虑的范围扩大,从而加速操做的一种思想

使用了倍增思想的算法有

  • 归并排序
  • 快速幂
  • 基于ST表的RMQ算法
  • 树上倍增找LCA等
  • FFT、后缀数组等高级算法
  • …… F F T FFT 有倍增的么……我可能学了假的 F F T FFT

some advantages

树上倍增和 R M Q RMQ 比较类似的,都采用了二进制的思想,因此时空复杂度低
其实树上倍增树链剖分在树题里面都用的不少

  • 两个都十分有趣,但倍增有着显而易见的优势——

  • 比起树链剖分,树上倍增代码短,查错方便,时空复杂度优(都是 O ( n l o g 2 n ) O(nlog_2n)

  • 只是功能欠缺了一些不要太在乎

即便如此,树上倍增也可以解决大部分的树型题目反正两个都要学


树上倍增(在线)求LCA

preparation

怎么样预处理以达到在线单次询问 O ( l o g 2 n ) O(log_2n) 的时间呢?

咱们须要构造倍增数组

  • a n c [ i ] [ j ] anc[i][j] 表示i节点的第2^j个祖先

  • 注意 a n c anc 数组开到 a n c [ n ] [ l o g 2 n ] anc[n][log_2n] ,由于树上任意一点最多有 2 l o g 2 n 2^{log_2n} 个祖先

  • 能够发现这个东西能够代替不路径压缩的并查集 a n c [ i ] [ 0 ] = f a t h e r ( i ) ∵anc[i][0]=father(i)
    (若 a n c [ i ] [ j ] = 0 anc[i][j]=0 则说明 i i 的第 2 j 2^j 祖先不存在)

而后,倍增的性质 (DP方程) 就清楚地出来了 a n c [ i ] [ j ] = a n c [ a n c [ i ] [ j 1 ] ] [ j 1 ] anc[i][j]=anc[anc[i][j-1]][j-1]

  • 用文字来表达它就是这样的

  • i的第 2 j 2^j 个父亲是i的第 2 j 1 2^{j-1} 个父亲的第 2 j 1 2^{j-1} 个父亲

  • 神奇不?

就是说暴力求 i i 的第 k k 个祖先的时间复杂度是 O ( k ) O(k) ,如今变成了 O ( l o g 2 k ) O(log_2k)
同时,用一个dfs处理出每一个点的深度 d e p t h [ x ] depth[x] a n c [ x ] [ 0 ] anc[x][0] ,时间复杂度 O ( n ) O(n)
预处理的时间复杂度即为 O ( n log 2 n ) O(n\log_2n)


code

procedure dfs(x,y:longint);
var
        now:longint;
begin
        now:=last[x];
        while now<>0 do
        begin
                if b[now]<>y then
                begin
                        anc[b[now],0]:=x;
                        depth[b[now]]:=depth[x]+1;
                        dfs(b[now],x);
                end;
                now:=next[now];
        end;
end;
for j:=1 to trunc(ln(n)/ln(2)) do
        for i:=1 to n do
                anc[i,j]:=anc[anc[i,j-1],j-1];

query

请务必认认真真地阅读如下内容不然树上倍增你就学不会了

树上倍增的运用中,最简单最实用的就是求 L C A LCA

  • 如今咱们须要求出 L C A ( x , y ) LCA(x,y) ,怎么倍增作?

还记得前面三种脑残 O ( n ) O(n) L C A LCA 的第三个方法吗?

用dfs记录每一个点的深度,求LCA时先调至统一深度,再一块儿向上跳

其实树上倍增运用的就是这个思想!只不过期间复杂度降至了飞快的 O ( log 2 n ) O(\log_2n)

  • 对于两个节点 u u v v ,咱们先把 u u v v 调至同一深度

  • 若此时 u = v u=v ,那么原来两点的 L C A LCA 即为当前点

  • 若是 d e p t h [ u ] = d e p t h [ v ] depth[u]=depth[v] u v u≠v ,就说明 L C A ( u , v ) LCA(u,v) 在更的地方

  • 咱们同时把 u u v v 向上跳 2 k 2^k k = l o g 2 d e p t h [ u ] k=log_2depth[u] ),直到 u = v u=v

明显这种方法确定能求出 L C A LCA ,由于 u u v v 必定会相遇
倍增比那种脑残方法优的是,脑残方法一步一步向上跳,倍增一次 2 k 2^k 步!

  • 但还存在着一些问题——若是这样跳,跳到了 L C A LCA 祖先节点怎么搞?

因此若是 u u v v 向上跳 2 k 2^k 步到达的节点相同,那咱们就不跳,让 u u v v 的第 2 k 2^k 祖先不一样便可跳
注意每下一次向上跳的距离是上一次跳的 1 2 1 \over 2 k = k 1 k=k-1 ),直到 u = v u=v L C A LCA 即已被求出

这样使 u u v v L C A LCA 的距离每次缩短一半时间复杂度也就是 O ( log 2 n ) O(\log_2n)

最后如何把 u u v v 调至同一深度?
实际上是同样的,先把较深的那个点调浅就好了


code

  • 以为上面说得抽象的话见标理解一下
function lca(x,y:longint):longint;
var
        k:longint;
begin
        if depth[x]<depth[y] then swap(x,y);
        k:=trunc(ln(depth[x]-depth[y]+1)/ln(2));
        while k>=0 do
        begin
                if depth[anc[x,k]]>depth[y] then x:=anc[x,k];
                dec(k);
        end;
        if depth[x]<>depth[y] then x:=anc[x,0];
        k:=trunc(ln(d[x])/ln(2));
        while k>=0 do
        begin
                if anc[x,k]<>anc[y,k] then
                begin
                        x:=anc[x,k];
                        y:=anc[y,k];
                end;
                dec(k);
        end;
        exit(x);
end;

以上三段 c o d e code 已经能解决树上倍增求 L C A LCA
树上倍增求 L C A LCA 时间复杂度 O ( n log 2 n + q log 2 n ) O(n\log_2n+q\log_2n)


树上倍增的真正意义

比赛里面直接求 L C A LCA 是没有什么用处的
其实,树上倍增的真正可怕之处是这个倍增的思想

  • 不信? 来看看这种很常见的题目

给你一棵树和两个点 x x y y ,求这两点间路径的路径最大/小的点权/边权

明显咱们要先求 x x y y L C A LCA ,由于惟一路径必须通过 L C A LCA
L C A LCA 好比说用 R M Q RMQ ,而后呢?暴力 O ( n ) O(n) 再遍历一次路径?
不可能的

  • 这种问题用树上倍增仍是能用 O ( log 2 n ) O(\log_2n) 的时间解决

  • 咱们能够设 d i s [ i ] [ j ] dis[i][j] 表示i到他的第 2 j 2^j 个祖先的路径最大值,就能够边求 L C A LCA 顺便求出两点距离

  • 由于 d i s dis 也符合
    d i s [ i ] [ j ] = m a x ( d i s [ i ] [ j 1 ] , d i s [ a n c [ i ] [ j 1 ] ] [ j 1 ] ) dis[i][j]=max(dis[i][j-1],dis[anc[i][j-1]][j-1])

还有求两点的路径上路径权值和呢?

  • s u m [ i ] [ j ] sum[i][j] 表示i到他的第 2 j 2^j 个祖先的路径权值和,同时也可边求 L C A LCA 边求和,由于
    s u m [ i ] [ j ] = s u m [ i ] [ j 1 ] + s u m [ a n c [ i ] [ j 1 ] ] [ j 1 ] ) sum[i][j]=sum[i][j-1]+sum[anc[i][j-1]][j-1])

这才是树上倍增的真正意义所在,像 R M Q RMQ L C A LCA ,是没法解决这类问题的
至此树上倍增讲的差很少了,看例题


例题1:【JZOJ1738】Heatwave

problem

Description 给你N个点的无向连通图,图中有M条边,第j条边的长度为: d_j.   如今有 K个询问。   每一个询问的格式是:A
B,表示询问从A点走到B点的全部路径中,最长的边最小值是多少?

Input

文件名为heatwave.in   第一行: N, M, K。   第2…M+1行: 三个正整数:X, Y, and D (1 <=
X <=N; 1 <= Y <= N). 表示X与Y之间有一条长度为D的边。   第M+2…M+K+1行: 每行两个整数A
B,表示询问从A点走到B点的全部路径中,最长的边最小值是多少?

Output

对每一个询问,输出最长的边最小值是多少。

Sample Input

6 6 8 1 2 5 2 3 4 3 4 3 1 4 8 2 5 7 4 6 2 1 2 1 3 1 4 2 3 2 4 5 1 6 2
6 1

Sample Output

5 5 5 4 4 7 4 5

Data Constraint

50% 1<=N,M<=3000 其中30% K<=5000 ​100% 1 <= N <= 15,000   1 <= M <=
30,000   1 <= d_j <= 1,000,000,000   1 <= K <= 20,000

Hint


think about other issues

先不考虑其余的,先考虑一下如何搞定题目所给的问题
注意这句话——

从A点走到B点的全部路径中,最长的边的最小值是多少?

想到什么了吗?

  • MST! 既然让最长的边最短,那么咱们就求这棵树的最小生成树便可

这样就能够保证两点间最长边最短 ,这里直接用kruskal+并查集艹过去就好了
邻接表存下便可


analysis

  • 求完 M S T MST 后,这道题是否是就是道裸题?维护 a n c [ i ] [ j ] anc[i][j] d i s [ i ] [ j ] dis[i][j] 便可
    照样用一个 d f s dfs 遍历整棵树,维护出 d e p t h [ x ] depth[x] a n c [ x ] [ 0 ] anc[x][0] d i s [ x ] [ 0 ] dis[x][0] ,以后 O ( n log 2 n ) O(n\log_2n) 预处理

  • 搞定以后,咱们对每一对 ( u , v ) (u,v) L C A LCA (设 u u 深度更深)注意求 L C A LCA 过程当中记录每一次的max值
    a n s = m a x ( d i s u , k ) ans=max(dis_{u,k})

  • a n s ans 即为答案

时间复杂度 O ( m l o g 2 m + n l o g 2 n + q l o g 2 n ) O(mlog_2m+nlog_2n+qlog_2n) 我乱算的


code

var
        b,c,next,last,father,depth:array[0..50000]of longint;
        anc,dis:array[0..15000,0..16]of longint;
        a:array[0..30000,0..3]of longint;
        n,m,q,i,j,x,y,tot:longint;

procedure swap(var x,y:longint);
var
        z:longint;
begin
        z:=x;
        x:=y;
        y:=z;
end;

function max(x,y:longint):longint;
begin
        if x>y then exit(x);
        exit(y);
end;

procedure qsort(l,r:longint);
var
        i,j,mid:longint;
begin
        i:=l;
        j:=r;
        mid:=a[(l+r)div 2,3];
        repeat
                while a[i,3]<mid do inc(i);
                while a[j,3]>mid do dec(j);
                if i<=j then
                begin
                        a[0]:=a[i];
                        a[i]:=a[j];
                        a[j]:=a[0];
                        inc(i);
                        dec(j);
                end;
        until i>j;
        if l<j then qsort(l,j);
        if i<r then qsort(i,r);
end;

function getfather(x:longint):longint;
begin
        if father[x]=x then exit(x);
        father[x]:=getfather(father[x]);
        exit(father[x]);
end;

function judge(x,y:longint):boolean;
begin
        exit(getfather(x)=getfather(y));
end;

procedure insert(x,y,z:longint);
begin
        inc(tot);
        b[tot]:=y;
        next[tot]:=last[x];
        last[x]:=tot;
        c[tot]:=z;
end;

procedure dfs(x,y:longint);
var
        now:longint;
begin
        now:=last[x];
        while now<>0 do
        begin
                if b[now]<>y then
                begin
                        anc[b[now],0]:=x;
                        depth[b[now]]:=depth[x]+1;
                        dis[b[now],0]:=c[now];
                        dfs(b[now],x);
                end;
                now:=next[now];
        end;
end;

function lca(x,y:longint):longint;
var
        k:longint;
begin
        lca:=0;
        if depth[x]<depth[y] then swap(x,y);
        k:=trunc(ln(depth[x]-depth[y]+1)/ln(2));
        while k>=0 do
        begin
                if depth[anc[x,k]]>depth[y] then
                begin
                        lca:=max(lca,dis[x,k]);
                        x:=anc[x,k];
                end;
                dec(k);
        end;
        if depth[x]<>depth[y] then
        begin
                lca:=max(lca,dis[x,0]);
                x:=anc[x,0];
        end;
        k:=trunc(ln(depth[x])/ln(2));
        while k>=0 do
        begin
                if anc[x,k]<>anc[y,k] then
                begin
                        lca:=max(max(lca,dis[x,k]),dis[y,k]);
                        x:=anc[x,k];
                        y:=anc[y,k];
                end;
                dec(k);
        end;
        if x=y then exit(lca);
        exit(max(lca,max(dis[x,0],dis[y,0])));
end;

begin                                             
        readln(n,m,q);
        for i:=1 to n do father[i]:=i;
        for i:=1 to m do readln(a[i,1],a[i,2],a[i,3]);
        qsort(1,m);
        for i:=1 to m do
        begin
                if (not judge(a[i,1],a[i,2])) then
                begin
                        insert(a[i,1],a[i,2],a[i,3]);
                        insert(a[i,2],a[i,1],a[i,3]);
                        father[getfather(a[i,1])]:=getfather(a[i,2]);
                end;
        end;
        depth[1]:=1;
        dfs(1,0);
        for j:=1 to trunc(ln(n)/ln(2)) do
        for i:=1 to n do
        begin
                anc[i,j]:=anc[anc[i,j-1],j-1];
                dis[i,j]:=max(dis[i,j-1],dis[anc[i,j-1],j-1]);
        end;
        for i:=1 to q do
        begin
                readln(x,y);
                writeln(lca(x,y));
        end;
end.

例题2:【JZOJ2753】树(tree)

problem

Description

在这个问题中,给定一个值S和一棵树。在树的每一个节点有一个正整数,问有多少条路径的节点总和达到S。路径中节点的深度必须是升序的。假设节点1是根节点,根的深度是0,它的儿子节点的深度为1。路径没必要必定从根节点开始。

Input

第一行是两个整数N和S,其中N是树的节点数。


   第二行是N个正整数,第i个整数表示节点i的正整数。


   接下来的N-1行每行是2个整数x和y,表示y是x的儿子。

Output

输出路径节点总和为S的路径数量。

Sample Input

3 3 1 2 3 1 2 1 3

Sample Output

2

Data Constraint

Hint

对于30%数据,N≤100;

对于60%数据,N≤1000;

对于100%数据,N≤100000,全部权值以及S都不超过1000。


analysis

相信这题应该难不住你了吧
正解即为树上倍增+二分

首先仍是同样,用 O ( n log 2 n ) O(n\log_2n) 的时间预处理 a n c anc 数组
至于权值,你能够顺便维护 d i s dis 数组,但为什么不用简单的前缀和呢?
剩下来的就比较简单

  • 枚举节点 i i ,二分一个 m i d mid 表示 i i 的第 m i d mid 个祖先

  • 而后经过 a n c anc 数组用 O ( log 2 n ) O(\log_2n) 的时间求出 i i 的第 m i d mid 个祖先是哪一个节点 (设为是第 k k 号)

  • p r e [ i ] p r e [ k ] &gt; s pre[i]-pre[k]&gt;s r i g h t = m i d 1 right=mid-1 p r e [ i ] p r e [ k ] &lt; s pre[i]-pre[k]&lt;s l e f t = m i d + 1 left=mid+1
    = s =s 就恰好找到了

  • 此时累加答案便可

时间复杂度 O ( n log 2 2 n ) O(n\log^2_2n)


code

#include<bits/stdc++.h>
#define MAXN 100001

using namespace std;

int last[MAXN],next[MAXN],tov[MAXN];
int a[MAXN],value[MAXN],depth[MAXN];
int f[MAXN][21];
int n,s,tot,ans;

void insert(int x,int y)
{
	next[++tot]=last[x];
	last[x]=tot;
	tov[tot]=y;
}

void dfs(int x)
{
	for (int i=last[x];i;i=next[i])
	{
		int j=tov[i];
		value[j]=value[x]+a[j];
		f[j][0]=x;
		depth[j]=depth[x]+1;
		dfs(j);
	}
}

int find(int x,int k)
{
	int t=0;
	while (k)
	{
		if(k&1)x=f[x][t];
		t++;k/=2;
	}
	return x;
}

bool judge(int x)
{
	int left=0,right=depth[x];
	while (left<=right)
	{
		int mid=(left+right)/2,temp=find(x,mid);
		if (value[x]-value[temp]==s) 
		{
			return 1;
		}
		else 
		{
			if (value[x]-value[temp]>s)
			{
				right=mid-1;
			}
			else 
			{
				left=mid+1;
			}
		}
	}
	return 0;
}

int main()
{
	scanf("%d%d",&n,&s);
	for (int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
	}
	for (int i=1;i<n;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		insert(x,y);
	}
	depth[1]=1,value[1]=a[1];
	dfs(1);
	for (int j=1;j<=floor(log(n)/log(2));j++)
	{
		for (int i=1;i<=n;i++)
		{
			f[i][j]=f[f[i][j-1]][j-1];
		}
	}
	for (int i=1;i<=n;i++)
	{
		if (judge(i))ans++;
	}
	printf("%d\n",ans);
	return 0;
}

例题3:【JZOJ5966】【NOIP2018】保卫王国

problem

Description Z国有n座城市,n-1条双向道路,每条双向道路链接两座城市,且任意两座城市都能经过若干条道路相互到达。
Z国的国防部长小Z要在城市中驻扎军队。驻扎军队须要知足以下几个条件: ①一座城市能够驻扎一支军队,也能够不驻扎军队。
②由道路直接链接的两座城市中至少要有一座城市驻扎军队。 ③在城市里驻扎军队会产生花费,在编号为i的城市中驻扎军队的花费是pi。
小Z很快就规划出了一种驻扎军队的方案,使总花费最小。可是国王又给小Z提出了m个要求,每一个要求规定了其中两座城市是否驻扎军队。小Z须要针对每一个要求逐一给出回答。具体而言,若是国王提出的第j个要求可以知足上述驻扎条件(不须要考虑第j个要求以外的其它要求),则须要给出在此要求前提下驻扎军队的最小开销。若是国王提出的第j个要求没法知足,则须要输出-1(1<=j<=m)
。如今请你来帮助小Z。

Input 输入文件名为defense.in。 第1行包含两个正整数n,m 和一个字符串type
,分别表示城市数、要求数和数据类型。type
是一个由大写字母A,B或C和一个数字1,2,3组成的字符串。它能够帮助你得到部分分。你可能不须要用到这个参数。这个参数的含义在【数据规模与约定】中有具体的描述。
第2 行n个整数pi ,表示编号i的城市中驻扎军队的花费。 接下来n-1行,每行两个正整数u,v ,表示有一条u 到v 的双向道路。 接下来
m行,第 j行四个整数a,x,b,y(a<>b)
,表示第j个要求是在城市a驻扎x支军队,在城市b驻扎y支军队。其中,x、y的取值只有0或1:若x为0,表示城市a不得驻扎军队,若x为1,表示城市a必须驻扎军队;若y为0,表示城市b不得驻扎军队,若y为1,表示城市b必须驻扎军队。
输入文件中每一行相邻的两个数据之间均用一个空格分隔。

Output 输出文件名为defense.out。 输出共
行,每行包含1个整数,第j行表示在知足国王第j个要求时的最小开销,若是没法知足国王的第j个要求,则该行输出-1。

Sample Input 输入1: 5 3 C3 2 4 1 3 9 1 5 5 2 5 3 3 4 1 0 3 0 2 1 3 1 1 0
5 0

Sample Output 输出1: 12 7
-1 【样例解释】 对于第一个要求,在4号和5号城市驻扎军队时开销最小。 对于第二个要求,在1号、2号、3号城市驻扎军队时开销最小。 第三个要求是没法知足的,由于在1号、5号城市都不驻扎军队就意味着由道路直接链接的两座城市中都没有驻扎军队。

Data Constraint


analysis

  • 正解倍增+矩乘+树形DP

  • 考虑一下 44 p t s 44pts 的树形 D P DP ,设 f [ i ] [ 0 / 1 ] f[i][0/1] 表示以 i i 为根的子树中, i i 选或不选的最优解,则
    f [ i ] [ 0 ] = f [ s o n ] [ 1 ] f[i][0]=\sum f[son][1] f [ i ] [ 1 ] = m i n ( f [ s o n ] [ 0 ] , f [ s o n ] [ 1 ] ) f[i][1]=\sum min(f[son][0],f[son][1])

  • 每次把某个 f f 赋值为 ,暴力从新 D P DP ,就能够获得 44 p t s 44pts 的好成绩

  • 接着考虑另外一个 D P DP ,设 g [ i ] [ 0 / 1 ] g[i][0/1] 表示以 i i 为根的子树 i i 选或不选的最优解

  • 因为咱们已经 D P DP 出了 f f 数组,咱们也能够从一个点的父亲来转移 g g ,则
    g [ i ] [ 0 ] = f [ i ] [ 0 ] + g [ f a ] [ 1 ] m i n ( f [ i ] [ 0 ] , f [ i ] [ 1 ] ) g[i][0]=f[i][0]+g[fa][1]-min(f[i][0],f[i][1]) g [ i ] [ 1 ] = f [ i ] [ 1 ] + m i n ( g [ f a ] [ 0 ] f [ i ] [ 1 ] , g [ f a ] [ 1 ] m i n ( f [ i ] [ 0 ] , f [ i ] [ 1 ] ) ) g[i][1]=f[i][1]+min(g[fa][0]-f[i][1],g[fa][1]-min(f[i][0],f[i][1]))

  • 这个 D P DP 也满好理解的,接下来是维护的问题

  • 注意到其实没有数据更改,那么咱们能够用倍增+矩乘的方法来求出一条链上的答案

  • d i s [ i ] [ x ] [ 0 / 1 ] [ 0 / 1 ] dis[i][x][0/1][0/1] 表示 i i 选或不选 i i 2 x 2^x 位祖先选或不选的最优解,则

d i s [ i ] [ 0 ] [ 0 ] [ 0 ] = dis[i][0][0][0]=∞ d i s [ i ] [ 0 ] [ 1 ] [ 0 ] = f [ f a ] [ 0 ] f [ i ] [ 1 ] dis[i][0][1][0]=f[fa][0]-f[i][1] d i s [ i ] [ 0 ] [ 0 ] [ 1 ] = d i s [ i ] [ 0 ] [ 1 ] [ 1 ] = f [ f a ] [ 1 ] m i n ( f [ i ] [ 0 ] , f [ i ] [ 1 ] ) dis[i][0][0][1]=dis[i][0][1][1]=f[fa][1]-min(f[i][0],f[i][1])

  • 这个应该不难理解, i i 和父亲不能都不选,因此是

  • 其余的都是用父亲的贡献减去儿子的贡献,应该不太难想

  • 而把 d i s dis 数组维护出来之后,就能够实现倍增了,用矩乘解决 d i s dis 的后两维

  • 注意倍增时的矩乘和平时的不太同样,修改一下才能够

  • 对于每个询问,若是两个点在一条链上,注意一下条件选或不选的判断就好了

  • 不然找出两点的 L C A LCA ,再倍增跳到 L C A LCA ,计算两种不一样状况的最优解便可

  • g g 减去所求的东西才是最终的答案,不要忘记两个点的 L C A LCA 选的时候有不一样的状况须要取 m i n min

  • 时间复杂度 O ( n log 2 n ) O(n\log_2n)


code

#pragma GCC optimize("O3")
#pragma G++ optimize("O3")
#include<stdio.h>
#include<string.h>
#include<algorithm>
#define MAXN 100005
#define MAXM MAXN*2
#define ll long long
#define reg register ll
#define INF 1000000000000000007ll
#define fo(i,a,b) for (reg i=a;i<=b;++i)
#define fd(i,a,b) for (reg i=a;i>=b;--i)
#define rep(i,a) for (reg i=last[a];i;i=next[i])
#define O3 __attribute__((optimize("-O3")))

ll last[MAXM],next[MAXM],tov[MAXM];
ll p[MAXN],depth[MAXN],fa[MAXN];
ll f[MAXN][2],g[MAXN][2];
ll anc[MAXN][21];
ll n,m,tot,ans;
char type[5];

struct node
{
	ll a[2][2];
}dis[MAXN][21],A,B,C;

O3 inline ll read()
{
	ll x=0,f=1;char ch=getchar();
	while (ch<'0' || '9'<ch){if (ch=='-')f=-1;ch=getchar();}
	while ('0'<=ch && ch<='9')x=x*10+ch-'0',ch=getchar();
	return x*f;
}
O3 inline ll min(ll x,ll y)
{
	return x<y?x:y;
}
O3 inline void swap(ll &x,ll &y)
{
	x=x+y,y=x-y,x=x-y;
}
O3 inline void link(ll x,ll y)
{
	next[++tot]=last[x],last[x]=tot,tov[tot]=y;
}
O3 inline void dfs1(ll x)
{
	depth[x]=depth[fa[x]]+1,anc[x][0]=fa[x];
	fo(i,1,20)anc[x][i]=anc[anc[x][i-1]][i-1];
	f[x][0]=0,f[x][1]=p[x];
	rep(i,x)if (tov[i]!=fa[x])
	{
		fa[tov[i]]=x,dfs1(tov[i]);
		f[x][0]+=f[tov[i]][1];
		f[x][1]+=min(f[tov[i]][0],f[tov[i]][1]);
	}
}
O3 inline void dfs2(ll x)
{
	if (x==1)g[x][0]=f[x][0],g[x][1]=f[x][1];
	else
	{
		g[x][0]=f[x][0]+g[fa[x]][1]-min(f[x][0],f[x][1]);
		g[x][1]=f[x][1]+min(g[fa[x]][0]-f[x][1],g[fa[x]][1]-min(f[x][0],f[x][1]));
	}
	rep(i,x)if (tov[i]!=fa[x])
	{
		dis[tov[i]][0].a[0][0]=INF;
		dis[tov[i]][0].a[1][0]=f[x][0]-f[tov[i]][1];
		dis[tov[i]][0].a[0][1]=f[x][1]-min(f[tov[i]][0],f[tov[i]][1]);
		dis[tov[i]][0].a[1][1]=f[x][1]-min(f[tov[i]][0],f[tov[i]][1]);
		dfs2(tov[i]);
	}
}
O3 inline node merge(node A,node B)
{
	memset(C.a,60,sizeof(C.a));
	fo(i,0,1)fo(j,0,1)fo(k,0,1)
	C.a[i][j]=min(C.a[i][j],A.a[i][k]+B.a[k][j]);
	return C;
}
O3 int main()
{
	freopen("defense.in","r",stdin);
	freopen("defense.out","w",stdout);
	//freopen("readin.txt","r",stdin);
	n=read(),m=read(),scanf("%s",&type);
	fo(i,1,n)p[i]=read();
	fo(i,1,n-1)
	{
		ll x=read(),y=read();
		link(x,y),link(y,x);
	}
	dfs1(1),dfs2(1);
	fo(j,1,20)fo(i,1,n)dis[i][j]=merge(dis[i][j-1],dis[anc[i][j-1]][j-1]);
	while (m--)
	{
		ll a=read(),x=read(),b=read(),y=read();
		if (x+y==0 && ((fa[a]==b) || (fa[b]==a)))
		{
			printf("-1\n");
			continue;
		}
		if (depth[a]<depth[b])swap(a,b),swap(x,y);
		memset(A.a,60,sizeof(A.a));
		memset(B.a,60,sizeof(B.a));
		A.a[x][x]=f[a][x],B.a[y][y]=f[b][y];
		fd(i,20,0)if (depth[anc[a][i]]>depth[b])
		{
			A=merge(A,dis[a][i]),a=anc[a][i];
		}
		if (fa[a]==b)
		{
			printf("%lld\n",y==0?g[b][0]-f[a][1]+A.a[x][1]:
			g[b][1]-min(f[a][0],f[a][1])+min(A.a[x][0],A.a[x][1]));
			continue;
		}
		if (depth[a]!=depth[b])A=merge(A,dis[a][0]),a=fa[a];
		fd(i,20,0)if (anc[a][i]!=anc[b][i])
		{
			A=merge(A,dis[a][i]),B=merge(B,dis[b][i]);
			a=anc[a][i],b=anc[b][i];
		}
		printf("%lld\n",min(g[fa[a]][0]-f[a][1]-f[b][1]+A.a[x][1]+B.a[y][1],
		g[fa[a]][1]-min(f[a][0],f[a][1])-min(f[b][0],f[b][1])+min(A.a[x][0],A.a[x][1])+min(B.a[y][0],B.a[y][1])));
	}
	return 0;
}

一些感想

树上倍增差很少算是那种必定要会的知识了
总之,这一种较为基础的算法,必定要熟练掌握并会运用它
(不要把正解当作暴力……)
接下来是丧心病狂 其实比树上倍增还简单 的树链剖分


树链剖分


介绍一下吧

零树剖基础的人,到这里应该仍是不懂树剖的
简要地说一下吧——

  • 树链剖分的思想是维护树上路径信息

  • 树链剖分先经过轻重边剖分将树分为多条链,保证每一个点属于且只属于一条链

  • 而后再经过数据结构 (树状数组、SBT、splay、线段树等) 来维护每一条链

以上就是树链剖分的思想了,不多,学完之后,其实树剖很简单……
问题是怎么分轻重边?怎么维护?


more advantages

既然树上倍增又好打又好调,干吗还用树链剖分?
但树上倍增的用途还就真没有树链剖分那么广
不信? 再来一类看起来也很常见的题目

在一棵树上进行路径的权值修改,询问路径权值和、路径权值极值

看起来简单,但不简单,线段树、树上倍增根本作不了
why? 后两个操做咱们固然能够用树上倍增作
关键是修改操做,树上倍增都预处理好了

  • 怎么修改?从新 O ( n log 2 n ) O(n\log_2n) 暴力预处理?

不可能的

这时咱们就要用树链剖分
因为咱们能够用数据结构来维护剖好的树,因此路径权值之类的固然是能够修改
因此并非说倍增就能真正彻底通用

  • 树剖的用处比倍增多,倍增能作的题树剖必定能作,反过来则否

  • 树剖的代码复杂度不算特别高,调试也不难

  • 在高级的比赛里,树剖是必备知识

其实树剖也是分类别的

  • 注意树剖有三种重链剖分长链剖分实虚链剖分

  • 实虚链剖分其实就是LCT

  • 因此我就分开来写好了

正式进入树链剖分的学习


重链剖分

请务必认认真真地阅读如下内容不然重链剖分你就学不会了


重链剖分的一些概念

轻儿子和重儿子

x s i z e x_{size} 表示以x为根的子树的节点个数
对于每一个非叶子节点y,y的儿子全部中 s i z e size 最大的儿子就是重儿子(两个或以上都最大的话,任意选一个)
y其它的儿子,都是轻儿子
(轻儿子一般没有用,咱们能够不去考虑它)


重边、轻边和重链

重儿子与其父亲之间的路径,就是重边
不是重边的边均为轻边
多条重边相连为一条重链

图中灰色的点为重儿子,加粗的边是重边,其余没有加粗的是轻边
这棵树中的长度大于1的重链有两条:2—5和1—3—6—10
全部轻儿子可视做一个长度为1的重链


something about properties

  • 若x是轻儿子,则有 x s i z e &lt; = f a t h e r ( x ) s i z e 2 x_{size}&lt;={father(x)_{size}\over 2}

  • 从根到某一点的路径上,不超过 l o g 2 n log_2n 重链,不超过 l o g 2 n log_2n 轻链

(不须要证实是由于我不会证)


预处理、维护和在线操做

其实和着概念一块儿看剩下三种东东,树剖还就真挺连模板都不用背
接下来的数据结构维护咱们使用简单一点的线段树


预处理

首先用一个dfs遍历,求出每一个点的 d e p t h depth f a t h e r father s i z e size 以及重儿子 h e a v y heavy _ s o n son
其次再用一个dfs剖分,第二个 d f s dfs 维护什么呢?

  • 优先走重边的原则,遍历出来一个dfs序
  • x t o p x_{top} 表示x位于的重链上的顶端节点编号
  • t o to _ t r e e [ x ] tree[x] 表示x在线段树中的位置
  • 对于线段树中的第y个位置, t o to _ n u m [ y ] num[y] 为它对应的dfs序中的位置

剖分完之后,每一条重链至关于一个区间,咱们维护处理完的数据结构便可

咱们把上面那棵树剖分完之后线段树中的点顺序是这样的:
1,3,6,10,8,9,7,2,5,4 (加粗的点是在重链上的点)
在同一条重链上的点,在线段树的顺序中要连在一块儿(不然怎么维护呢)

剖树的时间复杂度 O ( n ) O(n)


维护以及在线操做

change

由于已经搞过to_tree了,经过to_tree的标号在线段树中用单点修改便可
不要告诉我你不会单点修改
时间复杂度是线段树修改的 O ( log 2 n ) O(\log_2n)


query

询问 x x y y 路径的极值(或者路径和等等),和求 L C A LCA 同样边向上跳边记录答案
这里记录的答案经过查询数据结构获得
关于时间复杂度嘛……


关于时间复杂度?

对于咱们这些蒟蒻来讲最关键的仍是时间复杂度的问题
用最普通的数据结构线段树来维护树链剖分的时间复杂度 O ( n + q log 2 2 n ) O(n+q\log_2^2n)

后面那个 q log 2 2 n q\log^2_2n 什么东西?

因为求 L C A LCA O ( log 2 n ) O(\log_2n) 的,统计答案的过程当中还要查询数据结构又是 O ( log 2 n ) O(\log_2n) 的时间
因此每一个查询的时间复杂度就是 O ( log 2 2 n ) O(\log_2^2n)

  • 日常来讲,对于树剖,时间应该是比树上倍增那个 O ( n log 2 n ) O(n\log_2n) 的预处理

因此你看树剖仍是很是有用的
还有用 L C T LCT 维护能够作到 O ( n log 2 n ) O(n\log_2n) 的时间还有LCT什么的你如今怎么可能会呢


重链剖分(在线)求LCA

树剖固然也能求LCA啦!而且比倍增还好打耶!
具体以下

  • x t o p y t o p x_{top}≠y_{top} ,说明 x x y y 不在同一条重链上
  • x x y y 两点中top的深度较深的那个点为z
  • z z 跳到 z t o p f a t h e r z_{top_{father}}
  • 重复以上步骤,直到 x x y y 在同一条重链上(即 x t o p = y t o p x_{top}=y_{top}
  • 此时 x x y y d e p t h depth 小的点即为 L C A LCA

其实很好理解,跳到 z t o p f a t h e r z_{top_{father}} 的缘由是让z进入另外一条重链省得程序卡住
而选择 t o p top 浅的点而不是自己浅的点是防止跳到重链头超过 L C A LCA
不懂面壁去想,倍增你都听懂了,树剖的你听不懂就xxx了
那么时间复杂度 O ( log 2 n ) O(\log_2n)


code

  • 固然求 L C A LCA 没有各类权,不用打线段树啦!
#include<cstdio>
#define MAXN 30001

using namespace std;

int last[3*MAXN],next[3*MAXN],tov[3*MAXN];
int n,m,tot;

struct information
{
    int depth,father,size,heavy_son,top,to_tree,value;
}a[30001];

void insert(int x,int y)
{
    next[++tot]=last[x];
    last[x]=tot;
    tov[tot]=y;
}

void dfs1(int x,int father)
{
	int mx=-1;
	a[x].size=1;
	for (int i=last[x];i;i=next[i])
	{
		int j=tov[i];
		if (j!=father)
		{
			a[j].depth=a[x].depth+1;
			a[j].father=x;
			dfs1(j,x);
			a[x].size+=a[j].size;
			if (a[j].size>mx)
			{
				mx=a[j].size;
				a[x].heavy_son=j;
			}
		}
	}
}

void dfs2(int x,int father,int k)
{
	if (x==0)return;
	a[x].top=k;
	dfs2(a[x].heavy_son,x,k);
	for (int i=last[x];i;i=next[i])
	{
		int j=tov[i];
		if (j!=father && j!=a[x].heavy_son && j!=0)
		{
			dfs2(j,x,j);
		}
	}
}

int lca(int x,int y)
{
	while (a[x].top!=a[y].top)
	{
		if (a[a[x].top].depth>a[a[y].top].depth)
			x=a[a[x].top].father;
		else y=a[a[y].top].father;
	}
	return a[x].depth<a[y].depth?x:y;
}

int main()
{
	scanf("%d",&n);
	for (int i=1;i<n;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		insert(x,y);
		insert(y,x);
	}
	a[1].depth=1;
	a[1].father=0;
	dfs1(1,0);
	dfs2(1,0,1);
	scanf("%d",&m);
	for (int i=1;i<=m;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		printf("%d\n",lca(x,y));
	}
	return 0;
}

仍是看例题吧


例题1:【JZO1738】Heatwave

problem

题目上面有因此就不从新贴了


analysis

我说过了倍增能作的题树剖都能作的嘛,因此树剖固然也能作这道题

其实在树剖里面这题算是模板题,并且树链剖分还看得很清楚
求完了 M S T MST 后,咱们仍是照样把这棵 M S T MST 剖分一下,用线段树维护区间最大值
这里咱们求的是边权而不是点权,咱们就把边权放到儿子节点上当作点权就能够了

L C A LCA 时咱们是一条重链一条重链地向上跳,这里咱们是同样的——
每次跳一条重链的时候,用线段树维护好的区间最大值查询这条重链的点权最大值
(还有不要告诉我你不会线段树)

套上求 L C A LCA 的意义也就是这样——

  • x t o p y t o p x_{top}≠y_{top} ,记 x x y y 两点中top的深度较深的那个点为z
  • t e m p = m a x ( t e m p , t r e e q u e r y m a x ( 1 , 1 , n , a [ a [ z ] . t o p ] . t o t r e e , a [ z ] . t o t r e e ) ) temp=max(temp,tree_{query_{max}}(1,1,n,a[a[z].top].to_{tree},a[z].to_{tree}))
  • z z 跳到 z t o p f a t h e r z_{top_{father}}

最后的 t e m p temp 即为答案,每一个查询是 O ( log 2 2 n ) O(\log^2_2n) 时间复杂度

其实想想,线段树是把每条重链当作区间来维护,意义十分显然
(注意一点就是求最大值时必定不要算上 L C A ( x , y ) LCA(x,y) 的值)


code

#include<cstdio>
#include<algorithm>
#define MAXN 15001
#define INF 1000000007

using namespace std;

int last[4*MAXN],next[4*MAXN],tov[4*MAXN],dis[4*MAXN];
int to_num[4*MAXN],father[MAXN];
int n,m,k,tot,total;

struct information
{
	int depth,father,size,heavy_son,top,to_tree,value;
}a[4*MAXN];

struct point
{
	int mx,ans;
}tree[4*MAXN];

struct dist
{
	int u,v,w;
}line[4*MAXN];

bool cmp(dist a,dist b)
{
	return a.w<b.w;
}

int getfather(int x)
{
	if (father[x]==0)return x;
	father[x]=getfather(father[x]);
	return father[x];
}

void insert(int x,int y,int z)
{
	next[++tot]=last[x];
	last[x]=tot;
	tov[tot]=y;
	dis[tot]=z;
}

void dfs1(int x,int father)
{
	int mx=-1;
	a[x].size=1;
	for (int i=last[x];i;i=next[i])
	{
		int j=tov[i];
		if (j!=father)
		{
			a[j].depth=a[x].depth+1;
			a[j].father=x;
			a[j].value=dis[i];
			dfs1(j,x);
			a[x].size+=a[j].size;
			if (a[j].size>mx)
			{
				mx=a[j].size;
				a[x].heavy_son=j;
			}
		}
	}
}

void dfs2(int x,int father,int top)
{
	if (x==0)return;
	a[x].top=top;
	a[x].to_tree=++total;
	to_num[total]=x;
	dfs2(a[x].heavy_son,x,top);
	for (int i=last[x];i;i=next[i])
	{
		int j=tov[i];
		if (j!=father && j!=a[x].heavy_son && j!=0)
		{
			dfs2(j,x,j);
		}
	}
}

void maketree(int t,int l,int r)
{
	if (l==r)
	{
		tree[t].mx=a[to_num[l]].value;
		return;
	}
	int mid=(l+r)/2;
	maketree(t*2,l,mid);
	maketree(t*2+1,mid+1,r);
	tree[t].mx=max(tree[t*2].mx,tree[t*2+1].mx);
}

int tree_query_max(int t,int l,int r,int x,int y)
{
	if (l==x && r==y)
	{
		return tree[t].mx;
	}
	int mid=(l+r)/2;
	if (y<=mid)
	{
		return tree_query_max(t*2,l,mid,x,y);
	}
	else if (x>mid)
	{
		return tree_query_max(t*2+1,mid+1,r,x,y);
	}
	else return max(tree_query_max(t*2,l,mid,x,mid),tree_query_max(t*2+1,mid+1,r,mid+1,y));
}

int query_max(int x,int y)
{
	int temp=-1;
	while (a[x].top!=a[y].top)
	{
		if (a[a[x].top].depth>a[a[y].top].depth)
		{
			temp=max(temp,tree_query_max(1,1,n,a[a[x].top].to_tree,a[x].to_tree));
			x=a[a[x].top].father;
		}
		else
		{
			temp=max(temp,tree_query_max(1,1,n,a[a[y].top].to_tree,a[y].to_tree));
			y=a[a[y].top].father;
		}
	}
	if (x==y)return temp;
	if (a[x].depth<a[y].depth)
	{
		temp=max(temp,tree_query_max(1,1,n,a[a[x].heavy_son].to_tree,a[y].to_tree));
	}
	else
	{
		temp=max(temp,tree_query_max(1,1,n,a[a[y].heavy_son].to_tree,a[x].to_tree));
	}
	return temp;
}

int main()
{
	//freopen("readin.txt","r",stdin);
	scanf("%d%d%d",&n,&m,&k);
	for (int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&line[i].u,&line[i].v,&line[i].w);
	}
	sort(line+1,line+m+1,cmp);
	for (int i=1;i<=m;i++)
	{
		if (getfather(line[i].u)!=getfather(line[i].v))
		{
			father[getfather(line[i].u)]=getfather(line[i].v);
			insert(line[i].u,line[i].v,line[i].w);
			insert(line[i].v,line[i].u,line[i].w);
		}
	}
	a[1].depth=1;
	dfs1(1,0),dfs2(1,0,1);
	maketree(1,1,n);
	for (int i=1;i<=k;i++)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		printf("%d\n",query_max(x,y));
	}
	return 0;
}

必定读懂这段代码再看下面的题
不然会爽炸天


例题2:【JZO3534】【luoguP1967】货车运输

problem

Description

A 国有 n 座城市,编号从 1 到 n,城市之间有 m 条双向道路。每一条道路对车辆都有重量限制,简称限重。如今有 q
辆货车在运输货物,司机们想知道每辆车在不超过车辆限重的状况下,最多能运多重的货物。

Input

第一行有两个用一个空格隔开的整数 n,m,表示 A 国有 n 座城市和 m 条道路。

接下来 m 行每行 3 个整数 x、y、z,每两个整数之间用一个空格隔开,表示从 x 号城市到 y 号城市有一条限重为 z 的道路。注意:x
不等于 y,两座城市之间可能有多条道路。

接下来一行有一个整数 q,表示有 q 辆货车须要运货。

接下来 q 行,每行两个整数 x、y,之间用一个空格隔开,表示一辆货车须要从 x 城市运输货物到 y 城市,注意:x 不等于 y。

Output

输出共有 q 行,每行一个整数,表示对于每一辆货车,它的最大载重是多少。若是货车不能到达目的地,输出-1。

Sample Input

4 3

1 2 4

2 3 3

3 1 1

3

1 3

1 4

1 3

Sample Output

3

-1

3

Data Constraint

对于 30%的数据,0 < n < 1,000,0 < m < 10,000,0 < q < 1,000;

对于 60%的数据,0 < n < 1,000,0 < m < 50,000,0 < q < 1,000;

对于 100%的数据,0 < n < 10,000,0 < m < 50,000,0 < q < 30,000,0 ≤ z ≤
100,000。


analysis

又一个套路 M S T MST 配树链剖分就是不想上倍增

和上面heatwave不一样的是,这里的原图不必定连通,且两城之间有多条边

  • 虽然说不必定连通,仍是直接上一次最大生成树便可

  • 多条边就把多条边中最大权值的边留下,其余的边没必要管

剩下线段树维护点权最小值即和heatwave的如出一辙

但必须注意一点:判断联通性的问题
咱们不能只从 1 1 节点开始剖一次树,而是必须从全部未访问到的节点开始剖树

至于判断联通性,一开始我去作洛谷的时候,是在求 L C A LCA 途中判断是否连通,可是WA
其实除了 k r u s k a l kruskal 用的一次并查集外,咱们能够再用一次并查集,把 M S T MST 上的边再搞一次
须要访问 x , y x,y 城市时只需询问 g e t f a t h e r ( x ) = g e t f a t h e r ( y ) ? getfather(x)=getfather(y)? 便可


code

#include<bits/stdc++.h>
#define MAXN 10001
#define MAXM 50001

using namespace std;

int last[2*MAXM],next[2*MAXM],tov[2*MAXM],dis[2*MAXM];
bool visited[MAXN],flag[MAXM]; 
int to_num[MAXN],father[MAXN],tree[4*MAXN];
int n,m,q,tot,total;

struct array
{
	int x,y,z;
}dist[2*MAXM];

struct information
{
    int depth,father,size,heavy_son,top,to_tree,value;
}a[MAXN];

bool cmp(array a,array b)
{
	return a.z>b.z;
}

int min(int x,int y)
{
	return x<y?x:y;
}

int getfather(int x)
{
	if (father[x]==0)return x;
	return father[x]=getfather(father[x]);
}

void insert(int x,int y,int z)
{
	next[++tot]=last[x];
	last[x]=tot;
	tov[tot]=y;
	dis[tot]=z;
}

void dfs1(int x,int father)
{
	visited[x]=0;
	a[x].size=1;
	int mx=-1;
	for (int i=last[x];i;i=next[i])
	{
		int j=tov[i];
		if (j!=father)
		{
			if (visited[j]==0)
			{
				a[j].value+=dis[i];
				continue;
			}
			a[j].depth=a[x].depth+1;
			a[j].value=dis[i];
			a[j].father=x;
			dfs1(j,x);
			a[x].size+=a[j].size;
			if (a[j].size>mx)
			{
				a[x].heavy_son=j;
				mx=a[j].size;	
			} 
		}
	}
}

void dfs2(int x,int top)
{
	if (x==0)return;
	a[x].top=top;
	a[x].to_tree=++total;
	to_num[total]=x;
	dfs2(a[x].heavy_son,top);
	for (int i=last[x];i;i=next[i])
	{
		int j=tov[i];
		if (j!=a[x].father && j!=a[x].heavy_son && j!=0)
		{
			dfs2(j,j);
		}
	}
}

void maketree(int t,int l,int r)
{
	if (l==r)
	{
		tree[t]=a[to_num[l]].value;
		return;
	}
	int mid=(l+r)/2;
	maketree(2*t,l,mid),maketree(2*t+1,mid+1,r);
	tree[t]=min(tree[2*t],tree[2*t+1]);
}

bool judge(int x,int y)
{
	while (a[x].top!=a[y].top)
	{
		if ((x==a[x].top && a[x].father==0)||(y==a[y].top && a[y].father==0))return 0;
		if (a[a[x].top].depth>a[a[y].top].depth)
		{
			x=a[a[x].top].father;
		}
		else
		{
			y=a[a[y].top].father;
		}
	}
	return 1;
}

int tree_query_min(int t,int l,int r,int x,int y)
{
	if (l==x && y==r)
	{
		return tree[t];
	}
	int mid=(l+r)/2;
	if (y<=mid)
	{
		return tree_query_min(t*2,l,mid,x,y);
	}
	else if (x>mid)
	{
		return tree_query_min(t*2+1,mid+1,r,x,y);
	}
	else
	{
		return min(tree_query_min(t*2,l,mid,x,mid),tree_query_min(t*2+1,mid+1,r,mid+1,y));
	}
}

int query_min(int x,int y)
{
	int temp=1000000007;
	while (a[x].top!=a[y].top)
	{
		if (a[a[x].top].depth>a[a[y].top].depth)
		{
			temp=min(temp,tree_query_min(1,1,n,a[a[x].top].to_tree,a[x].to_tree));
			x=a[a[x].top].father;	
		}
		else
		{
			temp=min(temp,tree_query_min(1,1,n,a[a[y].top].to_tree,a[y].to_tree));
			y=a[a[y].top].father;
		}
	} 
	if (x==y)return temp;
	if (a[x].depth<a[y].depth)
	{
		return min(temp,tree_query_min(1,1,n,a[a[x].heavy_son].to_tree,a[y].to_tree));
	}
	else
	{
		return min(temp,tree_query_min(1,1,n,a[a[y].heavy_son].to_tree,a[x].to_tree));
	}
}

int main()
{
	//freopen("read.txt","r",stdin);
	scanf("%d%d",&n,&m);
	for (int i=1;i<=m;i++)
	{
		scanf("%d%d%d",&dist[i].x,&dist[i].y,&dist[i].z);
	}
	sort(dist+1,dist+m+1,cmp);
	for (int i=1;i<=m;i++)
	{
		int u=dist[i].x,v=dist[i].y,w=dist[i].z;
		if (getfather(u)!=getfather(v))
		{
			father[getfather(u)]=v;
			insert(u,v,w),insert(v,u,w);
			flag[i]=1;
		}
	}
	memset(visited,1,sizeof(visited));
	for (int i=1;i<=n;i++)
	if (visited[i])
	{
		a[i].depth=1;
		a[i].father=0;
		dfs1(i,0),dfs2(i,i);
	}
	maketree(1,1,n);
	memset(father,0,sizeof(father));
	for (int i=1;i<=m;i++)
	if (flag[i])
	{
		int u=dist[i].x,v=dist[i].y;
		father[getfather(u)]=getfather(v);
	}
	scanf("%d",&q);
	while (q--)
	{
		int x,y;
		scanf("%d%d",&x,&y);
		if (getfather(x)==getfather(y))
		{
			printf("%d\n",query_min(x,y));
		}
		else printf("-1\n");
	}
	return 0;
}

例题3:【JZOJ2256】【ZJOI2008】树的统计

problem

Description

一棵树上有n个节点,编号分别为1到n,每一个节点都有一个权值w。   咱们将如下面的形式来要求你对这棵树完成一些操做:
  I. CHANGE u t : 把结点u的权值改成t   II. QMAX u v: 询问从点u到点v的路径上的节点的最大权值
  III. QSUM u v: 询问从点u到点v的路径上的节点的权值和   注意:从点u到点v的路径上的节点包括u和v自己

Input

输入文件的第一行为一个整数n,表示节点的个数。   接下来n – 1行,每行2个整数a和b,表示节点a和节点b之间有一条边相连。
  接下来n行,每行一个整数,第i行的整数wi表示节点i的权值。   接下来1行,为一个整数q,表示操做的总数。
  接下来q行,每行一个操做,以“CHANGE u t”或者“QMAX u v”或者“QSUM u v”的形式给出。

Output

对于每一个“QMAX”或者“QSUM”的操做,每行输出一个整数表示要求输出的结果。

Sample Input
QMAX 3 4
QMAX 3 3
QMAX 3 2
QMAX 2 3
QSUM 3 4
QSUM 2 1
CHANGE 1 5
QMAX 3 4
CHANGE 3 6
QMAX 3 4
QMAX 2 4
QSUM 3 4

Sample Output

4 1 2 2 10 6 5 6 5 16

Data Constraint

Hint

【数听说明】
  对于100%的数据,保证1<=n<=30000,0<=q<=200000;中途操做中保证每一个节点的权值w在-30000到30000之间。


analysis

这题仍是树剖经典例题
看懂 h e a t w a v e heatwave 的树剖作法,这题就比较简单啦

这里的线段树要维护和查询两个值了,是区间最大值和区间和
仍是同样地,求 L C A LCA 的同时用线段树查询整条重链的区间最大值和区间和
我懒得再讲两种查询了

至于 c h a n g e change 操做,因为咱们已经把每一个点在线段树中的编号(to_tree)处理出来了
因此咱们直接用线段树的单点修改就能够在线段树里面直接修改指定节点
修改完之后,推回父亲节点,更新线段树便可,change成功解决
c h a n g e change 时间复杂度 O ( log 2 n ) O(\log_2n)


code

树剖打250行,醉了

#include<cstdio>
#define MAXN 30001
#define INF 1000000007

using namespace std;

int last[3*MAXN],next[3*MAXN],tov[3*MAXN];
int to_num[MAXN];
int n,m,tot,total;
char st[10];

struct information
{
    int depth,father,size,heavy_son,top,to_tree,value;
}a[MAXN];

struct point
{
	int mx,ans;
}tree[MAXN*4];

int max(int x,int y)
{
	return x>y?x:y;
}

void insert(int x,int y)
{
    next[++tot]=last[x];
    last[x]=tot;
    tov[tot]=y;
}

void dfs1(int x,int father)
{
    int mx=-1;
    a[x].size=1;
    for (int i=last[x];i;i=next[i])
    {
        int j=tov[i];
        if (j!=father)
        {
            a[j].depth=a[x].depth+1;
            a[j].father=x;
            dfs1(j,x);
            a[x].size+=a[j].size;
            if (a[j].size>mx)
            {
                mx=a[j].size;
                a[x].heavy_son=j;
            }
        }
    }
}

void dfs2(int x,int father,int k)
{
    if (x==0)return;
    a[x].top=k;
    a[x].to_tree=++total;
    to_num[total]=x;
    dfs2(a[x].heavy_son,x,k);
    for (int i=last[x];i;i=next[i])
    {
        int j=tov[i];
        if (j!=father && j!=a[x].heavy_son && j!=0)
        {
            dfs2(j,x,j);
        }
    }
}

void maketree(int t,int l,int r)
{
	if (l==r)
	{
		tree[t].ans=tree[t].mx=a[to_num[l]].value;
		return;
	}
	int mid=(l+r)/2;
	maketree(t*2,l,mid);
	maketree(t*2+1,mid+1,r);
	tree[t].ans=tree[t*2].ans+tree[t*2+1].ans;
	tree[t].mx=max(tree[t*2].mx,tree[t*2+1].mx);
}

void change(int t,int l,int r,int x,int y)
{
	if (l==r)
	{
		tree[t].ans=tree[t].mx=y;
		return;
	}
	int mid=(l+r)/2;
	if (x<=mid)
	{
		change(t*2,l,mid,x,y);
	}
	else
	{
		change(t*2+1,mid+1,r,x,y);
	}
	tree[t].mx=max(tree[t*2].mx,tree[t*2+1].mx);
	tree[t].ans=tree[t*2].ans+tree[t*2+1].ans;
}

int tree_query_max(int t,int l,int r,int x,int y)
{
    if (l==x && r==y)
    {
        return tree[t].mx;
    }
    int mid=(l+r)/2;
    if (y<=mid)
    {
        return tree_query_max(t*2,l,mid,x,y);
    }
    else if (x>mid)
    {
        return tree_query_max(t*2+1,mid+1,r,x,y);
    }
    else
    {
        return max(tree_query_max(t*2,l,mid,x,mid),tree_query_max(t*2+1,mid+1,r,mid+1,y));
    }
}

int tree_query_sum(int t,int l,int r,int x,int y)
{
    if (l==x && r==y)
    {
        return tree[t].ans;
	}
    int mid=(l+r)/2;
    if (y<=mid)
	{
        return tree_query_sum(t*2,l,mid,x,y);
    }
    else if (x>mid)
	{
        return tree_query_sum(t*2+1,mid+1,r,x,y);
    }
    else
    {
        return tree_query_sum(t*2,l,mid,x,mid)+tree_query_sum(t*2+1,mid+1,r,mid+1,y);
    }
}

int query_max(int x,int y)
{
	int temp=-INF;
    while (a[x].top!=a[y].top)
    {
        if (a[a[x].top].depth>a[a[y].top].depth)
		{
			temp=max(temp,tree_query_max(1,1,n,a[a[x].top].to_tree,a[x].to_tree));
			x=a[a[x].top].father;
		}
        else 
		{
			temp=max(temp,tree_query_max(1,1,n,a[a[y].top].to_tree,a[y].to_tree));
			y=a[a[y].top].father;
		}
    }
	if (a[x].depth<a[y].depth)
	{
        temp=max(temp,tree_query_max(1,1,n,a[x].to_tree,a[y].to_tree));
    }
    else
    {
	 	temp=max(temp,tree_query_max(1,1,n,a[y].to_tree,a[x].to_tree));
	}
    return temp;
}

int query_sum(int x,int y)
{
	int temp=0;
    while (a[x].top!=a[y].top)
	{
        if (a[a[x].top].depth>a[a[y].top].depth)
		{
			temp+=tree_query_sum(1,1,n,a[a[x].top].to_tree,a[x].to_tree);
			x=a[a[x].top].father;
		}
        else 
		{
			temp+=tree_query_sum(1,1,n,a[a[y].top].to_tree,a[y].to_tree);
			y=a[a[y].top].father;
		}
    }
	if (a[x].depth<a[y].depth)
	{
        temp+=tree_query_sum(1,1,n,a[x].to_tree,a[y].to_tree);
    }
    else
    {
	 	temp+=tree_query_sum(1,1,n,a[y].to_tree,a[x].to_tree);
	}
    return temp;
}

int main()
{
	//freopen("readin.txt","r",stdin);
	
    scanf("%d",&n);
    for (int i=1;i<n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        insert(x,y);
        insert(y,x);
    }
    for (int i=1;i<=n;i++)
    {
    	scanf("%d",&a[i].value);
	}
    a[1].depth=1,a[1].father=0;
    dfs1(1,0),dfs2(1,0,1);
    maketree(1,1,n);
    scanf("%d",&m);
    for (int i=1;i<=m;i++)
    {
    	int x,y;
    	scanf("%s %d %d",&st,&x,&y);
    	if (st[1]=='M')
    	{
    		printf("%d\n",query_max(x,y));
		}
		else if (st[1]=='S')
		{
			printf("%d\n",query_sum(x,y));
		}
		else
		{	
			change(1,1,n,a[x].to_tree,y);
		}
	}    
    return 0;
}

例题4:【JZOJ1175】【BZOJ2238】【IOI2008】生成树

problem

Description

给出一个N个点M条边的无向带权图,以及Q个询问,每次询问在图中删掉一条边后图的最小生成树。(各询问间独立,每次询问不对以后的询问产生影响,即被删掉的边在下一条询问中依然存在)

Input

第一行两个正整数N,M(N<=50000,M<=100000)表示原图的顶点数和边数。
下面M行,每行三个整数X,Y,W描述了图的一条边(X,Y),其边权为W(W<=10000)。保证两点之间至多只有一条边。
接着一行一个正整数Q,表示询问数。(1<=Q<=100000)
下面Q行,每行一个询问,询问中包含一个正整数T,表示把编号为T的边删掉(边从1到M按输入顺序编号)。

Output

Q行,对于每一个询问输出对应最小生成树的边权和的值,若是图不连通则输出“Not connected”

Sample Input

4 4 1 2 3 1 3 5 2 3 9 2 4 1 4 1 2 3 4

Sample Output

15 13 9 Not connected

Data Constraint

Hint

数据规模: 10%的数据N,M,Q<=100。 另外30%的数据,N<=1000 100%的数据如题目。


analysis

  • 还有第四道例题(不要看到“IOI”什么的就不敢作了)

正解仍是——MST+树链剖分
奇怪的是静态树上问题MST+树剖是否是万能的…?

想想即知,若是删去的边不在原图的 M S T MST 上,对原答案没有影响
可是若是删去了 M S T MST 上的边,那么其余的 M S T MST 树边必定不变
那此时要找的只是一条非 M S T MST 上的边,且要求最短,显然是在树链上

咱们在求完 M S T MST 后,用树链剖分维护每条非MST树边可以使多少条MST树边删去后连通
说着很拗口,但操做仍是很 s i m p l e simple 的,标记永久化一下便可
固然原图就是不连通的话, q q 个操做全都输出**“Not connected”**
时间复杂度 O ( n log 2 2 n ) O(n\log^2_2n)


code

#include<bits/stdc++.h>
#define MAXN 50001
#define MAXM 100001 

using namespace std;

int last[2*MAXM],next[2*MAXM],tov[2*MAXM],dis[2*MAXM];
int father[MAXN],where[MAXM],to_num[MAXN],tree[4*MAXN],ref[2*MAXN];
int n,m,q,tot,total,ans;

int min(int x,int y)
{
    return x<y?x:y;
}

int read()
{
    int x=0,f=1;
    char ch=getchar();
    while (ch<'0' || '9'<ch)
    {
        if (ch=='-')f=-1;
        ch=getchar();   
    }
    while ('0'<=ch && ch<='9')
    {
        x=x*10+ch-'0';
        ch=getchar();
    }
    return x*f;
}

struct information
{
    int depth,father,size,heavy_son,top,to_tree,value;
}a[MAXN];

struct dist
{
    int x,y,z,num;
    bool flag;
}line[MAXM];

bool cmp(dist a,dist b)
{
    return a.z<b.z; 
}

bool cmp1(dist a,dist b)
{
    return a.num<b.num;
}

int getfather(int x)
{
    return !father[x]?x:father[x]=getfather(father[x]);
}

void insert(int x,int y,int z)
{
    next[++tot]=last[x];
    last[x]=tot;
    tov[tot]=y;
    dis[tot]=z;
}

void dfs1(int x,int father)
{
    a[x].size=1;
    int mx=-1;
    for (int i=last[x];i;i=next[i])
    {
        int j=tov[i];
        if (j!=father && j!=0)
        {
            a[j].depth=a[x].depth+1;
            a[j].father=x;
            ref[dis[i]]=j;
            dfs1(j,x);
            a[x].size+=a[j].size;
            if (a[j].size>mx)
            {
                mx=a[j].size;
                a[x].heavy_son=j;
            }
        }
    }
}

void dfs2(int x,int top)
{
    if (x==0)return;
    a[x].top=top;
    a[x].to_tree=++total;
    to_num[total]=x; 
    dfs2(a[x].heavy_son,top);
    for (int i=last[x];i;i=next[i])
    {
        int j=tov[i];
        if (j!=a[x].father && j!=a[x].heavy_son && j!=0)
        {
            dfs2(j,j);
        }
    }
}

void change(int t,int l,int r,int x,int y,int z)
{
    if (l==x && y==r)
    {
        tree[t]=min(tree[t],z);
        return;
    }
    int mid=(l+r)/2;
    if (y<=mid)
    {
        change(t*2,l,mid,x,y,z);
    }
    else if (x>mid)
    {
        change(t*2+1,mid+1,r,x,y,z);
    }
    else
    {
        change(t*2,l,mid,x,mid,z);
        change(t*2+1,mid+1,r,mid+1,y,z);
    }
    //tree[t]=min(tree[t*2],tree[t*2+1]);
}

int query(int t,int l,int r,int x)
{
    if (l==r)
    {
        return tree[t];
    }
    int mid=(l+r)/2;
    if (x<=mid)return min(tree[t],query(t*2,l,mid,x));
    else return min(tree[t],query(t*2+1,mid+1,r,x));
}

void modify(int x,int y,int z)
{
    while (a[x].top!=a[y].top)
    {
        if (a[a[x].top].depth>a[a[y].top].depth)
        {
            change(1,1,n,a[a[x].top].to_tree,a[x].to_tree,z);
            x=a[a[x].top].father;
        }
        else
        {
            change(1,1,n,a[a[y].top].to_tree,a[y].to_tree,z);
            y=a[a[y].top].father;
        }
    }
    if (x==y)return;
    if (a[x].depth>a[y].depth)
    {
        change(1,1,n,a[a[y].heavy_son].to_tree,a[x].to_tree,z);
    }
    else
    {
        change(1,1,n,a[a[x].heavy_son].to_tree,a[y].to_tree,z);
    }
}

int main()
{
    //freopen("read.txt","r",stdin);
    n=read(),m=read(); 
    for (int i=1;i<=m;i++)
    {
        line[i].x=read(),line[i].y=read(),line[i].z=read();
        line[i].num=i;  
    }
    if (m<n-1)
    {
        q=read();
        while (q--)printf("Not connected\n");
        return 0;
    }
    sort(line+1,line+m+1,cmp);
    for (int i=1;i<=m;i++)
    {
        int u=line[i].x,v=line[i].y,w=line[i].z;
        if (getfather(u)!=getfather(v))
        {
            father[getfather(u)]=v;
            line[i].flag=1;
            ans+=w;
            insert(u,v,line[i].num);
            insert(v,u,line[i].num);
        }
    }
    a[1].depth=1;
    a[1].father=0;
    dfs1(1,0),dfs2(1,1);
    memset(tree,0x3f,sizeof(tree));
    sort(line+1,line+m+1,cmp1);
    for (int i=1;i<=m;i++)
    {
        if (!line[i].flag)
        {
            modify(line[i].x,line[i].y,line[i].z);
        }
    } 
    q=read();
    while (q--)
    {
        int x=read();
        if (!line[x].flag)
        {
            printf("%d\n",ans);
        }
        else
        {
            int xx=query(1,1,n,a[ref[x]].to_tree);
            if(xx==0x3f3f3f3f)
            {
                printf("Not connected\n");
            }
            else 
            {
                printf("%d\n",ans-line[x].z+xx); 
            }
        } 
    }
    return 0;
}

长链剖分

  • 待填坑

树剖大法好

最近作模拟赛,看到树上问题就开码树剖 (我变成无脑xxxx玄手了么)
可是树剖对付静态树问题基本都能完美解决
树剖用处确实很是很是多,不只要熟练运用,还要作到举一反三、用老知识解决新题目
我不是树剖讲师因此树剖学的仍是很差

动态树问题呢?树剖GG,祭出 L C T LCT
(LCT明明比树剖高级多了)


LCT(Link-Cut Tree)


玄♂妙的动态树问题

静态树问题在前面已经见识过了
就是树的形态不发生变化,只是树上的权值变化,用数据结构随便维护一下的问题罢了

可是树的形态若是会变化呢?例如添、删树边操做等
这就是 动态树问题(Dynamic Tree Problem)


然而Link-Cut Tree并不能吃

杨哲的《QTREE 解法的一些研究》 你应该据说过
因为百度像csdn同样要XX币才能下载那些文档,就百度云共享一波啦
这里下载,uuq0

LCT(Link-Cut Tree),解决动态树问题的一种算法
LCT≈splay+树剖,是解决动态树问题的一把利器
核心思想就是借鉴树剖的 轻重边 等概念,用比线段树灵活的 s p l a y splay 来维护一棵树(森林)
意会就好
(但真相是tarjan老爷子是在发明LCT以后才发明的splay)


the most advantages

动态树问题一般要求你对一棵树进行切割和拼接等操做,而后再在上面维护传统数据结构可维护的值
树剖作不了的软肋在于重儿子和重链早就剖分(预处理)好了
怎么添、删边?暴力重构全部重儿子和整个线段树?

100%GG

不用 L C T LCT 也能勉强作某些动态树问题了 (注意某些)
若是只有添边只有删边不要求强制在线的题目,用离线+树剖+splay/动态开点线段树是能够作的
虽然说树剖勉强支持删边操做,但这毕竟不是最通用、最完美的作法
什么东西能真正完美地解决这一类动态树问题呢?

Link-Cut Tree算法

并且 L C T LCT 能作的除了动态树问题还有

  • 任何树剖题

  • 可支持删边的并查集

  • 可作不用排序 k r u s k a l kruskal (动态加边的 M S T MST 并查集)

  • 等……

我以前不是说过树上倍增能作的题树剖必定能作吗
而树链剖分能作的题 L C T LCT 必定能作

LCA呢?
LCT是不能够求动态树上的LCA的……

为何不能求?这个等下再说吧


LCT之模样与概念

看看样子先

这就是一个LCT

L C T LCT 维护的其实算是一个连通性森林


一些概念

  • Preferred Child:重儿子,重儿子与父亲节点在同一棵splay中,一个节点至多一个重儿子
  • Preferred Edge:重边,链接父亲节点和重儿子的边
  • Preferred Path :重链,由重边及重边链接的节点构成的链

你看这个明显和树剖的东东一个鬼样因此能够略过
原来的那些名称都叫什么“偏好”XX的,感受用树剖名词就能够了

接下来是 L C T LCT 里一个独♂特的东西

  • Auxiliary Tree:辅助树
  • 由一条重链上的全部节点所构成的Splay称做这条链的辅助树
  • 每一个点的键值为这个点的深度,即这棵Splay的中序遍历是这条链从链顶到链底的全部节点构成的序列
  • 辅助树的根节点的父亲指向链顶的父亲节点,然而链顶的父亲节点的儿子并不指向辅助树的根节点
  • (也就是说父亲不认轻儿子只认重儿子,儿子都认父亲)
  • 这条性质为后来的操做提供了依据

上面框框里面是上网copy的,不知道怎么解释
必须注意辅助树(splay)的根≠原树的根

  • 首先不要把LCT 真的当成一棵树

LCT不是树!!!是个森林OK?LCA都求不了这叫作树么?
在同一棵splay里面固然能够暴力求LCA,但这有意义么?

  • 其次,为何LCT里的点不认轻重儿子?
    在一个splay里的点不就是父亲和重儿子和重儿子的重儿子……么?

  • 对上面的定义简单点说,在同一条“重链”里的全部点在一个splay中
    不懂就看上面那个 L C T LCT 本身理解,如 B B A A E E 是在一个splay里的
    这个明明很简单

  • splay过程当中,树的形态能够变化

可是原树是不会变的

  • LCT还有个特色,就是不用记录节点编号
    由于一个点的右儿子的深度是比它的大的,不像splay同样须要查找什么的:D

  • 其实更值得一提的是上面提到的虚边(也就是对应树剖的轻边)的那个东西
    虚边 p f pf 储存在一个splay的根节点, p f [ x ] pf[x] x x 在LCT里的父亲(即在不在当前辅助树里的那个)
    f a [ x ] fa[x] 则是splay中 x x 的父亲,不要把这个搞混
    例子:图中 p f [ G ] = A pf[G]=A f a [ G ] = 0 fa[G]=0 p f [ D ] = 0 pf[D]=0 f a [ D ] = B fa[D]=B
    一样本身去看图理解懒得多说

能够这样想:两个重链是用一条虚边链接的,没有虚边不就成一条重链了么?

看完这些,其实LCT仍是很简单的……

那么,不看懂概念怎么能够去学LCT的操做呢?


LCT之核心操做

请务必认认真真地阅读如下内容不然LCT你就学不会了
一句话说了N遍

operation 1:access

access操做是LCT里最基础、最重要的操做

定义 a c c e s s ( x ) access(x) 操做就是把 x x 的重儿子与 x x 断开,而后把从整个LCT的根到 x x 之间的路径都变为重边

access操做实际上是很简单的

  • 若如今要 a c c e s s access 节点 x x ,每一次把 x x 旋转到当前辅助树的根

  • 此时 x x 的右儿子深度比 x x 大,即右儿子是重儿子,咱们天然而然的就要把它给删掉

  • x x 连上 p f [ x ] pf[x] 所在的辅助树 (splay) 里,而后 p f [ t [ x ] [ 1 ] ] = x , f a [ t [ x ] [ 1 ] ] = 0 pf[t[x][1]]=x,fa[t[x][1]]=0

  • 简单点说就是把 x x 的右儿子做为辅助树的新根,并连一条向 x x 的虚边

  • 重复以上操做,直到 p f [ x ] = f a [ x ] = 0 pf[x]=fa[x]=0

这个不简单吗?


时间复杂度呢?

  • tarjan老爷子说, s p l a y splay 一次的时间复杂度 O ( log 2 n ) O(\log_2n)

  • 整个 a c c e s s access 要进行几回旋转操做呢?

  • 把若干个辅助树合并为一个辅助树,只进行一次 s p l a y splay 操做

  • 因为 s p l a y splay 均摊 O ( log 2 n ) O(\log_2n) ,又由于每次只对一个大的辅助树 s p l a y splay ,咱们能够大胆想象得出只有 O ( log 2 n ) O(\log_2n) 的时间

  • 因此 a c c e s s access 操做的均摊时间是 O ( log 2 n ) O(\log_2n)

之后任何对access操做时间复杂度有疑问的请去找tarjan老爷子喝茶
问我没用


code

cc人生赢家的LCT好漂亮啊

void down(int x) 
{
    if (a[x].rev) 
    {
        reverse(t[x][0]),reverse(t[x][1]);
        a[x].rev=0;
    }
}

void update(int x) 
{
	if(x)
	{
		a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
	} 
}

void downdata(int x) 
{
    while (x)st[++st[0]]=x,x=fa[x];	
    while (st[0])down(st[st[0]--]);
}
//从x到辅助树的根的整条路径上的边都要下传翻转标记
//……其实也就是暴力下传标记
int lr(int x) 
{
	return t[fa[x]][1]==x;
}
	
void rotate(int x) 
{
    int y=fa[x],k=lr(x);
    t[y][k]=t[x][!k]; 
	if(t[x][!k])fa[t[x][!k]]=y;
    fa[x]=fa[y];
	if(fa[y])t[fa[y]][lr(y)]=x;    
    t[x][!k]=y; 
	fa[y]=x; 
	pf[x]=pf[y];
    update(y),update(x);
}

void splay(int x,int y) 
{
    //downdata(x);
    while(fa[x]!=y)
	{
        if(fa[fa[x]]!=y)
        {
            if(lr(x)==lr(fa[x]))rotate(fa[x]); 
			else rotate(x);
        } 
        rotate(x);
    }
}

void access(int x) 
{
    for (int y=0;x;update(x),y=x,x=pf[x])
    {
        splay(x,0);
		fa[t[x][1]]=0;
		pf[t[x][1]]=x;
        t[x][1]=y;
		fa[y]=x;
		pf[y]=0;
    }
}

operation 2:makeroot

makeroot操做和access操做是LCT的最核心的操做
其余操做视题而定,但makeroot和access必不可少

定义 m a k e r o o t ( x ) makeroot(x) 操做就是将 x x 所在的splay翻转,使 x x 变为原LCT树的根

大概这样吧……
不算 a c c e s s access 的话 m a k e r o o t makeroot 只有splay一次的时间复杂度为 O ( l o g 2 n ) O(log_2n)

makeroot不就更简单了?

  • 首先 a c c e s s ( x ) access(x) 后,此时的 x x 是辅助树中深度最大的点,接着把 x x 旋转(splay)到LCT的根

  • 而此时在辅助树中,除了 x x 的其余点都是其父亲的左儿子

  • 而后再splay区间翻转便可,打上lazy-tag标记

  • 而后 m a k e r o o t makeroot 才成功解决,如上图的那种样子


code

void swap(int &x,int &y)
{
	int z=x;
	x=y;
	y=z;
}

void reverse(int x) 
{
	if(x)
	{
		a[x].rev^=1;
		swap(t[x][0],t[x][1]);
	}
}

void down(int x) 
{
	if (a[x].rev) 
	{
		reverse(t[x][0]),reverse(t[x][1]);
		a[x].rev=0;
	}
}

void makeroot(int x) 
{
    access(x); 
	splay(x,0); 
	reverse(x);
}

operation 3:link

link和cut嘛……因此LCT要叫Link-Cut Tree啊

定义 l i n k ( x , y ) link(x,y) 操做就是将节点 x x 连到节点 y y 的儿子
link太简单了不想放图

link操做就太简单了

  • m a k e r o o t ( x ) makeroot(x) 后,此时 x x 是原LCT的根

  • 此时 x x 没有 f a fa ,而从 y y 连向 x x 冲掉原来 y y f a fa

  • 因此从 x x y y 连一条虚边,即 p f [ x ] = y pf[x]=y


code

void link(int x,int y) 
{
    makeroot(x); 
	pf[x]=y;
}

operation 4:cut

定义 c u t ( x , y ) cut(x,y) 操做就是将节点 x x (x是y的儿子)与父亲 y y 断开
cut太简单了也不想放图

cut和link其实很相似

  • m a k e r o o t ( x ) makeroot(x) a c c e s s ( y ) access(y) 后, x x 是原树的根,且 x x y y 在同一splay里

  • 而后 s p l a y ( x , 0 ) splay(x,0) ,保证 x x 是根,又因为 x , y x,y 连通,因此此时 y y x x 的右儿子

  • 因此 t r e e [ x ] [ 1 ] = p f [ y ] = f a [ y ] = 0 tree[x][1]=pf[y]=fa[y]=0 就行了


code

void cut(int x,int y) 
{
    makeroot(x); 
    access(y);
    splay(x,0);
    t[x][1]=fa[y]=pf[y]=0;
    update(x);
}

operation 5:getroot

定义 g e t r o o t ( x ) getroot(x) x x 节点当前辅助树的根
退化版并查集路径压缩

g e t r o o t getroot 小学生级别难度

  • 暴力把 x x 向上跳,直到 f a [ x ] = 0 fa[x]=0

  • 没了


code

int getroot(int x)
{
    while (fa[x])x=fa[x];
    return x;
}

operation 6:judge

定义 j u d g e ( x , y ) judge(x,y) ,若 x , y x,y 联通则为 T r u e True ,不然为 F a l s e False
退化版并查集集合查询

g e t r o o t getroot 稍微难一丁点

  • m a k e r o o t ( x ) , a c c e s s ( y ) makeroot(x),access(y) 后, x x 为原树根, y y 打通到了最顶上的点(不必定是原树根)

  • x , y x,y 间联通,则 g e t r o o t ( y ) = x getroot(y)=x

  • 因此询问 g e t r o o t ( y ) = x ? getroot(y)=x? 便可


code

bool judge(int x,int y)
{
    makeroot(x);
    access(y);
    splay(x,0);
    return getroot(y)==x;
}

Link-Cut Tree常规套路

和树剖的差很少, L C T LCT 的题目仍是问你 x x 点到 y y 点间的路径路径和或者极值什么的
树剖很好作静态树的嘛

LCT怎么解决这类问题呢?

其实有常规套路

  • 好比询问 x x y y 路径和

  • m a k e r o o t ( y ) makeroot(y) 后,再 a c c e s s ( x ) , s p l a y ( x , 0 ) access(x),splay(x,0) ,此时 x x y y 就在同一棵splay里了

  • 而后这条路径不就职你摆布了?


注意事项

能够求LCA……才怪……

不考虑LCT求LCA的话会发现 L C T LCT 颇有某些优点

  • 代码复杂度的排序,应该是倍增优、LCT稍劣一点、树剖不好

  • 功能的排序固然是LCT最多、树剖少、倍增最少

忽然发现LCT很好打、很好用?

  • 时间复杂度的排序就是树剖最快、倍增较快、LCT龟速……

因为splay带来的巨大常数因子 L C T LCT 的时间大约是树剖的几倍……

好比这个

这是【ZJOI2008】树的统计
上面是树剖下面LCT,比对一下代码长度和时间

因此不要听tarjan那家伙BB说LCT时间很好
并且还有一个硬伤是求不了LCA
静态树上的两个点,LCA是惟一肯定的,维护的值怎么变都没有影响LCA

splay的LCA是肯定的么……
因此比赛里有动态树求LCA就赶快弃掉


点权、边权

因为 L C T LCT 使用灵活的 s p l a y splay 维护数据,维护点权易如反掌

怎么维护LCT的边权呢?

可能先想到的是采起树剖思想,边权当作儿子的点权

  • LCT也能够这么作么?

不行
因为 L C T LCT s p l a y splay 旋转,儿子旋转后就成为了父亲
那维护的权值不都乱套了?

解决方法很简单

把一条链接 x , y x,y 的边当作一个点,再把这个点和 x , y x,y 分别连起来
如此就能够维护LCT的边权了
必须注意要开两倍空间!


事实证实LCT很是好用
因此须要海量例题来让你刻骨铭心

例题1:【BZOJ2049】【luoguP2147】[SDOI2008]洞穴勘测

problem

题目描述

辉辉热衷于洞穴勘测。

某天,他按照地图来到了一片被标记为JSZX的洞穴群地区。通过初步勘测,辉辉发现这片区域由n个洞穴(分别编号为1到n)以及若干通道组成,而且每条通道链接了刚好两个洞穴。假如两个洞穴能够经过一条或者多条通道按必定顺序链接起来,那么这两个洞穴就是连通的,按顺序链接在一块儿的这些通道则被称之为这两个洞穴之间的一条路径。
洞穴都十分坚固没法破坏,然而通道不太稳定,时常由于外界影响而发生改变,好比,根据有关仪器的监测结果,123号洞穴和127号洞穴之间有时会出现一条通道,有时这条通道又会由于某种稀奇古怪的缘由被毁。

辉辉有一台监测仪器能够实时将通道的每一次改变情况在辉辉手边的终端机上显示:

若是监测到洞穴u和洞穴v之间出现了一条通道,终端机上会显示一条指令 Connect u v

若是监测到洞穴u和洞穴v之间的通道被毁,终端机上会显示一条指令 Destroy u v

通过长期的坚苦卓绝的手工推算,辉辉发现一个奇怪的现象:不管通道怎么改变,任意时刻任意两个洞穴之间至多只有一条路径。

于是,辉辉坚信这是因为某种本质规律的支配致使的。于是,辉辉更加夜以继日地坚守在终端机以前,试图经过通道的改变状况来研究这条本质规律。
然而,终于有一天,辉辉在堆积成山的演算纸中崩溃了……他把终端机往地面一砸(终端机也足够坚固没法破坏),转而求助于你,说道:“你老兄把这程序写写吧”。

辉辉但愿能随时经过终端机发出指令 Query u v,向监测仪询问此时洞穴u和洞穴v是否连通。如今你要为他编写程序回答每一次询问。
已知在第一条指令显示以前,JSZX洞穴群中没有任何通道存在。

输入输出格式

输入格式: 第一行为两个正整数n和m,分别表示洞穴的个数和终端机上出现过的指令的个数。
如下m行,依次表示终端机上出现的各条指令。每行开头是一个表示指令种类的字符串s("Connect”、”Destroy”或者”Query”,区分大小写),以后有两个整数u和v
(1≤u, v≤n且u≠v) 分别表示两个洞穴的编号。

输出格式: 对每一个Query指令,输出洞穴u和洞穴v是否互相连通:是输出”Yes”,不然输出”No”。(不含双引号)

输入输出样例

输入样例#1: 复制 200 5 Query 123 127 Connect 123 127 Query 123 127 Destroy
127 123 Query 123 127 输出样例#1: 复制 No Yes No 输入样例#2: 复制 3 5 Connect 1 2
Connect 3 1 Query 2 3 Destroy 1 3 Query 2 3 输出样例#2: 复制 Yes No 说明

数听说明

10%的数据知足n≤1000, m≤20000

20%的数据知足n≤2000, m≤40000

30%的数据知足n≤3000, m≤60000

40%的数据知足n≤4000, m≤80000

50%的数据知足n≤5000, m≤100000

60%的数据知足n≤6000, m≤120000

70%的数据知足n≤7000, m≤140000

80%的数据知足n≤8000, m≤160000

90%的数据知足n≤9000, m≤180000

100%的数据知足n≤10000, m≤200000

保证全部Destroy指令将摧毁的是一条存在的通道

本题输入、输出规模比较大,建议c\c++选手使用scanf和printf进行I\O操做以避免超时


analysis

  • ……智障题

  • 这题是最普通的link、cut操做和判断联通

  • m a k e r o o t ( x ) , a c c e s s ( y ) makeroot(x),access(y) 后,判断 g e t r o o t ( y ) = x ? getroot(y)=x? 便可

  • 不要告诉我说不知道怎么link和cut


code

#include<bits/stdc++.h>
#define MAXN 30001

using namespace std;

int t[MAXN][2];
int b[MAXN],fa[MAXN],pf[MAXN],st[MAXN];
char s[10];
int n,m;

struct node 
{
    int val,sum,mx,size;
    bool rev;
}a[MAXN];

int read()
{
    int x=0,f=1;
    char ch=getchar();
    while (ch<'0' || '9'<ch)
    {
        if (ch=='-')f=-1;
        ch=getchar();   
    }
    while ('0'<=ch && ch<='9')
    {
        x=x*10+ch-'0';
        ch=getchar();
    }
    return x*f;
}

void reverse(int x) 
{
    if(x)
    {
        a[x].rev^=1;
        swap(t[x][0],t[x][1]);
    }
}

void down(int x) 
{
    if (a[x].rev) 
    {
        reverse(t[x][0]),reverse(t[x][1]);
        a[x].rev=0;
    }
}

void update(int x) 
{
    if (x)
    {
        a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
    } 
}

void downdata(int x) 
{
    st[0]=0;
    while (x)st[++st[0]]=x,x=fa[x];
    while (st[0])down(st[st[0]--]);
}

int lr(int x)
{
    return t[fa[x]][1]==x;
}

void rotate(int x) 
{
    int y=fa[x],k=lr(x);
    t[y][k]=t[x][!k];

    if (t[x][!k])fa[t[x][!k]]=y;
    fa[x]=fa[y];

    if (fa[y])t[fa[y]][lr(y)]=x;    
    t[x][!k]=y;

    fa[y]=x,pf[x]=pf[y];
    update(y),update(x);
}

void splay(int x, int y) 
{
    downdata(x);
    while (fa[x]!=y)
    {
        if (fa[fa[x]]!=y)
        {
            if (lr(x)==lr(fa[x]))rotate(fa[x]); 
            else rotate(x);
        } 
        rotate(x);
    }
}

void access(int x) 
{
    for (int y=0;x;update(x),y=x,x=pf[x])
    {
        splay(x,0);
        fa[t[x][1]]=0;
        pf[t[x][1]]=x;
        t[x][1]=y;
        fa[y]=x;
        pf[y]=0;
    }
}

void makeroot(int x) 
{
    access(x); 
    splay(x,0); 
    reverse(x);
}

void link(int x,int y) 
{
    makeroot(x); 
    pf[x]=y;
}

void cut(int x,int y) 
{
    makeroot(x); 
    access(y);
    splay(x,0);
    t[x][1]=fa[y]=pf[y]=0;
    update(x);
}

int getroot(int x)
{
    while (fa[x])x=fa[x];
    return x;
}

bool judge(int x,int y)
{
    makeroot(x);
    access(y);
    splay(x,0);
    return getroot(y)==x;
}

int main()
{
    n=read(),m=read();
    while (m--)
    {
        scanf("%s",&s);
        int x=read(),y=read();
        if (s[0]=='C')link(x,y);
        else if (s[0]=='D')cut(x,y);
        else (judge(x,y))?(printf("Yes\n")):(printf("No\n"));
    }
}

例题2:【luoguP3203】 [HNOI2010]弹飞绵羊

problem

题目描述

某天,Lostmonkey发明了一种超级弹力装置,为了在他的绵羊朋友面前显摆,他邀请小绵羊一块儿玩个游戏。游戏一开始,Lostmonkey在地上沿着一条直线摆上n个装置,每一个装置设定初始弹力系数ki,当绵羊达到第i个装置时,它会日后弹ki步,达到第i+ki个装置,若不存在第i+ki个装置,则绵羊被弹飞。绵羊想知道当它从第i个装置起步时,被弹几回后会被弹飞。为了使得游戏更有趣,Lostmonkey能够修改某个弹力装置的弹力系数,任什么时候候弹力系数均为正整数。

输入输出格式

输入格式: 第一行包含一个整数n,表示地上有n个装置,装置的编号从0到n-1。

接下来一行有n个正整数,依次为那n个装置的初始弹力系数。

第三行有一个正整数m,

接下来m行每行至少有两个数i、j,若i=1,你要输出从j出发被弹几回后被弹飞,若i=2则还会再输入一个正整数k,表示第j个弹力装置的系数被修改为k。

输出格式: 对于每一个i=1的状况,你都要输出一个须要的步数,占一行。

输入输出样例

输入样例#1: 复制 4 1 2 1 1 3 1 1 2 1 1 1 1 输出样例#1: 复制 2 3 说明

对于20%的数据n,m<=10000,对于100%的数据n<=200000,m<=100000


analysis

  • LCT板子题

  • 咱们能够建一个 n + 1 n+1 号节点 x x 号节点连到表示 n + 1 n+1 号节点就表示被弹飞

  • 若要更改,直接先 c u t cut l i n k link 一波,关键怎么询问弹飞几回呢?暴力splay?

  • 咱们能够 m a k e r o o t ( n + 1 ) makeroot(n+1) ,使 n + 1 n+1 号节点变为原LCT的根

  • 接着咱们 a c c e s s ( x ) access(x) s p l a y ( x , 0 ) splay(x,0) 也就是把 x x 旋转到原树的根

  • 那么 x x 号节点会弹 x s i z e 1 x_{size}-1 次被弹飞,可是为何呢?

  • 因为辅助树按照深度关键字排序因此x的子树大小-1就是要被弹飞几回了

  • 因此明显正确


code

#include<stdio.h>
#include<string.h>
#define MAXN 200001

using namespace std;

int t[MAXN][2];
int b[MAXN],fa[MAXN],pf[MAXN],st[MAXN];
int n,m;

struct node 
{
    int size,rev;
}a[MAXN];

int min(int x,int y) 
{
    return x<y?x:y;
}

void swap(int &x,int &y)
{
    int z=x;
    x=y;
    y=z;
}

void reverse(int x) 
{
    if(x)
    {
        a[x].rev^=1;
        swap(t[x][0],t[x][1]);
    }
}

void down(int x) 
{
    if (a[x].rev) 
    {
        reverse(t[x][0]),reverse(t[x][1]);
        a[x].rev=0;
    }
}

void update(int x) 
{
    if(x)
    {
        a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
    } 
}

void downdata(int x) 
{
    while (x)st[++st[0]]=x,x=fa[x]; 
    while (st[0])down(st[st[0]--]);
}

int lr(int x) 
{
    return t[fa[x]][1]==x;
}

void rotate(int x) 
{
    int y=fa[x],k=lr(x);
    t[y][k]=t[x][!k]; 
    if(t[x][!k])fa[t[x][!k]]=y;
    fa[x]=fa[y];
    if(fa[y])t[fa[y]][lr(y)]=x;    
    t[x][!k]=y; 
    fa[y]=x; 
    pf[x]=pf[y];
    update(y),update(x);
}

void splay(int x, int y) 
{
    downdata(x);
    while(fa[x]!=y)
    {
        if(fa[fa[x]]!=y)
        {
            if(lr(x)==lr(fa[x]))rotate(fa[x]); 
            else rotate(x);
        } 
        rotate(x);
    }
}

void access(int x) 
{
    for (int y=0;x;update(x),y=x,x=pf[x])
    {
        splay(x,0);
        fa[t[x][1]]=0;
        pf[t[x][1]]=x;
        t[x][1]=y;
        fa[y]=x;
        pf[y]=0;
    }
}

void makeroot(int x) 
{
    access(x); 
    splay(x,0); 
    reverse(x);
}

void link(int x,int y) 
{
    makeroot(x); 
    pf[x]=y;
}

void cut(int x,int y) 
{
    makeroot(x); 
    access(y);
    splay(x,0);
    t[x][1]=fa[y]=pf[y]=0;
    update(x);
}

int main() 
{
    scanf("%d",&n);
    for (int i=1;i<=n;i++) 
    {
        scanf("%d",&b[i]);
        link(i,min(n+1,i+b[i]));
    }
    scanf("%d",&m);
    while (m--)
    {
        int z,x; 
        scanf("%d%d",&z,&x),x++;
        if (z==1) 
        {
            makeroot(n+1);
            access(x);
            splay(x,0);
            printf("%d\n",a[x].size-1);
        } 
        else 
        {
            cut(x,min(x+b[x],n+1));
            scanf("%d",&b[x]);
            link(x,min(x+b[x],n+1));
        }
    }
}

例题3:【JZOJ2256】【ZJOI2008】树的统计

problem

题目上面有因此就不从新贴了


analysis

  • 前面是用树剖作的,如今用LCT重作了一次

  • 其实LCT的代码复杂度简直秒杀树剖好伐

  • 都说了LCT能作全部树剖题,然而这题连cut操做都没有

  • 那不就简单了?

  • 像普通套路同样,询问 x , y x,y 间的路径极值或权值和,就 m a k e r o o t ( y ) , a c c e s s ( x ) , s p l a y ( x , 0 ) makeroot(y),access(x),splay(x,0)

  • 而后直接输出 a [ x ] . X X X a[x]._{XXX} 就行了

  • 至于修改 x x 节点的操做,把 x x 旋到根,直接修改便可

然而WA90

  • 为何呢?

  • 每次输入的时候,都要把当前节点旋到根,读入后再 u p d a t e update

  • 这样才终于切了


code

#include<bits/stdc++.h>
#define MAXN 30001

using namespace std;

int t[MAXN][2];
int b[MAXN],fa[MAXN],pf[MAXN],st[MAXN];
char s[10];
int n,m;

struct node 
{
    int val,sum,mx,size,rev;
}a[MAXN];

int read()
{
    int x=0,f=1;
    char ch=getchar();
    while (ch<'0' || '9'<ch)
    {
        if (ch=='-')f=-1;
        ch=getchar();   
    }
    while ('0'<=ch && ch<='9')
    {
        x=x*10+ch-'0';
        ch=getchar();
    }
    return x*f;
}

void reverse(int x) 
{
    if(x)
    {
        a[x].rev^=1;
        swap(t[x][0],t[x][1]);
    }
}

void down(int x) 
{
    if (a[x].rev) 
    {
        reverse(t[x][0]),reverse(t[x][1]);
        a[x].rev=0;
    }
}

void update(int x) 
{
    if (x)
    {
        a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
        a[x].sum=a[x].val+a[t[x][0]].sum+a[t[x][1]].sum;
        a[x].mx=max(a[x].val,max(a[t[x][0]].mx,a[t[x][1]].mx));
    } 
}

void downdata(int x) 
{
	st[0]=0;
    while (x)st[++st[0]]=x,x=fa[x]; 
    while (st[0])down(st[st[0]--]);
}

int lr(int x)
{
    return t[fa[x]][1]==x;
}

void rotate(int x) 
{
    int y=fa[x],k=lr(x);
    t[y][k]=t[x][!k];
    
    if (t[x][!k])fa[t[x][!k]]=y;
    fa[x]=fa[y];
    
    if (fa[y])t[fa[y]][lr(y)]=x;    
    t[x][!k]=y;
    
    fa[y]=x,pf[x]=pf[y];
    update(y),update(x);
}

void splay(int x, int y) 
{
    downdata(x);
    while (fa[x]!=y)
    {
        if (fa[fa[x]]!=y)
        {
            if (lr(x)==lr(fa[x]))rotate(fa[x]); 
            else rotate(x);
        } 
        rotate(x);
    }
}

void access(int x) 
{
    for (int y=0;x;update(x),y=x,x=pf[x])
    {
        splay(x,0);
        fa[t[x][1]]=0;
        pf[t[x][1]]=x;
        t[x][1]=y;
        fa[y]=x;
        pf[y]=0;
    }
}

void makeroot(int x) 
{
    access(x); 
    splay(x,0); 
    reverse(x);
}

void link(int x,int y) 
{
    makeroot(x); 
    pf[x]=y;
}

int main()
{
	n=read();
	for (int i=1;i<n;i++)link(read(),read());
	a[0].mx=-2147483647,a[0].sum=a[0].val=0;
	for (int i=1;i<=n;i++)
	{
		splay(i,0);
		a[i].mx=a[i].sum=a[i].val=read();
		update(i);
	}
	
	m=read();
	while (m--)
	{
		scanf("%s",&s);
		int x=read(),y=read();
		if (s[1]=='M')//max
		{
			makeroot(y);
			access(x);
			splay(x,0);
			printf("%d\n",a[x].mx);
		}
		else if (s[1]=='S')//sum
		{
			makeroot(y);
			access(x);
			splay(x,0);
			printf("%d\n",a[x].sum);
		}
		else //change
		{
			splay(x,0);
			a[x].val=y;
			update(x);
		}
	}
}

例题4:【BZOJ3282】【luoguP3690】【模板】Link Cut Tree (动态树)

problem

题目背景

动态树

题目描述

给定n个点以及每一个点的权值,要你处理接下来的m个操做。操做有4种。操做从0到3编号。点从1到n编号。

0:后接两个整数(x,y),表明询问从x到y的路径上的点的权值的xor和。保证x到y是联通的。

1:后接两个整数(x,y),表明链接x到y,若x到y已经联通则无需链接。

2:后接两个整数(x,y),表明删除边(x,y),不保证边(x,y)存在。

3:后接两个整数(x,y),表明将点x上的权值变成y。

输入输出格式

输入格式: 第1行两个整数,分别为n和m,表明点数和操做数。

第2行到第n+1行,每行一个整数,整数在[1,10^9]内,表明每一个点的权值。

第n+2行到第n+m+1行,每行三个整数,分别表明操做类型和操做所需的量。

输出格式: 对于每个0号操做,你须输出x到y的路径上点权的xor和。

输入输出样例

输入样例#1: 复制 3 3 1 2 3 1 1 2 0 1 2 0 1 1 输出样例#1: 复制 3 1 说明

数据范围: 1 \leq N, M \leq 3 \cdot {10}^5 1≤N,M≤3⋅10 5


analysis

  • 仍是LCT板子题差很少是板子在手天下我有

  • 注意两点就好

  • l i n k , c u t link,cut 的两个点不必定联通,用 j u d g e judge 特判一下

  • 还有就是异或和的话 a [ i ] . s u m = ( a [ i ] . v a l ) x o r ( a [ t [ i ] [ 0 ] ] . s u m ) x o r ( a [ t [ i ] [ 1 ] ] . s u m ) a[i].sum=(a[i].val)xor(a[t[i][0]].sum)xor(a[t[i][1]].sum)

  • 如此简单


code

#include<bits/stdc++.h>
#define MAXN 300001

using namespace std;

int t[MAXN][2];
int b[MAXN],fa[MAXN],pf[MAXN],st[MAXN];
char s[10];
int n,m;

struct node 
{
    int val,sum,mx,size;
    bool rev;
}a[MAXN];

int read()
{
    int x=0,f=1;
    char ch=getchar();
    while (ch<'0' || '9'<ch)
    {
        if (ch=='-')f=-1;
        ch=getchar();   
    }
    while ('0'<=ch && ch<='9')
    {
        x=x*10+ch-'0';
        ch=getchar();
    }
    return x*f;
}

void reverse(int x) 
{
    if(x)
    {
        a[x].rev^=1;
        swap(t[x][0],t[x][1]);
    }
}

void down(int x) 
{
    if (a[x].rev) 
    {
        reverse(t[x][0]),reverse(t[x][1]);
        a[x].rev=0;
    }
}

void update(int x) 
{
    if (x)
    {
        a[x].size=a[t[x][0]].size+a[t[x][1]].size+1;
        a[x].sum=a[x].val^a[t[x][0]].sum^a[t[x][1]].sum;
    } 
}

void downdata(int x) 
{
    st[0]=0;
    while (x)st[++st[0]]=x,x=fa[x];
    while (st[0])down(st[st[0]--]);
}

int lr(int x)
{
    return t[fa[x]][1]==x;
}

void rotate(int x) 
{
    int y=fa[x],k=lr(x);
    t[y][k]=t[x][!k];

    if (t[x][!k])fa[t[x][!k]]=y;
    fa[x]=fa[y];

    if (fa[y])t[fa[y]][lr(y)]=x;    
    t[x][!k]=y;

    fa[y]=x,pf[x]=pf[y];
    update(y),update(x);
}

void splay(int x, int y) 
{
    downdata(x);
    while (fa[x]!=y)
    {
        if (fa[fa[x]]!=y)
        {
            if (lr(x)==lr(fa[x]))rotate(fa[x]); 
            else rotate(x);
        } 
        rotate(x);
    }
}

void access(int x) 
{
    for (int y=0;x;update(x),y=x,x=pf[x])
    {
        splay(x,0);
        fa[t[x][1]]=0;
        pf[t[x][1]]=x;
        t[x][1]=y;
        fa[y]=x;
        pf[y]=0;
    }
}

void makeroot(int x) 
{
    access(x); 
    splay(x,0); 
    reverse(x);
}

int getroot(int x)
{
    while (fa[x])x=fa[x];
    return x;
}

bool judge(int x,int y)
{
    makeroot(x);
    access(y);
    splay(x,0);
    return getroot(y)==x;
}

void link(int x,int y) 
{
    if (!judge(x,y))
    {
        makeroot(x); 
        pf[x]=y;
    }
}

void cut(int x,int y) 
{
    if (judge(x,y))
    {
        makeroot(x); 
        access(y);
        splay(x,0);
        t[x][1]=fa[y]=pf[y]=0;
        update(x);
    }
}

int main()
{
    //freopen("readin.txt","r",stdin);
    n=read(),m=read();
    a[0].val=a[0].sum=0;
    for (int i=1;i<=n;i++)
    {
        splay(i,0);
        a[i].val=read();
        update(i);
    }
    while (m--)
    {
        int z=read(),x=read(),y=read();
        if (z==0)
        {
            makeroot(y);
            access(x);
            splay(x,0);
            printf("%d\n",a[x].sum);
        }
        else if (z==1)link(x,y);
        else if (z==2)cut(x,y);
        else 
        {
            splay(x,0);
            a[x].val=y;
            update(x);
        }
    }
}

上面的都是裸的LCT题目,直接维护

因此接下来的例题不会再有裸题


例题5:【JZOJ3754】【luoguP2387】【NOI2014】魔法森林

problem

Description

为了获得书法你们的真传,小 E 同窗下定决心去拜访住在魔法森林中的隐士。魔法森林能够被当作一个包含 n 个节点 m
条边的无向图,节点标号为1,2,3, … , n,边标号为 1,2,3, … , m。初始时小 E 同窗在 1 号节点,隐士则住在 n
号节点。小 E 须要经过这一片魔法森林,才可以拜访到隐士。

魔法森林中居住了一些妖怪。每当有人通过一条边的时候,这条边上的妖怪就会对其发起攻击。 幸运的是, 在 1 号节点住着两种守护精灵: A
型守护精灵与B 型守护精灵。小 E 能够借助它们的力量,达到本身的目的。

只要小 E 带上足够多的守护精灵, 妖怪们就不会发起攻击了。具体来讲, 无向图中的每一条边 ei 包含两个权值 ai 与 bi 。
若身上携带的 A 型守护精灵个数很多于 ai ,且 B 型守护精灵个数很多于 bi
,这条边上的妖怪就不会对经过这条边的人发起攻击。当且仅当经过这片魔法森林的过程当中没有任意一条边的妖怪向小 E 发起攻击,他才能成功找到隐士。

因为携带守护精灵是一件很是麻烦的事,小 E 想要知道, 要可以成功拜访到隐士,最少须要携带守护精灵的总个数。守护精灵的总个数为 A
型守护精灵的个数与 B 型守护精灵的个数之和。

Input

输入文件的第 1 行包含两个整数 n, m,表示无向图共有 n 个节点, m 条边。

接下来 m 行,第 i + 1 行包含 4 个正整数 Xi, Yi, ai, bi, 描述第 i
条无向边。其中Xi与Yi为该边两个端点的标号,ai与bi的含义如题所述。

注意数据中可能包含重边与自环。

Output

输出一行一个整数:若是小 E 能够成功拜访到隐士,输出小 E 最少须要携带的守护精灵的总个数;若是不管如何小 E
都没法拜访到隐士,输出“-1” (不含引号) 。

Sample Input

【样例输入 1】

4 5

1 2 19 1

2 3 8 12

2 4 12 15

1 3 17 8

3 4 1 17

【样例输入 2】

3 1

1 2 1 1

Sample Output

【样例输出 1】

32

【样例输出 2】

-1

Data Constraint

Hint

【样例说明 1】

若是小 E 走路径 1→2→4,须要携带 19+15=34 个守护精灵;

若是小 E 走路径 1→3→4,须要携带 17+17=34 个守护精灵;

若是小 E 走路径 1→2→3→4,须要携带 19+17=36 个守护精灵;

若是小 E 走路径 1→3→2→4,须要携带 17+15=32 个守护精灵。

综上所述,小 E 最少须要携带 32 个守护精灵。

【样例说明 2】

小 E 没法从 1 号节点到达 3 号节点,故输出-1。


analysis

  • 好像MST的样子,但思考一下就能够发现kruskal是错的

  • 正解LCT,但据说SPFA也可AC?

  • 维护动态增删边MST就能够了

  • 注意此处维护边权,就按日常套路,把边当作点连起来,边权当作点权

  • 首先将 a a 升序排序,保证 a a 最优,接下来维护 b b MST,怎么弄呢?

  • 咱们从新定义一下 a [ i ] . m a x a[i].max i i 节点子树内权值最大的点的标号

  • 若是当前第 j j 条边的 b b 小于当前LCT里面 b b 最大的边,咱们不就能够把这条边换掉了?

  • 经过查找 q u e r y m a x ( x , y ) query_{max}(x,y) 来查找并切断那一条较劣的边便可

  • 而后中途只要 1 1 n n 是联通的,就统计答案 a n s = m i n ( f [ i ] . a + a [ q u e r y m a x ( 1 , n ) ] . v a l ) ans=min(f[i].a+a[query_{max}(1,n)].val)

  • 搞定


code

#include<bits/stdc++.h>
#define MAXN 200001
#define MAXM 500001
#define INF 1000000007

using namespace std;

int t[MAXN][2];
int fat[MAXN],fa[MAXN],pf[MAXN],st[MAXN];
char s[10];
int n,m,ans;

struct node 
{
    int val,mx,size;
    bool rev;
}a[MAXN];

struct edge
{
    int x,y,a,b;
}f[MAXM];

bool cmp(edge x,edge y)
{
    return x.a<y.a;
}

int getfa(int x)
{
    return !fat[x]?x:fat[x]=getfa(fat[x]);
}

int read()
{
    int x=0,f=1;
    char ch=getchar();
    while (ch<'0' || '9'<ch)
    {
        if (ch=='-')f=-1;
        ch=getchar();   
    }
    while ('0'<=ch && ch<='9')
    {
        x=x*10+ch-'0';
        ch=getchar();
    }
    return x*f;
}

void reverse(int x) 
{
    if(x)
    {
        a[x].rev^=1;
        swap(t[x][0],t[x][1]);
    }
}

void down(int x) 
{
    if
相关文章
相关标签/搜索