基本概念
作用: 在有向图中,当询问从起点到终点的路径必须经过的点,即去掉这个点以及周围的边,就不能从起点到达终点的点(类似于无向图中的割点)。这时,可与通过建立支配树来解决问题。
结构: 支配树理所当然是一个树状结构。图的起点作为根节点,每一个节点到达根节点的路径都是必经点。如果能建立这样的树状结构,那么基本可以通过搜索求得关于必经点的所有信息。
在DAG上的建树方法:
总的来说就是,点x在支配树上的父亲就是所有能走到它的点在支配树上的LCA(这个感性理解)。
这样就可以在O(nlog(n))的时间里求解,而且还是多个点的;
具体讲,就是先进行拓扑排序,然后从前往后依次遍历,没遍历到一个点,需要根据反向边来寻找那哪几个边能到达这个节点,然后求出几个点的最近公共组先,就是在支配树上这个节点的父亲。所以需要用倍增找最近公共组先的方法来动态维护二维 f【】【】 数组(倍增j倍之后i的父节点)和一维 d【】数组(i号节点的深度)。
在普通的有向图中建立支配树:
在普通的有向图中建树还是有一定难度的,看了一遍一知半解,大概思路懂了但是具体实现想的不太清楚,之后持续更新。
一个裸的DAG题目:
链接:https://www.luogu.org/problem/P2597
题目是中文,就不说了。
具体方法就是找每一个点是从起点到几个点的必经点。输出数量。裸的DAG上的支配树建立,建树之后之需要搜索一遍求子树大小即可。
AC代码:
#include<bits/stdc++.h>
#include<algorithm>
#include<complex>
#include<iostream>
#include<iomanip>
#include<ostream>
#include<cstring>
#include<string.h>
#include<string>
#include<cstdio>
#include<cctype>
#include<vector>
#include<cmath>
#include<queue>
#include<set>
#include<stack>
#include<map>
#include<cstdlib>
#include<time.h>
#include<ctime>
#include<bitset>
// #include<ext/pb_ds/assoc_container.hpp>
// #include<ext/pb_ds/hash_policy.hpp>
#define pb push_back
#define _fileout freopen("out.txt","w",stdout)
#define _filein freopen("in.txt","r",stdin)
#define ok(i) printf("ok%d\n",i)
using namespace std;
// using namespace __gnu_pbds;
typedef double db;
typedef long long ll;
typedef pair<int,int>PII;
const double PI = acos(-1.0);
const ll MOD=1e9+7;
const ll NEG=1e9+6;
const int MAXN=1e6+10;
const int INF=0x3f3f3f3f;
const ll ll_INF=9223372036854775807;
const double eps=1e-9;
int head[MAXN],nex[MAXN],e[MAXN],o;//正向图
void add(int x,int y)//加正向边
{
e[++o]=y;
nex[o]=head[x];head[x]=o;
}
int head2[MAXN],nex2[MAXN],e2[MAXN],o2;//反向图
void add2(int x,int y//加逆向边
{
e2[++o2]=y;
nex2[o2]=head2[x];head2[x]=o2;
}
int n;
vector<int>v;
queue<int>q;
int du[MAXN];
// int vis[MAXN];
void getv()//拓扑排序更新拓扑序列v
{
int x;
// memset(vis,0,sizeof(vis));
while(!q.empty())
{
x=q.front();
q.pop();
// vis[x]=1;
v.pb(x);
for(int i=head[x];i;i=nex[i])
{
int y=e[i];
// if(vis[y])continue;
du[y]--;
// printf("x=%d y=%d,du=%d\n",x,y,du[y]);
if(!du[y])q.push(y);
}
}
}
int head_zp[MAXN],nex_zp[MAXN],e_zp[MAXN],tot;//支配树
void add_zp(int x,int y)//加支配树的边
{
e_zp[++tot]=y;
nex_zp[tot]=head_zp[x];head_zp[x]=tot;
}
int f[MAXN][20],d[MAXN];
int t;
int lca(int x,int y)//求最近公共祖先
{
if(d[x]>d[y])swap(x,y);
for(int i=t;i>=0;i--)
{
if(d[f[y][i]]>=d[x])y=f[y][i];
}
if(x==y)return x;
for(int i=t;i>=0;i--)
{
if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
}
return f[x][0];
}
int size[MAXN];
void dfs(int x)//搜索求每个节点的子树大小
{
size[x]=1;
for(int i=head_zp[x];i;i=nex_zp[i])
{
int y=e_zp[i];
if(size[y]){size[x]+=size[y];continue;}
dfs(y);
size[x]+=size[y];
}
}
int main()
{
scanf("%d",&n);
t=(int)(log(n)/log(2))+1;
for(int i=1;i<=n;i++)
{
int x;
while(~scanf("%d",&x)&&x)
{
add(x,i);
// printf("x=%d i=%d\n",x,i);
add2(i,x);
du[i]++;
}
// printf("du[%d]=%d\n",i,du[i]);
if(du[i]==0)
q.push(i);
}
getv();
int len=v.size();
// for(int i=0;i<len;i++)
// {
// printf("%d ",v[i]);
// }
// printf("\n");
for(int i=0;i<len;i++)
{
int x=v[i];
int mid=e2[head2[x]];
for(int j=head2[x];j;j=nex2[j])
{
int y=e2[j];
// ok(0);
// printf("x=%d y=%d\n",x,y);
mid=lca(mid,y);
}
// printf("%d %d\n",mid,x);
// printf("lca 2,3=%d\n",lca(2,3));
// ok(1);
add_zp(mid,x);
// ok(2);
f[x][0]=mid;
d[x]=d[mid]+1;
for(int l=1;l<=t;l++)
{
f[x][l]=f[f[x][l-1]][l-1];
}
}
// for(int i=1;i<=n;i++)
// {
// for(int j=head_zp[i];j;j=nex_zp[j])
// {
// int y=e_zp[j];
// printf("%d %d\n",i,y);
// }
// }
for(int i=0;i<len;i++)
{
if(!size[v[i]])dfs(v[i]);
// ok(3);
}
for(int i=1;i<=n;i++)
{
printf("%d\n",size[i]-1);
}
return 0;
}
赏心悦目的AC结果: