说实话冬令营之前我都没见过支配树的题,也不知道这是个什么东西(但是学完之后感觉这个东西已经烂大街了qaq)
支配树主要能干这么一件事:
给定起点r,对于一个终点x,如果r到x的所有路径都必须经过y,我们称y支配x
设支配x的点集
S
=
p
1
,
p
2
,
p
3.....
p
k
S=p1,p2,p3.....pk
S=p1,p2,p3.....pk,
其在r到x的任意一条路径上的排列为
r
.
.
.
p
1...
p
2......
p
k
.
.
.
x
r...p1...p2......pk...x
r...p1...p2......pk...x,
支配树能够对每个点x
(
x
≠
r
)
(x \neq r)
(x=r)求出
p
k
pk
pk,简单的讲就是对每个点x,求出支配他的所有点中离他最近的点idom[x]
如果每个点x连出指向idom[x],那么就构成了一棵内向树,我们称他为支配树
支配树有很多食用方法,OI中常见的似乎是求出idom[x]后利用性质什么的做dp或者做一些判断性问题
然后关于支配树的建树,有一种比较简单的O((n+m)n)做法, 就是枚举删掉每个点p,然后从起点r遍历一次图G,求出不能到哪些点q,那么p就支配了q
但是tarjan发明了一种更高效的算法Lengauer-Tarjan,他能够在
O
(
n
α
(
n
)
)
O(n\alpha(n))
O(nα(n))(emmmmmm好像可以被卡到logn,但他好像又介绍了另一种不会被卡,能取到
α
\alpha
α的方法当然啦我不会qaq)
接下来讲一下这个算法
先遍历一遍图G,求出每个点的dfs序,下文为了方便默认每个点p的dfs序dfsi[p]=p,下文中节点编号的比较指的都是dfs序编号
为了求idom[x],我们要定义一个辅助的东西sdom[x] (
s
e
m
i
−
d
o
m
i
n
a
t
o
r
semi-dominator
semi−dominator),他的定义是dfs序最小的,满足p到x存在一条路径
P
=
p
,
a
1
,
a
2......
a
k
,
x
其中
∀
a
i
>
x
,
k
>
=
0
P=p,a1,a2......ak,x\ \ 其中\forall a_i>x,k>=0
P=p,a1,a2......ak,x 其中∀ai>x,k>=0 的点p,这个东西可以理解为绕了一条不那么直接的路,他往往形如这样
图中u是sdom[x],标红的路径是”直接的路径“也就是搜索树上直接从u走到x的路径,标绿的路径即上述的路径
P
P
P,也就是不那么直接的路径,他往往是像这样从u走了一些树边(或者前向边),走一条横叉边,再走一些返祖边(后向边)到达x,所经过的点dfs序都比x大(除起点终点外)
为什么要求这个东西?因为有一些关于这个的结论(可以自己yy或者画个图意会一下,严谨的证明我不太会)
(下文中点x指的都是除了起点r之外的点,祖先指的都是搜索树上的祖先)
结论1:sdom[x]一定是x的祖先
结论2:idom[x]是sdom[x]或者是sdom[x]的祖先
结论3:sdom[x]< x
这两个结论说明了sdom[x]有潜力成为idom[x]
然后下面的两个结论说明了怎么求sdom[x]和idom[x](结论5我连不严谨的证明都不太会qaq)
结论4:
s
d
o
m
[
x
]
=
m
i
n
{
{
v
∣
v
是
x
的父亲
}
⋁
{
s
d
o
m
[
p
]
∣
p
>
x
,
v
>
x
,
(
v
,
x
)
∈
G
,
p
是
v
的祖先
}
}
sdom[x]=min\{ \{v|v是x的父亲\} \bigvee \{ sdom[p]\ |\ p>x,v>x,(v,x)\in G,p是v的祖先\} \}
sdom[x]=min{{v∣v是x的父亲}⋁{sdom[p] ∣ p>x,v>x,(v,x)∈G,p是v的祖先}}
结论5:
令 𝑃 为搜索树上,𝑠𝑑𝑜𝑚[𝑥]到𝑥的路径上,除了𝑠𝑑𝑜𝑚[𝑥]之外的所有点的点集
找到𝑢∈𝑃, 且 𝑠𝑑𝑜𝑚[𝑢] 是 𝑃 中所有点𝑠𝑑𝑜𝑚 的最小值
若
s
d
o
m
[
u
]
=
s
d
o
m
[
x
]
sdom[u]=sdom[x]
sdom[u]=sdom[x],则
i
d
o
m
[
x
]
=
s
d
o
m
[
x
]
idom[x]=sdom[x]
idom[x]=sdom[x]
否则
i
d
o
m
[
x
]
=
i
d
o
m
[
u
]
idom[x]=idom[u]
idom[x]=idom[u]
有了这些结论我们就可以开始Lengauer-Tarjan算法了
要维护的东西
par[x],x的父亲
pred[x],有边连向x的点集
V[x],以x为sdom的点集
要资瓷2种操作
link(x,y),将y接到x的子树下
query(x),询问x到其所在树根路径上sdom的最小值,和拥有这个最小值的点
这两种操作可以用带权并查集做到复杂度均摊
O
(
α
(
n
)
)
O(\alpha(n))
O(α(n))(这里不讨论出题人恶意卡并查集qaq)
具体实现:
(1)为了方便可以先将每个点x的idom和sdom初始化为x
(2)先dfs一遍原图,求出每个点的dfs序
然后按dfs序从大到小处理每个点x,
3)将x的pred里面的每个点y取出来,query(y),并尝试用y到根的sdom最小值去更新sdom[x](这样做在y=par[x]时还y< x,未访问y,y所在的树只有自己,返回的最小值就是y,如果y>x,y已经访问过,按照下文(5)的link方式,这2种情况就是结论4)
(4)接着取出x的V[x],因为按dfs序从大到小处理点,此时x的子树中所有点都已经处理完,对于以x为sdom的每个点y,按照(5)的link方式,y到x的树边路径上符合结论5的点v在并查集中都成为了y的祖先,query(y)之后我们能找到u,按照结论5,若sdom[u]=x,idom[y]=x,否则idom[y]=idom[u],此时我们并不知道idom[u]是几,实现上我们可以先令idom[y]=u,在(7)中求出idom[y]
(5)此时已经处理完了点x的信息,可以将x在并查集中与其孩子合并了,枚举x的出边(x,y),若par[y]=x,则这条是树边,link(x,y)
(6)把x加入到sdom[x]的集合V中
(7)按dfs序从大到小处理完后,我们再从小到大,对于每个点x,若idom[x]!=sdom[x],那么此时的idom[x]就是(4)中标记的u,就令idom[x]=idom[idom[x]]
然后我们就建出dominator tree啦
支配树的题要特别注意多组数据的清空和图不连通的情况qaq
然后是几道例题
hdu4694,建出支配树后做个简单的dp,注意图有不连通的情况
code:
#include<set>
#include<map>
#include<deque>
#include<queue>
#include<stack>
#include<cmath>
#include<ctime>
#include<bitset>
#include<string>
#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<complex>
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;
const int maxn = 51000;
const int maxm = 110000;
int n,m;
int sdom[maxn],idom[maxn];
struct edge{int y,nex;}a[maxm]; int len,fir[maxn];
inline void ins(const int x,const int y){a[++len]=(edge){y,fir[x]};fir[x]=len;}
int par[maxn],dfn[maxn],To[maxn],id;
void build(const int x)
{
To[dfn[x]=++id]=x;
for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(!dfn[y])
par[y]=x,build(y);
}
int fa[maxn],fas[maxn];
void findfa(const int x)
{
if(fa[x]==x) return;
findfa(fa[x]);
if(dfn[sdom[fas[fa[x]]]]<dfn[sdom[fas[x]]]) fas[x]=fas[fa[x]];
fa[x]=fa[fa[x]];
}
vector<int>pred[maxn],V[maxn];
ll ans[maxn];
int main()
{
//freopen("tmp.in","r",stdin);
//freopen("tmp.out","w",stdout);
while(scanf("%d%d",&n,&m)!=EOF)
{
len=id=0; for(int i=1;i<=n;i++) fir[i]=0,dfn[i]=0,par[i]=0;
for(int i=1;i<=n;i++) pred[i].clear(),V[i].clear();
for(int i=1;i<=m;i++)
{
int x,y; scanf("%d%d",&x,&y);
ins(x,y); pred[y].push_back(x);
}
build(n);
for(int i=1;i<=n;i++) fa[i]=i,fas[i]=sdom[i]=i,idom[i]=i;
for(int i=id;i>=1;i--)
{
int x=To[i],&semi=sdom[x];
for(int j=0;j<pred[x].size();j++)
{
int y=pred[x][j]; if(!dfn[y]) continue;
findfa(y);
if(dfn[sdom[fas[y]]]<dfn[semi]) semi=sdom[fas[y]];
}
for(int j=0;j<V[x].size();j++)
{
int y=V[x][j]; findfa(y);
if(dfn[sdom[fas[y]]]<i) idom[y]=fas[y];
else idom[y]=x;
}
for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(par[y]==x)
fa[y]=x;
V[semi].push_back(x);
}
for(int i=1;i<=id;i++)
{
int x=To[i];
if(idom[x]!=sdom[x]) idom[x]=idom[idom[x]];
}
for(int i=1;i<=id;i++) ans[To[i]]=ans[idom[To[i]]]+To[i];
for(int i=1;i<=n;i++) if(!dfn[i]) ans[i]=0;
for(int i=1;i<n;i++) printf("%lld ",ans[i]),ans[i]=0;
printf("%lld\n",ans[n]),ans[n]=0;
}
return 0;
}
BZOJ2815: [ZJOI2012]灾难
其实就是求支配树的子树节点数
新建一个点作为起点,连向所有生产者,建支配树
这是个DAG,所以也可以不用这个算法直接跑每个点pred里面的点在支配树上的LCA
code:
#include<set>
#include<map>
#include<deque>
#include<queue>
#include<stack>
#include<cmath>
#include<ctime>
#include<bitset>
#include<string>
#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<complex>
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;
inline void read(int &x)
{
char c; while(!((c=getchar())>='0'&&c<='9'));
x=c-'0';
while((c=getchar())>='0'&&c<='9') (x*=10)+=c-'0';
}
const int maxn = 110000;
const int maxm = 2100000;
int n;
int sdom[maxn],idom[maxn];
vector<int>V[maxn];
struct edge{int y,nex;}a[maxm]; int len,fir[maxn];
inline void ins(const int x,const int y){a[++len]=(edge){y,fir[x]};fir[x]=len;}
vector<int>pred[maxn];
int dfn[maxn],To[maxn],id,par[maxn];
void build(const int x)
{
To[dfn[x]=++id]=x;
for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(!dfn[y])
par[y]=x,build(y);
}
int fa[maxn],fas[maxn];
void findfa(const int x)
{
if(fa[x]==x) return;
findfa(fa[x]);
if(dfn[sdom[fas[fa[x]]]]<dfn[sdom[fas[x]]]) fas[x]=fas[fa[x]];
fa[x]=fa[fa[x]];
}
int ans[maxn];
int main()
{
//freopen("tmp.in","r",stdin);
//freopen("tmp.out","w",stdout);
read(n);
for(int i=1;i<=n;i++)
{
int x,t=0;
while(1)
{
read(x); if(!x) break;
++t;
ins(x,i),pred[i].push_back(x);
}
if(!t) ins(n+1,i),pred[i].push_back(n+1);
}
build(n+1);
for(int i=1;i<=n+1;i++) fa[i]=i,fas[i]=i,sdom[i]=idom[i]=i;
for(int i=id;i>=1;i--)
{
int x=To[i],&semi=sdom[x];
for(int j=0;j<pred[x].size();j++)
{
int y=pred[x][j]; if(!dfn[y]) continue;
findfa(y);
if(dfn[semi]>dfn[sdom[fas[y]]]) semi=sdom[fas[y]];
}
for(int j=0;j<V[x].size();j++)
{
int y=V[x][j]; findfa(y);
if(dfn[sdom[fas[y]]]<i) idom[y]=fas[y];
else idom[y]=x;
}
V[semi].push_back(x);
for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(par[y]==x)
fa[y]=x;
}
for(int i=1;i<=id;i++)
{
int x=To[i];
if(idom[x]!=sdom[x]) idom[x]=idom[idom[x]];
}
for(int i=id;i>1;i--)
{
int x=To[i];
ans[idom[x]]+=ans[x]+1;
}
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
return 0;
}
BZOJ3281: 小P的烦恼
“为了方便”我把边看作成一个点,边权附到了点权上,然后必经边也可以看作支配点(事实证明我这样把这个问题变复杂了)
建出支配树后,把出发点到终点路径上的支配点取出来做个前缀后缀dp,因为必经点必经的性质,相邻必经点之间的最短距离可以直接从起点跑一边dijskral后用distance[b]-distance[a]算得,如果是直接做这个问题,dp部分应该挺简单的,但是我把边拆成点后还要处理一个点不完全被绳覆盖的情况,还有两条绳拼在一个点上的情况…完全变复杂了qwq
同样的注意图不连通的情况
(还有为啥我的程序跑的这么慢…)
code:
#include<set>
#include<map>
#include<deque>
#include<queue>
#include<stack>
#include<cmath>
#include<ctime>
#include<bitset>
#include<string>
#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<complex>
#include<iostream>
#include<algorithm>
#define ll long long
#define inf 1e9
using namespace std;
inline void read(int &x)
{
char c; while(!((c=getchar())>='0'&&c<='9'));
x=c-'0';
while((c=getchar())>='0'&&c<='9') (x*=10)+=c-'0';
}
const int maxn = 310000;
const int maxm = 410000;
int n,m,N,S,T,l;
int val[maxn];
struct edge{int y,nex;}a[maxm]; int len,fir[maxn];
inline void ins(const int x,const int y){a[++len]=(edge){y,fir[x]};fir[x]=len;}
vector<int>pred[maxn];
struct node
{
int x,i;
friend inline bool operator <(const node x,const node y){return x.x>y.x;}
};
priority_queue<node>q;
int dis[maxn];
void dij()
{
for(int i=1;i<=N;i++) dis[i]=inf;
dis[S]=0; q.push((node){0,S});
while(!q.empty())
{
const node now=q.top(); q.pop();
int x=now.i;
for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(dis[y]==inf)
dis[y]=dis[x]+val[y],q.push((node){dis[y],y});
}
}
int dfn[maxn],To[maxn],id,par[maxn];
void build(const int x)
{
To[dfn[x]=++id]=x;
for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(!dfn[y])
par[y]=x,build(y);
}
int idom[maxn],sdom[maxn];
vector<int>V[maxn];
int fa[maxn],fas[maxn];
void findfa(const int x)
{
if(fa[x]==x) return;
findfa(fa[x]);
if(dfn[sdom[fas[fa[x]]]]<dfn[sdom[fas[x]]]) fas[x]=fas[fa[x]];
fa[x]=fa[fa[x]];
}
int t[maxn],tp;
int f[maxn],g[maxn];
int main()
{
//freopen("tmp.in","r",stdin);
//freopen("tmp.out","w",stdout);
int tcase; scanf("%d",&tcase);
while(tcase--)
{
read(n),read(m),read(S),read(T),read(l); S++,T++; N=n+m;
len=id=0; for(int i=1;i<=N;i++) fir[i]=dfn[i]=par[i]=val[i]=0;
for(int i=1;i<=N;i++) pred[i].clear(),V[i].clear();
for(int i=1;i<=m;i++)
{
int x,y,c; read(x),read(y),read(c);
x++,y++;
val[n+i]=c;
ins(x,n+i),pred[n+i].push_back(x);
ins(n+i,y),pred[y].push_back(n+i);
}
dij();
if(dis[T]==inf) { puts("-1");continue; }
build(S);
for(int i=1;i<=N;i++) fa[i]=fas[i]=idom[i]=sdom[i]=i;
for(int i=id;i>=1;i--)
{
int x=To[i],&semi=sdom[x];
for(int j=0;j<pred[x].size();j++)
{
int y=pred[x][j]; if(!dfn[y]) continue;
findfa(y);
if(dfn[sdom[fas[y]]]<dfn[semi]) semi=sdom[fas[y]];
}
for(int j=0;j<V[x].size();j++)
{
int y=V[x][j]; findfa(y);
if(dfn[sdom[fas[y]]]<i) idom[y]=fas[y];
else idom[y]=x;
}
V[semi].push_back(x);
for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(par[y]==x)
fa[y]=x;
}
for(int i=2;i<=id;i++)
{
int x=To[i];
if(idom[x]!=sdom[x]) idom[x]=idom[idom[x]];
}
tp=0; int tx=T;
while(t[tp]!=S) t[++tp]=tx,tx=idom[tx];
t[tp+1]=0;
int L,R,temp=0; L=1;
for(R=1;R<=tp;R++)
{
temp+=L!=R?val[t[R]]:0;
while(dis[t[L]]-dis[t[R]]+val[t[R]]-val[t[L]]>l) temp-=val[t[++L]];
int cc=temp+min(l-(dis[t[L]]-dis[t[R]]+val[t[R]]-val[t[L]]),val[t[L]]);
f[R]=max(f[R-1],cc);
}
g[tp+1]=0; R=tp; temp=0;
for(L=tp;L>=1;L--)
{
temp+=L!=R?val[t[L]]:0;
while(dis[t[L]]-dis[t[R]]>l) temp-=val[t[--R]];
int cc=temp+min(l-(dis[t[L]]-dis[t[R]]),val[t[R]]);
g[L]=max(g[L+1],cc);
}
int ans=0;
for(int i=1;i<=tp;i++) ans=max(ans,f[i-1]+g[i]);
l=2*l;
g[tp+1]=0; R=tp; temp=0;
for(L=tp;L>=1;L--)
{
temp+=L!=R?val[t[L]]:0;
while(dis[t[L]]-dis[t[R]]>l) temp-=val[t[--R]];
int cc=temp+min(l-(dis[t[L]]-dis[t[R]]),val[t[R]]);
g[L]=max(g[L+1],cc);
}
ans=max(ans,g[1]);
ans=-ans;for(int i=1;i<=tp;i++) ans+=val[t[i]];
printf("%d\n",ans);
}
return 0;
}
2014-2015 ACM-ICPC, NEERC, Southern Subregional Contest Problem L
Link
给定起点后判哪些边可能在一条简单路径上
考虑搜索树中的几类边,树边,前向边,横叉边显然都可以在一条简单路径上,唯一要考虑的就是返祖边(后向边)是否可能在一条简单路径上,对于一条返祖边(u,v),如果v不是u的必经点,就存在一条简单路径否则不存在,证明显然
建出支配树后就是一个判断v是不是u的祖先的问题
还有注意图不连通的情况,以及答案边集为空的话输出的第二行要是一个空行…
code:
#include<set>
#include<map>
#include<deque>
#include<queue>
#include<stack>
#include<cmath>
#include<ctime>
#include<bitset>
#include<string>
#include<vector>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<complex>
#include<iostream>
#include<algorithm>
#define ll long long
using namespace std;
const int maxn = 410000;
const int maxm = 410000;
const int maxd = 20;
int n,m;
bool ve[maxm];
int e[maxm][2];
struct edge{int y,i,nex;}a[maxm]; int len,fir[maxn];
inline void ins(const int x,const int y,const int i){a[++len]=(edge){y,i,fir[x]};fir[x]=len;}
vector<int>pred[maxn];
int dfn[maxn],id,To[maxn],par[maxn];
bool insta[maxn];
void build(const int x)
{
To[dfn[x]=++id]=x; insta[x]=true;
for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y)
{
if(!dfn[y]) par[y]=x,build(y);
else if(insta[y]) ve[a[k].i]=true;
}
insta[x]=false;
}
vector<int>V[maxn];
int idom[maxn],sdom[maxn];
int fa[maxn],fas[maxn];
void findfa(const int x)
{
if(fa[x]==x) return;
findfa(fa[x]);
if(dfn[sdom[fas[fa[x]]]]<dfn[sdom[fas[x]]]) fas[x]=fas[fa[x]];
fa[x]=fa[fa[x]];
}
int dep[maxn],top[maxn][maxd];
void dfs(const int x)
{
for(int i=1;(1<<i)<=dep[x];i++) top[x][i]=top[top[x][i-1]][i-1];
for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y)
dep[y]=dep[x]+1,top[y][0]=x,dfs(y);
}
int ans[maxn],an;
int main()
{
//freopen("tmp.in","r",stdin);
//freopen("tmp.out","w",stdout);
while(scanf("%d%d",&n,&m)!=EOF)
{
len=id=0; for(int i=1;i<=n;i++) fir[i]=dfn[i]=par[i]=0;
for(int i=1;i<=n;i++) pred[i].clear(),V[i].clear();
for(int i=1;i<=m;i++)
{
ve[i]=false;
int x,y; scanf("%d%d",&x,&y);
e[i][0]=x,e[i][1]=y;
ins(x,y,i),pred[y].push_back(x);
}
build(1);
for(int i=1;i<=n;i++) fa[i]=fas[i]=idom[i]=sdom[i]=i;
for(int i=id;i>=1;i--)
{
int x=To[i],&semi=sdom[x];
for(int j=0;j<pred[x].size();j++)
{
int y=pred[x][j]; if(!dfn[y]) continue;
findfa(y);
if(dfn[sdom[fas[y]]]<dfn[semi]) semi=sdom[fas[y]];
}
for(int j=0;j<V[x].size();j++)
{
int y=V[x][j]; findfa(y);
if(dfn[sdom[fas[y]]]<i) idom[y]=fas[y];
else idom[y]=x;
}
V[semi].push_back(x);
for(int k=fir[x],y=a[k].y;k;k=a[k].nex,y=a[k].y) if(par[y]==x)
fa[y]=x;
}
len=0;for(int i=1;i<=n;i++) fir[i]=0;
for(int i=2;i<=id;i++)
{
int x=To[i];
if(idom[x]!=sdom[x]) idom[x]=idom[idom[x]];
ins(idom[x],x,0);
}
dep[1]=1; dfs(1);
an=0; for(int i=1;i<=m;i++)
{
if(!dfn[e[i][0]]) continue;
if(!ve[i]) ans[++an]=i;
else
{
int x=e[i][0],y=e[i][1];
for(int j=maxd-1;j>=0;j--) if(dep[x]-dep[y]>=(1<<j))
x=top[x][j];
if(x!=y) ans[++an]=i;
}
}
printf("%d\n",an);
for(int i=1;i<an;i++) printf("%d ",ans[i]);
if(an) printf("%d",ans[an]);
puts("");
}
return 0;
}