树论算法复习笔记

树论算法复习笔记

省选前写的,现在发出来

树的直径

定义:树的直径是树上的最长路径。
值得注意的是,直径并不唯一

直径的性质

我们这里只讨论无负权的情况

  1. 直径两端点一定是叶子节点。
  2. 距任意点最远点一定是直径的端点,据所有点最大值最小的点一定是直径的中点。
  3. 两棵树相连,新直径的两端点一定是原四个端点中的两个
  4. 两棵树相连,新直径长度最小为\(\max(\max(直径1,直径2),半径1+半径2+新边长度)\).其中直径长度为奇数时半径上取整。
  5. 一棵树上接一个叶子结点,直径最多改变一个端点
  6. 若一棵树存在多条直径,那么这些直径一定交于一点,且交点是直径的严格中点(中点可能在某条边内)
  7. 从一个点出发的最长路径的终点一定是直径的端点

BFS求法

从一个点出发的最长路径的终点一定是直径的端点
因此我们可以先随便找一个点,从它出发BFS找到距离最远的点\(s\),再从\(s\)出发BFS找到距它最远的点\(t\),\(s\)\(t\)的路径就是直径. 注意如果树有负权,就不能用此方法。

node bfs(int s){
    memset(used,0,sizeof(used));
    queue<node>q;
    q.push(node(s,0));
    node now,nex;
    int tx,ty;
    int maxt=0,maxx=s;
    used[s]=1;
    while(!q.empty()){
        now=q.front();
        q.pop();
        int u=now.x;
        if(now.t>maxt){
            maxx=now.x;
            maxt=now.t;
        }
        for(int i=head[u];i!=0;i=E[i].next){
            if(!used[E[i].to]){
                used[E[i].to]=1;
                q.push(node(E[i].to,now.t+E[i].len));
            }
        }
    }
    return node(maxx,maxt);
}
int main(){
    node tmp1=bfs(1);
    node tmp2=bfs(tmp1.x);
    printf("%d\n",tmp2.t);//输出直径长度
}

树形DP求法

\(f_x\)表示当前已经合并的子树内到\(x\)的最长路径
在合并每个子树前更新答案\(d=\max(d,f_x+f_y+w(x,y))\),然后\(f_x=\max_{y \in son(x)}(f_x,f_y+w(x,y))\)

void dfs(int x,int fa){
    for(int y=head[x];y;y=E[i].next){
        int y=E[i].to;
        if(y!=fa){
            dfs(y,x);
            ans=max(ans,f[x]+f[y]+E[i].len);
            f[x]=max(f[x],f[y]+E[i].len);
        }
    }
}

这种算法可以处理负边权.

[APIO2016]巡逻
在一个地区中有 n 个村庄,编号为 1, 2, ..., n。有 n – 1 条道路连接着这些村 庄,每条道路刚好连接两个村庄,从任何一个村庄,都可以通过这些道路到达其 他任一个村庄。每条道路的长度均为 1 个单位。 为保证该地区的安全,巡警车每天要到所有的道路上巡逻。警察局设在编号 为 1 的村庄里,每天巡警车总是从警察局出发,最终又回到警察局

为了减少总的巡逻距离,该地区准备在这些村庄之间建立 K 条新的道路, 每条新道路可以连接任意两个村庄。两条新道路可以在同一个村庄会合或结束。 一条新道路甚至可以是一个环,其两端连接到同一 个村庄。 由于资金有限,K 只能是 1 或 2。同时,为了不浪费资金,每天巡警车必须 经过新建的道路正好一次。计算出最佳 的新建道路的方案使得总的巡逻距离最小,并输出这个最小的巡逻距离。
当不建立新的道路时,路线总长度显然为\(2(n-1)\),因为每条边被走了2次

\(K=1\),建立一条新的道路\((x,y)\),会形成一个环,原来\(x\)\(y\)的路径上的边会少走一次。那么让\(x\),\(y\)为直径的两个端点最优。答案是\(2(n-1)-d_1+1\).其中\(d_1\)为原树的直径。

\(K=2\),再建立一条新的道路\((x',y')\),又会形成一个环。假如两个环不重叠,那么\(x',y'\)之间的路径上的边会少走一次.但如果环相交,那么相交的那些边会被减去两次,相当于没被巡逻到。因此我们要把第一次的路径\((x,y)\)上的边权设为\(-1\),这样减掉-1之后相当于加回来一次,就合法了。在新的树上求直径\(d_2\),答案为\(2(n-1)-d_1+1-d_2+1=2n-d_1-d_2\).

树的重心

定义:在树上删掉这个点后使得剩下连通块大小的最大值最小的点称为树的重心。

重心的性质

  1. 树中所有点到某个点的距离和中,到重心的距离和是最小的.
  2. 把两棵树通过一条边相连,新的树的重心在原来两棵树重心的连线上。
  3. 一棵树添加或者删除一个节点,树的重心最多只移动一条边的位置。
  4. 一棵树最多有两个重心,且这两个重心一定相邻。
  5. 删掉树的重心后,每个连通块的大小一定不超过原树大小的一半(这是保证点分治复杂度正确的基础)

求法

用树形DP,设\(f_x\)为去掉\(x\)后的连通块大小最大值,则\(f_x=\max(n-sz_x,\max_{y \in \operatorname{son}(x)}(sz_y)\),其中\(sz_x\)\(x\)的子树大小.这是因为去掉\(x\)后树会被分为\(x\)的儿子的子树和整棵树去掉\(x\)子树后的树

void dfs(int x,int fa){
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(y!=fa){
            dfs(y,x);
            sz[x]+=sz[y];
            f[x]=max(f[x],sz[y]);
        }
    }
    f[x]=max(f[x],n-sz[x]);
    if(f[x]<f[root]) root=x;
}

[AGC 018D]给出一个n个点的带边权的树T,再给出一个n个点的完全图,完全图中每两个点之间的距离为这两个点在树上的距离,求完全图最长的哈密顿路(\(n \leq 10^5\))
贪心考虑,对于原树中的一条边\((u,v,w)\),子树大小为\(sz\),不难发现它的贡献上界是\(\min(sz_u,n-sz_u)\cdot w\).并且存在一种方案使得每条边都取到上界。那就是把树分成大小为\(\lfloor \frac{n}{2} \rfloor\)的两半,然后在两半之间交替走。

但是重心只能访问一次。如果有1个重心,那就说明最后一步不能回到重心,找一条与重心相连的最小的边不走,把它从答案中减去。如果有2个重心,那么不走的一定是两个重心之间的边。

代码

LCA

定义:对于有根树上两点\(x,y\),它们公共的祖先节点中深度最深的节点称为它们的最近公共祖先(LCA),记为\(\operatorname{LCA}(x,y)\)

LCA的求法

显然暴力向上跳可以求,复杂度为\(O(树高)\),在一些保证树高为\(\log\)级别的树上(如并查集)跑的很快,因此可以和一些数据结构嵌套。

LCA的高效求法有很多种,且都依赖于树相关的其他一些知识点。

  1. 树上倍增求LCA,在线,复杂度\(O(n\log n)-O(\log n)\)(分别指预处理和查询的时间复杂度,下同)
  2. 树链剖分求LCA,在线,复杂度\(O(n)-O(\log n)\)
  3. ST表+欧拉序求LCA,在线,复杂度\(O(n\log n)-O(1)\),可以用毒瘤的奇技淫巧优化到\(O(n)-O(1)\)
  4. LCA的Tarjan算法,离线,复杂度\(O(n\alpha(n))-O(1)\),用处不大,这里不赘述。

在学习LCA的性质之前先掌握1.之后要熟练掌握1和2.

树上差分

树上两点距离公式:
\(d_x\)表示\(x\)到根距离,那么\(\operatorname{dist}(x,y)=d_x+d_y-2d_{\operatorname{LCA}(x,y)}\)
这是因为\(\operatorname{LCA}(x,y)\)向上到根的路径被算了两次。

[BZOJ 3307]Cow Politics
给出一棵N个点的树,树上每个节点都有颜色。对于每种颜色,求该颜色距离最远的两个点之间的距离。N≤200000
显然对于每种颜色建立一棵虚树是可行的。但是有编码复杂度更低的方法。显然某种颜色距离最远的两个点中,一个肯定是这种颜色的点中深度最深的(贪心考虑,如果还有更深的,那么选更深的一定更优)。那么我们只要找出每种颜色深度最深的点,然后向该种颜色的每一个点暴力求距离即可。

由于所有颜色的点的个数加起来为n,总时间复杂度\(O(n\log n)\)

代码&题解

那么对于多次路径修改,仅一次查询的问题,我们可以修改每个路径时,在\(x\)处和\(y\)处打一个添加标记。然后在\(\operatorname{LCA}(x,y)\)处打2个删除标记。(如果是对点修改,那就要在\(\operatorname{LCA}(x,y)\)\(\operatorname{fa}_{\operatorname{LCA}(x,y)}\)处各打1个删除标记,因为LCA这个点要被算一次).最后自底向上累加标记。

[Codeforces 191C]给出一棵树,再给出k条树上的简单路径,求每条边被不同的路径覆盖了多少次
在树上,每个节点初始权值为0,对于每条路径(x,y),我们令节点x的权值+1,节点y的权值-1,节点LCA(x,y)的权值-2。最后进行一次DFS,求出F[x]表示x为根的子树中各节点的权值之和,F[x]就是x与它的父节点之间的树边被覆盖的次数

代码&题解

[BZOJ3307]雨天的尾巴
给出一棵N个点的树,M次操作在链上加上某一种类别的物品,完成所有操作后,要求询问每个点上最多物品的类型。N, M≤100000
对于每条链(x,y),我们在x,y打一个+标记,lca(x,y)和lca(x,y)的父亲打一个-标记。然后在每个节点建立一棵权值线段树,下标v维护物品v的个数。如果有物品v,就把下标为v的位置+1,如果有-标记,就-1.线段树push_up的时候可以计算出最多物品的类型
然后从下往上线段树合并,合并到某个节点的时候就更新该节点的答案。

代码&题解

树上倍增

树上倍增可以用来\(O(n\log n)\)预处理,\(O(\log n)\)维护树上的路径信息.设\(anc_{i,j}\)表示节点\(i\)向上\(2^j\)深度走到的节点,\(f_{i,j}\)表示节点\(i\)向上\(2^j\)深度这条路径上,我们要维护的信息。
那么:
\(anc_{i,j}=anc_{anc_{i,j-1},j-1}\)

\(f_{i,j}=merge(f_{i,j-1},f_{anc_{i,j-1},j-1})\)

其中\(merge\)表示合并两个答案,比如求\(\max\)
这是因为向上\(2^j\)的路径可以拆成长度为\(2^{j-1}\)的两段,\(i \to anc_{i,j-1}\)\(anc_{i,j-1} \to anc_{i,j}\)

初始值\(anc_{x,0}=\operatorname{fa}_x\),\(f_{x,0}\)就代表\(x\)到父亲的边的权值,这样递推即可。复杂度\(O(n\log n)\).

树上倍增求LCA

先树上倍增维护出\(anc\)数组。考虑如何查询。我们从\(x\)\(y\)中先选一个较深的点,然后将两个点跳到同一深度。跳的过程类似二进制拆分,从\(\log_2n\)\(0\)枚举\(i\),尝试向上跳\(2^i\)步,如果跳的太多\(deep_{anc_{x,i}} < y\),就不跳,否则向上跳。此时有可能\(x,y\)重合,说明\(x,y\)是祖先后代关系,直接返回。否则一起从大到小往上跳,保证\(anc_{x,i}=anc_{y,i}\)

int lca(int x,int y){
    if(deep[x]<deep[y]) swap(x,y);
    for(int i=log2n;i>=0;i--){
        if(deep[anc[x][i]]>=deep[y]){
            x=anc[x][i];
        }
    }
    if(x==y) return x;
    for(int i=log2n;i>=0;i--){
        if(anc[x][i]!=anc[y][i]){
            x=anc[x][i];
            y=anc[y][i];
        }
    }
    return anc[x][0];
}

对于树上信息的查询,过程和LCA类似,比如维护树上边权和最大值

long long lca_query(int x,int y){
    if(deep[x]>deep[y]) swap(x,y);
    long long maxl=0;
    for(int i=log2n;i>=0;i--){//先将x和y调整到同一深度
        if(deep[fa[y][i]]>=deep[x]){
            maxl=max(maxl,mlen[y][i]);//y上升同时更新maxl
            y=fa[y][i];
        }
    }
    if(x==y) return maxl;//如果LCA(x,y)=x,直接返回
    for(int i=log2n;i>=0;i--){//x,y同时上升,直到差一条边相遇
        if(fa[x][i]!=fa[y][i]){
            maxl=max(maxl,max(mlen[x][i],mlen[y][i]));
            x=fa[x][i];
            y=fa[y][i];
        }
    }
    maxl=max(maxl,max(mlen[x][0],mlen[y][0]));//最后再更新一次
    return maxl;
}

[Codeforces 609E]
给定一个无向连通带权图G,对于每条边(u,v,w),求包含这条边的生成树大小的最小值
先求出整张图的最小生成树大小tlen,对于每一条边(u,v,w),我们最小生成树中去掉树上从u到v的路径上权值最大,最大值为mlen的一条边,再加上w,得到的一定是包含这条边的生成树大小的最小值tlen−mlen+w

树上倍增维护最大边权即可.
代码&题解

[HNOI2016]树
给出一棵n个点的模板树和大树,根为1,初始的时候大树和模板树相同。接下来操作m次,每次从模板树里取出一棵子树,把它作为新树里节点y的儿子。操作完之后有q个询问,询问新树上两点之间的距离\(n,m,q \leq 1 \times 10^5\)
显然直接把所有节点存下来是不行的,因为节点的个数最多可以到\(10^{10}\)。发现本质不同的子树只有n个,我们考虑把子树缩成一个点,构造一棵新树。

新树上的点有3种编号,注意区分:

​1.小节点的询问编号,即图中淡黑色数字(1~9),询问和加点的时候被输入,可能会爆int
​2.所在大节点编号,即图中加粗的黑色数字
​3.在模板树上对应的点的编号,即图中绿色数字

定义两个大节点之间的边权为大节点对应子树的树根和被挂上的节点在模板树上的距离+1。每一个大节点需要存储:

​1. 子树内小节点询问编号的范围idl[x],idr[x]
​2. 根节点在模板树上对应的点的编号from[x]
3. 这棵子树接到的节点的询问编号link[x]

然后写两个函数

  1. get_root(x) 找到询问编号为x的节点所在大节点的编号。只需要二分答案,找到满足idl[k]<=x的最小k即可
  2. get_tpid(x) 找到询问编号为x的节点在模板树上的编号。由于新加入大树的结点是按照在模板树中编号的顺序重新编号,那么他们的大小顺序不变。我们先找到x所在大节点rt,再找到rt在模板树上对应的点的编号from[rt],显然答案是from[rt]的子树中第x-idl[rt]+1小的节点编号。只需要在模板树上按照dfs序建出主席树,维护编号的出现情况即可。

然后在大节点构成的树上进行树上倍增,维护lca和x往上走2^i步的边权和。

准备工作已经做完,我们来考虑如何求(x,y)距离

如图,我们先加上模板树上x到rtx的距离,然后在新树上像求lca一样往上跳,同时累计距离。直到x,y的父亲相同。

但是这里跳到最后一步的时候会出问题。如图,我们求9到5的距离,从rty直接跳到rty的父亲1会出问题,因为这样并不是最短路径,应该从rty跳到link[rty]才对,所以最后一步特判一下即可,x,y最后一步的距离为1+1+模板树上link[x]到link[y]的距离,其中1表示从a跳到link[a]的距离为1

代码&题解

树的遍历

前序,中序,后序遍历在这里不再赘述.现在介绍两种把树上问题转化为序列问题维护的方法。一般来说,用DFS序维护子树,欧拉序维护路径

DFS序

从根节点开始DFS,第一次访问某节点的时候给它标号,每个节点的标号就是它的DFS序.代表DFS访问的顺序.

任意节点子树内的DFS序是连续的
也就是说,把节点按DFS序排序后,\(x\)的子树是一个连续区间,左端点为\(x\)的DFS序,右端点是\(x\)子树内DFS序的最大值。那么我们就可以把子树问题转化为区间问题,进而用线段树等数据结构来维护。

int tim=0;
void dfs(int x,int fa){
    dfn[x]=++tim;//x的dfs序
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(y!=fa) dfs(y,x);
    }
    dfnr[x]=tim;//记录子树内的最大DFS序
}

[SDOI2015]寻宝游戏
小B最近正在玩一个寻宝游戏,这个游戏的地图中有N个村庄和N-1条道路,并且任何两个村庄之间有且仅有一条路径可达。游戏开始时,玩家可以任意选择一个村庄,瞬间转移到这个村庄,然后可以任意在地图的道路上行走,若走到某个村庄中有宝物,则视为找到该村庄内的宝物,直到找到所有宝物并返回到最初转移到的村庄为止。小B希望评测一下这个游戏的难度,因此他需要知道玩家找到所有宝物需要行走的最短路程。但是这个游戏中宝物经常变化,有时某个村庄中会突然出现宝物,有时某个村庄内的宝物会突然消失,因此小B需要不断地更新数据,但是小B太懒了,不愿意自己计算,因此他向你求助。为了简化问题,我们认为最开始时所有村庄内均没有宝物
本质上是在一棵树上取出若干节点,询问把这几个节点访问一遍的距离。可以发现如果我们按照dfs序将节点排序,然后将排序后的相邻节点距离相加,最后再加上序列首尾距离,就能求出答案,如序列为{1,3,4,5},则答案为dist(1,3)+dist(3,4)+dist(4,5)+dist(5,1)。因为我们访问节点一定是像dfs一样访问才能得到最短路径,所以正确性显然

我们用一个set维护这个排序后的节点序列,可以发现,每次新加入一个节点之后只会改变节点的前驱和后继相关的距离,更新一下即可。这个问题也叫做树链的并,是DFS序的经典应用。

代码&题解

[51nod 1681]公共祖先
给出两棵n(n<=100000)个点的树,对于所有点对求它们在两棵树中公共的公共祖先数量之和。
如图,对于点对(2,4),它们在第一棵树里的公共祖先为{1,3,5},在第二棵树里的公共祖先为{1},因此公共的公共祖先数量为2
把所有点对的这个数量加起来,就得到了最终答案

\(O(n^3)\)的暴力不讲了,先考虑\(O(n^2)\)的做法

枚举点对复杂度太高,不可行。我们考虑每个节点x作为公共的公共祖先的次数。设树A上的节点x,在树B上对应的节点是x'(实际上x'和x的编号是相同的,只是这样方便描述).则如果点对既在x的子树中,对应到B上后又在x'的子树中,则这个点对的公共的公共祖先就包含x .注意一个小细节,如果x是y的父亲,x不算做x和y的祖先,所以这里的“子树”应该不包含x.

如这张图中,A中1的子树中节点有{2,3,4,5},{2,3,4,5}对应到B中均在1的子树内。这4个节点中任选一对,它们的公共祖先都包含1

那么我们只要考虑x的子树中有多少个点对应过去在树B上x'的子树中即可。暴力枚举x子树中的每个节点,然后判断。设这样的点个数为cnt,则x作为公共的公共祖先的次数就是\(C_{cnt}^2\),把它累加进答案

那么我们怎么把它优化呢?我们发现,节点编号是离散的,不好判断。但子树中节点的dfs序是连续的。我们把A中节点x的dfs序标记到树B上对应的位置x‘。然后我们遍历树A的每个节点x,它子树的dfs序范围为[l[x]+1,r[x]] (不包含x)。那么问题就变成在树B上编号为x的节点的子树中有多少个节点的标记落在[l[x]+1,r[x]]的范围内

如图,我们想求A中3的子树中有多少个节点对应到B中也在3的子树里,l[3]=2,r[3]=5,B中3的子树中的dfs序有{2,4},落在[2+1,5]的范围内的只有4,所以有1个节点

这是线段树合并的经典问题。用权值线段树合并就可以了,节点x的线段树的节点\([l,r]\) 存储有x的子树中多少个值落在\([l,r]\)内。(有些题解用了可持久化线段树,其实没有必要)。我们遍历的时候从下往上合并,合并到节点x的时候就更新x的cnt值。时间复杂度\(O(n\log n)\)

代码&题解

欧拉序

不同于DFS序.对有根树进行深度优先遍历,无论是递归还是回溯,每次到达一个节点就把编号记录下来,得到一个长度为\(2n−1\)的序列,称为树的欧拉序列。

ST表+欧拉序求LCA

定理:记\(st_x\)表示\(x\)在欧拉序中第一次出现的位置,\(x\)\(y\)的LCA是欧拉序在区间\([st_x,st_y]\)的节点中深度最小的节点。
证明:
如果\(x\)\(y\)是祖先后代关系,显然成立。否则要从\(x\)走到\(y\),要先DFS\(x\)的子树,再回溯到\(lca(x,y)\)处,继续DFS包含\(y\)的子树。

那么就可以查询静态区间最小值,用ST表可以做到\(O(n\log n)-O(1)\),用\(\plusmn1\)RMQ可以做到\(O(n)-O(1)\),但代码极其毒瘤。

int seq[maxn*2+5];//按欧拉序排序后的节点序列
int first[maxn+5];//即st
int deep[maxn*2+5];//深度
void dfs(int x,int fa,int d) {
    seq[++cnt]=x;
    deep[cnt]=d;
    first[x]=cnt;
    for(int i=head[x]; i; i=E[i].next) {
        int y=E[i].to;
        if(y!=fa) {
            dis[y]=dis[x]+E[i].len;
            dfs(y,x,d+1);
            seq[++cnt]=x;
            deep[cnt]=d;
        }
    }
}
struct ST {//ST表求lca
    int log2[maxn*2+5];
    int f[maxn*2+5][maxlogn+5];//维护区间深度最小的节点的位置
    void ini(int n) {
        log2[0]=-1;
        for(int i=1; i<=n; i++) log2[i]=log2[i>>1]+1;
        for(int i=1; i<=n; i++) f[i][0]=i;
        for(int j=1; (1<<j)<=n; j++) {
            for(int i=1; i+(1<<j)-1<=n; i++) {
                int x=f[i][j-1];
                int y=f[i+(1<<(j-1))][j-1];
                if(deep[x]<deep[y]) f[i][j]=x;
                else f[i][j]=y;
            }
        }
    }
    int query(int l,int r) {//返回位置
        int k=log2[r-l+1];
        int x=f[l][k];
        int y=f[r-(1<<k)+1][k];
        if(deep[x]<deep[y]) return x;
        else return y;
    }
} S;
inline int lca(int x,int y) {
    x=first[x];
    y=first[y];
    if(x>y) swap(x,y);
    return seq[S.query(x,y)];//返回最小值位置,再找到那个位置的编号
}

[51nod 1766]树上的最远点对
给出一棵N个点的树,Q次询问一点编号在区间[l1,r1]内,另一点编号在区间[l2,r2]内的所有点对距离最大值。\(N, Q≤100000\)
区间\([l,r]\)存储编号在\([l,r]\)内的点组成的一棵树的直径端点和长度

考虑如何合并区间。,设两个区间的直径分别为(a,b),(c,d),则新区间的直径端点肯定也是a,b,c,d中的一个。(运用直径的性质3,4,那么新区间的直径就是\(\max(\operatorname{dist}(a,b),\operatorname{dist}(a,c),\operatorname{dist}(a,d),\operatorname{dist}(b,c),\operatorname{dist}(b,d),\operatorname{dist}(c,d))\)

那么直接线段树维护就行了,pushup的时候按上面的那样合并。最后查询得到[l1,r1]内的直径(a,b),[l2,r2]内的直径(c,d) ,答案就是\(\max(\operatorname{dist}(a,c),\operatorname{dist}(b,d),\operatorname{dist}(b,c),\operatorname{dist}(a,d))\)

如果用树上倍增求lca,时间复杂度为\(O(n\log^2n)\),改用欧拉序+ST表求lca,查询只需要在ST表中求最值,是O(1)的,时间复杂度\(O(n\log n)\)

代码&题解

树链剖分

树链剖分指的是把树上的点划分成一些链,然后用数据结构维护每条链。链上的边称作重边,连接链之间的边称为轻边。由多条重边连接而成的路径称为重链,由多条轻边连接而成的路径称为轻链。比如按子树大小的轻重链剖分和长链剖分,还有LCT的实链剖分。(不同剖分方法对两种边的叫法不同,但本质相同)

轻重链剖分

对于每个点,我们把子树大小\(sz\)最大的儿子称为重儿子,记作\(son_x\)(为了避免混淆,所有的儿子集合记作\(child_x\)). 那么我们把每个点和它的重儿子连成一条链,树就被剖分成了很多链。记\(top_x\)表示\(x\)所在重链的顶端节点。

另外为了能用数据结构维护重链,我们优先DFS重节点,这样重链的DFS序就是连续的一段,可以快速维护。比如\(x\)上方的重链的DFS序范围就是\([dfn_{top_x},dfn_x]\)

int dfn[maxn+5];//点的DFS序
int fa[maxn+5];//点的父亲
int son[maxn+5];//重儿子
int sz[maxn+5];//子树大小
int deep[maxn+5];//深度
int top[maxn+5];//重链的顶端节点
void dfs1(int x,int f){
    fa[x]=f;
    sz[x]=1;
    deep[x]=deep[f]+1;
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(y!=f){
            dfs1(y,x);
            sz[x]+=sz[y];
            if(sz[son[x]]<sz[y]) son[x]=y;//找到重儿子
        }
    }
}
int tim=0;
void dfs2(int x,int t){
    top[x]=t;
    dfn[x]=++tim;
    if(son[x]) dfs2(son[x],t);//优先DFS重儿子
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(y!=fa[x]&&y!=son[x]){//对于轻儿子,以它为起点开始一条重链
            dfs2(y,y);
        }
    }
}

轻重链剖分有如下性质:

1.对于轻边\((x,y)(deep_x<deep_y)\),有\(sz_x>2sz_y\).
因为\(y\)是轻儿子,所以必定存在另一个儿子的子树大小\(>sz_y\),

2.任意节点\(x\)到根的路径上轻边个数为\(O(\log n)\)
根据性质1,每次往上跳一条轻边,子树大小会\(\times 2\),所以至多\(\log_2 n\)条。
换句话说,经过的重链最多\(O(\log n)\)

树链剖分求LCA

我们先把\(x\),\(y\)中深度大的跳到重链顶端\(top_x\),再跳到下一条重链(即\(fa_{top_x}\)).这样一直进行这个过程,直到\(x,y\)跳到同一条重链。此时因为在同一条重链上,深度浅的就是答案。

int lca(int x,int y){
    while(top[x]!=top[y]){
        if(deep[top[x]]>deep[top[y]]) x=fa[top[x]];
        else y=fa[top[y]];
    }
    return deep[x]<deep[y]?x:y;
}

维护路径信息

对于一条路径\((x,y)\),我们也像求LCA那样,轮流向上跳。这样就把问题转化成了\(2\log_2n\)个的区间修改和区间查询。复杂度\(O(\log n)\)

void update(int x,int y,int v){
    while(top[x]!=top[y]){
        if(deep[top[x]]<deep[top[y]]) swap(x,y);
        T.update(dfn[top[x]],dfn[x],v);
        x=fa[top[x]];
    }
    if(deep[x]>deep[y]) swap(x,y);
    T.update(dfn[x],dfn[y],v);
}
ll query(int x,int y){
    ll ans=0;
    while(top[x]!=top[y]){
        if(deep[top[x]]<deep[top[y]]) swap(x,y);
        ans+=T.query(dfn[top[x]],dfn[x]);
        x=fa[top[x]];
    }
    if(deep[x]>deep[y]) swap(x,y);
    ans+=T.query(dfn[x],dfn[y]);
    return ans;
}

[LNOI2019]LCA
给出一棵N个点的树,要求支持Q次询问,每次询问一个点z与编号为区间\([l,r]\)内的点分别求最近公共祖先得到的最近公共祖先深度和。N, Q≤50000
对于一个点i,我们把i到根节点的路径全部标记+1,然后从z往上找,第一个碰到的标记不为0的节点就是\(\operatorname{LCA}(z,i)\)。而i的深度恰好就是z到根节点路径上的标记和。显然这样的标记是可以叠加的,对于区间\([l,r]\),我们把编号在\([l,r]\)内的节点到根的路径都标记+1,那么答案就在z到根路径上的标记和。

但是这样直接做还是\(O(n^2)\)的,考虑离线。注意到标记是可减的,那么询问\(query(l,r,z)\)就相当于\(query(1,r,z)-query(1,l-1,z)\)

那么我们分两部分维护答案,记\(query(1,r,z)=ansr,query(1,l-1,z)=ansl\),真正的答案就是\(ansr-ansl\).我们对于每个点,保存左端点l-1在此的询问编号,右端点同理。我们从1~n遍历每个节点i,把i到根的路径标记+1。然后看看有没有左端点在i的询问,如果有,就更新ansl,右端点同理
代码&题解

此题还有一个加强版

[SPOJ2666]Query on a tree IV & [ZJOI2007]捉迷藏

给定一棵包含 N 个结点的树,带边权且边权可能为负。每个节点要么是黑色(亮灯),要么是白色(不亮灯)。初始时每个节点都是白色。
要求模拟两种操作:(1)改变某个结点的颜色。(2)询问最远的两个白色结点之间的距离。
\(N \leq 10^5\)
首先对树进行轻重链剖分。对于每个节点,记\(d_1(x)\)\(d_2(x)\)分别表示该节点到子树中的白色节点的最长距离和次长距离,且两条路径仅在根节点处相交.如果不存在,则记为\(- \infin\)

对于每条链上的节点,我们要维护以下三个变量:

  1. \(lmax\): \(x\)所在重链的最浅节点到\(x\)子树中最远白点的距离
  2. \(rmax\): \(x\)所重链的最深节点到\(x\)子树中最远白点的距离
  3. \(mlen\):与\(x\)所在重链相交的,\(x\)子树中两个白点中间的路径的最长长度.

因为重链上节点的dfs序是连续的,那么重链对应一个区间\([l,r]\),记\(id_{l}\)为dfs序为\(l\)的节点编号,最浅的节点为\(id_l\),最深的节点为\(id_r\)。因此我们可以对每条重链开一棵线段树来维护这几个变量。
\(dist(i,j)\)\(i,j\)间距离,\(p\)为区间\([l,r]\)对应的线段树节点,\(lp,rp\)\(p\)的左右儿子。\(mid=\frac{l+r}{2}\),那么有:

\[lmax(p)=\max(lmax(lp),dist(id_{l},id_{mid+1})+lmax(rp)) \]

第二项就是把rp对应的一个前缀接到链\([l,mid]\)

\[rmax(p)=\max(rmax(rp),rmax(lp)+dist(id_{mid},id_r) \]

\[mlen(p)=\max(mlen(lp),mlen(rp),rmax(lp)+dist(id_{mid},id_{mid+1})+lmax(rp)) \]

由于是一条链,\(dist\)可以\(O(1)\)算出。直接在线段树里 push_up即可.

void push_up(int x) {
    int l=tree[x].l,r=tree[x].r,mid=(l+r)>>1;
    tree[x].lmax=max(tree[lson(x)].lmax,dist[hash_dfn[mid+1]]-dist[hash_dfn[l]]+tree[rson(x)].lmax);//注意线段树是按dfs序存的
    tree[x].rmax=max(tree[rson(x)].rmax,dist[hash_dfn[r]]-dist[hash_dfn[mid]]+tree[lson(x)].rmax);
    tree[x].mlen=max(max(tree[lson(x)].mlen,tree[rson(x)].mlen),
                        tree[lson(x)].rmax+dist[hash_dfn[mid+1]]-dist[hash_dfn[mid]]+tree[rson(x)].lmax);
}

叶子节点的初始值可以这样设置
\(id_p\)是黑点,有:
\(lmax(p)=rmax(p)=d_1(id_p)\)
\(mlen(p)=d_1(id_p)+d_2(id_p)\) 因为\(d_1,d_2\)保证了交点只有一个,它们可以接起来

\(id_p\)是白点,有
\(lmax(p)=rmax(p)=\max(d_1(id_p),0)\) (把自己作为路径结尾,所以和0取max)
\(mlen(p)=\max(d_1(id_p)+d_2(id_p),d_1(id_p),0)\)
和 (可以把自己作为路径结尾,也可以两条路接在一起)

\(d_1\)\(d_2\)可以用一个支持插入和删除任意元素的大根堆维护,可以用STL中的multiset实现.每个节点开一个这样的数据结构\(h[x]\),存储可能的路径长度。 初始化的时候只需遍历\(x\)的轻儿子\(y\),用下面一层的重链更新上面的答案,插入\(y\)\(lmax+dist(x,y)\)即可。因此建树的时候一定要从深到浅建。

for(int i=head[x]; i; i=E[i].next) {
    int y=E[i].to;
    if(y!=fa[x]&&y!=son[x]){
        h[x].insert(tree[root[top[y]]].lmax+E[i].len);
        //累加下面一层重链的答案
    }
}

处理查询:
类似\(d_1\)\(d_2\),我们维护一个全局的multisetans存储每条重链的答案(链顶lmax)。查询的时候输出最大值

处理修改:
修改是最复杂的部分。我们沿着\(x\)往上跳,修改每一条重链。

  1. 要删除当前重链对上方重链的影响,对于链顶节点父亲,我们在\(h\)中删去当前链顶的lmax+dist.
  2. 修改线段树。如果是在被修改节点的重链上,就找到该节点,否则找到重链的最深节点。由于下面的重链已经修改完,我们可以用下面重链更新当前的答案。所以我们要在堆里插入新的\(lmax+dist\).然后求出新的\(d_1,d_2\)来更新\(lmax,rmax,mlen\).接着上推即可。
  3. \(ans\)里删除旧的答案(链顶lmax),插入新的答案

实际上,这个过程和动态DP的修改是类似的。
代码&题解

DSU on Tree

DSU on Tree(它和并查集(DisjointSetUnion,DSU)没有关系))它能求解的问题一般是:没有修改,对于树上的每个点,求它的子树内有多少满足某种性质的节点。(也可以离线处理询问)
一般来说,统计这些节点的暴力过程会用到数据结构(比如求值为v的节点个数需要一个桶),且每次统计完后要清空该数据结构。

考虑暴力,我们可以对每个子树暴力统计这些点的个数,统计完后清空。复杂度\(O(n^2)\).但是之前数过的个数可以被其他点所用,比如求它的父亲\(fa_x\)的答案时,如果\(x\)的答案没有删除,就不需要重新统计\(x\)的子树里的节点。但是如果每个节点都不清空影响,又会造成重复统计。DSU on Tree算法利用轻重链剖分的性质在这两者之间取得了平衡.

对于节点\(x\),我们执行以下步骤
\(solve(x)\):

  1. 对于轻儿子\(y\),递归下去\(solve(y)\),并删除其影响
  2. 对于重儿子,递归下去处理\(solve(son_x)\)
  3. 统计轻儿子对当前点\(x\)答案的影响
  4. 更新\(x\)的答案
  5. 删除第3步中轻儿子对\(x\)答案的影响

(伪)代码

void calc(int x,int type){//type=1加入,type=-1擦除
    //判断x是否满足该性质
    for(int y : E[x]){
        if(vis[y]||y==fa[x]) continue;//算过的就不用继续
        calc(y,type);//递归下去计算轻儿子
    }
}
void dfs2(int x,int is_heavy){
    for(int y : E[x]){//1.递归求轻儿子的答案
        if(y!=fa[x]&&y!=son[x]){
            dfs2(y,0);
        }
    }
    if(son[x]){//2.递归求重儿子的答案
        dfs2(son[x],1);
        vis[son[x]]=1;//通过标记vis,使得重儿子的影响不会被擦除
    }
    calc(x,1);//3.统计轻儿子对x答案的影响
    vis[son[x]]=0;//这是因为若x不是父亲的重儿子,它的所有儿子都要擦除,所以把vis设成0
    ans[x]=//4.更新x的答案
    if(!is_heavy) calc(x,-1);//5.如果x不是父亲的重儿子,就擦除影响
}

正确性证明:第一次递归下去(1,2步)求子树中其他点的答案,并且重儿子对答案的影响被保留。第二次递归(第3步)是统计轻儿子是求当前点的答案。,未被擦除的重儿子和现在统计的所有轻儿子加在一起,就构成了当前点的答案。因此能做到不重不漏。

复杂度证明:一个节点被统计的的次数等于他到根节点路径上的轻边数+1. 这是因为只有在每个轻边的上端会执行第3步统计轻儿子,统计到该节点。+1是我们的solve过程会遍历每个点一次求答案。又因为我们在轻重链剖分中证明了任意节点到根的路径上轻边个数为\(O(\log n)\), 因此总复杂度为\(O(n\log n)\).

[Codeforces600E]一棵树有n个结点,每个结点都是一种颜色,每个颜色有一个编号,求树中每个子树的最多的颜色(可能有多个)编号的和。\(n \leq 10^5\)
考虑如何统计答案,即calc函数如何写。开一个桶存储每个颜色出现的次数。再开一个桶维护每个出现次数对应的和,同时记录top表示最大出现次数。如果在插入和删除某个颜色之后,某个出现次数的和变成了0,就减少top。如果新增了某个出现次数,就增加top.

void calc(int x,int type){//type=1加入,type=-1擦除 
    sum[cnt[a[x]]]-=a[x];
    cnt[a[x]]+=type;
    sum[cnt[a[x]]]+=a[x];
    if(sum[top+1]) top++;
    else if(top>0&&sum[top]==0) top--;
    //答案就是sum[top]
    for(int y : E[x]){
        if(vis[y]||y==fa[x]) continue;
        calc(y,type);
    }
}

然后套上面DSU on Tree的板子即可。

代码

[Codeforces 208E] Blood Cousins
给出一个有根树森林,点集总大小为\(n\)。有\(m\)个询问,每个询问包含两个数\(v_i,p_i\),询问\(v_i\)\(p_i\)级祖先的子树内有多少和\(v_i\)深度相同
可以对每个深度维护一个按DFS序排的节点序列,然后二分查找出对应的区间。考虑DSU on Tree如何处理

先把询问离线。相同深度点的个数转化成:询问x子树内深度为k的个数-1.直接套dsu on tree板子,用桶来统计深度。k级祖先用倍增求。

代码&题解

长链剖分

长链剖分可以把维护子树中只与深度有关的信息。对于每个点,我们找子树内最大深度最大的儿子作为重儿子。那么我们就把树剖分成了"长链",意义和重链类似。

int maxl[maxn+5];//子树内最大深度
int son[maxn+5];
void dfs1(int x,int fa){
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(y!=fa){
            dfs1(y,x);
            if(maxl[y]>maxl[son[x]]) son[x]=y;
        }
    }
    maxl[x]=maxl[son[x]]+1;
}

长链剖分有以下性质:

1.所有链长之和为\(O(n)\)
所有点仅会被恰好一条长链包含。

2.任意一个点\(x\)\(k\)次祖先\(y\)所在长链长度\(\geq k\)
\(x\)\(y\)的路径被长链包含,那么它的长度大于路径长度\(k\)
若有轻链,那么长链的深度肯定\(\geq k\),否则以这条链为长链更优

3.任意一个点向上跳重链的次数不会超过\(O(\sqrt n)\)
每次从重链跳到另外一条重链,新的重链长度至少加1.又因为所有链长之和是\(O(n)\),那么在最坏情况下,重链长度分别为\(1,2,3 \dots \sqrt{n}\). 因此用长链剖分来解决LCA和树上路径查询问题,复杂度不是很优秀。

虽然不能高效处理路径问题,长链剖分可以优化以深度为下标的树形DP。这是因为每个点子树里的深度范围不是\([1,n]\)而是\([1,maxl_x]\),再根据长链剖分的性质就可以保证复杂度。思路和DSU on Tree类似,每次先继承重儿子的信息,然后再加上轻儿子。因为每个点仅属于一条长链,且一条长链只会在链顶位置作为轻儿子暴力合并一次,所以时间复杂度\(O(n)\)。在\(O(1)\)继承重儿子信息这点上有不同的实现方式,一个巧妙的方法是利用指针实现.

[Codeforces 1009F]
给出一棵树,设\(f_{i,j}\)表示i的子树中距离点i距离为j的点的个数,现在对于每个点i要求出使得\(f_{i,j}\) 取得最大值的那个j。
显然能写出DP,\(dp_{x,j}\)表示距离为\(j\)的点的个数,则\(dp_{x,j}=\sum_{y \in child(x)} f_{y,j-1}\). 再扫一遍DP数组就能得出最大\(j\). 链上信息用指针实现\(O(1)\)转移,链与链之间直接暴力合并。

int ini[maxn+5];
int *id=ini;//把长度n的内存初始化为0,然后分给各个重链,因为重儿子暴力继承了之后只有轻儿子位置需要新增DP数组,答案为sum(maxl[x])
int *dp[maxn+5];
int ans[maxn+5];
void dfs2(int x,int fa){
    dp[x][0]=1;
    if(son[x]){
        dp[son[x]]=dp[x]+1;//通过指针,让dp[son[x]][j]直接和dp[x][j+1]同步(因为是儿子,距离少了1),这样就不用复制
        dfs2(son[x],x);
        ans[x]=ans[son[x]]+1;
    }
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(y!=fa&&y!=son[x]){
            dp[y]=id;
            id+=maxl[y];//给y分配maxl的空间,因为重链长度之和为O(n),所以正好在ini的范围里
            dfs2(y,x);
            for(int j=1;j<=maxl[y];j++){
                dp[x][j]+=dp[y][j-1];
                if(dp[x][j]>dp[x][ans[x]]) ans[x]=j;
                else if(dp[x][j]==dp[x][ans[x]]&&j<ans[x]) ans[x]=j;
            }
        }
    }
    if(dp[x][ans[x]]==1) ans[x]=0;
}

代码

虚树

虚树可以解决一类特殊的树形DP问题。每次询问给出\(n\)个点的树上的一个点集\(S\),且保证\(\sum |S| \leq n\),求这些点组成的树上的一些信息。如果把\(S\)中任意两点之间的所有点建出来,然后跑树形DP,每次询问的复杂度可以达到\(O(n)\).

虚树算法可以在\(O(|S|\log |S|)\)的时间复杂度下构建出一棵树,树上的点只包含询问点和询问点的LCA.这样相当于对原树进行了压缩,压缩后的树就称为虚树,虚树的规模不会超过\(2|S|\).

虚树的构造算法是一个增量算法。我们先把所有点按DFS序排序,然后依次插入。插入的时候需要用到一个栈\(s\),栈中存储上一次插入的点到根节点的链上的虚树节点(这些虚树节点之间的边还没被构建),按深度排序,栈顶深度最大。我们考虑当前点\(x\)和栈中链的关系:

  1. \(\operatorname{LCA}(s_{top},x)=s_{top}\),说明\(x\)会接到这条链下方,此时向栈里加入\(x\),这次插入结束
  2. \(\operatorname{LCA}(s_{top},x) \neq s_{top}\),说明\(x\)不在原来链上,出现了一条支链。而我们按DFS序排序,就说明不会有节点再接到栈中\(\operatorname{LCA}(s_{top},x) \to s_{top}\)这条链下方。于是将满足\(deep_{s_{top-1}} \geq deep_{\operatorname{LCA}(s_{top},x)}\)的栈中节点两两弹出,加边\((s_{top-1},s_{top})\).弹出完毕后原树又恢复成了一条链,加边\((s_{top},\operatorname{LCA}(s_{top},x)\)(若恰好等于新的\(s_{top}\),就不用加). \(s_{top}\)已经加完了边,我们可以直接将栈顶设为LCA,然后加入\(x\)

最后栈中还剩一条链没加,再按顺序给这些点加边

void insert(int x){
    if(top<=1){//1或2个点的特判
        s[++top]=x;
        return;
    }
    int lc=lca(x,s[top]);
    if(lc==s[top]){//情况1
        s[++top]=x;
        return;
    }
    //情况2
    while(top>1&&deep[s[top-1]]>=deep[lc]){
        T2.add_edge(s[top-1],s[top]);
        top--;
    }
    if(s[top]!=lc) T2.add_edge(lc,s[top]);
    s[top]=lc;//把栈顶
    s[++top]=x;
}
int cmp(int x,int y){
    return dfn[x]<dfn[y];
}
void solve(int *in,int k){
    sort(in+1,in+1+k,cmp);//按dfs序排序
    int root=lca(in[1],in[2]);//找出虚树的根
    for(int i=3;i<=k;i++) root=lca(root,in[i]);
    top=0;
    s[++top]=root;
    for(int i=1;i<=k;i++){//依次插入
        if(in[i]!=root)insert(in[i]);
    }
    while(top>1){//对于栈中剩下的点加边
        T2.add_edge(s[top-1],s[top]);
        top--;
    }
}

[BZOJ3611]大工程
国家有一个大工程,要给一个非常大的交通网络里建一些新的通道。我们这个国家位置非常特殊,可以看成是一个单位边权的树,城市位于顶点上。在 2 个国家 a,b 之间建一条新通道需要的代价为树上 a,b 的最短路径。现在国家有很多个计划,每个计划都是这样,我们选中了 k 个点,然后在它们两两之间 新建 \(\rm{C}_{k}^2\)条 新通道。现在对于每个计划,我们想知道:
1.这些新通道的代价和
2.这些新通道中代价最小的是多少
3.这些新通道中代价最大的是多少
显然需要建虚树,我们考虑如何在虚树上DP(注意:以下定义的点和子树都是虚树上的)

\(sz_x\)表示x的子树内有多少个询问点,\(sum_x\)表示x子树内的路径长度和,\(dmin_x\)表示x子树内到x的最小路径长度,\(dmax_x\)表示 x子树内到x的最大路径长度。对于x的子节点y,我们可以写出如下的状态转移方程
\(sum_x=\sum_{y \in child(x)} sum_y+(k-sz_y) · sz_y ·dist(x,y)\)
\(dmin_x=\min_{y \in child(x)}(dmin_x,dmin_y+dist(x,y))\)
\(dmax_x=\max_{y \in child(x)}(dmax_x,dmax_y+dist(x,y))\)

其中\(dist(x,y)\)表示原树上\(x\)\(y\)的距离。合并的时候用\(dmin_x+dmin_y+dist(x,y)\)去更新最小值,其余同理。
代码&题解

树分治

点分治

树链剖分主要处理书上单条路径的查询,而点分治可以用来统计树上满足一定条件的所有简单路径数量。

既然是分治,我们每次选择一个点,计算出经过这个点的所有路径。然后递归下去处理去掉这个点之后的所有子树。如果我们每一次都选择当前子树的重心,递归下去的子树大小一定不超过原来的一半。因此递归层数不会超过\(O(\log n)\).

再考虑如何计算出经过这个点的所有路径。我们先对\(x\)的子树\(y\)DFS,统计到\(y\)的路径,然后类似树形DP合并两棵子树,将两个子树接起来得到经过\(x\)的路径.在合并时要注意去掉路径两端在同一个子树内的情况。如果合并的复杂度为\(O(sz_y)\),结合递归层数,总复杂度\(O(n\log n)\).如果合并过程需要用到二分查找等,总复杂度\(O(n\log^2n)\). 如果合并过程的复杂度和

每题的计算方式都有所不同,需要具体问题具体分析。

void solve(int x){
    vis[x]=1;
    calc(x,0,1);//计算以x为根的答案
    for(int i=head[x];i;i=E[i].next){
        int y=E[i].to;
        if(!vis[y]){
            calc(y,1,-1);//容斥,减去一条边经过两次的答案
            root=0;
            tot_sz=sz[y];
            get_root(y,0);
            solve(root);
        }
    }
}

[BZOJ3697]采药人的路径
采药人的药田是一个树状结构,每条路径上都种植着同种药材。采药人以自己对药材独到的见解,对每种药材进行了分类。大致分为两类,一种是阴性的,一种是阳性的。采药人每天都要进行采药活动。他选择的路径是很有讲究的,他认为阴阳平衡是很重要的,所以他走的一定是两种药材数目相等的路径。采药工作是很辛苦的,所以他希望他选出的路径中有一个可以作为休息站的节点(不包括起点和终点),满足起点到休息站和休息站到终点的路径也是阴阳平衡的。他想知道他一共可以选择多少种不同的路径
设阴性和阳性的边权分别为1和-1,那么平衡就是路径长度为0

考虑点分治的时候dfs每一颗分治子树,用\(mark_i\)来标记当前节点到分治中心的路径上距离的出现情况,如果\(mark_{deep_x}=1\),就说明\(x\)与某个祖先到中心出现过一个相同的距离,也就是说这一段上一定存在休息点。

然后考虑合并分治中心出来的若干子树。设\(g_{0/1,i}\)表示当前子树中子树中距离为i的路径数目,0/1表示是否有休息点。合并的时候类似树形dp,为了防止一个子树内重复合并,要再定义一个数组\(f_{0/1,i}\)表示当前已经合并过子树中距离为i的路径数目.每新来一个子树,先dfs一遍算出g,再把g和f合并。
每棵子树对答案的贡献是

\[\begin{aligned}&g_{0,0} \times (f_{0,0}-1) \text{(两条路径都平衡,分治中心是休息站,-1是去掉路径经过点数为0的情况)} \\ &+ \sum_{i}g_{1,-i} \times f_{1,i}+ g_{0,-i}\times f_{1,i}+ g_{1,-i}\times f_{0,i} \text{(两条路径和为0,平衡,且至少一条路径上存在休息站)}\end{aligned} \]

实现上要注意负下标的问题,可以通过重载[]运算符来避免

代码&题解

[BZOJ3451]Normal
给你一棵 n个点的树,对这棵树进行随机点分治,每次随机一个点作为分治中心。定义消耗时间为每层分治的子树大小之和,求消耗时间的期望。
此题考察了对点分治的理解。根据期望的线性性,答案是\(\sum_{i=1}^n(i的期望子树大小)=\sum_{i=1}^n \sum_{j=1}^n [j在i的点分治子树内]\)

考虑j在i的点分治子树内的条件,显然i到j的路径上的所有点中,i是第一个被选择为分治中心的。否则如果选的点不是i,那么i和j会被分到两棵子树中。第一个被选择的的概率是\(\frac{1}{dist(i,j)+1}\)(\(dist(i,j)\)表示i到j的距离)。那么上式就可以写成\(\sum_{i=1}^n \sum_{j=1}^n \frac{1}{dist(i,j)+1}\)

转换一下,设\(cnt[d]\)表示\(dist(i,j)=d\)\((i,j)\)个数,那么答案为\(\sum_{d=0}^{n-1} \frac{cnt[d]}{d+1}\)。考虑如何求\(cnt[k]\)

我们在点分治的过程中,dfs出深度为i的节点个数cd[i]。那么求经过根节点的答案的时候就是\(cnt[i]=\sum_{j=0}^i cd[j]cd[i-j]\).容易看出这是一个卷积的形式,直接用cd和自身FFT求卷积即可。注意最后要像一般的点分治一样容斥一下.

时间复杂度满足递推式\(T(n)=2T(\frac{n}{2})+\frac{1}{2}n\log n\).根据主定理答案是\(\Theta (n\log^2 n)\)

代码&题解

动态点分治

咕咕咕~

posted @ 2020-07-01 16:24  birchtree  阅读(670)  评论(0编辑  收藏  举报