支配树学习思路/模板

Posted Kalzn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了支配树学习思路/模板相关的知识,希望对你有一定的参考价值。

首先解释什么是支配树。
首先我们先看一下DAG,我们现在有一个DAG:

如果我们从1出发,对于图中的一个点 p p p,在从1到 p p p的不同路径中,总有一些点是必经的,例如图中的8号点,我们发现在从1到8的不管是那一条路径上,5都是必须经过的。(当然1号点和8号点也可以理解为必经的)。对于每一个点,如果把该点向上最近的一个必经点和该点连有向边。我们就得到了这个图的支配树。
还是那这个DAG举例,我们按照上述的方法连边(红线):

红边就是该DAG的支配树。对于一个点 p p p,从其到1的树链上的每一个点,都是1到 p p p的必经点。
我们仔细观察这个图,我们发现,对于一个点,他最近的必经点(支配树父亲)都是他的所有入点的“LCA”。例如,5号点的入点V= 6,2,3 ,其”LCA”为1。同理8号点,入点的“LCA”为5。那么我们怎么求这个所谓的“LCA”呢?
我们都知道,“LCA”的概念仅存在于树上,对于DAG,对于一个点集没有所谓固定的“LCA”(因为一个点可能不止一个父亲(入点))。但是对于上图,我们可以考虑,如果一个点的所有入点向上的支配树已经建成,那么该点的最近必经点就是这些入点在当前已建支配树上的LCA
有点绕。。。我们看例子,比如现在我们要确定5号点的最近必经点。且V= 6,2,3 向上的支配树已经建成。那么此时图是这样的:

从图上非常明显的知道:V= 6,2,3 的在已建成的支配树上的LCA为1。这时我们连边<1, 5>

对于其他的点,同理。所有我们知道,我们在依次建立支配树的边时,必须保证该点的所有入点的支配树都已建成。所以,我们必须先处理每个点的入点,在处理该点。。这不就是 拓扑排序么?我们可以按照拓扑顺序处理完这个DAG,建成支配树。下面看一个例题。
P2597 [ZJOI2012]灾难
相信这个题,已经在各种大佬博客、题解中看了不止一遍了。我们如果用上述方法建成该食物网的支配树。那么每个点的答案,就该点的支配树子树的大小了。
我们看代码:首先建图:


void add(int x, int y)

    ver[++tot] = y;
    ne[tot] = he[x];
    he[x] = tot;
    verf[tot] = x;
    nef[tot] = hef[y];
    hef[y] = tot;

scanf("%d", &n);
for (int i = 1; i <= n; i++)

	int x;
	while(scanf("%d", &x), x)
	di[i]++;add(x, i);

可以看到,在建图的时候,我们统计入度,并同时建出反图(一会提到为什么)
然后我们拓扑排序,得到点的拓扑序:

void topu()

    for (int i = 1; i <= n; i++) if (!di[i])
        q.push(i);
    while(q.size())
    
        int now = q.front();
        q.pop();
        topp[++cnt] = now;//记录拓扑序
        for (int i = he[now]; i; i = ne[i])
        
            int y = ver[i];
            di[y]--;
            if (!di[y])
                q.push(y);
        
    

然后我们我们按照拓扑顺序建立支配树:

void adda(int x, int y)

    vera[++tot] = y;
    nea[tot] = hea[x];
    hea[x] = tot;

for (int i = 1; i <= n; i++)

    int x = verf[hef[topp[i]]];//利用反图,找到topp[i]点的入点
    for (int j = hef[topp[i]]; j; j = nef[j])
        x = lca(x, verf[j]);//遍历所有入点,算出lca
    adda(x, topp[i]);//联边建树
    d[topp[i]] = d[x] + 1;
    f[topp[i]][0] = x;
    for (int j = 1; j <= 25; j++)
        f[topp[i]][j] = f[f[topp[i]][j-1]][j-1];//边建树便记录f数组

此时hea系列数组记录的就是支配树信息。
最后我们dfs一下支配树,记录子树大小就可以了。
下面是完整ac代码:

#include <iostream>
#include <cstdio>
#include <queue>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <vector>
#include <map>
#include <cstdlib>
#define ll long long
using namespace std;
const int N = 2e5+5;
int tot, he[N], ver[N], ne[N];
int hef[N], verf[N], nef[N];
int tota, hea[N], vera[N], nea[N];
int f[N][30], d[N];
int di[N];
int n,m, cnt, topp[N];
queue<int> q;
void add(int x, int y)

    ver[++tot] = y;
    ne[tot] = he[x];
    he[x] = tot;
    verf[tot] = x;
    nef[tot] = hef[y];
    hef[y] = tot;

void adda(int x, int y)

    vera[++tot] = y;
    nea[tot] = hea[x];
    hea[x] = tot;

void topu()

    for (int i = 1; i <= n; i++) if (!di[i])
        q.push(i);
    while(q.size())
    
        int now = q.front();
        q.pop();
        topp[++cnt] = now;
        for (int i = he[now]; i; i = ne[i])
        
            int y = ver[i];
            di[y]--;
            if (!di[y])
                q.push(y);
        
    

int lca(int x, int y)

    if (d[x] > d[y]) swap(x, y);
    for (int i = 25; i >= 0; i--)
    
        if (d[f[y][i]] < d[x]) continue;
        y = f[y][i];
    
    if (x == y) return x;
    for (int i = 25; i >= 0; i--)
        if (f[x][i] != f[y][i]) x = f[x][i], y = f[y][i];
    return f[x][0];

int siz[N];
int dfs(int now)

    siz[now] = 1;
    for (int i = hea[now]; i; i = nea[i])
    
        siz[now] += dfs(vera[i]);
    
    return siz[now];

int main()

    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
    
        int x;
        while(scanf("%d", &x), x)
        di[i]++;add(x, i);
    
    topu();
    for (int i = 1; i <= n; i++)
    
        int x = verf[hef[topp[i]]];
        for (int j = hef[topp[i]]; j; j = nef[j])
            x = lca(x, verf[j]);
        adda(x, topp[i]);
        d[topp[i]] = d[x] + 1;
        f[topp[i]][0] = x;
        for (int j = 1; j <= 25; j++)
            f[topp[i]][j] = f[f[topp[i]][j-1]][j-1];
    
    dfs(0);
    for (int i = 1; i <= n; i++)
        printf("%d\\n", siz[i]-1);
    return 0;


很好,我们现在探讨完DAG的情况了,终于进入最普遍的支配树的学习了。即:一般有向图的支配树与支配点(即必经点)。
下面有一个有向图:
(蓝色边为其dfs搜索树的边,为方便每个点的编号都是它的dfn)

这里我引入半支配点的概念:对于图,若存在一个简单路径<x,y>。如果从x到y所经历的所有点(除了x,y)的时间戳都大于y的时间戳,我们认为x为y的半支配点
例如图中的6号点,他的半支配点集为V = 4,7,8,1。因为从这些点出发都可以找到一个简单路径到达6,且满足上述条件(如图红边所示):

对于每一个点,我们找到他的半支配点集中的dfn最小的一个,记做 s e m i [ x ] semi[x] semi[x]
然后对于每一个< s e m i [ x ] semi[x] semi[x], x x x>连边。(如图红边所示)

为什么要这么做?因为我们如果把所有非搜索树的边删去,链接< s e m i [ x ] semi[x] semi[x], x x x>,那么对于每个点,其支配性不变。而又因为,对于每个点 x x x s e m i [ x ] semi[x] semi[x]都是它搜索树上的祖先
对于一个点 x x x找出所有的边 ( y , x ) (y,x) (y,x)中对应的 y y y
d f n [ y ] < d f n [ x ] dfn[y]<dfn[x] dfn[y]<dfn[x] d f n [ y ] dfn[y] dfn[y]比当前找到的 s e m i [ x ] semi[x] semi[x] d f n dfn dfn小 那么就用 s e m i [ x ] = y semi[x]=y semi[x]=y
d f n [ y ] > d f n [ x ] dfn[y]>dfn[x] dfn[y]>dfn[x]找打树上 y y y的一个祖先 z z z 并且 d f n [ z ] > d f n [ x ] dfn[z]>dfn[x] dfn[z]>dfn[x]比较 s e m i [ z ] semi[z] semi[z] s e m i [ x ] semi[x] semi[x]决定是否用前者更新后者。
这时,我们发现图变成了下面的样子:

没错,它变成了一个DAG。结合上述定理(支配性不变)。我直接按照DAG的支配树求法去求这个图的支配树。除此之外,但是我们有更好的办法。
对于一个点的最近支配点,我们记为 i d o m [ x ] idom[x] idom[x],我们知道,如果求出这个玩意,然后对于每个点建边< i d o m [ x ] , x idom[x],x idom[x],x>就是支配树了。。那么我们怎么求 i d o m [ x ] idom[x] idom[x]呢?
(摘自洛谷)
我们令 P P P是从 s e m i [ x ] semi[x] semi[x] x x x的搜索树上路径点集(不包括 s e m i [ x ] semi[x] semi[x]) 而且 z

以上是关于支配树学习思路/模板的主要内容,如果未能解决你的问题,请参考以下文章

支配树

leetcode-236-二叉树公共祖先

灭绝树(支配树)

leetcode-235二叉搜索树的最近公共祖先

50树中两个节点的公共祖先

支配树