【学习笔记】基环树
基环树
无向图
- 若一个无向连通图有 \(n\) 个点和 \(n\) 条边,即是在树上加一条边后构成的图中恰好包含一个环的图,则称它是一棵基环树。
- 若一个无向图有 \(n\) 个点和 \(n\) 条边,即是由若干棵基环树组成的森林,则称它是一个基环树森林。
有向图
- 若一个有向连通图有 \(n\) 个点和 \(n\) 条边,每个节点有且仅有一条入边,则称它是一棵基环外向树。
- 若一个有向连通图有 \(n\) 个点和 \(n\) 条边,每个节点有且仅有一条出边,则称它是一棵基环内向树。
- 基环内向树和基环外向树统称基环树。
- 若一个有向图有 \(n\) 个点和 \(n\) 条边,每个节点有且仅有一条出(入)边,即是由若干棵基环外(内)向树组成的森林,则称它是一个基环外(内)向树森林。
例题
luogu P8655 [蓝桥杯 2017 国 B] 发现环
-
基环树找环板子。
-
\(DFS+\) 并查集
点击查看代码
-
\(Tarjan\)
点击查看代码
struct node { int nxt,to; }e[200001]; int head[200001],dfn[200001],low[200001],ins[200001],c[200001],cnt=0,tot=0,ans=0,rt=0; vector<int>scc[200001]; stack<int>s; void add(int u,int v) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; head[u]=cnt; } void tarjan(int x,int fa) { int i,k=0; tot++; dfn[x]=low[x]=tot; ins[x]=1; s.push(x); for(i=head[x];i!=0;i=e[i].nxt) { if(e[i].to!=fa) { if(dfn[e[i].to]==0) { tarjan(e[i].to,x); low[x]=min(low[x],low[e[i].to]); } else { if(ins[e[i].to]==1) { low[x]=min(low[x],dfn[e[i].to]); } } } } if(dfn[x]==low[x]) { ans++; while(x!=k) { k=s.top(); ins[k]=0; c[k]=ans; scc[ans].push_back(k); s.pop(); } if(scc[ans].size()>=2) { rt=ans; } } } int main() { int n,i,u,v; cin>>n; for(i=1;i<=n;i++) { cin>>u>>v; add(u,v); add(v,u); } for(i=1;i<=n;i++) { if(dfn[i]==0) { tarjan(i,0); } } sort(scc[rt].begin(),scc[rt].end()); for(i=0;i<scc[rt].size();i++) { cout<<scc[rt][i]<<" "; } return 0; }
-
拓扑排序
点击查看代码
CF131D Subway
luogu P2921 [USACO08DEC] Trick or Treat on the Farm G
-
判断是否有自环。若没有则为基环内向树。
-
若节点 \(x\) 在环上,则答案为环的大小。
-
若节点 \(x\) 不在环上,则答案为节点 \(x\) 到环的距离 \(+\) 环的大小。
点击查看代码
struct node { int next,to; }e[100010]; stack<int>s; int head[100010],dfn[100010],low[100010],ins[100010],scc[100010],c[100010],u[100010],v[100010],dis[100010],cnt=0,tot=0,ans=0; void add(int u,int v) { cnt++; e[cnt].next=head[u]; e[cnt].to=v; head[u]=cnt; } void tarjan(int x) { int i,k=0; tot++; dfn[x]=low[x]=tot; ins[x]=1; s.push(x); for(i=head[x];i!=0;i=e[i].next) { if(dfn[e[i].to]==0) { tarjan(e[i].to); low[x]=min(low[x],low[e[i].to]); } else { if(ins[e[i].to]==1) { low[x]=min(low[x],dfn[e[i].to]); } } } if(dfn[x]==low[x]) { ans++; while(x!=k) { k=s.top(); ins[k]=0; c[k]=ans; scc[ans]++; s.pop(); } } } void dfs(int rt,int x,int sum) { if(dis[x]==0) { dfs(rt,v[x],sum+1); } else { dis[rt]=dis[x]+sum; } } int main() { int n,i,j,sum; cin>>n; for(i=1;i<=n;i++) { u[i]=i; cin>>v[i]; add(u[i],v[i]); } for(i=1;i<=n;i++) { if(dfn[i]==0) { tarjan(i); } } for(i=1;i<=n;i++) { if(u[i]==v[i]) { dis[i]=1; } else { dis[i]=(scc[c[u[i]]]>=2)*scc[c[u[i]]]; } } for(i=1;i<=n;i++) { if(dis[i]==0) { dfs(u[i],v[i],1); } } for(i=1;i<=n;i++) { cout<<dis[i]<<endl; } return 0; }
luogu P2607 [ZJOI2008] 骑士
-
将自己最痛恨的骑士向自己连一条有向边,这样就形成了基环外向树森林。
-
基环外向树森林内每棵基环外向树是相互独立的,需要单独处理。
-
对于每棵基环外向树,任取环上一点 \(x\) ,断开 \(x\) 到 \(fa_{x}\) 的有向边,外向树就变成了一棵以 \(x\) 为根的树。
-
设 \(f_{x,0/1}\) 表示 \(x\) 不出征/出征时,以 \(x\) 为根的子树的最大战斗力,状态转移方程为 \(\begin{cases} f_{x,0}=\sum\limits_{y \in Son(x)}\max(f_{y,0},f_{y,1}) \\ f_{x,1}=w_{x}+\sum\limits_{y \in Son(x)}f_{y,0} \end{cases}\) 。
- 状态转移方程同 luogu P1352 没有上司的舞会 。
-
将原问题拆成两部分。第一部分为 \(x\) 不限制 \(fa_{x}\) ,断开 \(x\) 到 \(fa_{x}\) 的有向边,在以 \(x\) 为根的树上进行 \(DP\) ,得到的结果为 \(\max(f_{x,0},f_{x,1})\) ;第二部分为 \(x\) 限制 \(fa_{x}\) 。故再次以 \(fa_{x}\) 为根进行 \(DP\) ,得到的结果为 \(f_{fa_{x},0}\) 。这两部分合起来能够覆盖整个问题,故二者取 \(\max\) 即可。
点击查看代码
struct node { ll nxt,to; }e[1000010]; ll head[1000010],vis[1000010],u[1000010],v[1000010],w[1000010],f[1000010][2],rt,cnt=0; void add(ll u,ll v) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; head[u]=cnt; } ll dfs_huan(ll x) { vis[x]=1; return (vis[u[x]]==1)?x:dfs_huan(u[x]); } void dfs(ll x) { vis[x]=1; f[x][0]=0; f[x][1]=w[x]; for(ll i=head[x];i!=0;i=e[i].nxt) { if(e[i].to==rt) { f[e[i].to][1]=-0x3f3f3f3f; } else { dfs(e[i].to); f[x][0]+=max(f[e[i].to][0],f[e[i].to][1]); f[x][1]+=f[e[i].to][0]; } } } int main() { ll n,ans=0,maxx,i; cin>>n; for(i=1;i<=n;i++) { v[i]=i; cin>>w[i]>>u[i]; add(u[i],v[i]); } for(i=1;i<=n;i++) { if(vis[i]==0) { rt=dfs_huan(i); dfs(rt); maxx=max(f[rt][0],f[rt][1]); rt=u[rt]; dfs(rt); ans+=max(maxx,f[rt][0]); } } cout<<ans<<endl; return 0; }
luogu P1453 城市环路
-
处理下无向图中的环即可。
点击查看代码
struct node { ll nxt,to; }e[200010]; ll head[200010],vis[200010],dfn[200010],low[200010],ins[200010],c[200010],w[200010],f[200010][2],id,rt,cnt=0,tot=0,ans=0; vector<ll>scc[200010]; stack<ll>s; void add(ll u,ll v) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; head[u]=cnt; } void tarjan(int x,int fa) { int i,k=0; tot++; dfn[x]=low[x]=tot; ins[x]=1; s.push(x); for(i=head[x];i!=0;i=e[i].nxt) { if(e[i].to!=fa) { if(dfn[e[i].to]==0) { tarjan(e[i].to,x); low[x]=min(low[x],low[e[i].to]); } else { if(ins[e[i].to]==1) { low[x]=min(low[x],dfn[e[i].to]); } } } } if(dfn[x]==low[x]) { ans++; while(x!=k) { k=s.top(); ins[k]=0; c[k]=ans; scc[ans].push_back(k); s.pop(); } if(scc[ans].size()>=2) { id=ans; } } } void dfs(ll x,ll fa) { vis[x]=1; f[x][0]=0; f[x][1]=w[x]; for(ll i=head[x];i!=0;i=e[i].nxt) { if(e[i].to!=fa) { if(e[i].to==rt) { f[e[i].to][1]=-0x3f3f3f3f; } else { dfs(e[i].to,x); f[x][0]+=max(f[e[i].to][0],f[e[i].to][1]); f[x][1]+=f[e[i].to][0]; } } } } int main() { ll n,u,v,ans=0,maxx,i; double k; cin>>n; for(i=1;i<=n;i++) { cin>>w[i]; } for(i=1;i<=n;i++) { cin>>u>>v; u++; v++; add(u,v); add(v,u); } cin>>k; for(i=1;i<=n;i++) { if(dfn[i]==0) { tarjan(i,0); } } rt=scc[id][0]; dfs(rt,scc[id][1]); maxx=max(f[rt][0],f[rt][1]); rt=scc[id][1]; dfs(rt,scc[id][0]); ans+=max(maxx,f[rt][0]); printf("%.1lf\n",1.0*ans*k); return 0; }
luogu P1543 [POI2004] SZP
-
多倍经验: BZOJ3037 创世纪
-
将 \(a_{i}\) 向 \(i\) 连一条有向边,这样就形成了基环外向树森林。
-
设 \(f_{x,0/1}\) 表示 \(x\) 不选/选时,以 \(x\) 为根的子树的最多选择个数,状态转移方程为 \(\begin{cases} f_{x,0}=\sum\limits_{y \in Son(x)} \max(f_{y,0},f_{y,1}) \\ f_{x,1}=1+\max\limits_{y \in Son(x)} \{ f_{y,0}+\sum\limits_{z \in Son(x),z \ne y} \max(f_{z,0},f_{z,1}) \}=1+f_{x,0}- \min\limits_{y \in Son(x)} \{ \max(f_{y,0},f_{y,1})-f_{y,0} \} \end{cases}\) 。
点击查看代码
struct node { int nxt,to; }e[2000010]; int head[2000010],vis[2000010],u[2000010],v[2000010],f[2000010][2],rt,cnt=0; void add(int u,int v) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; head[u]=cnt; } int dfs_huan(int x) { vis[x]=1; return (vis[u[x]]==1)?x:dfs_huan(u[x]); } void dfs(int x) { int minn=0x3f3f3f3f; vis[x]=1; f[x][0]=0; for(int i=head[x];i!=0;i=e[i].nxt) { if(e[i].to==rt) { f[e[i].to][1]=-0x3f3f3f3f; } else { dfs(e[i].to); f[x][0]+=max(f[e[i].to][0],f[e[i].to][1]); minn=min(minn,max(f[e[i].to][0],f[e[i].to][1])-f[e[i].to][0]); } } f[x][1]=1+f[x][0]-minn; } int main() { int n,ans=0,maxx,i; cin>>n; for(i=1;i<=n;i++) { v[i]=i; cin>>u[i]; add(u[i],v[i]); } for(i=1;i<=n;i++) { if(vis[i]==0) { rt=dfs_huan(i); dfs(rt); maxx=max(f[rt][0],f[rt][1]); rt=u[rt]; dfs(rt); ans+=max(maxx,f[rt][1]); } } cout<<ans<<endl; return 0; }
luogu P5022 [NOIP2018 提高组] 旅行
-
当 \(m=n-1\) 时,贪心选取子节点中编号最小优先遍历即可。
-
当 \(m=n\) 时,暴力枚举删的是哪条边,使其变成一棵树,然后当成 \(m=n-1\) 的情况来做。
点击查看代码
int vis[10010],u[10010],v[10010],brokeu,brokev; vector<int>e[10010],ans,ls; bool check(int u,int v) { return ((u==brokeu&&v==brokev)||(u==brokev&&v==brokeu))?false:true; } bool cmp() { for(int i=0;i<ans.size();i++) { if(ans[i]>ls[i]) { return true; } if(ans[i]<ls[i]) { return false; } } return false; } void dfs1(int x,int fa) { ans.push_back(x); for(int i=0;i<e[x].size();i++) { if(e[x][i]!=fa) { dfs1(e[x][i],x); } } } void dfs2(int x,int fa) { vis[x]=1; ls.push_back(x); for(int i=0;i<e[x].size();i++) { if(vis[e[x][i]]==0&&check(x,e[x][i])==true) { dfs2(e[x][i],x); } } } int main() { int n,m,i; cin>>n>>m; for(i=1;i<=m;i++) { cin>>u[i]>>v[i]; e[u[i]].push_back(v[i]); e[v[i]].push_back(u[i]); } for(i=1;i<=n;i++) { sort(e[i].begin(),e[i].end()); } if(m==n-1) { dfs1(1,0); } else { for(i=1;i<=m;i++) { brokeu=u[i]; brokev=v[i]; memset(vis,0,sizeof(vis)); ls.clear(); dfs2(1,0); if(ls.size()==n) { if(ans.size()==0||cmp()==true) { ans=ls; } } } } for(i=0;i<ans.size();i++) { cout<<ans[i]<<" "; } return 0; }
luogu P4381 [IOI2008] Island
-
发现无向图不是很好处理,遂当成有向图来处理。
-
基环树的最长链来自两种情况。
- 在去掉环上所有边后的某棵子树中。
- 经过环,其两端分别在在去掉环上所有边后的两棵不同子树中。
-
设 \(f_{x}\) 表示在去掉环上所有边后的以 \(x\) 为根的子树中,以 \(x\) 为起点的最长链长度; \(g_{x}\) 表示在去掉环上所有边后的以 \(x\) 为根的子树中的最长链长度; \(dis_{x,y}\) 表示环上 \(x,y\) 之间的最短距离。
-
对于环上任意两点 \(s_{i},s_{j}(s_{i} \ne s_{j})\) ,对答案的贡献为 \(\max(g_{s_{i}},g_{s_{j}},f_{s_{i}}+f_{s_{j}}+dis_{s_{i},s_{j}})\) 。
-
\(f_{s_{i}}+f_{s_{j}}+dis_{s_{i},s_{j}}\) 类似 AcWing 289. 环路运输 一样处理即可。
点击查看代码
struct node { ll nxt,to,w; }e[2000010]; ll head[2000010],din[2000010],vis[2000010],f[2000010],g[2000010],u[2000010],v[2000010],w[2000010],cnt=0; void add(ll u,ll v,ll w) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; e[cnt].w=w; head[u]=cnt; } void top_sort(ll n) { queue<ll>q; ll x,i; for(i=1;i<=n;i++) { if(din[i]==0) { q.push(i); vis[i]=1; } } while(q.empty()==0) { x=q.front(); q.pop(); for(i=head[x];i!=0;i=e[i].nxt) { din[e[i].to]--; g[e[i].to]=max(g[e[i].to],max(f[e[i].to]+f[x]+e[i].w,g[x])); f[e[i].to]=max(f[e[i].to],f[x]+e[i].w); if(din[e[i].to]==0) { q.push(e[i].to); vis[e[i].to]=1; } } } } ll ask(ll y) { ll x=y,ans1=g[x],ans2=-0x3f3f3f3f,len=w[x],maxx1=f[x],maxx2=f[x]; for(y=v[x];y!=x;y=v[y]) { vis[y]=1; ans1=max(ans1,max(g[y],maxx1+f[y]+len)); ans2=max(ans2,f[y]+maxx2-len); maxx1=max(maxx1,f[y]-len); maxx2=max(maxx2,f[y]+len); len+=w[y]; } return max(ans1,ans2+len); } int main() { ll n,ans=0,i; cin>>n; for(i=1;i<=n;i++) { u[i]=i; cin>>v[i]>>w[i]; add(u[i],v[i],w[i]); din[v[i]]++; } top_sort(n); for(i=1;i<=n;i++) { if(vis[i]==0) { vis[i]=1; ans+=ask(i); } } cout<<ans<<endl; return 0; }
[ABC357E] Reachability in Functional Graph
luogu P6486 [COCI2010-2011#4] DUGOVI
CF835F Roads in the Kingdom
- 多倍经验: luogu P1399 [NOI2013]快餐店
- 断的边一定是环上的边,先把环转化成序列。
luogu P6258 [ICPC2019 WF] Hobson's Trains
luogu P3533 [POI2012]RAN-Rendezvous
BZOJ4203 同桌的你
参考资料
《算法竞赛进阶指南》——李煜东
本文来自博客园,作者:hzoi_Shadow,原文链接:https://www.cnblogs.com/The-Shadow-Dragon/p/18102277,未经允许严禁转载。
版权声明:本作品采用 「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0) 进行许可。