tarjan 算法
强连通分量
在无向图中取一个 DFS 树, 若出现返祖边,则出现了 "环",环中的点可互达.
$\mathrm{tarjan}$ 算法中的 $\mathrm{low[x]}$ 就是环上深度最浅的点.
$\mathrm{low[x]}$ 有两种求法:
1. 子孙的 $\mathrm{x}$ 可以更新 $\mathrm{x}$ 的 $\mathrm{low[x]}$.
2. 返祖边指向的点 $\mathrm{v}$ 可以更新 $\mathrm{low[x]}$.
若 $\mathrm{stack}$ 中的点没有清空至 $\mathrm{x}$, 说明 $\mathrm{x}$ 和没被清空的点所构成的连通块可互达.
根据 $\mathrm{DFS}$ 树的性质,强连通分量的编号越小,拓扑序越大.
所以在求解强连通分量问题时拓扑序是天然的,很多时候不必再求.
#include <cstdio> #include <stack> #include <cstring> #include <vector> #include <algorithm> #define N 100009 #define ll long long #define pb push_back #define setIO(s) freopen(s".in","r",stdin) using namespace std; stack<int>S; vector<int>G[N],node[N]; int low[N],dfn[N],vis[N], f[N], val[N], sum[N], fin[N], scc,n,m,id; void tarjan(int x) { vis[x]=1; low[x]=dfn[x]=++scc; S.push(x); for(int i=0;i<G[x].size();++i) { int v=G[x][i]; if(!vis[v]) { tarjan(v); low[x]=min(low[x], low[v]); } else if(vis[v] != -1) low[x]=min(low[x], dfn[v]); } if(low[x] == dfn[x]) { ++id; for(;;) { int p = S.top(); S.pop(); vis[p] = -1; fin[p] = id; node[id].pb(p); sum[id] += val[p]; if(p == x) break ; } } } int main() { setIO("input"); scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) { scanf("%d",&val[i]); } for(int i=1;i<=m;++i) { int x, y; scanf("%d%d",&x,&y); G[x].pb(y); } for(int i=1;i<=n;++i) { if(vis[i] == 0) tarjan(i); } int ans = 0; for(int i=1;i<=id;++i) { f[i] = sum[i]; for(int j=0;j<node[i].size();++j) { int x = node[i][j]; for(int k = 0; k < G[x].size(); ++k) { int v = G[x][k]; if(fin[v] != i) f[i] = max(f[i], f[fin[v]] + sum[i]); } } ans = max(ans, f[i]); } printf("%d\n",ans); return 0; }
割点与桥
在无向图中,若将点 $\mathrm{x}$ 删掉后图会变成两个或多个连通块,$\mathrm{x}$ 就是割点.
在无向图中,若将边 $\mathrm{v}$ 删掉后图会变成两个连通块,$\mathrm{v}$ 就是桥(割边)
割点的求法与桥是类似的.
对于树边 $(u,v)$ 只需判断 $\mathrm{low[v]}$ 是否大于等于 $\mathrm{dfn[u]}$ 即可.
若满足上述条件,则说明 $\mathrm{v}$ 无法到达 $\mathrm{u}$ 的祖先.
在根节点处特判一下即可.
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> #define N 200009 #define ll long long #define pb push_back #define setIO(s) freopen(s".in","r",stdin) using namespace std; vector<int>ans; int low[N],dfn[N],vis[N],mk[N]; int hd[N],to[N<<1],nex[N<<1],edges,n,m,scc,root; void add(int u,int v) { nex[++edges]=hd[u]; hd[u]=edges; to[edges]=v; } void tarjan(int x) { vis[x]=1; low[x]=dfn[x]=++scc; int son=0; for(int i=hd[x];i;i=nex[i]) { int v=to[i]; if(!vis[v]) { tarjan(v); ++son; if((low[v] >= dfn[x] && x != root) || (x == root && son > 1)) mk[x]=1; low[x] = min(low[x], low[v]); } else low[x] = min(low[x], dfn[v]); } } int main() { // setIO("input"); scanf("%d%d",&n,&m); for(int i=1;i<=m;++i) { int x, y; scanf("%d%d",&x,&y); add(x, y); add(y, x); } for(int i=1;i<=n;++i) if(!vis[i]) root=i, tarjan(i); int cnt=0; for(int i=1;i<=n;++i) if(mk[i]) ++ cnt; printf("%d\n",cnt); for(int i=1;i<=n;++i) if(mk[i]) printf("%d ",i); return 0; }
边双联通分量
定义:在无向图中,一个联通分量内任意两点都有两种边不相交的路径(至少)
通俗的讲,任意两个点都在一个换上(或者多个环共用一个点)
求法和 $\mathrm{tarjan}$ 求强联通分量算法几乎一样.
这是因为虽然是无向图,但是在构建 $\mathrm{DFS}$ 树的时候相当于给边定向了.
由于要求两个点存在不相交的路径,所以返祖边不可以是树边,这就和 $\mathrm{tarjan}$ 基本一致了.
void tarjan(int x, int fa) { // printf("%d %d\n",x, fa); s.push(x); vis[x] = 1; low[x] = dfn[x] = ++scc; for(int i = hd[x]; i ; i = nex[i]) { int v = to[i]; if( i == fa) continue; if(!vis[v]) { tarjan(v, i ^ 1); low[x] = min(low[x], low[v]); } else if(vis[v] != -1) low[x] = min(low[x], dfn[v]); } if(low[x] == dfn[x]) { ++ id; for( ; ; ) { int p = s.top(); s.pop(); node[id].pb(p); fin[p] = id, vis[p] = -1; if(p == x) break ; } } }
在遍历边的时候特判一下是不是返祖边即可.
点双联通分量
无向图的点双联通的定义是任意两个分量内的点必须有至少两条点不相交的路径.
点双联通分量比边双连通分量的限制还有更强,因为不经过点就一定不经过边.
不经过点就说明任意两点都能构成一个环.
求点双的话还是利用 $\mathrm{tarjan}$ 算法,然后注意不要经过父边.
每次求点双集合的话就和强连通分量一样弹栈,不过 $\mathrm{u}$ 可以是很多个连通分量内的点.
所以每次弹栈时不能等到 DFS 树中的儿子都遍历完,而是边遍历边弹.
例题:洛谷P3225 [HNOI2012]矿场搭建
对原图跑一个点双,然后标记所有的割点.
以任意一个割点为根再跑一次 $\mathrm{tarjan}$.
第二次运行 $\mathrm{tarjan}$ 的时候若 $\mathrm{x}$ 为 $\mathrm{v}$ 的割点:
1. $\mathrm{v}$ 的子树内没有安排出口,则安排一个.
2. $\mathrm{v}$ 的子树内已经安排出口了,则不用管.
运行完毕之后特判一下整个图都是点双的情况,此时要安排两个点.
这里特别说明一下以割点为根节点第二次运行 $\mathrm{tarjan}$ 的理由:
若根节点不是割点,则会出现非常麻烦的情况,而根节点为割点可以规避掉.
#include <cstdio> #include <vector> #include <cstring> #include <algorithm> #define N 509 #define ll long long #define pb push_back #define setIO(s) freopen(s".in","r",stdin) using namespace std; int n, m, ans, root; ll a2; int fin[N], iscut[N], cnt; int hd[N], size[N], to[N<<1], nex[N<<1],low[N], dfn[N], edges, scc; void add(int u, int v) { nex[++edges]=hd[u]; hd[u]=edges; to[edges]=v; } void tarjan(int x, int ff) { low[x]=dfn[x]=++scc; int son=0; for(int i=hd[x];i;i=nex[i]) { int v=to[i]; if(i==ff) continue; if(!dfn[v]) { ++son; // c0=v; tarjan(v, i^1); low[x]=min(low[x], low[v]); if(low[v]>=dfn[x]) iscut[x]=1; } else low[x]=min(low[x], dfn[v]); } if(x==root&&son==1) iscut[x]=0; cnt += iscut[x]; } void DFS(int x, int ff) { size[x]=1; low[x]=dfn[x]=++scc; for(int i=hd[x];i;i=nex[i]) { if(i==ff) continue; int v=to[i]; if(!dfn[v]) { DFS(v, i ^ 1); // 遍历 v 这边. low[x]=min(low[x], low[v]); size[x] += size[v]; if(low[v] >= dfn[x]) { // x 成为了割点. // v 中选一个. if(!fin[v]) { fin[v] = 1; a2*=size[v]; ++ ans; } } fin[x] += fin[v]; } else low[x]=min(low[x], dfn[v]); } } int TEST; void solve() { edges=1,n=0,cnt=0,scc=0,ans=0; memset(fin,0,sizeof(fin)); memset(iscut,0,sizeof(iscut)); memset(size,0,sizeof(size)); memset(hd,0,sizeof(hd)); memset(to,0,sizeof(to)); memset(nex,0,sizeof(nex)); memset(low,0,sizeof(low)); memset(dfn,0,sizeof(dfn)); ++TEST; printf("Case %d: ",TEST); n=0, edges = 1; for(int i=1;i<=m;++i) { int x, y; scanf("%d%d",&x,&y); add(x, y); add(y, x); n = max(n, max(x, y)); } root=1,tarjan(1, 0); // 得到 cnt, flag; if(cnt == 0) { // 无割. size[1] = n ; ans += min(2, size[1]); ll p2 = (size[1] == 1 ? 1 : (ll)size[1]*(size[1]-1)/2); printf("%d %lld\n",ans,p2); } else { // cnt > 0, 有割点. int tar = 0; for(int i=1;i<=n;++i) if(iscut[i]) { tar=i; break; } memset(dfn, 0, sizeof(dfn)); memset(low, 0, sizeof(low)); a2=1ll; DFS(tar, 0); printf("%d %lld\n", ans,a2); } } int main() { // setIO("input"); while(1) { scanf("%d",&m); if(!m) { break ; } solve(); } return 0; }