支配树
Dominator Tree
被支配哩。自闭哩。。。
没有详细的证明。
Dominator
对于一个任意的有向图,我们钦定一个入口 \(s\),对于任意一个节点 \(u\),
如果从 \(s \to u\) 的任意路径都经过节点 \(v\),称为 \(v\) 支配 \(u\),\(v\) 也是 \(u\) 的一个支配点,记作 \(v \ dom \ u\)。
容易发现,支配关系具有传递性,自反性和反对称性。
- 若 \(v \ dom \ u\) 且 \(u \ dom \ w\),那么 \(v \ dom \ w\)。
- 若 \(u \ dom \ v\) 且 \(v \ dom \ u\),那么 \(u=v\)。
- \(u \ dom \ u\)。
支配满足偏序关系,若直接建图只能得到一个 DAG,不够优秀,所以我们考虑另一种方法。
Tree
在 \(u\) 的支配点中,我们找到除了 \(u\) 的距离 \(u\) 最近的一个点 \(v\),把 \(v\) 称为 \(u\) 的 直接支配点,记作 \(v \ idom \ u\)。
我们将 \(idom_u\) 向 \(u\) 连边,发现容易得到一棵树,
这棵树就叫做 支配树,
而对于一个节点 \(u\) 它的支配点就是它的祖先。
如何求这棵树呢?
我们需要引入 半支配点 这一概念,利用半支配点求解支配点。
首先我们从起点跑一次 dfs,求出了每一个点的 \(dfn\) 序。
那么一个点 \(u\) 的半支配点 \(v\) 满足从 \(v\) 出发存在一条路径 \(v \to v_1 \to v_2 \to \dots \to v_k \to u\) 满足 \(\forall i\in[1,k],dfn_{v_i} \gt dfn_u\),
其中 \(v\) 是满足条件的 \(dfn\) 序最小的那一个,记作 \(v \ sdom \ u\)。
而这个半支配点会有许多性质:
-
\(sdom_u\) 是 \(u\) 的祖先。
感觉挺显然的。因为如果不是,那么 \(fa(sdom_u)\) 也可以成为半支配点,与定义矛盾。
-
\(idom_u\) 是 \(sdom_u\) 的祖先。
如果不是,那么就存在一条路径不经过 \(idom_u\) 了,与定义矛盾。
如何求解半支配点?
发现其实一个点的半支配点就是 dfn 比他大且能到他的点的到其祖先路径上的半支配点最小的一个。
可以尝试画图理解,感觉感性理解还是好的,不会理性证明。
这样我们可以求得半支配点,现在考虑如何求支配点呢?
我们就需要用到半支配点的两个定理:
- 对于一个点 \(u\),若 dfs 树上从 \(sdom_u\) 到 \(u\) 的路径上任意节点 \(v\) 满足 \(sdom_v \ge sdom_u\),那么 \(idom_u =sdom_u\)。
- 对于一个点 \(u\),从 \(sdom_u\) 到 \(u\) 的路径上所有节点半支配点最小的那个点是 \(u\),满足 \(sdom_v \le sdom_u\) 且 \(idom_v = idom_u\)。
这里需要强大的感性理解,感受那股劲你就懂了。
感觉不如看代码。
于是这个样子我们用并查集维护一个点到祖先路径上的半支配点的最小值,
按照 dfs 序倒叙枚举即可。
时间复杂度是 \(\mathcal O(n \log n)\),这样我们就做完了。
放上一个板子。封装过了!
namespace DT{
int n,idx=0,dfn[N],rev[N],fa[N],f[N],mn[N],idom[N],sdom[N];
vector<int> e[N],ie[N],tr[N];
void add(int u,int v){e[u].pb(v);ie[v].pb(u);}
void init(int _n){
n=_n;idx=0;
for(int i=1;i<=n;i++){
f[i]=sdom[i]=mn[i]=i;
dfn[i]=rev[i]=idom[i]=fa[i]=0;
e[i].clear(),ie[i].clear(),tr[i].clear();
}
}
void dfsp(int u){
dfn[u]=++idx;rev[idx]=u;
for(auto v:e[u]) if(!dfn[v]) fa[v]=u,dfsp(v);
}
int find(int x){
if(x==f[x]) return x;
int nw=find(f[x]);
if(dfn[sdom[mn[f[x]]]]<dfn[sdom[mn[x]]]) mn[x]=mn[f[x]];
return f[x]=nw;
}
void build(int rt){
dfsp(rt);
for(int i=n,u;i>1;i--){
u=rev[i];
for(auto v:ie[u]){
find(v);
if(dfn[sdom[mn[v]]]<dfn[sdom[u]]) sdom[u]=sdom[mn[v]];
}
f[u]=fa[u];
tr[sdom[u]].pb(u);
u=fa[u];
for(auto v:tr[u]) find(v),idom[v]=(u==sdom[mn[v]])?u:mn[v];
tr[u].clear();
}
for(int i=2;i<=n;i++) if(idom[rev[i]]!=sdom[rev[i]]) idom[rev[i]]=idom[idom[rev[i]]];
}
void getans(){
for(int i=n;i>=2;i--) ans[idom[rev[i]]]+=++ans[rev[i]];
++ans[1];
}
}
支配树还是非常好看的呢!感觉建出树来性质就非常优美啊。。。
Exercise
一些题目,里面也有我学习支配树的来源——恶心的联考啊。
P5180 【模板】支配树
很板了吧。
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
const int N=2e5+5;
int n,m,ans[N];
namespace DT{
int n,idx=0,dfn[N],rev[N],fa[N],f[N],mn[N],idom[N],sdom[N];
vector<int> e[N],ie[N],tr[N];
void add(int u,int v){e[u].pb(v);ie[v].pb(u);}
void init(int _n){
n=_n;idx=0;
for(int i=1;i<=n;i++){
f[i]=sdom[i]=mn[i]=i;
dfn[i]=rev[i]=idom[i]=fa[i]=0;
e[i].clear(),ie[i].clear(),tr[i].clear();
}
}
void dfsp(int u){
dfn[u]=++idx;rev[idx]=u;
for(auto v:e[u]) if(!dfn[v]) fa[v]=u,dfsp(v);
}
int find(int x){
if(x==f[x]) return x;
int nw=find(f[x]);
if(dfn[sdom[mn[f[x]]]]<dfn[sdom[mn[x]]]) mn[x]=mn[f[x]];
return f[x]=nw;
}
void build(int rt){
dfsp(rt);
for(int i=n,u;i>1;i--){
u=rev[i];
for(auto v:ie[u]){
find(v);
if(dfn[sdom[mn[v]]]<dfn[sdom[u]]) sdom[u]=sdom[mn[v]];
}
f[u]=fa[u];
tr[sdom[u]].pb(u);
u=fa[u];
for(auto v:tr[u]) find(v),idom[v]=(u==sdom[mn[v]])?u:mn[v];
tr[u].clear();
}
for(int i=2;i<=n;i++) if(idom[rev[i]]!=sdom[rev[i]]) idom[rev[i]]=idom[idom[rev[i]]];
}
void getans(){
for(int i=n;i>=2;i--) ans[idom[rev[i]]]+=++ans[rev[i]];
++ans[1];
}
}
int main(){
/*2024.1.22 H_W_Y P5180 【模板】支配树 dominator tree*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m;DT::init(n);
for(int i=1,u,v;i<=m;i++) cin>>u>>v,DT::add(u,v);
DT::build(1);
DT::getans();
for(int i=1;i<=n;i++) cout<<ans[i]<<' ';
return 0;
}
P2597 [ZJOI2012] 灾难
支配树的起源,依旧很板。
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
const int N=2e5+5;
int n,m,ans[N],rt[N],cnt=0;
namespace DT{
int n,idx=0,dfn[N],rev[N],f[N],fa[N],idom[N],sdom[N],mn[N];
vector<int> e[N],ie[N],tr[N];
void add(int u,int v){e[u].pb(v),ie[v].pb(u);}
void init(int _n){
n=_n;idx=0;
for(int i=1;i<=n;i++){
f[i]=sdom[i]=mn[i]=i;
dfn[i]=rev[i]=idom[i]=fa[i]=0;
e[i].clear(),ie[i].clear(),tr[i].clear();
}
}
void dfsp(int u){
dfn[u]=++idx;rev[idx]=u;
for(auto v:e[u]) if(!dfn[v]) fa[v]=u,dfsp(v);
}
int find(int x){
if(f[x]==x) return x;
int nw=find(f[x]);
if(dfn[sdom[mn[f[x]]]]<dfn[sdom[mn[x]]]) mn[x]=mn[f[x]];
return f[x]=nw;
}
void build(int rt){
int lst=idx+1;
dfsp(rt);
for(int i=idx,u;i>lst;i--){
u=rev[i];
for(auto v:ie[u]){
find(v);
if(dfn[sdom[mn[v]]]<dfn[sdom[u]]) sdom[u]=sdom[mn[v]];
}
f[u]=fa[u];
tr[sdom[u]].pb(u);
u=fa[u];
for(auto v:tr[u]) find(v),idom[v]=(u==sdom[mn[v]])?u:mn[v];
tr[u].clear();
}
for(int i=lst+1;i<=idx;i++) if(idom[rev[i]]!=sdom[rev[i]]) idom[rev[i]]=idom[idom[rev[i]]];
for(int i=idx;i>lst;i--) ans[idom[rev[i]]]+=++ans[rev[i]];
ans[rev[lst]]++;
}
}
int main(){
/*2024.1.22 H_W_Y P2597 [ZJOI2012] 灾难 支配树*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n;DT::init(n);
for(int i=1,x;i<=n;i++){
cin>>x;
if(x==0) rt[++cnt]=i;
while(x) DT::add(x,i),cin>>x;
}
for(int i=1;i<=cnt;i++) DT::build(rt[i]);
for(int i=1;i<=n;i++) cout<<ans[i]-1<<'\n';
return 0;
}
CF Gym 103860 - H(CCPC Finals 2021)
还记得那个冬日周六的傍晚,在 cdqz 504 机房,有三个人收到了一条神秘通知(是谁我不说)。
不仅如此,他们还收到了一条鼓励:
于是有个人,打开了 codeforces.com/gyms,把 Page 2 - 9 的比赛翻完了一遍,
没有找到很满意的题目,而这些不满意的题目中,这道题是最好的了,虽然考得有点偏,但是个人感觉挺有意思的。
由于只有一天的时间,于是搬题人迅速学习了 支配树,
赶着时间线终于完成了任务。而搬题人也因此被支配树支配了整整两天。/fn/fn/fn
求给定的⼆分图中有多少个点对满⾜删掉这两个点后最⼤匹配数不变。
\(1 \le n,m \le 2 \times 10^5\)。
很妙的一道题,在自己写完题解完全领悟之后倍感快乐。
首先去跑一次二分图最大匹配,在残图上面做是一定不劣的。
观察残图,如何才能在不改变流量的情况下删去一个点呢?
发现与 \(S\) 相连的点和与 \(T\) 相连的点是相互独立的,所以我们可以分开考虑,以与 \(S\) 相连的点 \(u\) 为例:
- 当边 \((S,u)\) 残余容量 \(\gt 0\),则直接删去边 \((S,u)\)。
- 当边 \((S,u)\) 残余容量 \(=0\),那么需要先找到一条包含 \((u,S)\) 的增广路径,让 \((S,u)\) 恢复容量,再删去。
容易发现,左侧点 \(u\) 能被删除等价于存在一条从 \(S\) 到 \((S,u)\) 或 \((u,S)\) 的增广路,具体实现过程中,为了方便,我们将边 \((S,u)\) 拆成 \((S,u')\) 和 \((u',u)\),于是就只需要判断是否存在一条从 \(S\) 到虚点 \(u'\) 的增广路。
同理右侧点我们就从 \(T\) 出发,判断是否有到虚点的增广路即可。
现在回到题目中的两种情况:
-
异侧点:由于残图的两侧是相互独立的,那么我们只要求出 \(S\) 能到达的左侧点个数和 \(T\) 能到达的右侧点的个数,乘起来就是异侧点的方案数。
-
同侧点:以左侧点 \(u,v\) 为例,是否可以从 \(S\) 到 \(u'\) 和 \(v'\) 分别进行一次增广等价于是否存在两条没有公共路径的 \((S,u')\) 和 \((S,v')\)。于是我们可以对于每一条边都进行拆点,并以 \(S\) 为起点求支配树,存在这样的两条路径当且仅当 这两个虚点再支配树上的左右公共祖先均不是拆边的虚点,所以这样的方案数只需要再支配树上面统计即可。
因为如果他们的支配点是虚点,那么这两个点就一定有相同的增广路,于是就一定不满足条件了。
具体实现时,可以直接把连向汇点的边反向连到原点上面,这样就只用跑一次支配树。
计数的时候直接在支配树上面 dfs,记录一下子树的虚点个数即可,代码好懂一些。
总时间复杂度 \(\mathcal O(m\sqrt n+ m\log m)\)。
支配树帮我们解决了找到两条路径他们没有公共点的方案数——非常强大。
令人感叹。
实现时我们如果把所有边拆了跑网络流需要卡常,
所以建议在跑完网络流之后再拆边,这样就可以轻松通过。
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define ll long long
const int N=1e6+5;
int n,m,S,T,L[N],R[N],lc=0,rc=0,c,c1,c2;
ll res=0;
bool vis[N];
vector<int> g[N],G[N];
namespace DT{//Dominator Tree
int n,idx=0,dfn[N],rev[N],f[N],fa[N],mn[N],idom[N],sdom[N];
vector<int> e[N],ie[N],tr[N];
void add(int u,int v){e[u].pb(v),ie[v].pb(u);}
void init(int _n){
n=_n;idx=0;
for(int i=1;i<=n;i++){
f[i]=sdom[i]=mn[i]=i;
fa[i]=rev[i]=dfn[i]=idom[i]=0;
e[i].clear(),ie[i].clear(),tr[i].clear();
}
}
void dfsp(int u){
dfn[u]=++idx;rev[idx]=u;
for(auto v:e[u]) if(!dfn[v]) fa[v]=u,dfsp(v);
}
int find(int x){
if(f[x]==x) return x;
int nw=find(f[x]);
if(dfn[sdom[mn[f[x]]]]<dfn[sdom[mn[x]]]) mn[x]=mn[f[x]];
return f[x]=nw;
}
void build(int rt){
dfsp(rt);
for(int i=n,u;i>1;i--){
u=rev[i];
for(auto v:ie[u]){
find(v);
if(dfn[sdom[mn[v]]]<dfn[sdom[u]]) sdom[u]=sdom[mn[v]];
}
f[u]=fa[u];
tr[sdom[u]].pb(u);
u=fa[u];
for(auto v:tr[u]) find(v),idom[v]=(u==sdom[mn[v]])?u:mn[v];
tr[u].clear();
}
for(int i=2;i<=n;i++) if(idom[rev[i]]!=sdom[rev[i]]) idom[rev[i]]=idom[idom[rev[i]]];
}
}
struct MF{//Max Flow
const int inf=1e9;
int n,tot=1,head[N],lv[N],cur[N],S,T,ans;
struct edge{int v,nxt,w;}e[N<<1];
void init(){n=S=T=0,tot=1,memset(head,0,sizeof(head));}
void add(int u,int v,int w){
e[++tot]=(edge){v,head[u],w};head[u]=tot;
e[++tot]=(edge){u,head[v],w^1};head[v]=tot;
}
bool bfs(){
memset(lv,-1,sizeof(lv));
queue<int> Q;Q.push(S),lv[S]=1,cur[S]=head[S];
while(!Q.empty()){
int u=Q.front();Q.pop();
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v,w=e[i].w;
if(w&&lv[v]==-1) lv[v]=lv[u]+1,cur[v]=head[v],Q.push(v);
}
}
return lv[T]!=-1;
}
int dfs(int u,int flow){
if(!flow||u==T) return flow;
int res=flow;
for(int i=cur[u];i;i=e[i].nxt){
cur[u]=i;int v=e[i].v,w=e[i].w;
if(w&&lv[v]==lv[u]+1){
int c=dfs(v,min(res,w));
res-=c,e[i].w-=c,e[i^1].w+=c;
}
if(!res) break;
}
return flow-res;
}
void Dinic(int _S,int _T){
S=_S,T=_T;ans=0;
while(bfs()) ans+=dfs(S,inf);
}
}G1,G2;
void dfscol(int u,int col){
!col?L[++lc]=u:R[++rc]=u;vis[u]=true;
for(auto v:g[u]) if(!vis[v]) dfscol(v,col^1);
}
int dfs(int u,int t){
int sz=0;t&=(u<=c);
for(auto v:G[u]){
int nw=dfs(v,t);sz+=nw;
if(t) res-=1ll*nw*nw;
}
if(t) res+=1ll*sz*sz;
return sz+(u>c&&u<=c1);
}
int main(){
// freopen("gentle.in","r",stdin);
// freopen("gentle.out","w",stdout);
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m;res=0;S=n+1,T=c=n+2;
for(int i=1,u,v;i<=m;i++) cin>>u>>v,g[u].pb(v),g[v].pb(u);
for(int i=1;i<=n;i++) if(!vis[i]) dfscol(i,0);
G1.init();G2.init();
for(int i=1;i<=lc;i++) G1.add(S,L[i],1);
for(int i=1;i<=rc;i++) G1.add(R[i],T,1);
for(int i=1;i<=lc;i++) for(auto x:g[L[i]]) G1.add(L[i],x,1);
G1.Dinic(S,T);
c1=c;
for(int i=G1.head[S];i;i=G1.e[i].nxt) G2.add(S,++c1,G1.e[i].w),G2.add(c1,G1.e[i].v,G1.e[i].w);
for(int i=G1.head[T];i;i=G1.e[i].nxt) G2.add(G1.e[i].v,++c1,G1.e[i].w^1),G2.add(c1,T,G1.e[i].w^1);
c2=c1;
for(int u=1;u<=lc;u++) for(int i=G1.head[L[u]];i;i=G1.e[i].nxt) if(G1.e[i].v!=S) G2.add(L[u],++c2,G1.e[i].w),G2.add(c2,G1.e[i].v,G1.e[i].w);
DT::init(c2);G2.S=S,G2.T=T;G2.bfs();
for(int u=1;u<=c2;u++)
for(int i=G2.head[u];i;i=G2.e[i].nxt){
int v=G2.e[i].v,w=G2.e[i].w;G2.e[i].w^=1;
if(w&&G2.lv[u]!=-1&&G2.lv[v]!=-1) DT::add(u,v);
}
swap(G2.S,G2.T);G2.bfs();
for(int u=1;u<=c2;u++)
for(int i=G2.head[u];i;i=G2.e[i].nxt){
int v=G2.e[i].v,w=G2.e[i].w;
if(w&&G2.lv[u]!=-1&&G2.lv[v]!=-1) DT::add(u,v);
}
DT::add(S,T);DT::build(S);
for(int i=2;i<=DT::idx;i++) G[DT::idom[DT::rev[i]]].pb(DT::rev[i]);
dfs(S,1);
cout<<res/2<<'\n';
return 0;
}
好题啊好题。
CF757F Team Rocket Rises Again
CF757F Team Rocket Rises Again
给定一个 \(n\) 个点,\(m\) 条边的带权无向图,和起点 \(s\)。
选一个点 \(u(u \neq s)\),使在图中删去点 \(u\) 后,有尽可能多的点到 \(s\) 的最短距离改变,输出点数。
\(1 \le n \le 2 \times 10^5,1 \le m \le 3 \times 10^5\)。
感觉也挺板的,但我没有秒掉。
很明显首先用 dijsktra 跑一个最短路,
于是我们就可以对图进行一个遍历,重新构建一个有关最短路的图,就是把在最短路上面的边用有向边建出来。
这样直接跑一个支配树就可以了。
还是统计子树大小。
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define ll long long
#define pii pair<ll,int>
#define fi first
#define se second
const int N=1e6+5;
const ll inf=1e18;
int n,m,S,head[N],tot=0;
ll d[N];
struct edge{int v,nxt;ll w;}e[N<<1];
bool vis[N];
namespace DT{
int fa[N],f[N],idom[N],sdom[N],dfn[N],n,idx=0,rev[N],mn[N],ans[N],mx=0;
vector<int> e[N],ie[N],tr[N];
void add(int u,int v){e[u].pb(v),ie[v].pb(u);}
void init(int _n){
n=_n;idx=mx=0;
for(int i=1;i<=n;i++){
dfn[i]=rev[i]=idom[i]=fa[i]=0;
f[i]=sdom[i]=mn[i]=i;
e[i].clear(),ie[i].clear(),tr[i].clear();
}
}
void dfsp(int u){
dfn[u]=++idx;rev[idx]=u;
for(auto v:e[u]) if(!dfn[v]) fa[v]=u,dfsp(v);
}
int find(int x){
if(x==f[x]) return x;
int res=find(f[x]);
if(dfn[sdom[mn[f[x]]]]<dfn[sdom[mn[x]]]) mn[x]=mn[f[x]];
return f[x]=res;
}
int build(int rt){
dfsp(rt);
for(int i=idx,u;i>1;i--){
u=rev[i];
for(auto v:ie[u]){
find(v);
if(dfn[sdom[mn[v]]]<dfn[sdom[u]]) sdom[u]=sdom[mn[v]];
}
f[u]=fa[u];
tr[sdom[u]].pb(u);
u=fa[u];
for(auto v:tr[u]) find(v),idom[v]=(sdom[mn[v]]==u)?u:mn[v];
tr[u].clear();
}
for(int i=idx;i>=2;i--) ans[idom[rev[i]]]+=++ans[rev[i]];
++ans[rev[1]];
for(int i=1;i<=n;i++) if(i!=rt) mx=max(mx,ans[i]);
return mx;
}
}
void add(int u,int v,ll w){
e[++tot]=(edge){v,head[u],w};head[u]=tot;
e[++tot]=(edge){u,head[v],w};head[v]=tot;
}
void dij(){
for(int i=1;i<=n;i++) d[i]=inf;
priority_queue<pii> Q;
Q.push({0,S});d[S]=0;
while(!Q.empty()){
int u=Q.top().se;Q.pop();
if(vis[u]) continue;
vis[u]=true;
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v;ll w=e[i].w;
if(d[v]>d[u]+w){
d[v]=d[u]+w;
Q.push({-d[v],v});
}
}
}
}
void build(){
dij();
for(int u=1;u<=n;u++)
for(int i=head[u];i;i=e[i].nxt){
int v=e[i].v;ll w=e[i].w;
if(d[v]==d[u]+w) DT::add(u,v);
}
}
int main(){
/*2024.1.23 H_W_Y CF757F Team Rocket Rises Again Dominator Tree*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m>>S;DT::init(n);
for(int i=1,u,v,w;i<=m;i++) cin>>u>>v>>w,add(u,v,w);
build();
cout<<DT::build(S)<<'\n';
return 0;
}
P7520 [省选联考 2021 A 卷] 支配
给定一张 \(n\) 个点 \(m\) 条边的有向图 \(G\),其顶点从 \(1\) 到 \(n\) 编号。
对于任意两个点 \(u, v\),若从顶点 \(1\) 出发到达顶点 \(v\) 的所有路径都需要经过顶点 \(u\),则称顶点 \(u\) 支配顶点 \(v\)。特别地,每个顶点支配其自身。
对于任意一个点 \(v\),我们将图中支配顶点 \(v\) 的顶点集合称为 \(v\) 的受支配集 \(D_v\)。
现在有 \(q\) 次互相独立的询问,每次询问给出一条有向边,请你回答在图 \(G\) 中加入该条边后,有多少个顶点的受支配集发生了变化。
\(1 \le n \le 3 \times {10}^3\),\(1 \le m \le 2 \times n\),\(1 \le q \le 2 \times {10}^4\)。
首先很明显可以先建立一棵支配树。
于是可以手玩一下样例。
容易发现,对于一条加入的边 \(x \to y\),
我们希望的就是有多少个点从 \(1\) 出发走到 \(i\) 可以不经过 \(idom_i\),这样答案的贡献就是 \(i\) 的子树大小。
发现这个东西是容易预处理出来的,对于每一个节点,我们预处理出从 \(1\) 出发,不经过 \(i\) 能到的点,
从 \(i\) 出发,走反边而不经过 \(idom_i\) 能到达的点,
那么实际的判断中如果 \(1\) 能不经过 \(idom_i\) 走到 \(x\) 并且 \(i\) 能通过反边不经过 \(idom_i\) 走到 \(y\),那么这个点就是会改变的。
这样最后再进行一次 dfs 统计答案即可。
时间复杂度 \(\mathcal O(n \log n + nq)\),比较板。
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
const int N=5e3+5;
int n,m,Q;
vector<int> G[N];
namespace DT{
int n,idx=0,dfn[N],fa[N],f[N],rev[N],sdom[N],mn[N],idom[N];
vector<int> e[N],ie[N],tr[N];
void add(int u,int v){e[u].pb(v),ie[v].pb(u);}
void init(int _n){
n=_n;idx=0;
for(int i=1;i<=n;i++){
mn[i]=sdom[i]=f[i]=i;
fa[i]=dfn[i]=rev[i]=idom[i]=0;
e[i].clear(),ie[i].clear(),tr[i].clear();
}
}
void dfsp(int u){
dfn[u]=++idx;rev[idx]=u;
for(auto v:e[u]) if(!dfn[v]) dfsp(v),fa[v]=u;
}
int find(int x){
if(x==f[x]) return x;
int res=find(f[x]);
if(dfn[sdom[mn[f[x]]]]<dfn[sdom[mn[x]]]) mn[x]=mn[f[x]];
return f[x]=res;
}
void build(int rt){
dfsp(rt);
for(int i=idx;i>1;i--){
int u=rev[i];
for(auto v:ie[u]){
find(v);
if(dfn[sdom[mn[v]]]<dfn[sdom[u]]) sdom[u]=sdom[mn[v]];
}
f[u]=fa[u];
tr[sdom[u]].pb(u);
u=fa[u];
for(auto v:tr[u]) find(v),idom[v]=(sdom[mn[v]]==u)?u:mn[v];
tr[u].clear();
}
for(int i=2;i<=idx;i++) if(idom[rev[i]]!=sdom[rev[i]]) idom[rev[i]]=idom[idom[rev[i]]];
for(int i=2;i<=idx;i++) G[idom[rev[i]]].pb(rev[i]);
}
}
bitset<N> g1[N],g2[N],vis,tg;
int sz[N],ans=0;
void dfs1(int u,int nw){
vis[u]=g1[nw][u]=1;
for(auto v:(DT::e[u])) if(!vis[v]&&v!=nw) dfs1(v,nw);
}
void dfs2(int u,int nw,int id){
vis[u]=g2[id][u]=1;
for(auto v:(DT::ie[u])) if(!vis[v]&&v!=nw) dfs2(v,nw,id);
}
void dfs(int u){
sz[u]=1;
for(auto v:G[u]) dfs(v),sz[u]+=sz[v];
}
void getans(int u){
if(tg[u]) return ans+=sz[u],void();
for(auto v:G[u]) getans(v);
}
int main(){
freopen("dominator.in","r",stdin);
freopen("dominator.out","w",stdout);
/*2024.1.30 H_W_Y P7520 [省选联考 2021 A 卷] 支配 Dominator Tree*/
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>n>>m>>Q;DT::init(n);
for(int i=1,u,v;i<=m;i++) cin>>u>>v,DT::add(u,v);
DT::build(1);
for(int i=2;i<=n;i++)
vis.reset(),dfs1(1,i),vis.reset(),dfs2(i,DT::idom[i],i);
dfs(1);
while(Q--){
ans=0;int x,y;
cin>>x>>y;tg.reset();
for(int i=2;i<=n;i++) if(g1[DT::idom[i]][x]&&g2[i][y]) tg[i]=1;
getans(1);
cout<<ans<<'\n';
}
return 0;
}
Conclusion
支配树可以帮我们解决询问有多少对点他们存在一条都到 \(S\) 的路径,两路径不相交。