图论 —— 支配树

【概述】

给定一个有向图与一个起点 S,如果要去掉起点 S 到某个点 p 的中间的某个点 x 后无法到达,那么称点 x 支配点 p,x 是 p 的一个支配点

显然支配点 x 可以有多个,支配点的集合记为 {Xp},对于起点以为的点,它们都有两个平凡的支配点,一个是其自身,一个是起点。

在支配 p 的点中,若一个支配点 i≠p,满足 i 被 p 剩下的所有非平凡的支配点支配,则称 i 为 p 的最近支配点,记作:idom(p)

将图的起点记作 root,那么除 root 外的每一点,均存在唯一的最近支配点,因此,连上所有 root 以外的 idom(u)->u 的边,就能得到一棵树,其中每个点支配它子树中的所有点,这棵树即为支配树

对于来说,其本身就是自己的支配树

对于 DAG 来说,可以简单的看做是拓扑排序加上 LCA 求最近公共祖先的问题

对于一般有向图来说,采用 Lengauer Tarjan 算法来解决

【DAG 图】

对于 DAG 图来说,如果要询问从点 x 到点 y 之间有多少个点被删掉后,使得这两个点中的任何一个无法到达另一个,那么我们可以建立支配树来解决。

对于 DAG 图,我们首先进行反向拓扑,然后按照拓扑序建立支配树

由于某点的支配树上的父亲就是该点在 DAG 中的所有出点在支配树上的 LCA,因此对于询问 query(x,y),结果也就是 x 的深度与 y 的深度的和减去 LCA(x,y) 的深度

int n, m, q;
vector<int> G[N];  //原图
vector<int> Gf[N]; //反图
int in[N];
int a[N], tot;
int deep[N], dp[N][22];
void topSort() { //对反图进行拓扑排序
    queue<int> Q;
    for (int i = 1; i <= n; i++)
        if (in[i] == 0)
            Q.push(i);

    while (!Q.empty()) {
        a[++tot] = Q.front();
        Q.pop();
        int now = a[tot];
        for (int i = 0; i < Gf[now].size(); i++) {
            int to = Gf[now][i];
            in[to]--;
            if (in[to] == 0)
                Q.push(to);
        }
    }
}
int getLCA(int x, int y) { //获取LCA
    if (deep[x] < deep[y])
        swap(x, y);
    int cnt = deep[x] - deep[y];
    for (int i = 0; i < 20; i++)
        if ((1 << i) & cnt)
            x = dp[x][i];
    if (x == y)
        return x;
    for (int i = 19; i >= 0; i--) {
        if (dp[x][i] != dp[y][i]) {
            x = dp[x][i];
            y = dp[y][i];
        }
    }
    return dp[x][0];
}
void init() {
    tot = 0;
    memset(in, 0, sizeof(in));
    memset(dp, 0, sizeof(dp));
    for (int i = 0; i <= n; i++) {
        G[i].clear();
        Gf[i].clear();
    }
}
int query(int x, int y) { 
    return deep[x] + deep[y] - deep[getLCA(x, y)]; 
}
int main() {
    init();

    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        int x, y;
        scanf("%d%d", &x, &y);
        G[x].push_back(y);
        Gf[y].push_back(x);
        in[x]++;
    }

    topSort();

    for (int i = 1; i <= n; i++) {
        int x = a[i];
        if (G[x].size() == 0) {
            G[0].push_back(x);
            deep[x] = 1;
            continue;
        }
        int y = G[x][0];
        for (int i = 1; i < G[x].size(); i++)
            y = getLCA(y, G[x][i]);
        deep[x] = deep[y] + 1;
        dp[x][0] = y;
        for (int i = 1; i < 20; i++)
            dp[x][i] = dp[dp[x][i - 1]][i - 1];
    }

    scanf("%d", &q);
    while (q--) {
        int x, y;
        scanf("%d%d", &x, &y);
        printf("%d\n", query(x, y));
    }

    return 0;
}

【Lengauer Tarjan 算法】

从点 y 出发到点 x,若存在这样一条路径:路径上的点(不包含 x、y 点)的 dfn 均大于 dfn[x],此时,称 y 是 x 的半支配点

用 sdom[x] 表示 x 的半必经点中 dfn 最小的点,这样一来,当删除原图中的非树边,然后连接 (sdom[x],x) 后,不仅不会改变原图中的支配点关系,而且还将原图转为一个 DAG 图,这样就可以使用 DAG 的做法了

但对于一个一般有向图来说,除了上述方法外,利用 Lengauer Tarjan 算法可以快速构造支配树的算法,其可在 O(nlogn) 的时间复杂度内构造一个支配树。

1.算法原理

1)构建 dfs 树

首先对图进行 dfs 遍历,求出 dfs 序

2)找半支配点

对于一个点 x,要找出所有的边 (y,x) 中对应的 y,那么有:

  • 若 dfn[y]<dfn[x] 且 dfn[y] 比当前找到的 sdom[x] 的 dfn 小,那么有 sdom[x]=y
  • 若 dfn[x]>dfn[y],那么找到树上 y 的一个满足 dfn[z]>dfn[x] 的祖先 z,然后比较 dfn[sdom[z]] 与 dfn[sdom[x]] 的关系,来决定是否用前者更新后者

3)半支配点到支配点

对于点 x 来说,我们要找的是 idom[x],当求出 sdom[x] 后,我们需要利用 sdom[x] 来找出 idom[x]

令 P 是从 sdom[x] 到 x 的树上路径点集,且 P 中不包含 sdom[x],并设 y 是 P 中 dfn[sdom[y]] 最小的点

那么:如果 sdom[y]=sdom[x],则 idom[x]=sdom[x],否则 idom[x]=idom[z]

2.算法流程

Lengauer Tarjan 算法的实现一般采用带权并查集

首先对原图进行一遍 dfs 来建立 dfs 树并求出 dfs 序,然后按照 dfs 序从大到小进行处理,利用带权并查集寻找每个点的半支配点 sdom

每一次处理完毕一个点后,将这个点同其在 dfs 树上的父亲在并查集进行连边,边的权值是这个点到根结点的路径上的所有点中最小的 dfn[sdom[x]] 对应的 x

然后根据 dfs 的入度建立 dfs 树的反图,并进行拓扑排序,建立支配树

最后直接在支配树上进行 dfs,根据求出的 sdom[i] 直接计算 idom[i] 即可

3.模版

struct MAP {
    struct Edge {
        int to, next;
    } edge[N << 1];
    int tot, head[N];
    void addEdge(int x, int y) {
        edge[++tot].to = y;
        edge[tot].next = head[x];
        head[x] = tot;
    }
};
MAP G, GF;             //原图、反图
MAP dfsTree, dfsTreeF; // dfs树、dfs树的反图
MAP dominate;          //支配树

MAP xx;
int n, m;
int father[N];          // dfs树上的父节点
int dfn[N], id[N], tim; // dfs序、标号、时间戳

void dfs(int x) {
    id[++tim] = x;
    dfn[x] = tim;

    for (int i = G.head[x]; i; i = G.edge[i].next) {
        int to = G.edge[i].to;
        if (!dfn[to]) {
            dfs(to);
            father[to] = x;
            dfsTree.addEdge(x, to);
        }
    }
}

int sdom[N]; //半支配点
int mn[N]; // mn[i]表示i点的dfs树上的sdom最小的祖先,因此有sdom[mn[i]]=sdom[i]
int anc[N];       // anc[i]代表i的祖先
int find(int x) { //路径压缩的带权并查集
    if (x != anc[x]) {
        int t = anc[x];
        anc[x] = find(anc[x]);
        if (dfn[sdom[mn[x]]] > dfn[sdom[mn[t]]])
            mn[x] = mn[t];
    }
    return anc[x];
}
void LengauerTarjan() { //寻找半支配点
    for (int i = 1; i <= n; i++) {
        anc[i] = i;
        sdom[i] = i;
        mn[i] = i;
    }
    for (int j = n; j >= 2; j--) {
        int x = id[j];
        if (!x)
            continue;

        int pos = j;
        for (int i = GF.head[x]; i; i = GF.edge[i].next) {
            int y = GF.edge[i].to;
            if (!dfn[y])
                continue;
            if (dfn[y] < dfn[x])
                pos = min(pos, dfn[y]);
            else {
                find(y); //寻找树上y的一个满足dfn[z]>dfn[x]的祖先z
                pos = min(pos, dfn[sdom[mn[y]]]);
            }
        }
        sdom[x] = id[pos];
        anc[x] = father[x];
        dfsTree.addEdge(sdom[x], x); //在dfs树上连边
    }
}

int deep[N], dp[N][25];
int getLCA(int x, int y) { //获取LCA
    if (deep[x] < deep[y])
        swap(x, y);
    int del = deep[x] - deep[y];
    for (int i = 0; i <= 20; i++)
        if ((1 << i) & del)
            x = dp[x][i];
    if (x == y)
        return x;
    for (int i = 20; i >= 0; i--) {
        if (dp[x][i] != dp[y][i]) {
            x = dp[x][i];
            y = dp[y][i];
        }
    }
    return dp[x][0];
}
void buildDominate(int x) { //建立支配树
    int to = dfsTreeF.edge[dfsTreeF.head[x]].to;
    for (int i = dfsTreeF.head[x]; i; i = dfsTreeF.edge[i].next) {
        int y = dfsTreeF.edge[i].to;
        to = getLCA(to, y);
    }
    deep[x] = deep[to] + 1;
    dp[x][0] = to;
    dominate.addEdge(to, x);

    for (int i = 1; i <= 20; i++)
        dp[x][i] = dp[dp[x][i - 1]][i - 1];
}
int in[N]; // dfs树的入度
void topSort() {
    for (int i = 1; i <= n; i++) {
        for (int j = dfsTree.head[i]; j; j = dfsTree.edge[j].next) {
            int to = dfsTree.edge[j].to;
            in[to]++;
            dfsTreeF.addEdge(to, i); //对DFS树的反图建边
        }
    }
    for (int i = 1; i <= n; i++) {
        if (!in[i]) {
            dfsTree.addEdge(0, i);  // dfs树建边
            dfsTreeF.addEdge(i, 0); // dfs树的反图建边
        }
    }

    queue<int> Q;
    Q.push(0);
    while (Q.size()) {
        int x = Q.front();
        Q.pop();
        for (int i = dfsTree.head[x]; i; i = dfsTree.edge[i].next) {
            int y = dfsTree.edge[i].to;
            if ((--in[y]) <= 0) {
                Q.push(y);
                buildDominate(y); //建立支配树
            }
        }
    }
}

int idom[N];
void dfsDominate(int x) { //在支配树上搜索idom
    idom[x] = 1;
    for (int i = dominate.head[x]; i; i = dominate.edge[i].next) {
        int y = dominate.edge[i].to;
        dfsDominate(y);
        idom[x] += idom[y];
    }
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        int x, y;
        scanf("%d%d", &x, &y);
        G.addEdge(x, y);
        GF.addEdge(y, x);
    }

    dfs(1);           // dfs,求出dfs序
    LengauerTarjan(); //计算半支配点sdom
    topSort();      //根据dfs树建立dfs树的反图,并对进行拓扑排序从而建立支配树
    dfsDominate(0); //在支配树上寻找答案

    for (int i = 1; i <= n; i++)
        printf("%d ", idom[i]);
    printf("\n");
    return 0;
}

【例题】

  • Blow up the city(HDU-6604)(DAG 图的支配树)点击这里
  • 支配树(洛谷-P5180)(一般有向图的支配树)点击这里
  • 5
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值