Living-Dream 系列笔记 第86期
边双连通分量
概念:若在无向图 \(G\) 中,存在一个极大子图 \(G'\),使得 \(G'\) 中没有割边,则称 \(G'\) 为 \(G\) 的一个边双连通分量,记作 \(\texttt{E-DCC}\)。
使用场景:将无向图转化为一棵树(即无向图上的缩点)。
求解步骤:确定割边,再遍历所有点且不经过割边,那么能联通的点都是即在同一个 \(\texttt{E-DCC}\) 中。
T103489
板子。
code
#include<bits/stdc++.h> using namespace std; const int N=3e5+5; int n,m,cnt,id; int dfn[N],low[N],dcc[N]; bool bridge[N]; struct NODE{ int v,i; }; vector<NODE> G[N]; void tarjan(int cur,int edg){ dfn[cur]=low[cur]=++cnt; for(auto nxt:G[cur]){ if(!dfn[nxt.v]){ tarjan(nxt.v,nxt.i); low[cur]=min(low[cur],low[nxt.v]); if(low[nxt.v]>dfn[cur]) bridge[nxt.i]=1; } else if(nxt.i!=edg){ low[cur]=min(low[cur],dfn[nxt.v]); } } } void dfs(int cur){ dcc[cur]=id; for(auto nxt:G[cur]){ if(dcc[nxt.v]||bridge[nxt.i]) continue; dfs(nxt.v); } } int main(){ ios::sync_with_stdio(0); cin.tie(0); cin>>n>>m; for(int i=1,u,v;i<=m;i++){ cin>>u>>v; G[u].push_back({v,i}); G[v].push_back({u,i}); } for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0); for(int i=1;i<=n;i++) if(!dcc[i]) ++id,dfs(i); cout<<id; return 0; }
POJ3352
结论题。
考虑缩点,形成的树上就都是割边了。
现在要连边成环从而消去割边带来的影响。
要求连边最少,也就是每次消去的割边要尽可能多。
怎么消去的最多?这等价于问树上哪条路径最长。
那显然就是连直径的两个端点。
而众所周知,直径的端点只有可能是度数为 \(1\) 的节点。
即叶子节点 + 度数为 \(1\) 的根(根在度数 \(>1\) 时显然不会比叶子节点更优)。
于是寻找度数为 \(1\) 的根两两相连即可,有落单的则还需连一次。
令度数为 \(1\) 的点有 \(x\) 个,这说明答案即为 \(\lceil \frac{x}{2} \rceil\)。
code
#include <cstdio> #include <iostream> #include <algorithm> #include <cstring> #include <string> #include <stdlib.h> #include <vector> #include <queue> #include <cmath> #include <stack> #include <map> #include <set> using namespace std; const int N=1e3+5; int n,m,cnt,id; int dfn[N],low[N],dcc[N],dg[N]; bool bridge[N],ok[N][N]; struct NODE{ int v,i; }; vector<NODE> G[N]; struct EDGE{ int u,v; }e[N]; void tarjan(int cur,int edg){ dfn[cur]=low[cur]=++cnt; for(int nxt=0;nxt<G[cur].size();nxt++){ if(!dfn[G[cur][nxt].v]){ tarjan(G[cur][nxt].v,G[cur][nxt].i); low[cur]=min(low[cur],low[G[cur][nxt].v]); if(low[G[cur][nxt].v]>dfn[cur]) bridge[G[cur][nxt].i]=1; } else if(G[cur][nxt].i!=edg){ low[cur]=min(low[cur],dfn[G[cur][nxt].v]); } } } void dfs(int cur){ dcc[cur]=id; for(int nxt=0;nxt<G[cur].size();nxt++){ if(dcc[G[cur][nxt].v]||bridge[G[cur][nxt].i]) continue; dfs(G[cur][nxt].v); } } int main(){ ios::sync_with_stdio(0); cin.tie(0); cin>>n>>m; for(int i=1,u,v;i<=m;i++){ cin>>u>>v; e[i]={u,v}; G[u].push_back({v,i}); G[v].push_back({u,i}); } for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0); for(int i=1;i<=n;i++) if(!dcc[i]) ++id,dfs(i); for(int i=1;i<=m;i++){ if(bridge[i]){ dg[dcc[e[i].u]]++; dg[dcc[e[i].v]]++; } } int ans=0; for(int i=1;i<=id;i++) if(dg[i]==1) ans++; cout<<(ans+1)/2; return 0; }
总结:做题时注意题面信息的转化。
POJ3694
如果只有一次询问,那么答案就是两个点所在的 \(\texttt{E-DCC}\) 在缩点后的树上的简单路径长度。
考虑多组询问,其实没什么区别,就是可能有重复的边,
于是我们不能直接求了,只能一步步往 LCA 跳,
但是这个时候再去走这些重复边就是浪费,于是我们用并查集优化一下即可。
思维难度很低对吧,但是我这个唐诗儿调了一上午 /fn/fn/fn
code
#include <cstdio> #include <iostream> #include <algorithm> #include <cstring> #include <string> #include <stdlib.h> #include <vector> #include <queue> #include <cmath> #include <stack> #include <map> #include <set> using namespace std; const int N=1e5+5,M=2e5+5; int n,m,q; int cnt,id,ans,tot,test; int dfn[N],low[N],dcc[N]; int dp[N][18],dep[N],fa[N]; bool bridge[M<<1]; struct NODE{ int v,i; }; vector<NODE> G[N]; struct EDGE{ int u,v; }e[M]; vector<int> T[N]; inline int read(){ int x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); } while(ch>='0' && ch<='9') x=x*10+ch-'0',ch=getchar(); return x*f; } void write(int x){ if(x<0) putchar('-'),x=-x; if(x>9) write(x/10); putchar(x%10+'0'); return; } void tarjan(int cur,int edg){ dfn[cur]=low[cur]=++cnt; for(int nxt=0;nxt<G[cur].size();nxt++){ if(!dfn[G[cur][nxt].v]){ tarjan(G[cur][nxt].v,G[cur][nxt].i); low[cur]=min(low[cur],low[G[cur][nxt].v]); if(low[G[cur][nxt].v]>dfn[cur]) bridge[G[cur][nxt].i]=1; } else if(G[cur][nxt].i!=edg){ low[cur]=min(low[cur],dfn[G[cur][nxt].v]); } } } void dfs(int cur){ dcc[cur]=id; for(int nxt=0;nxt<G[cur].size();nxt++){ if(dcc[G[cur][nxt].v]||bridge[G[cur][nxt].i]) continue; dfs(G[cur][nxt].v); } } void init(){ memset(dfn,0,sizeof dfn); memset(low,0,sizeof low); memset(dcc,0,sizeof dcc); memset(bridge,0,sizeof bridge); cnt=id=0; for(int i=1;i<=n;i++) G[i].clear(); } void Init(){ for(int i=1;i<=id;i++) T[i].clear(),fa[i]=i; } void init_lca(int cur,int father){ dep[cur]=dep[father]+1; dp[cur][0]=father; for(int i=1;(1<<i)<=dep[cur];i++) dp[cur][i]=dp[dp[cur][i-1]][i-1]; for(int nxt=0;nxt<T[cur].size();nxt++){ if(T[cur][nxt]==father) continue; init_lca(T[cur][nxt],cur); } } int get_lca(int x,int y){ if(dep[x]<dep[y]) swap(x,y); for(int i=17;i>=0;i--) if(dep[dp[x][i]]>=dep[y]) x=dp[x][i]; if(x==y) return x; for(int i=17;i>=0;i--) if(dp[x][i]!=dp[y][i]) x=dp[x][i],y=dp[y][i]; return dp[x][0]; } int fnd(int x){ return fa[x]==x?x:fa[x]=fnd(fa[x]); } void jump(int x,int ed){ x=fnd(x); while(dep[x]>dep[ed]){ fa[x]=dp[x][0]; ans--; x=fnd(x); } } int main(){ while(1){ n=read(),m=read(); if(n==0&&m==0) break; cout<<"Case "<<++test<<":\n"; init(); for(int i=1,u,v;i<=m;i++){ u=read(),v=read(); e[i]={u,v}; G[u].push_back({v,i}); G[v].push_back({u,i}); } for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i,0); for(int i=1;i<=n;i++) if(!dcc[i]) ++id,dfs(i); Init(); for(int i=1;i<=m;i++){ if(bridge[i]){ T[dcc[e[i].u]].push_back(dcc[e[i].v]); T[dcc[e[i].v]].push_back(dcc[e[i].u]); } } q=read(); init_lca(1,0); ans=id-1; while(q--){ int a,b; cin>>a>>b; a=dcc[a],b=dcc[b]; int lca=get_lca(a,b); jump(a,lca),jump(b,lca); write(ans); putchar('\n'); } putchar('\n'); } return 0; }
总结:做题从简单情况入手。
CF231E
简单题,10 min 一遍过。tj。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具