【模板】支配树

学习博客

支配树学习笔记
与图论的邂逅03:Lengauer-Tarjan ← 原理介绍的不错
算法 - 支配树 (Lengauer Tarjan)


一些相关的定义

搜索树:有向无环图从根节点dfs时最先经过的边形成的树

在搜索树上的边:任意(u→v),均存在 d f n u < d f n v dfn_u<dfn_v dfnu<dfnv
不在搜索树上的边:任意(u→v),均存在 d f n u > d f n v dfn_u>dfn_v dfnu>dfnv

支配点,也叫必经点: d o m dom dom
d o m x dom_x domx :可以到达x的所有必经点的集合

最近支配点: i d o m idom idom
i d o m x idom_x idomx :dfn序离x最近的那个支配点

半支配点: s e m i semi semi
s e m i x semi_x semix :能通过走非树枝边到达x的深度最小的点的祖先y(不理解的看第2个学习博客)
s e m i x semi_x semix的性质:

  1. d f n s e m i x < d f n x dfn_{semi_x} < dfn_x dfnsemix<dfnx
  2. s e m i semi semi 不一定是支配点
  3. i d o m x idom_x idomx s e m i x semi_x semix在搜索树上的祖先

因为性质3(证明?),所以算法的关键是利用半支配点去求最近支配点

支配树求解的具体过程:

  1. 每计算一个点时,把这个点放进生成森林中,用并查集维护
  2. 根据半必经点定理,若dfn[x]>dfn[y],计算semi[y]时则需要考虑x祖先中dfn大于y的点
  3. 由于按时间戳从大到小的顺序计算,比y时间戳小的点还未加入生成森林,所以直接在生成森林中考虑x的祖先即可
  4. 令dfn[semi[x]]为x到其父亲的边的权值,用带权并查集可以求出边权的最小值

板子

题目:HDU4694

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5 + 10;
int n, m;

namespace DominantorTree {//支配树
	// 建边
	vector<int> pre[N];//前驱
    vector<int> suf[N];//后继
    
    vector<int> dom[N];//支配点集合 dom[x]表示所有可以到达x的支配点的集合

    int fa[N];//节点在搜索树中的父亲
    int dfn[N];
    int semi[N];// dfn最小的半支配点
    int idom[N];// dfn最大支配点 最近
    int anc[N];// 并查集的祖先
    int best[N];// 祖先链中dfn值最小的semi
    int id[N];// 时间戳为x的原值
    
    ll sum[N];// 每个点的支配点的编号之和

    void insert(int u, int v) { // 建边 u->v
        suf[u].push_back(v);
        pre[v].push_back(u);
    }

    int num = 0;//时间戳
    
    void init() {
        for (int i = 1; i <= n; i++) {
            suf[i].clear();
            pre[i].clear();
            dom[i].clear();
            anc[i] = semi[i] = best[i] = i; 
            sum[i] = dfn[i] = id[i] = fa[i] = idom[i] = 0;
        }
        num = 0;
    }

	// dfs求时间戳
    void dfs(int u) {
        dfn[u] = ++num;
        id[num] = u;
        for (int v:suf[u]) {
            if (!dfn[v]) {
                dfs(v);
                fa[dfn[v]] = dfn[u];
            }
        }
    }

    // 带权并查集
    int find(int x) {
        if (x == anc[x]) return x;
        int y = find(anc[x]);
        if (semi[best[x]] > semi[ best[anc[x]] ])
            best[x] = best[anc[x]];
             // 利用并查集求出集合里面dfn最小的点 best定义为祖先链中dfn值最小的semi 记录的是dfn不是点值
        return anc[x] = y;
    }

    // 求出支配树
    // 原理:
    // 1.建出搜索树并算出每个点的时间戳
  // 2.根据半必经点定理按时间戳从大到小计算出每个点的半必经点
	// 3.根据必经点定理,通过算出的半必经点得出每个点的最近必经点
    void Lengauer_Tarjan() {
        for (int y = num; y > 1; y--) {
            int x = fa[y];
            
			//求出半支配点 
            for (int i = 0; i < pre[id[y]].size(); i++) {
                int z = dfn[ pre[id[y]][i] ];
                if (!z) continue;
                find(z);
                semi[y] = min(semi[y], semi[ best[z] ]);
            }

            dom[semi[y]].push_back(y);
            anc[y] = x;

            for (int i = 0; i < dom[x].size(); i++) {
                int z = dom[x][i];
                find(z);
                idom[z] = (semi[ best[z] ] < x) ? best[z] : x; // 半必经点定理
            }

            dom[x].clear();
        }

        for (int i = 2; i <= num; i++) {
            if (idom[i] != semi[i]) idom[i] = idom[ idom[i] ];
            dom[ idom[i] ].push_back(i);
        } // 必经点定理

        idom[1] = 0;
    }

}
using namespace DominantorTree;


void calc(int x) {
    sum[id[x]] += id[x];
    for (int i = 0; i < dom[x].size(); i++) {
        int y = dom[x][i];
        sum[id[y]] = sum[id[x]];
        calc(y);
    }
}

int main() {
    while (cin >> n >> m) { // n个点 m条边

        init();

        for (int i = 1, u, v; i <= m; i++) {
            cin >> u >> v;
            insert(u, v);
        }

        dfs(n);//题意要求 根节点为n
        Lengauer_Tarjan();
        calc(1);

        for (int i = 1; i < n; i++) {
            cout << sum[i] << ' ';
        }
        cout << sum[n] << endl;
    }

    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值