支配树学习思路/模板

首先解释什么是支配树。
首先我们先看一下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 z z P P P s e m i [ z ] semi[z] semi[z]最小的点。如果 s e m i [ z ] = s e m i [ x ] semi[z]=semi[x] semi[z]=semi[x]那么 i d o m [ x ] = s e m i [ x ] idom[x]=semi[x] idom[x]=semi[x]否则就是 i d o m [ x ] = i d o m [ z ] idom[x]=idom[z] idom[x]=idom[z]
我们标注出每个点的支配点(逗号后面):
在这里插入图片描述
最后连边建成支配树:
在这里插入图片描述
万事大吉了。:

这里给出几个性质:
1、每个点的 s e m i semi semi是唯一 的。
2、一个点的 s e m i semi semi一定是其dfs树祖先。
3、x的半支配点不一定是x的支配点。
4、semi的深度大于idom的深度
5、如果节点U,V满足,V是U的祖先。那么:可以 得出V是 i d o m [ U ] idom[U] idom[U]的祖先,或者 i d o m [ U ] idom[U] idom[U] i d o m [ V ] idom[V] idom[V]的祖先。
P5180 【模板】支配树
下面是ac代码:

// % everyone
#include <cstdio>
#include<iostream>
#include<cstring>
#include <map>
#include <queue>
#include <set>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <vector>
#include <string>
#include <list>
#include <bitset>
#include <array>
#include <cctype>
#include <unordered_map>
#include <time.h>

namespace OI {
    double start_time = 0.0;
    void read_f(int flag = 0) { freopen("0.in", "r", stdin); if(!flag) freopen("0.out", "w", stdout); }
    void fast_cin() { std::ios::sync_with_stdio(false); std::cin.tie(); }
    void run_time() { std::cout << "\nESC in : " << ( clock() - start_time ) * 1000.0 / CLOCKS_PER_SEC << "ms" << std::endl; }
    void ct() { start_time = clock(); return; }
}
using namespace OI;
template <typename T>
bool bacmp(const T & a, const T & b) { return a > b; }
template <typename T>
bool pecmp(const T & a, const T & b) { return a < b; }

#define ll long long
#define ull unsigned ll
#define _min(x, y) ((x)>(y)?(y):(x))
#define _max(x, y) ((x)>(y)?(x):(y))
#define max3(x, y, z) ( max( (x), max( (y), (z) ) ) )
#define min3(x, y, z) ( min( (x), min( (y), (z) ) ) )
#define pr make_pair
#define pb push_back
using namespace std;

const int N = 2e5+5;
int n, m;
struct Node
{
    int tot = 0;
    int ver[N<<1], he[N], ne[N<<1];
    void add(int x, int y)
    {
        ver[++tot] = y;
        ne[tot] = he[x];
        he[x] = tot;
    }
}a, b, c, d;
int dfn[N], id[N], fa[N], cnt;
void dfs(int u)
{
    dfn[u] = ++cnt; id[cnt] = u;
    for (int i = a.he[u]; i; i = a.ne[i])
    {
        int y = a.ver[i];
        if (dfn[y]) continue;
        fa[y] = u; dfs(y);
    }
}
int semi[N], idom[N], bel[N], val[N];
int fi(int x)
{
    if (x == bel[x]) return x;
    int fs = fi(bel[x]);
    if (dfn[semi[ val[bel[x] ] ] ] < dfn[ semi[val[x] ] ]) val[x] = val[bel[x]];
    return bel[x] = fs;
}
void tarjan()
{
    for (int s = cnt; s>1; s--)
    {
        int u = id[s];
        for (int i = b.he[u]; i; i = b.ne[i])
        {
            int y = b.ver[i];
            fi(y);
            if (dfn[semi[val[y]]] < dfn[semi[u]]) semi[u] = semi[val[y]];
        }
        c.add(semi[u], u);
        bel[u] = fa[u];
        u = fa[u];
        for (int i = c.he[u]; i; i = c.ne[i])
        {
            int y = c.ver[i];
            fi(y);
            if (semi[val[y]] == u) idom[y] = u;
            else idom[y] = val[y];
        }
    }
    for (int i = 2; i <= cnt; i++)
    {
        int u = id[i];
        if (idom[u] != semi[u])
            idom[u] = idom[idom[u]];
    }
}
int ans[N];
void dfs_ans(int u)
{
    ans[u] = 1;
    for (int i = d.he[u]; i; i = d.ne[i])
    {
        int y = d.ver[i];
        dfs_ans(y);
        ans[u] += ans[y];
    }
}
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++)
    {
        int x, y; scanf("%d%d", &x, &y);
        a.add(x, y);
        b.add(y, x);
    }
    for (int i = 1; i <= n; i++) semi[i] = bel[i] = val[i] = i;
    dfs(1);
    tarjan();
    for (int i = 2; i <= n; i++) d.add(idom[i], i);
    dfs_ans(1);
    for (int i = 1; i <= n; i++) printf("%d ", ans[i]);
    puts("");
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值