图论小专题B

谁都会走 提交于 2019-11-26 14:57:41

2 树

2.1 树的定义

一个只有\(N-1\)条边,且任意两个点连通的图叫做树。通过这样定义的树往往是一棵无根树,而我们通常会任意选定一个根节点使其变成有根树。有根树可以定义“父亲和儿子”的层次关系,这往往有利于构造最优子结构,进行DP和搜索等操作。

特别的,如果在树上任意加上一条边,那么整个树上就会多出一个环。我们称这样的树是“基环树”。基环树不是树,但是它只有一个环。将整个环作为一个“广义根”,然后将根和连在环上的子树分开处理,同样可以套用树的许多算法。

2.2 树上的DP算法

通常选定一个根,然后用DFS计算。至于递归接口应该放在转移之前还是之后呢?那就看方程怎么写了。在写程序的时候,只要满足“已知推未知”的原则就行。

如果给定一棵无根树,答案要求给出最优的根使得某个值最优化,这时可以采用“换根法”。先任意选定一个根计算出规划值\(F_1\),然后从数学上推导出以任意点为根的规划值\(F_2\)。《进阶指南》上有相关的例题。

2.2.1 树的参量

子树大小size
最基础的量。转移方程简记为\(F(x)=1+\sum F(\text{son}(x))\)
树的重心
和size一样。如果子树\(x\)的大小是\(\text{size}(x)\),那么剩下树的大小就是\(N-\text{size}(x)\)。在求\(\text{size}\)时可以顺带求出。
树的直径
有一种DP方程,还有一种搜索方法。

第一种方法,设\(F_1(x)\)表示点\(x\)到它的子树最长的距离。有方程:
\[ F_1(x)=\max_{y \in \text{son}(x)}\{F(y)+d(x,y)\} \]
然后以此推导出经过\(x\)的,在\(x\)子树内的最长链。设它为\(F_2(x)\)
根据定义,我们在子树里面找出两条过\(x\)的路径,这两条路径最长即符合要求。有方程:
\[ F_2(x)=\max_{y_1,y_2\in \text{son}(x)}\{F_1(y_1)+d(x,y_1)+F_1(y_2)+d(x,y_2)\} \]
这两个值,一个一定是最大值,一个一定是次大值。我们根据\(F_1\)的定义,得到:
\[ F_2(x)=\max_{y_2\in \text{son}(x),y_2 \neq y_1,F_1(x)=F(y_1)+d(x,y_1)}\{F_1(x)+F_1(y_2)+d(x,y_2)\} \]
这样方程会相当麻烦。我们换一种思路:
\(F_1(x)\)表示\(x\)\(x\)子树叶子的最大距离,\(G_1(x)\)表示次大距离。这样我们有两个方程:
\[ \begin{cases} G_1(x)=F_1(x),F_1(x)=F_1(y)+d(x,y) & F_1(x) < F_1(y)+d(x,y)\\ G_1(x)=F_1(y)+d(x,y) & \text{else if }G_1(x)<F_1(y)+d(x,y) \end{cases}\\ y \in \text{son}(x) \]
这样\(F_2(x)=G_1(x)+F_1(x)\)就是原来所求了。枚举最大的\(F(i)\)即可求得答案。

也可以用两次BFS或DFS。先任意一个点\(root\),搜索出离\(root\)最远的点\(p_1\);然后再搜索出离\(p\)点最远的点\(p'\)。那么两点的距离\(pp'\)就是树的直径。

2.2.2 LCA

如果节点\(u\)既是\(x\)的祖先,又是\(y\)的祖先,则\(u\)\(x,y\)的公共祖先。当这个公共祖先深度最深时,记\(u=LCA(x,y)\)\(x,y\)的最近公共祖先。
求LCA有若干种方法:

向上标记法\(O(qN)\)
对于要求的\(LCA(x,y)\),我们先选一个节点\(x\)走到根节点,将路径上的点全部标记。然后,我们再让\(y\)同时往上走,\(y\)遇到的第一个被标记的点就是\(LCA(x,y)\)

树上倍增法\(O(q\log N)\)
首先我们令\(F(x,i)\)表示\(x\)\(2^i\)辈祖先,也就是\(x\)往上走\(2^i\)步得到的节点。初始时有\(F(x,0)=\text{father}(x)\),然后以\(i\)为阶段,以\(F(x,i)=F(F(x,i - 1), i - 1)\)为转移方程,就可以处理出所有的\(F(x,i)\)

接下来我们选择一个点往上跳(设这个点为\(x\))。可以进行交换使得\(\text{deep}(x)\geq \text{deep}(y)\)。依次尝试让\(x\)向上走\(2^{\log N},\cdots,2^2,2^1,2^0\)步,使得每一步都恰好满足\(\text{deep}(x) \geq \text{deep}(y)\)。最后一步应有\(\text{deep}(x)=\text{deep}(y)\)。如果有\(x=y\),那么\(LCA(x,y)=y\)

否则,我们让\(x\)\(y\)同时向上跳\(2^{\log N},\cdots,2^2,2^1,2^0\)步,使得每一步都有\(x \neq y\)。在最后一步的时候,一定有\(\text{father}(x)=\text{father}(y)=LCA(x,y)\)

树上倍增法有非常广泛的应用。举个例子,有道题就需要维护树上路径的最大值,这时就可以用“树上ST表”。这个结构就是树上倍增法的体现。

树链剖分\(O(q\log N)\)
重点是用两次dfs处理出top数组,即每条树链的顶端。每次询问时,如果\(x\)\(y\)都在同一条链上,则\(LCA(x,y)\)就是深度较小的那个节点。否则,我们就让深度较大的节点往它的上面的链跳,即令\(x=\text{father}(\text{top}(x))\)

实现起来代码长度较长,会有一定常数,但是在时间复杂度上面应该还是略优于倍增法。且这种方法可扩展性强,可以配套其他的操作。

LCA的Tarjan算法\(O(q+N)\)
用并查集对向上标记法的优化。

在DFS搜索一棵树时,每个节点有三个状态:UNMARKEDTRAVERSALBACKTRACKED,分别表示“未标记”,“已遍历”,“已回溯”。
当一个节点\(x\)正处于TRAVERSAL状态时,其沿着父亲至根节点一定有一条TRAVERSAL链。此时对于任意一个处于BACKTRACKED状态的节点\(y\)\(LCA(x,y)\)就是\(y\)沿着父亲路径,遇到的第一个TRAVERSAL节点。这是向上标记法的实质。
这里对于UNMARKED\(y\)是不成立的,因为TRAVERSAL链和BACKTRACKED链的交点相当于一个不同时刻决策的分支,是第一个使得\(x\)\(y\)分立的节点。否则,沿着\(y\)的一段路径会标记成TRAVERSAL,而不是BACKTRACKED

在这里,我们用一个并查集来维护这个路径。对于一个BACKTRACKED节点,我们定义它的支点\(top(y)\)表示节点\(y\)沿着父亲路径,向上遇到的第一个TRAVERSAL节点。
当一个节点\(x\)TRAVERSAL变成BACKTRACKED时,它的父亲一定是TRAVERSAL的(根据遍历回溯的顺序可以得到)。这时一定有\(top(x)=top(\text{father}(x))\)
当我们在访问\(top(y)\)时,可以顺便进行路径压缩,即\(top(y)=top(top(y))\)。这对答案是没有影响的。这一点和并查集类似,可以用并查集的get操作完成。

总结一下,对于每一个UNMARKED节点\(x\),我们先标记为TRAVERSAL,并遍历它的儿子\(y\),然后令\(top(y)=x\)。随后,对于每一个和\(x\)有关的询问\(LCA(u_i,x)\),如果\(u_i\)BACKTRACKED的,我们可以直接由get(u[i])得到答案。
将询问离线处理,预处理和每个节点有关的询问,然后运行这个算法。时间复杂度\(O(q+N)\),其中并查集合并的时间复杂度可以忽略不计。

unsigned short state[MAXN];
#define UNMARKED 0
#define TRAVERSAL 1
#define BACKTRACKED 2

int top[MAXN];
inline int get(int cur)
{
    if(cur == top[cur])
        return cur;
    return top[cur] = get(top[cur]);
}
inline void init()
{
    for(rg int i = 1; i <= N; ++ i)
        top[i] = i;
}

void DFS(int cur)
{
    state[cur] = TRAVERSAL;
    for(rg int e = head[cur]; e; e = edge[e].next)
    {
        int to = edge[e].to;
        if(state[to] != UNMARKED)
            continue;
        DFS(to);
        top[to] = cur;
    }
    
    for(rg int i = 0; i < queryNode[cur].size(); ++ i)
    {
        int node = queryNode[cur][i], rank = queryRank[cur][i];
        if(state[node] != BACKTRACKED)
            continue;
        ans[rank] = get(node);
    }
    state[cur] = BACKTRACKED;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!