图论--连通性--tarjan

  tarjan打法博大精深,有向图强连通,无向图点双连通、边双连通、割点、割边~~学得寡人头大,今天整理整理,明天继续干 ~(*^*)~

有向图求强连通:

//强连通是对于有向图而言的,无需考虑某个边的重复访问
//单个点也是一个强连通分量,要根据题目的意思对结果进行特判
//dfs自上而下,而强连通的分离自下而上,故用栈这个数据结构来存储


int mystack[maxv], dfn[maxv], low[maxv], indx, top;
int belong[maxv], scc_cnt;  //所属的强连通的编号
bool in_stack[maxv];

void
tarjan(const int& q)
{ dfn[q]
= low[q] = ++indx;  //时间戳 mystack[++top] = q; in_stack[q] = true; for(int i = head[q]; i; i = e[i].next){ if(!dfn[e[i].to]){ tarjan(e[i].to); low[q] = min(low[q], low[e[i].to]); }else if(in_stack[e[i].to]){ low[q] = min(low[q], dfn[e[i].to]); }else continue; } int v = 0; if(dfn[q] == low[q]){ scc_cnt += 1; do{ v = mystack[top--]; in_stack[v] = false; belong[v] = scc_cnt; }while(q != v); } return; }

接下来的操作都是对于无向图,由于无向图的双向性,以下的方法都要避免一条无向边的多次访问,标记点或是标记边。

无向图求割点:

  • 标记点     因为重边对割点无影响

割点是对于无向图而言的,note that 重边对于割点的判断没有影响,故重边都看作一条边!

由于无向图的边双向,为了防止某条边的重复访问,引入一个root

root的作用:

  1)root是第一个调用tarjan()函数的点的编号,此时root是dfs搜索树的根。对于根是否是割点--至少有两个子树的根是割点,所以对于根类型,我们要统计它的子树个数。有许多人都是直接统计子树最后再判断是否是根,我觉得那样逻辑不太清楚~也是因为这个他人的代码我理解了大半天~

  2)在搜索过程中root用于标记当前访问点的上一个节点,所以在访问dfn[nv]!=0的点时,即之前访问过的点时,若root == nv说明这条边之前访问过,只是边是双向仍然可以通回,所以对于这个nv点就没必要进行访问。所以仅当(root != nv)我们才进行low[u] = min(low[u], dfn[nv])的判断。但这种因用法人而异,有的人不这么写,而是直接将每次root固定函数内递归时仍然为tarjan(nv, root),这样也没问题,因为root == u的情况只出现 在根节点,所以递归传入的"root"是root或是u问题不大,只是既然都写多了个参数还是好好利用利用吧~

割点的判断:

  1)u是根节点:当u有>=2个子树时,u是割点

  2)u不是根节点:当u存在下一个子节点nv无论如何最多只能回溯到u或者u的子节点时,也可以说是nv无法回溯到u的严格祖先时,u是割点

  稍微解释下,nv要访问u的祖先们只能通过u,说明u为nv回溯的必经之点。注意这里已经考虑过重边的问题,同样建议纸上模拟模拟,理解更深哦~

*我们求的是割点,所以不需要栈什么之类的

尤其注意区分时间戳dfn【】,于最早回溯编号low【】


int dfn[mav], low[maxv], indx;
bool is_cut[maxv];

void
tarjan_v(int u, int root){ //u root 相当于一条边的两端 dfn[u] = low[u] = ++indx; int subtree = 0; for(int i = head[u]; i; i = e[i].next) { int nv = e[i].to; if(!dfn[nv]) { tarjan(nv, u); low[u] = min(low[u], low[nv]); //最小能够到达的节点 if(u != root && low[nv] >= dfn[u])//非根节点且其节点为根节点的子树中任意节点最多回溯到其本身,只要存在一个即可!
       {                     //注意是nv的最早能回溯到的编号low【】与u的时间戳dfn【】(dfs第一次遍历到的次序)相比较!
is_cut[u] = true;
       }
if(u == root) //是根节点则统计子树 subtree +=1; }else if(nv != root) low[u] = min(low[u], dfn[nv]); } if(u == root && subtree >= 2)//是根节点,且有两个子树 is_cut[u] = true; return; }

 

 

无向图边双连通

边双连通类似有向图的“强连通”:标记边   因为重边会影响边双连通的判断

  • 某个极大子图中去掉任意一条边都不会改变此图的连通性,则该极大子图为边双连通分量。
  • 若某极大子图为边双连通分量 <==> 该图上任意两点至少有两条所有边完全不同的路径相连(例如一个环上两点,既可以顺时针来,也可以逆时针来)
  • 简而言之--不存在桥(割边)的极大子图为边双连通分量

 

求法也几乎与有向图中的强连通类似:只是要考虑双向边重复访问的,即两点间有多条双向边的问题。由于两点间可能有多条边,相比单向图的强连通球法我们引入多一个参数--eid,这代表到达u这个点刚经过的边,着重讲讲防止重复遍历一条边:

以链式向前星的存图方式为例。首先了解一下与1异或这个小技巧,对于偶数与1异或相当于+1,对于奇数与1异或相当于-1,存双向边本质上是存了两个反向的单向边,所以恰好是成对存储的(如果你用其他神仙操作不成对存,那在下就告辞聊)。为了方便我们将边的编号从2开始编号,这样一条双向边的编号就是一对<偶数,奇数>,这样我们就恰好可以利用与1异或的性质来根据当前的编号判断当前边是否是访问过的eid边的成对的另一条边。所以这就是为何有那句 if(i == (1^eid)) continue;

对于其他存图方式则依据这种原理利用其他方法判断即可

 

注意图中的每个点只可能属于一个边双连通分量,等会讲点双连通分量,注意区分!!



int dfn[maxv], low[maxv], indx;
int belong[maxv], bcc_cnt;
int mystack[maxv], top;

void
tarjan( int u, int eid){  //经过eid这条边到达的u点 dfn[u] = low[u] = ++indx; mystack[++top] = u; in_stack[u] = true; for(int i = head[u]; i; i = e[i].next) { if(i == (1^eid)) continue;  //i边是到达u访问过的,是与eid成对的双向边的单向边之一 int nv = e[i].to; if(!dfn[nv]) { tarjan(nv, i); low[u] = Min(low[u], low[nv]); }else if(in_stack[nv]){ low[u] = Min(low[u], dfn[nv]); }else continue; } if(low[u] == dfn[u]){ ++bcc_cnt; int tmp = 0; do{ tmp = mystack[top--]; in_stack[tmp] = false; belong[tmp] = bcc_cnt; //缩点(将该连通分量上的点同一用一个编号表示) }while(u != tmp); } return; }

无向图求点双连通

参考自:https://www.cnblogs.com/LiHaozhe/p/9527136.html

点双连通:标记点  因为重边不影响割点或是树根的判断,故再多重边都当作一边处理,弄清这里的逻辑哦~

  • 对于无向图中,任意两点至少存在两条点完全不同的路径相连,则该图点双连通。
  • 简而言之,无割点的图是点双连通图。

  三个重要的性质:

    1)bcc中无割点

    2)若两个bcc有公共点,则该公共点为两个bcc组成的图的割点

    3)每一个割点至少属于两个bcc,非割点只属于一个bcc。(注意由于割点的定义,形如 a--b 也称作以个点双连通,做题时注意题目要求特判。

由于割点会同时属于多个bcc,所以有很多其他博主都用栈存边的方式来查找,这里介绍一种仍然存点的方式。这里的u和root与求割点中的意义相同。给一个结论每个bcc都在其最先发现的点(必是割点或dfs树根)的子树中。所以,关键是找到割点或树根,找到割点、树根就找到了点连通分量,所以主要操作与找割点很像,唯一不同在于无需将树根(root == u)的情况另外考虑,只需要满足(low[nv] >= dfn[u])就可以,因为此时要么是割点,要么是树根。之后将该子树与当前节点加入到bcc中。

*要注意得到点双连通分量后出栈时,出到根/割点之前就停止,这是用存点的方法求点双连通的一个关键点,因为割点必然属于多个点双连通分量,若一下子全部出栈会造成其他点连通点的缺失,这里建议在纸上自己模拟一次,理解了才不容易犯错!

 


int dfn[maxv], low[maxv], indx;
int mystack[maxv], top;
int bcc[maxv], bcc_cnt;

void
tarjan_bcc_v(int u, int root) //root u 相当于一条边的两端点 { dfn[u] = low[u] == ++indx; for(int i = head[u]; i; i = e[i].next) { int nv = e[i].to; if(!dfn[nv]) { mystack[++top] = nv; //搜索到的点入栈 tarjan(nv, u); low[u] = min[low[u], low[nv]]; if(low[nv] >= dfn[u]){ //是割点或根 bcc_cnt += 1; int tmp = mystack[top--]; while(tmp != nv) { bcc[bcc_cnt].push_back(tmp); tmp = mystack[top--]; }//一直出栈到割点之前 bcc[bcc_cnt].push_back(tmp); bcc[bcc_cnt].push_back(u); //虽然加入到了一个bcc中,当前割点仍然在栈中 } }else if(nv != root) low[u] = min(low[u], dfn[nv]); } return; }

 无向图求割边(桥)

嗯,对的,割边最后讲,因为有两种求法:

  1)先将无向图用边双连通的方法缩点处理,得到一个新图,此时所有新点间的边都是桥

  2)在tarjan中若low[nv] > dfn[u],则u与v间相连的边是桥,因为nv最早能够回溯到的点在u之后,所以u与nv间的边不可或缺。

法一:在进行边双连通操作时查找

 

void tarjan(int u, int eid){
    dfn[u] = low[u] = ++indx;for(int i = head[u]; i; i = e[i].next)
  {
if(i == (1^eid)) continue; int nv = e[i].to; if(!dfn[nv]) { tarjan(nv, i); low[u] = Min(low[u], low[nv]); if(low[nv] > dfn[u]){ // 只要存在一个则是桥 **编号为i这条边是桥(怎么保存,看题目而定吧)**
       } }
else if(in_stack[nv]) low[u] = Min(low[u], dfn[nv]); else continue; }return; }

 

法二:缩点后在新图上查找

void tarjan(int u, int eid){
    dfn[u] = low[u] = ++indx;
    mystack[++top] = u;
    in_stack[u] = true;
    for(int i = head[u]; i; i = e[i].next){
        if(i == (1^eid)) continue;
        int nv = e[i].to;
        if(!dfn[nv])
        {
            tarjan(nv, i);
            low[u] = Min(low[u], low[nv]);
        }else if(in_stack[nv])
            low[u] = Min(low[u], dfn[nv]);
        else continue;
    }
    if(low[u] == dfn[u]){
        bcc_cnt += 1;
        int tmp = 0;
        do{
            tmp = mystack[top--];
            in_stack[tmp] = false;
            belong[tmp] = bcc_cnt;    //缩点
        }while(tmp != u);
    }
    return;
}
void find_minBridge(){
    for(int u = 1; u<=n; ++u){
        for(int t = head[u]; t; t = e[t].next){
            int nv = e[t].to;
            if(belong[u] != belong[nv]){//两点间的边是桥
                u与nv之间的边t是桥
       } } }
return; }

 

 

emm, last but not least,点双连通一定是边双连通,边双连通不一定是点双连通。

posted @ 2019-07-14 23:04  Bankarian  阅读(315)  评论(0编辑  收藏  举报