摘要图片来源:http://www.javashuo.com/article/p-hzuafiqx-vc.htmlhtml
Tarjan这东西,最基础的用法就是求强连通份量和环,而后还能够求割点、割边,以及能够缩点。看题目:数组
首先,你须要知道:强连通、强连通图、强连通份量、dfs序、dfs树
那么,这都是些啥?在一个有向图中,点\([u,v]\)能够互相到达,那么咱们就称\([u,v]\)是强连通。而强连通图,就是对于任意两个\([u,v]\)都能互相到达。而强连通份量,就是在一个子图中,知足对于任意\([u,v]\)都是强连通。
而dfs序,则是在dfs的过程当中,第一次被遍历到的时间戳。dfs树就是在dfs过程当中所造成的树,显然,从\(u\)dfs到\(v\),那\(u\)就是\(v\)的父节点,\(v\)就是\(u\)的子节点。不过这棵树还可能从一个结点回到本身的祖先。
那接下来就讲如何用Tarjan求强连通份量吧。首先,咱们从任意一个点开始dfs,而后咱们须要两个数组:\(low\)和\(dfn\)。\(low_i\)表示点\(i\)所在的强连通份量的根(咱们把dfs树中的一个强连通份量当作一棵子树,那这个强连通份量的根就是在这个强连通份量中第一个被遍历到的点)。而\(dfn_i\)则表示点\(i\)的dfs序。接着,咱们每遍历到一个点\(u\),就记录她的\(dfn\),而后让\(low_u\)的初值等于\(dfn_u\),再把她放进栈里。接着遍历她所链接的点\(v\),\(v\)有两种状况,一种状况是不在栈中,也就是没被遍历过,也就是儿子结点,那么就再作dfs(v),而后更新\(low_u = min(low_u,low_v)\)。第二种是在栈中,也就是是她的祖先,那么\(low_u = min(low_u,dfn_v)\)。最后,若是\(low_u\)仍是等于\(dfn_u\),那就说明\(u\)是这个强连通份量的根,那么就让\(u\)和\(u\)上面的点所有出栈。你觉得这样就行了吗?不是的,因为这个图不必定连通,因此最好循环\(1~n\),若是\(dfn\)没有更新,那么就是没走过,那就要作Tarjan。
接下来来看上面给的模板,明显是要求点的个数大于1的强连通份量的个数。(注意,只有1个点也是强连通份量)那咱们只要在出栈是记录数量就能够了,由于在\(u\)上面的点都是\(u\)所在的强连通份量中的。
code:spa
#include<cstdio> #include<stack> using namespace std; int n,m,ans; int index,low[10005],dfn[10005],vis[10005]; //index用来记录当前搜了几个点,也就是当前dfn应该取几,而vis[i]表示以i为根的强连通份量有几个点 stack<int>s; bool f[10005];//用来判断是否在栈里 struct graph { int tot,hd[10005]; int nxt[50005],to[50005]; void add(int x,int y) { tot++; nxt[tot]=hd[x]; hd[x]=tot; to[tot]=y; return ; } }g;//链式前向星 void Tarjan(int x) { dfn[x]=low[x]=++index; s.push(x); f[x]=true; //初始化 for(int i=g.hd[x];i;i=g.nxt[i])//遍历全部连通的点 if(!dfn[g.to[i]])//子节点 { Tarjan(g.to[i]);//继续Tarjan low[x]=min(low[x],low[g.to[i]]);//更新答案 } else if(f[g.to[i]])//祖先 low[x]=min(low[x],dfn[g.to[i]]);//更新答案 if(dfn[x]==low[x])//若是是根 { vis[x]=1;//更新vis while(s.top()!=x) { f[s.top()]=false; s.pop(); vis[x]++;//更新vis }//出栈 f[x]=false; s.pop(); //出栈 } return ; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int u,v; scanf("%d%d",&u,&v); g.add(u,v); } for(int i=1;i<=n;i++) { if(!dfn[i]) Tarjan(i); if(vis[i]>1) ans++; } printf("%d",ans); return 0; }
什么是缩点呢?缩点,就是把一堆点缩成一个点。固然不能随便缩。若是有一堆点,她们能互相到达,也就是说这是一个强连通份量,那么把她们缩成一个点,是否是对题目自己没有影响?但缩完点以后他就变成了一个DAG,并且点和边的数量也大大减小了。这就是缩点。那么明白什么是缩点以后,那就很好写了,咱们记录\(scc_i\),表示i所在的强连通份量的编号。而后对每一个点遍历,对于点\(i\),把本身的权值赋给\(scc_i\),而后枚举全部出边,若是终点不在同一个强连通份量中,那么把这条边记录下来。注意,不能在原图上作,也就是咱们要新建一个图。接下来,咱们就能够根据题目要求作了。
接下来看模板,那这个就是缩完点以后,作一遍拓扑排序,而后求最大值。
code:code
#include<cstdio> #include<stack> #include<queue> using namespace std; int mx,n,m,vv[10005]; int index,low[10005],dfn[10005]; stack<int>s; int f[10005]; int scc_cnt,scc[10005]; queue<int>q; int in[10005],vis[10005],dp[10005]; int max(int x,int y){return x>y?x:y;} struct graph { int tot,hd[10005]; int nxt[100005],to[100005]; void add(int x,int y) { tot++; nxt[tot]=hd[x]; hd[x]=tot; to[tot]=y; } }g,sg; void Tarjan(int x)//求强连通份量 { dfn[x]=low[x]=++index; s.push(x); f[x]=true; for(int i=g.hd[x];i;i=g.nxt[i]) if(!dfn[g.to[i]]) { Tarjan(g.to[i]); low[x]=min(low[x],low[g.to[i]]); } else if(f[g.to[i]]) low[x]=min(low[x],dfn[g.to[i]]); if(dfn[x]==low[x])//出栈并记录scc { scc[x]=++scc_cnt; while(s.top()!=x) { scc[s.top()]=scc_cnt; f[s.top()]=false; s.pop(); } f[x]=false; s.pop(); } return ; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&vv[i]); for(int i=1;i<=m;i++) { int u,v; scanf("%d%d",&u,&v); g.add(u,v); } for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i); for(int i=1;i<=n;i++) { vis[scc[i]]+=vv[i]; for(int j=g.hd[i];j;j=g.nxt[j]) { int u=scc[i],v=scc[g.to[j]]; if(v!=u) { sg.add(u,v); in[v]++; } } }//建新图 for(int i=1;i<=scc_cnt;i++) { dp[i]=vis[i]; if(!in[i]) q.push(i); } while(!q.empty()) { int x=q.front(); q.pop(); for(int i=sg.hd[x];i;i=sg.nxt[i]) { int y=sg.to[i]; dp[y]=max(dp[y],dp[x]+vis[y]); if(!--in[y]) q.push(y); } } //拓扑排序 for(int i=1;i<=scc_cnt;i++) mx=max(mx,dp[i]);//求得答案 printf("%d",mx); return 0; }
割点,又叫割顶,这个东西的定义就是,在一个无向图中,若是有一个点\(u\),把她和链接她的边去掉后,连通数增长了,那么这个点\(u\)就是割点。
上面讲到的连通数,你能够理解为在无向图中的强连通份量的个数。
那这个割点怎么求呢?很简单,仍是作Tarjan,而后,若一个点\(u\)的子节点中存在一个\(v\),使得\(low_v \ge dfn_u\),那么\(u\)就是割点。缘由很简单,由于这样就表示有至少一棵子树不能到达她上面。但这只对不是根结点的点有效。那么根节点怎么判断呢?那就更简单了,只要有两棵及以上的子树,那她就必定是割点,缘由本身想。
最后再提醒一下,并非一个点链接的全部点都是她的子节点。
code:htm
#include<cstdio> #include<stack> #include<queue> #include<vector> using namespace std; int n,m,ans,visit[20005]; int index,low[20005],dfn[20005]; stack<int>s; bool f[20005]; vector<int>son[20005]; struct graph { int tot,hd[20005]; int nxt[500005],to[500005]; void add(int x,int y) { tot++; nxt[tot]=hd[x]; hd[x]=tot; to[tot]=y; return ; } }g; void Tarjan(int x)//作Tarjan { dfn[x]=low[x]=++index; s.push(x); f[x]=true; for(int i=g.hd[x];i;i=g.nxt[i]) if(!dfn[g.to[i]]) { Tarjan(g.to[i]); low[x]=min(low[x],low[g.to[i]]); son[x].push_back(g.to[i]);//记录子节点 } else if(f[g.to[i]]) low[x]=min(low[x],dfn[g.to[i]]); if(dfn[x]==low[x]) { while(s.top()!=x) { f[s.top()]=false; s.pop(); } f[x]=false; s.pop(); } return ; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int u,v; scanf("%d%d",&u,&v); g.add(u,v); g.add(v,u); } for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i); for(int i=1;i<=n;i++) { if(dfn[i]==low[i])//特判根结点 { if(son[i].size()>1) visit[++ans]=i; continue; } for(int j=0;j<son[i].size();j++) if(low[son[i][j]]>=dfn[i])//知足条件 { visit[++ans]=i; break; } } printf("%d\n",ans); for(int i=1;i<=ans;i++) printf("%d ",visit[i]); //输出答案 return 0; }
割边又叫割桥,割边的定义和割点类似,就是若是去掉一条边使得连通数增长,那么这条边就是割边。
割边的求法也和割点类似,就是对于一条边\([u,v]\),若\(low_v > dfn_u\),那么这条边就是割边。这里不取等于的缘由是,若\(v\)能够经过另外一条边到达\(u\),那么这条边天然不是割边了。
割边不用判断根节点(毕竟找的是边不是点),但须要特判通往父节点的那条边(缘由显然)。
接下来看模板,这题就是在求完割边以后要再排一遍序,确保输出的是有序的。
code:blog
#include<cstdio> #include<stack> #include<queue> #include<vector> #include<algorithm> using namespace std; int n,m,ans; int index,low[20005],dfn[20005]; stack<int>s; bool f[20005]; vector<int>son[20005]; struct srt { int u,v; }a[20005]; bool cmp(srt x,srt y) { return x.u<y.u||(x.u==y.u&&x.v<y.v); } struct graph { int tot,hd[20005]; int nxt[500005],to[500005]; void add(int x,int y) { tot++; nxt[tot]=hd[x]; hd[x]=tot; to[tot]=y; return ; } }g; void Tarjan(int x,int dad) { dfn[x]=low[x]=++index; s.push(x); f[x]=true; for(int i=g.hd[x];i;i=g.nxt[i]) if(!dfn[g.to[i]]) { Tarjan(g.to[i],x); low[x]=min(low[x],low[g.to[i]]); son[x].push_back(g.to[i]); } else if(f[g.to[i]]&&g.to[i]!=dad)//特判父节点 low[x]=min(low[x],dfn[g.to[i]]); if(dfn[x]==low[x]) { while(s.top()!=x) { f[s.top()]=false; s.pop(); } f[x]=false; s.pop(); } return ; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=m;i++) { int u,v; scanf("%d%d",&u,&v); g.add(u,v); g.add(v,u); } for(int i=1;i<=n;i++) if(!dfn[i]) Tarjan(i,0); for(int i=1;i<=n;i++) { for(int j=0;j<son[i].size();j++)//上面两重循环是枚举每条边 if(low[son[i][j]]>dfn[i])//符合条件 { ans++; a[ans].u=i; a[ans].v=son[i][j]; } } sort(a+1,a+ans+1,cmp);//注意要按顺序输出哦 for(int i=1;i<=ans;i++) printf("%d %d\n",a[i].u,a[i].v); return 0; }