Tarjan算法 学习笔记
前排提示:先学习拓扑排序,再学习Tarjan有奇效。
--------------------------
Tarjan算法一般用于有向图里强连通分量的缩点。
强连通分量:有向图里能够互相到达的点的集合。(大概是这么个意思,自己意会)
因为能够互相到达,所以宏观上我们可以把它们看成一个点,边权也相应的加起来即可。
下面是Tarjan过程的代码解释:
我们开两个数组,分别为dfn[]和low[]。dfn表示此点的时间戳,low表示最早的时间戳。(即进入某一个环最早的时间戳)
遇到一个没有记录过的点,就把它扔到栈里,不停dfs,直到dfn[]==low[],即某一个环已经遍历完了,我们就弹栈,将这一个环合并。
代码如下:
void tarjan(int now) { dfn[now]=low[now]=++cnt; st[++top]=now; vis[now]=1; for (int i=head[now];i;i=edge[i].next) { int to=edge[i].to; if (!dfn[to]) tarjan(to),low[now]=min(low[now],low[to]); else if (vis[to]) low[now]=min(low[now],dfn[to]); } if (dfn[now]==low[now]) { tot++; while(st[top+1]!=now) { pos[st[top]]=tot; sum[tot]+=val[st[top]]; vis[st[top--]]=0; } } }
配合拓扑排序,一张新的有向无环图就构造了出来。
-------------------------------------------------------------------------------------
【模板】 缩点
给定一个含有n个点m条边的有向图每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。
允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。
第一行两个整数,为$n$,$m$。
第二行到第$m+1$行有每行有3个整数,分别为$u,v,d$,表示$u\rightarrow v$有一条长度为$d$的边。
输出格式:一行结果。
-------------------------------------------------
Tarjan模板题,只不过加了一点小DP,记忆化搜索即可。注意的地方就是拓扑排序后的重新建图。
#include<bits/stdc++.h> using namespace std; const int maxn=200005; int cnt,dfn[maxn],vis[maxn],low[maxn]; int f[maxn],sum[maxn],ans; int jishu,head[500005]; int top,st[maxn],pos[maxn],tot; int n,m,x[maxn],y[maxn],val[maxn]; struct node { int next,to; }edge[500005]; inline int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();} while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();} return x*f; } inline void add(int from,int to) { edge[++jishu].next=head[from]; edge[jishu].to=to; head[from]=jishu; } void tarjan(int now) { dfn[now]=low[now]=++cnt; st[++top]=now; vis[now]=1; for (int i=head[now];i;i=edge[i].next) { int to=edge[i].to; if (!dfn[to]) tarjan(to),low[now]=min(low[now],low[to]); else if (vis[to]) low[now]=min(low[now],dfn[to]); } if (dfn[now]==low[now]) { tot++; while(st[top+1]!=now) { pos[st[top]]=tot; sum[tot]+=val[st[top]]; vis[st[top--]]=0; } } } void search(int x) { if (f[x]) return; f[x]=sum[x]; int maxx=0; for (int i=head[x];i;i=edge[i].next) { int to=edge[i].to; if (!f[to]) search(to); maxx=max(maxx,f[to]); } f[x]+=maxx; } void clear() { jishu=0; memset(edge,0,sizeof(edge)); memset(head,0,sizeof(head)); } int main() { n=read(),m=read(); for (int i=1;i<=n;i++) val[i]=read(); for (int i=1;i<=m;i++) { x[i]=read(),y[i]=read(); add(x[i],y[i]); } for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i); clear(); for (int i=1;i<=m;i++) if (pos[x[i]]!=pos[y[i]]) add(pos[x[i]],pos[y[i]]); for (int i=1;i<=n;i++) search(i),ans=max(ans,f[i]); printf("%d\n",ans); return 0; }
另,如果是无向图缩点,一定要加上这句话:
if (edge[i].to==fa) continue;
【模板】割点
给定一个含有n个点m条边的无向图,求图的割点。
------------------------------------------
割点是指去掉这个点整个图便不连通的点。
我们可以同样用Tarjan算法,建立一颗搜索树。
如果此点为根节点,则判断是否有>=2棵子树。如果存在,那么此点为割点。
对于非根节点,如果low[v]>=dfn[u]($u\rightarrow v$)那么u点为割点(因为从u点开始无论怎么走都不可能回到原来的点了)。
思路还是比较清晰的,直接放代码:
#include<bits/stdc++.h> using namespace std; const int maxn=200005; int head[500005],jishu; int dfn[maxn],low[maxn],cnt; int n,m,ans; bool cut[maxn]; struct node { int next,to; }edge[500005]; inline int read() { int x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();} while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();} return x*f; } inline void add(int from,int to) { edge[++jishu].next=head[from]; edge[jishu].to=to; head[from]=jishu; } void tarjan(int now,int fa) { dfn[now]=low[now]=++cnt; int child=0; for (int i=head[now];i;i=edge[i].next) { int to=edge[i].to; if (!dfn[to]){ tarjan(to,fa); low[now]=min(low[now],low[to]); if (low[to]>=dfn[now]&&now!=fa) cut[now]=1; if (now==fa) child++; } low[now]=min(low[now],dfn[to]); } if (child>=2&&now==fa) cut[now]=1; } int main() { n=read(),m=read(); for (int i=1;i<=m;i++) { int u=read(),v=read(); add(u,v);add(v,u); } for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i,i); for (int i=1;i<=n;i++) if (cut[i]) ans++; printf("%d\n",ans); for (int i=1;i<=n;i++) if (cut[i]) printf("%d ",i); return 0; }