支配树 note

支配树

在一个有向图中,有一个起点R,对于任意点W,对于R->W的任意路径都经过点P,则称P为W的支配点。设idom[i]表示距离i最近的支配点。在原图基础上,idom[i]向i连边构成一颗新树,称为支配树

支配树的性质

1.支配树是以R为根的一棵树
2.对于任意点i,到根r路径上经过的点集{xi}是原图上r->i的必经点
3.对于任意的i,它是子树中每个点的必经点

半必经点

在dfs搜索树中,对于一个节点Y,存在某个点X能够通过一系列点pi(不包含X和Y)到达点Y且∀i dfn[i]>dfn[Y],我们就称X是Y的半必经点,记做semi[Y]=X
通俗理解:semi[x]就是x在dfs树中所有祖先中z,能不经过 z​ 和 x​ 之间的树上的点而到达 x​ 的点中深度最小的。

半必经点性质

  • semi[x]一定是x的祖先
  • semi[x]一定是确定的
  • 半必经点不一定是必经点
  • semi[x]深度不小于idom[x],即idom[x]在semi[x]祖先链上

计算semi

对于点x,有边(y,x)

  • 若dfn[y]<dfn[x](树边或前向边) 且dfn[y]<dfn[semi[x]] ,semi[x]=y

  • 若dfn[y]>dfn[x](后向边或横叉边),找到y的一个祖先semi值最小的z且dfn[z]>dfn[x],用semi[z]更新semi[x]

计算idom

设点集 P 是 semi(x)(不包括) 到 x 路径上经过的点,t 是点集 P 中 semi 最小的点。

  • 如果 semi(x)=semi(t),那么 idom(x)=semi(x)。 只需证明semi(x)是 x 的必经点。用力感受一下,如果能绕过 semi(x) 的话,肯定会先跳到点集P中。

  • 如果 semi(x)≠semi(t),那么 idom(x)=idom(t)。 首先证明 idom(t)是 x 的必经点。如果 idom(t) 不能用的话,在 idom(t) 到 t 的那一段都不能用,由于 t 到 x 的那段 semi不够小,所以绕不过。
    然后证明 idom(x)的深度不能更大。反证,如果深度更大,那么首先能到 idom(t)。此时利用 semi(x)→x 和 idom(t)→t 两个传送门,删除 idom(t) 和 x 之间的任意一个结点也无济于事。(注意到 t 在 semi(x) 和 x 之间)

代码

//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <queue>
#include <stack>
#include <set>
#include <map>
#include <algorithm>
#include <cmath>
#include <functional>
#define INF 999999999
#define N 200010
#define M 300010
#define ll long long
using namespace std;
struct A
{
	int s,e,nxt;
}way[M];
int n,m,cnt,root,ans[N];
int id[N],dfn[N],semi[N],idom[N];//id[x]:dfn为x的点;semi[x]:x的半支配点;idom[x]:x的支配点
int f[N],val[N];//f[x]:用于记录带权并查集的父亲结点;val[x]:带权并查集的权,记录x到f[x](包括x和f[x])中semi最小的点
int head[N],fa[N];//fa[x]记录x在dfs树的父亲结点
//reidom[x]存idom为x的点;resemi[x]存semi为x的点;pre[x]存原图中x的前驱(只要有直连边到x)
vector <int> reidom[N],pre[N],resemi[N];
void get_dfn(int x)//求dfn序,记录dfn,id,fa
{
	int l;
	cnt++;
	dfn[x]=cnt;
	id[cnt]=x;
	for(int i=head[x];i!=0;i=way[i].nxt)
	{
		l=way[i].e;
		if(dfn[l]==0)
		{
			get_dfn(l);
			fa[l]=x;
		}
	}
	return;
}
int find(int x)//带权并查集的路径压缩操作,返回值其实没用
{
	int k;
	if(f[x]==x) return x;
	else
	{
		k=find(f[x]);
		if(dfn[semi[val[f[x]]]]<dfn[semi[val[x]]])
			val[x]=val[f[x]];
		f[x]=k;
		return f[x];
	}
}
void tarjan()
{
	int k,l;
	for(int i=cnt;i>=1;i--)
	{
		k=id[i];//按dfn序逆序操作(目前k的dfn序是最大的)
		for(int j=0;j<pre[k].size();j++)
		{
			l=pre[k][j];//有一条边(l,k)
			if(dfn[l]==0) continue;//可能有root走不到的点
            

            //其实不需要这个判断,都写成if中的操作也是对的
			if(dfn[l]>dfn[k])//如果l已经操作过,说明semi[l]已经确定了
			{
				find(l);//更新l的并查集,让他直接连到并查集头目(路径压缩)
                //用semi[val[l]]-->val[l]-->l-->k来更新semi[k],这条路径上只有起点semi[val[l]]的dfn可能小于k
				if(dfn[semi[val[l]]]<dfn[semi[k]])
					semi[k]=semi[val[l]];
			}
			else//如果l没有操作过,用l来更新semi[k]
			{
				if(dfn[l]<dfn[semi[k]])
					semi[k]=l;
			}
		}
		//此时semi[k]已更新完毕
		resemi[semi[k]].push_back(k);


        f[k]=fa[k];
        //此时如果有点l满足semi[l]=fa[k],那么l并查集中的头目为fa[k](fa[k]可以到l,而且dfn大于dfn[k]的都尚未访问)
		for(int j=0;j<resemi[fa[k]].size();j++)
		{
			l=resemi[fa[k]][j];
			find(l);
			if(semi[val[l]]==fa[k]) idom[l]=fa[k];//如果semi[l]==fa[k]且semi[val[l]]==fa[k],那么idom[l]=fa[k]
			else idom[l]=val[l];//如果semi[l]==fa[k]且semi[val[l]]!=fa[k],那么idom[l]=idom[val[l]],这里相当于暂时存一下
		}
	}
	for(int i=2;i<=cnt;i++)//找当时semi[l]==fa[k]且semi[val[l]]!=fa[k]的点,按dfn从小到大访问
	{
		k=id[i];
		if(idom[k]!=semi[k])
			idom[k]=idom[idom[k]];
	}
	return;
}
void sum(int x)
{
	int l;
	ans[x]++;
	for(int i=0;i<reidom[x].size();i++)
	{
		l=reidom[x][i];
		sum(l);
		ans[x]+=ans[l];
	}
	return;
}
int main()
{
	cin >> n >> m;
	for(int i=1;i<=m;i++)
	{
		scanf("%d%d",&way[i].s,&way[i].e);
		way[i].nxt=head[way[i].s];
		head[way[i].s]=i;
		pre[way[i].e].push_back(way[i].s);
	}
	root=1;
	for(int i=1;i<=n;i++)
	{
		semi[i]=i;
		f[i]=i;
		val[i]=i;
	}
	get_dfn(root);
	tarjan();

	for(int i=1;i<=n;i++)//建立支配树
	{
		if(dfn[i])
		{
			reidom[idom[i]].push_back(i);
		}
	}
    
	sum(root);
	for(int i=1;i<=n;i++)
		printf("%d ",ans[i]);
	return 0;
}

参考

https://www.luogu.com.cn/problem/P5180
https://www.cnblogs.com/Mr-Spade/p/10106905.html
https://zerol.me/2018/10/22/dominator-tree/
https://www.cnblogs.com/BeyondStars/p/12380664.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值