第一阶段的集训结束了w,不得不说oi太长时间不整是会退步的。
怎么说好呢,集训这几天过的很充实,知识收货很多,题调的也不少,自己的目标更明确了吧,不过这几天集训也是可以看出蒟蒻就是蒟蒻,还是太菜了。。。。不过会努力的w。
首先得感谢一下兔哥,假期还来给我们上课,还要饱受牙疼的折磨。。。再次感谢兔哥,%%%%%大神就是大神w。
结束之后想想这两天都记住什么了w,第一天讲了tarjan,印象最深的还是圆方树这个知识点吧,毕竟这个是个木有学习过的知识。
算了,要不我还是顺道复习一下强联通分量blabla什么的吧。。。。。。。
+ 强联通分量(不要告诉我博客园不支持markdown)。。。。
在有向图G中,如果两个顶点u,v之间存在一条路径u到v的路径而且也存在一条v到u的路径,则称这两个顶点u,v是强联通的。如果有向图G的每两个顶点都强联通,称G是一个强联通图。有向非强联通图的极大强联通子图,称之为强联通分量。若将有向图中的强联通分量都缩成一个点,则原图会形成一个DAG。
+ 那么我们一般求强联通分量的方法是tarjan,那么它是怎么实现的w?
其实,tarjan本质上是个dfs,dfs过程中向栈中push当前节点。当找到一个强联通分量的时候,(其包含的节点一定都在栈中连续排列),so我们把它们全部都pop掉,这些元素构成一个强联通分量。
每个节点u有两个值——dfn[u]和low[u]。dfn[u]就是dfs序中的位置,low[u]就是u经过最多一条栈中横叉边所能到达的dfn最小的点。这里我们解释一下什么是横叉边,因为我们tarjan是dfs,这棵dfs树上有原图上的边,在dfs树上由父亲指向儿子的叫做树枝边,由父亲指向儿子下面其他后代的叫做前向边,由后代指向祖先的叫做后向边,像其他的边(一个子树中的点指向另一个子树中的点)称之为横叉边。栈中横叉边,顾名思义,就是终点是栈里面元素的边。
那么问题来了,如何求我们这个所谓的“经过最多一条栈中横叉边能到达的dfn最小的点的dfn”(也就是low)?;
当我们dfs到的当前点为点u,枚举u能到达的点v。若v没被dfs过,递归dfs点v,并且用low[v]更新low[u](就是low[u] = min(low[u],low[v])),如果v被dfs过且仍在栈中,说明u—>v这条边是一条栈中横叉边,用dfn[v]更新low[u]。
如果所有v枚举完以后,low[u]仍等于dfn[u],则此时栈中u及u以上的节点同属于一个强联通分量,要一起弹出。
此处应该有图片。。。。。我也不知道它能不能看到,应该可吧
然后我们说一说tarjan的性质:
+ 各个强联通分量出栈的顺序是一定的——就是缩点形成的DAG的拓扑序的逆序。
拓扑序就是DAG中的一种排序,要求任一条边的终点排在起点之后。
so,tarjan可以求拓扑序,好写???
tarjan的代码的话。
void tarjan(int u){ dfn[u] = low[u] = ++idx; stk[+top] = u,ins[u] = 1; for(int i = adj[u],v;i;i = nxt[i]){ if(!dfn[v = go[i]]){ tarjan(v); low[u] = min(low[u],low[v]); } else if(ins[v]){ low[u] = min(low[u],low[v]); } } if(low[u] == dfn[u]){ ncnt++; do{ bel[stk[top]] = ncnt; ins[stk[top]] = 0; }while(stk[top--] != u); } }
例题讲了挺多w,像联通数(暴力bfs加02爆锤标程),以及迷失在公园里找不到方向(逛公园)
我记得还讲了无向图的tarjan来着????
这个好像需要引入几个概念???
+ 割点:将这个点删去后图不连通;
+ 割边: 讲这条边删去后图不连通;
+ 点双联通分量:无割点的极大联通子图。
+ 边双联通分量:无割边的极大联通子图。
然后如何用tarjan求割边,边双?
在无向图的dfs树中,原图中的边不可能是横叉边,只可能是前(后)向边或树枝边。所以无向图的tarjan相比有向图很好写,无需讨论某条边是否为栈中横叉边。
一条无向边(u,v)是割边(dfn[u] < dfn[v])当且仅当它是树枝边,而且low[v] > dfn[u](这时子树v中所有点不能到达子树之外)
这时我们发现,删去原图中所有割边,剩下的连通块都是边双。
然后还有tarjan求割点qww
当u不是dfs树的根的时候,对于树枝边(u,v)(u为v的父亲),如果low[v] >= dfn[u],就是子树v中的点都不通往到子树u之外,那么删去点u,子树v中的点与子树u以外的点不了联通,u为割点。
但是呢,当u是dfs的根的时候,判断low[v] >= dfn[u]是不够的。因为对于任意的v都满足这个条件啊w,(当子树u以外没有点,上面判断割点的理由就不成立了w)。
so,我们怎么判断呢?如果u有至少两个子树,则u是割点,只有一个子树,u不是割点。
还有要值得注意的是,删去所有割点,剩下的联通块并不是点双qww。(这个还挺显然的吧)。
那么我们怎么求点双呢???
其中一种求点双的方法是把边push进栈中,当发现low[v] >= dfn[u]时,把边(u,v)及它以上的所有边从栈中弹出,这些边所涉及的点共同形成一个点双(显然包括u)。
另一种方法是像别的tarjan算法一样把点压入栈中,当发现low[v] >= dfn[u]时,把v及v以上的点从栈中弹出,再加上u,共同形成一个点双。代码的话。。。。。
tarjan求割点:
void tarjan(int u,int pre){ int v,child_cnt = 0; dfn[u] = low[u] ++index; for(int i = head[u];i;i = nxt[i]){ if(!dfn[v = to[i]]){ tarjan(v,u); low[u] = min(low[u],low[v]); if(low[v] >= dfn[u]){ child_cnt++; iscut[u] = 1; } else if(v != pre){ low[u] = min(low[u],dfn[v]); } } if(u == 1 && child_cnt == 1) iscut[u] = 0; }
求割边,边双代码:
void tarjan(int u,int pre){ stk[++top] = u; dfn[u] = low[u] = ++idx; for(int i = head[u],v;i;i = nxt[i]){ if(!dfn[v = to[i]]){ tarjan(v,u); low[u] = min(low[u],low[v]); if(low[v] > dfn[u]){ is_bridge[i] = is_bridge[i ^ 1] = 1; cnt++; do bel[stk[top]] = cnt; while(stk[top--] != v); } } else if(v != pre){ low[u] = min(low[u],dfn[u]); } } }
下面终于进入了我们正题了,圆方树!!!!!!!!!!!
what is it?
一个无向联通图可以对应一棵圆方树(广义的)。
圆方树中有圆点和方点。
圆点是原图中存在的点,方点对应原图的点双。原图中跨点双的边予以保留,点双内部的边删掉,同时所有方点与其对应点双中的圆点连边。
此处应该有图片:
那么我们怎么建一棵圆方树呢?
我们新建一个链前,按照定义对每个点双建一个方点,然后把它和点双中的所有点连边,再把原图中跨点双的边也连上。
显然,可以一边tarjan求点双,一边把边连了。
在这里举一道棵题:
给出一张图,点有点权,每次询问两点之间的简单路径中,权值的最小值最小是多少。n,m,q <=1000000.
这个题我们把圆方树建出,方点权值为包含圆点权值最小值,然后这个问题就转化成了求链上最小值。可倍增,可树剖。。。。over