浅说——tarjan
Tarjan陪伴强连通分量,
生成树完成后思路才闪光。
Euler跑过的七桥古塘
让你,心驰神往……
相关连接,感谢以下链接(提供部分内容):
https://big-news.cn/2019/02/07/%E5%BC%BA%E8%BF%9E%E9%80%9A%E5%88%86%E9%87%8F/
https://www.langlangago.xyz/index.php/archives/83/
Tarjan是干啥的?
以上摘自百科
你也看不懂,对吧。那看个毛啊!
tarjan说白了三个用:
1.求强连通分量
2.求LCA
3.无向图中,求割点和桥
----------------------------------------------------------------------------------------------------------------------
强连通分量
1.啥是强连通分量?
这么说吧,强连通是指这个图中每两点都能够互相到达。
强连通分量也就是最大的强连通子图,很easy吧?
如图,有三个强连通分量(1,2,3)&(4)&(5)。
2.怎么求强连通分量?
噫......很明显,通过肉眼可以很直观地看出(1,2,3)是一组强连通分量,但很遗憾,代码并没有眼睛,所以该怎么判断强连通分量呢?
如果仍是上面那张图,我们对它进行dfs遍历。
可以注意到红边非常特别,因为如果按照遍历时间来分类的话,其他边都指向在自己之后被遍历到的点,而红边指向的则是比自己先被遍历到的点。
如果存在这么一条边,那么我们可以yy一下
从一个点出发,一直向下遍历,然后忽得找到一个点,那个点竟然有条指回这一个点的边!
那么想必这个点能够从自身出发再回到自身
想必这个点和其他向下遍历的该路径上的所有点构成了一个环,
想必这个环上的所有点都是强联通的。
但只是强联通啊,我们需要求的可是强连通分量啊......
比如说图中红色为强连通分量,而蓝色只是强联通图
因此我们只需要知道这个点u下面的所有子节点有没有连着这个点的祖先就行了。
但似乎还有一个问题啊......
我们怎么知道这个点u它下面的所有子节点一定是都与他强联通的呢?
这似乎是不对的,这个点u之下的所有点不一定都强联通
那么怎么在退回到这个点的时候,知道所有和这个点u构成强连通分量的点呢?
开个栈记录就行了
什么?!这么简单?
没错~就是这么简单~
如果在这个点之后被遍历到的点已经能与其下面的一部分点(也可能就只有他一个点)已经构成强连通分量,即它已经是最大的。
那么把它们一起从栈里弹出来就行了。
所以最后处理到点u时如果u的子孙没有指向其祖先的边,那么它之后的点肯定都已经处理好了,一个常见的思想,可以理解一下。
所以就可以保证栈里留下来u后的点都是能与它构成强连通分量的。
似乎做法已经明了了,用程序应该怎么实现呢?
3.怎么码代码?
首先介绍一下辅助数组
(1)、dfn[ ],表示这个点在dfs时是第几个被搜到的。
(2)、low[ ],表示这个点以及其子孙节点连的所有点中dfn最小的值
(3)、stack[ ],表示当前所有可能能构成强连通分量的点。
(4)、vis[ ],表示一个点是否在stack[ ]数组中。
那么按照之上的思路,我们来考虑这几个数组的用处以及算法的具体过程。
假设现在开始遍历点u:
-
首先初始化dfn[u]=low[u]=第几个被dfs到 dfn可以理解,但为什么low也要这么做呢? 因为low的定义如上,也就是说如果没有子孙与u的祖先相连的话,dfn[u]一定是它和它的所有子孙中dfn最小的(因为它的所有子孙一定比他后搜到)。
-
将u存入stack[ ]中,并将vis[u]设为true stack[ ]有什么用? 如果u在stack中,u之后的所有点在u被回溯到时u和栈中所有在它之后的点都构成强连通分量。(也就是上文中所说的开个栈记录)
-
遍历u的每一个能到的点,如果这个点dfn[ ]为0,即仍未访问过,那么就对点v进行dfs,然后low[u]=min{low[u],low[v]} low[ ]有什么用? 应该能看出来吧,就是记录一个点它最大能连通到哪个祖先节点(当然包括自己) 如果遍历到的这个点已经被遍历到了,那么看它当前有没有在stack[ ]里,如果有那么low[u]=min{low[u],low[v]} 如果已经被弹掉了,说明无论如何这个点也不能与u构成强连通分量,因为它不能到达u 如果还在栈里,说明这个点肯定能到达u,同样u能到达他,他俩强联通。
-
假设我们已经dfs完了u的所有的子树,那么之后无论我们再怎么dfs,u点的low值已经不会再变了。 那么如果dfn[u]=low[u]这说明了什么呢? 再结合一下dfn和low的定义来看看吧 dfn表示u点被dfs到的时间,low表示u和u所有的子树所能到达的点中dfn最小的。 这说明了u点及u点之下的所有子节点没有边是指向u的祖先的了,即我们之前说的u点与它的子孙节点构成了一个最大的强连通图即强连通分量 此时我们得到了一个强连通分量,把所有的u点以后压入栈中的点和u点一并弹出,将它们的vis[ ]置为false,如有需要也可以给它们染上相同颜色(后面会用到)
于是tarjan求强连通分量的部分到此结束
代码大概是这样的
void tarjan(int u) { s.push(u); dfn[u]=low[u]=++k; //dfn[u]表示u是第几个(++k)被搜到的(时间戳),low[u]表示u的连接点中最先被搜到的点v,点v是第几个被搜到的就是low[u]的值 (时间戳) for(int i=head[u];i;i=e[i].next) { int v=e[i].v; if(!dfn[v]) //没有搜过v,就去搜v { tarjan(v); low[u]=min(low[u],low[v]); } else if(!vis[v]) //搜过v,但是还在栈里 { low[u]=min(low[u],dfn[v]); } } int v=-1; if(dfn[u]==low[u]) { sum++; while(u!=v) //u==v就跳出了呗 { num[sum]++; v=s.top(); vis[v]=1; s.pop(); } } }
来道题练练手P2863 [USACO06JAN]牛的舞会The Cow Prom
题意为:给定一个图,要求图中节点数大于一的强联通分量个数。(模板题)
#include<cstdio> #include<string> #include<stack> using namespace std; int read() { int x=0,f=1;char c=getchar(); while(!isdigit(c)){if(c=='-')f=-1;c=getchar();} while(isdigit(c)){x=x*10+c-'0';c=getchar();} return x*f; } const int maxn=10005,maxm=50005; int n,m; struct edge{ int v,next; }e[maxm*2]; int cnt,head[maxn],k; int dfn[maxn],low[maxn]; bool vis[maxn]; stack <int> s; int sum,num[maxn]; int ans; void add(int u,int v) //建树大家都知道 { e[++cnt].v=v; e[cnt].next=head[u]; head[u]=cnt; } void readdata() //呵呵快读,_|^^|● { n=read(),m=read(); for(int i=1;i<=m;i++) { int a,b; a=read(),b=read(); add(a,b); } } void tarjan(int u) //tar……jan { s.push(u); dfn[u]=low[u]=++k; //dfn[u]表示u是第几个(++k)被搜到的(时间戳),low[u]表示u的连接点中最先被搜到的点v,点v是第几个被搜到的就是low[u]的值 (时间戳) for(int i=head[u];i;i=e[i].next) { int v=e[i].v; if(!dfn[v]) //没有搜过v,就去搜v { tarjan(v); low[u]=min(low[u],low[v]); } else if(!vis[v]) //搜过v,但是还在栈里 { low[u]=min(low[u],dfn[v]); } } int v=-1; if(dfn[u]==low[u]) { sum++; while(u!=v) //u==v就跳出了呗 { num[sum]++; v=s.top(); vis[v]=1; s.pop(); } } } void out() { printf("%d",ans); } void work() { for(int i=1;i<=n;i++) { if(!dfn[i]) tarjan(i); } for(int i=1;i<=sum;i++) //大于1的强连通分量 { if(num[i]>1) { ans++; } } out(); //shuchu,输出 } int main() { readdata(); work(); return 0; }
缩点
1.什么时候要用缩点
众所周知,有向无环图总是有着一些蜜汁优越性,因为没有环,你可以放心的在上面跑dfs,搞DP,但如果是一张有向有环图,事情就会变得尴尬起来了
思考一下会发现如果不打vis标记就会t飞(一直在环里绕啊绕),但是如果打了,又不一定能保证最优解
而你一看题目却发现显然根据一些贪心的原则,这个环上每个点的最大贡献都是整个环的总贡献
这个时候缩点就显得很有必要了,因为单个点的贡献和整个环相同,为什么不去把整个环缩成一个超级点呢?
这个环只是为了好理解,事实上他应该是一个强连通分量,显然如果只缩掉一个强连通图,图中仍然有环存在
缩点的一个栗子
2.怎么缩点
一般用染色法(就是把强连通分量的所有点归为一组)
所以,我们再来分析一下这道题。
首先,不难发现,如果这所有的牛都存在同一个强联通分量里。那么它们一定互相受欢迎。
那么,我们怎么来找明星呢。
很简单,找出度为0的强联通分量中的点。这样可以保证所有的人都喜欢它,但是它不喜欢任何人,所以说不存在还有人事明星。
此题还有一个特殊情况:
如果有两个点分别满足出度为零的条件,则没有明星,这样无法满足所有的牛喜欢他。
有了上边的解释,题目就不是那么难了,代码如下
#include<cstdio> #include<string> #include<iostream> #include<stack> using namespace std; const int maxn=10005,maxm=50005; int n,m; struct edge{ int v,next; }e[maxm]; int head[maxn],cnt; int dfn[maxn],low[maxn],vis[maxn],index,sum; int belong[maxn]; int k,temp[maxn],forever[maxn]; int ans[maxn]; int cord[maxn]; int t,key; stack <int> q; int read(); void readdata(); int add(int u,int v); void work(); int tarjan(int u); int read(); int find(int he); void out(); int main() { readdata(); work(); out(); return 0; } int read() //快读 { int x=0,f=1;char c=getchar(); while(!isdigit(c)){if(c=='-')f=-1;c=getchar();} while(isdigit(c)){x=x*10+c-'0';c=getchar();} return x*f; } void out() //输出 { for(int i=1;i<=sum;i++) { //printf("%d\n",forever[i]); if(!cord[i]) { t++; key=forever[i]; if(t==2) //出度数超过一个,则没有明星 { printf("0"); return; } } } printf("%d",key); //输出明星个数 } void readdata() //读入 { n=read(),m=read(); for(int i=1;i<=m;i++) { int a,b; a=read(),b=read(); add(a,b); } } int add(int u,int v) //建树 { e[++cnt].v=v; e[cnt].next=head[u]; head[u]=cnt; } void work() //tarjan预备操作 { for(int i=1;i<=n;i++) { if(!dfn[i]) { tarjan(i); } } } int tarjan(int u) //tarjan { q.push(u); dfn[u]=low[u]=++index; vis[u]=1; for(int i=head[u];i;i=e[i].next) { int v=e[i].v; if(!dfn[v]) { tarjan(v); low[u]=min(low[u],low[v]); } else if(vis[v]) { low[u]=min(low[u],dfn[v]); } } int v=0; if(dfn[u]==low[u]) { ++sum; while(u!=v) { v=q.top(); q.pop(); belong[v]=sum; vis[v]=0; forever[sum]++; //记录强连通分量中的结点个数 temp[forever[sum]]=v; //保存该点 } cord[sum]=find(sum); //出度数 } } int find(int he) //搜索每个强连通分量的出度数 { int ans=0; for(int i=1;i<=forever[he];i++) { for(int j=head[temp[i]];j;j=e[j].next) //枚举强连通分量中的每个点的出度数 { if(belong[e[j].v]!=he) { ans++; } } } return ans; }
缩点练习2P4742 [Wind Festival]Running In The Sky
自己看题吧……QAQ
思路:tarjan+重新建图+DAGdp
#include<cstdio> #include<string> #include<stack> #include<iostream> #include<queue> using namespace std; const int maxn=200005,maxm=500005; int n,m; int k[maxn]; struct edge{ int v,next; }e[maxm]; struct edge2{ int v,next; }edag[maxm]; int head[maxn],cnt; int dfn[maxn],low[maxn],vis[maxn]; int index; int sum; int maxk[maxn],sumk[maxn],belong[maxn],forever[maxn],temp[maxn]; int degin[maxn]; stack <int> s; int head2[maxn]; int f[maxn][2]; queue <int> q; void readdata(); //读入 int read(); //快读 void work(); //计算&调用程序 void addedge(int u,int v); //建树 void tarjan(int u); //tarjan缩点 void findin(); //dp的预处理 void adddag(int u,int v); //缩点后新建树 void topsort(); //拓扑排序dp void out(); //输出 int main() //主函数 { readdata(); work(); topsort(); out(); return 0; } int read() { int x=0,f=1;char c=getchar(); while(!isdigit(c)){if(c=='-')f=-1;c=getchar();} while(isdigit(c)){x=x*10+c-'0';c=getchar();} return x*f; } void readdata() { n=read(),m=read(); for(int i=1;i<=n;i++) k[i]=read(); for(int i=1;i<=m;i++) { int a,b; a=read(),b=read(); addedge(a,b); } cnt=0; //比较懒,后面要用,懒得申明新的变量,直接刷0 QAQ } void addedge(int u,int v) { e[++cnt].v=v; e[cnt].next=head[u]; head[u]=cnt; } void work() { for(int i=1;i<=n;i++) //tarjan正常操作 { if(!dfn[i]) tarjan(i); } findin(); } void tarjan(int u) { s.push(u); dfn[u]=low[u]=++index; vis[u]=1; for(int i=head[u];i;i=e[i].next) { int v=e[i].v; if(!dfn[v]) { tarjan(v); low[u]=min(low[u],low[v]); } else if(vis[v]) { low[u]=min(low[u],dfn[v]); } } if(dfn[u]==low[u]) { ++sum; int v=0; while(u!=v) { v=s.top(); s.pop(); vis[v]=0; sumk[sum]+=k[v]; //缩点后该点的总值 maxk[sum]=max(maxk[sum],k[v]); //缩点后该点的最大值 belong[v]=sum; //记录该点的颜色 temp[++forever[sum]]=v; //temp记录该颜色的一些点 } } } void findin() { for(int i=1;i<=n;i++) { for(int j=head[i];j;j=e[j].next) { if(belong[e[j].v]!=belong[i]) //不在同一个强连通分量中 { adddag(belong[i],belong[e[j].v]); //新建边 degin[belong[e[j].v]]++; //改颜色入读数++ } } } } void adddag(int u,int v) { edag[++cnt].v=v; edag[cnt].next=head2[u]; head2[u]=cnt; } void topsort() //DAGdp { for(int i=1;i<=sum;i++) //先把入读为零的加了 { if(!degin[i]) { q.push(i); } } while(!q.empty()) { int u=q.front(); q.pop(); f[u][0]+=sumk[u]; //f[u][0]是到该点的最大总值 f[u][1]=max(f[u][1],maxk[u]); //f[u][1]是到该点的最大总值中的最大值 for(int i=head2[u];i;i=edag[i].next) { int v=edag[i].v; degin[v]--; if(!degin[v]) { q.push(v); } if(f[u][0]>f[v][0]) //自己想吧 { f[v][0]=f[u][0]; //至于这里为什么不把当前值算上,因为这里加的不一定是最大的,要没有了入度就会在while里会算上。 f[v][1]=f[u][1]; } else if(f[u][0]==f[v][0]) { f[v][1]=max(f[v][1],f[u][1]); } } } } void out() { int ans=-1,maxx=-1; for(int i=1;i<=sum;i++) { if(f[i][0]>ans) //同理 { ans=f[i][0]; maxx=f[i][1]; } else if(f[i][0]==ans) { maxx=max(f[i][1],maxx); } } printf("%d %d",ans,maxx); }
割顶
也就是:割顶是去掉以后让图不连通的点。
思路
首先选定一个根节点,从该根节点开始遍历整个图(使用DFS)。
对于根节点,判断是不是割点很简单——计算其子树数量,如果有2棵即以上的子树,就是割点。因为如果去掉这个点,这两棵子树就不能互相到达。
通过非父子边(回边),能够回溯到的最早的点(dfn最小)的dfn值(但不能通过连接u与其父节点的边)。对于边(u, v),如果low[v]>=dfn[u],此时u就是割点。
OK?
#include<cstdio> #include<string> #include<iostream> using namespace std; const int maxn=20005,maxm=100005; int n,m; struct edge{ int v,next; }e[maxm*2]; int head[maxn],cnt; int dfn[maxn],low[maxn],index; bool map[maxn]; int ans; int read(); void readdata(); void add(int u,int v); void work(); void tarjan(int u,int fa); void out(); int main() { readdata(); work(); out(); return 0; } int read() { int x=0,f=1;char c=getchar(); while(!isdigit(c)){if(c=='-')f=-1;c=getchar();} while(isdigit(c)){x=x*10+c-'0';c=getchar();} return x*f; } void readdata() { n=read(),m=read(); for(int i=1;i<=m;i++) { int a,b; a=read(),b=read(); add(a,b); add(b,a); } } void add(int u,int v) { e[++cnt].v=v; e[cnt].next=head[u]; head[u]=cnt; } void work() { for(int i=1;i<=n;i++) { if(!dfn[i]) { tarjan(i,i); } } } void tarjan(int u,int fa) { dfn[u]=low[u]=++index; int child=0; for(int i=head[u];i;i=e[i].next) { int v=e[i].v; if(!dfn[v]) { tarjan(v,fa); low[u]=min(low[u],low[v]); if(u!=fa&&low[v]>=dfn[u]) map[u]=1; if(u==fa) { child++; } } low[u]=min(low[u],dfn[v]); } if(u==fa&&child>=2) map[u]=1; } void out() { for(int i=1;i<=n;i++) { if(map[i]) ans++; } printf("%d\n",ans); for(int i=1;i<=n;i++) { if(map[i]) printf("%d ",i); } }