双连通份量(点-双连通份量&边-双连通份量)

概念:

双连通份量有点双连通份量和边双连通份量两种。若一个无向图中的去掉任意一个节点(一条边)都不会改变此图的连通性,即不存在割点(桥),则称做点(边)双连通图。node

一个无向图中的每个极大点(边)双连通子图称做此无向图的点(边)双连通份量。求双连通份量可用Tarjan算法。--百度百科ios

Tip:先学一下tarjan算法以及求割点割边的算法以后,再看会比较好理解一些。算法

点双连通和边双连通

  • 连通的概念:在无向图中,全部点能互相到达
  • 连通份量:互相联通的子图
  • 点双连通:删掉一个点以后,图仍联通
  • 边双连通:删掉一条边以后,图仍联通

 


 

概述

在一个无向图中,若任意两点间至少存在两条“点不重复”的路径,则说这个图是点双连通的(简称双连通,biconnected)c#

在一个无向图中,点双连通的极大子图称为点双连通份量(简称双连通份量,Biconnected Component,BCC)数据结构

 

性质

  1. 任意两点间至少存在两条点不重复的路径等价于图中删去任意一个点都不会改变图的连通性,即BCC中无割点
  2. 若BCC间有公共点,则公共点为原图的割点
  3. 无向连通图中割点必定属于至少两个BCC,非割点只属于一个BCC

 

算法

 在Tarjan过程当中维护一个栈,每次Tarjan到一个结点就将该结点入栈,回溯时若目标结点low值不小于当前结点dfn值就出栈直到目标结点(目标结点也出栈),将出栈结点和当前结点存入BCCide

(说实话我以为存点不比存边难理解和实现啊……下面会解释)优化

 

理解

首先申明一下,在我找到的BCC资料中,在算法实现中均将两个点和一条边构成的图称为BCC,此文章也沿用此的规定ui

以下图:spa

我猜测多是由于割点的定义,此图中两个点均不为割点,因此此图也属于BCC?.net

总之作题时注意题面要求,若要求的不含此种BCC则判断每一个BCC的大小便可

 

无向连通图中割点必定属于至少两个BCC,非割点只属于一个BCC

有了上面的规定咱们也不难理解这一条了:割点就算相邻也会属于至少两个BCC;BCC间的交点都是割点,因此非割点只属于一个BCC

 

到一个结点就将该结点入栈

为何用栈存储呢?由于DFS是由上到下的,而分离BCC是自下而上的,须要后进先出的数据结构——栈

 

回溯时若目标结点low值不小于当前结点dfn值就出栈直到目标结点(目标结点也出栈),将出栈结点和当前结点存入BCC

对于每一个BCC,它在DFS树中最早被发现的点必定是割点或DFS树的树根

证实:割点是BCC间的交点,故割点在BCC的边缘,且BCC间经过割点链接,因此BCC在DFS树中最早被发现的点是割点;特殊状况是对于开始DFS的点属于的BCC,其最早被发现的点就是DFS树的树根

上面的结论等价于每一个BCC都在其最早被发现的点(一个割点或树根)的子树中

 这样每发现一个BCC(low[v]>=dfn[u]),就将该子树出栈,并将该子树和当前结点(割点或树根)加入BCC中。上面的操做与此描述等价

(我就是由于这个条件“将子树出栈”没理解写错告终果调了一夜poj2942)

 

综上,存点是否是很好理解?存边虽然不会涉及重复问题(割点属于至少两个BCC),但会有不少无用操做。我的以为存点也是个不错的选择。

 

模板

#include<cstdio>
#include<cctype>
#include<vector>
using namespace std;
struct edge
{
    int to,pre;
}edges[1000001];
int head[1000001],dfn[1000001],dfs_clock,tot;
int num;//BCC数量 
int stack[1000001],top;//
vector<int>bcc[1000001];
int tarjan(int u,int fa)
{
    int lowu=dfn[u]=++dfs_clock;
    for(int i=head[u];i;i=edges[i].pre)
        if(!dfn[edges[i].to])
        {
            stack[++top]=edges[i].to;//搜索到的点入栈 
            int lowv=tarjan(edges[i].to,u);
            lowu=min(lowu,lowv);
            if(lowv>=dfn[u])//是割点或根 
            {
                num++;
                while(stack[top]!=edges[i].to)//将点出栈直到目标点 
                    bcc[num].push_back(stack[top--]);
                bcc[num].push_back(stack[top--]);//目标点出栈 
                bcc[num].push_back(u);//不要忘了将当前点存入bcc 
            }
        }
        else if(edges[i].to!=fa)
            lowu=min(lowu,dfn[edges[i].to]);
    return lowu;
}
void add(int x,int y)//邻接表存边 
{
    edges[++tot].to=y;
    edges[tot].pre=head[x];
    head[x]=tot;
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y),add(y,x);
    }
    for(int i=1;i<=n;i++)//遍历n个点tarjan 
        if(!dfn[i])
        {
            stack[top=1]=i;
            tarjan(i,i);
        }
    for(int i=1;i<=num;i++)
    {
        printf("BCC#%d: ",i);
        for(int j=0;j<bcc[i].size();j++)
            printf("%d ",bcc[i][j]);
        printf("\n");
    }
    return 0;
}

 


 

 

简介

在阅读下列内容以前,请务必了解图论基础部分。

相关阅读:割点和桥

定义

割点和桥更严谨的定义参见图论基础

在一张连通的无向图中,对于两个点u和v,若是不管删去哪条边(只能删去一条)都不能使它们不连通,咱们就说u和v边双连通 。

在一张连通的无向图中,对于两个点u和v,若是不管删去哪一个点(只能删去一个,且不能删 和 本身)都不能使它们不连通,咱们就说u和v点双连通 。

边双连通具备传递性,即,若x,y边双连通, y,z边双连通,则x,z边双连通。

点双连通  具备传递性,反例以下图, A,B点双连通, B,C点双连通,而 A,C点双连通。

bcc-counterexample.png

DFS

对于一张连通的无向图,咱们能够从任意一点开始 DFS,获得原图的一棵生成树(以开始 DFS 的那个点为根),这棵生成树上的边称做 树边 ,不在生成树上的边称做 非树边 。

因为 DFS 的性质,咱们能够保证全部非树边链接的两个点在生成树上都知足其中一个是另外一个的祖先。

DFS 的代码以下:

void DFS(int p) {
 visited[p] = true;
 for (int to : edge[p])
   if (!visited[to]) DFS(to);
}

 

DFS 找桥并判断边双连通

首先,对原图进行 DFS。

bcc-1.png

如上图所示,黑色与绿色边为树边,红色边为非树边。每一条非树边链接的两个点都对应了树上的一条简单路径,咱们说这条非树边 覆盖 了这条树上路径上全部的边。绿色的树边 至少 被一条非树边覆盖,黑色的树边不被 任何 非树边覆盖。

咱们如何判断一条边是否是桥呢?显然,非树边和绿色的树边必定不是桥,黑色的树边必定是桥。

如何用算法去实现以上过程呢?首先有一个比较暴力的作法,对于每一条非树边,都逐个地将它覆盖的每一条树边置成绿色,这样的时间复杂度为O(nm) 

怎么优化呢?能够用差分。对于每一条非树边,在其树上深度较小的点处打上 -1 标记,在其树上深度较大的点处打上 +1 标记。而后O(n)求出每一个点的子树内部的标记之和。对于一个点u,其子树内部的标记之和等于覆盖了u和u的父亲之间的树边的非树边数量。若这个值非0,则u和u的父亲之间的树边不是桥,不然是桥。

用以上的方法O(n+m)求出每条边分别是不是桥后,两个点是边双连通的,当且仅当它们的树上路径中  包含桥。

DFS 找割点并判断点双连通

bcc-2.png

如上图所示,黑色边为树边,红色边为非树边。每一条非树边链接的两个点都对应了树上的一条简单路径。

考虑一张新图,新图中的每个点对应原图中的每一条树边(在上图中用蓝色点表示)。对于原图中的每一条非树边,将这条非树边对应的树上简单路径中的全部边在新图中对应的蓝点连成一个连通块(这在上图中也用蓝色的边体现出来了)。

这样,一个点不是桥,当且仅当与其相连的全部边在新图中对应的蓝点都属于同一个连通块。两个点点双连通,当且仅当它们在原图的树上路径中的全部边在新图中对应的蓝点都属于同一个连通块。

蓝点间的连通关系能够用与求边双连通时用到的差分相似的方法维护,时间复杂度 

 

 

 


 

【双连通份量】

1、边双连通份量定义

在份量内的任意两个点总能够找到两条边不相同的路径互相到达。总而言之就是一个圈,正着走反着走均可以相互到达,至少只有一个点。

2、点双连通份量的定义

参照上面,惟一的不一样:任意两个点能够找到一条点不一样的路径互相到达。也是一个圈,正反走均可以,至少为一个点。

3、边、点双连通份量模板代码要注意的地方

边双连通份量

1.每一个节点的全部儿子遍历后才开始计算份量大小,请与点双连通相区分;

2.割顶只能属于一个份量,请与割边区分;(容易搞混)

3.要注意j是不是i的父节点;

上述几点以下:

void DFS(int i,int fd)//fd是父边 
{
    low[i]=dfn[i]=++dfs_clock;
    vis[i]=1;
    stk[++top]=i;//栈存节点 
    for(int p=last[i];p;p=E[p].pre)
    {
        int j=E[p].to,id=E[p].id;
        if(vis[j])
        {
            if(dfn[j]<dfn[i]&&fd!=id) low[i]=min(low[i],dfn[j]);
            continue;
        }
        DFS1(j,id);
        low[i]=min(low[i],low[j]); 
    }
    
    //全部儿子遍历完再求 
    if(low[i]==dfn[i])
    {
        cc++;
        int x;
        while(1)
        {
            x=stk[top--];
            belong[x]=cc;
            size[cc]++;
            if(x==i) break;//注意是等于i才跳出,也就是i只能属于一个边连通份量 
        }
        maxcc=max(maxcc,size[cc]);
    }
}

 

点双连通份量

 

1.每遍历一个儿子就计算是否有点连通份量;

2.割顶能够属于多个连通份量,请注意与割边区分;

3.当i为根节点时,至少要有两个儿子才能是割点;

上述几点以下:

void DFS(int i,int fd)//fd是父边 
{
    low[i]=dfn[i]=++dfs_clock;
    stk[++top]=i;//栈存节点 
    int chd=0;//统计儿子数 
    
    for(int p=last[i];p;p=E[p].pre)
    {
        
        int j=E[p].to,id=E[p].id;
        if(dfn[j])
        {
            if(dfn[j]<dfn[i]&&id!=fd) low[i]=min(low[i],dfn[j]);
            continue;
        }
        
        
        chd++;
        DFS(j,id);
        low[i]=min(low[i],low[j]);
        
        
        if(low[j]>=dfn[i])//遍历完一个儿子就看是否有连通份量 
        {
            cut[i]=1;//初步判断i是割顶(还不必定,要看最后的条件) 
            bcc_cnt++;
            bcc[bcc_cnt].push_back(i);//只是把i给存进去,而不存i属于哪一个份量,由于i是割顶,可能也属于别的份量 
            int x;
            while(1)
            {
                x=stk[top--];
                bcc[bcc_cnt].push_back(x); 
                if(x==j) break;//注意到j结束 
            }
        }
        
    }
    
    
    if(fd==0&&chd==1) cut[i]=0;//这个结论应该都知道 
}

 

 

【强连通份量】

1、定义

有向图上的环,不啰嗦,与上面两种相似,至少为一个点;

 

2、模板代码注意的地方

1.每一个点全部儿子遍历完才开始求份量;(相似边双连通份量)

2.每一个点只能属于一个份量;

void DFS(int i)
{
    low[i]=dfn[i]=++dfs_clock;
    stk[++top]=i;
    for(int p=last[i];p;p=E[p].pre)
    {
        int j=E[p].v;
        if(dfn[j])
        {
            if(!belong[j]) low[i]=min(low[i],dfn[j]);
            continue;
        }
        
        DFS(j);
        low[i]=min(low[i],low[j]); 
    }
    
    if(dfn[i]==low[i])
    {
        scc++;
        while(1)
        {
            int x=stk[top--];
            belong[x]=scc;
            size[scc]++;
            if(x==i) break;
        }
    }
}

 


 

【强连通份量和双连通份量常见的模型和问法】

双连通份量

1.给出的图是非连通图,如:

a.有一些点,一些边,加最少的边,要使得整个图变成双联通图。

大体方法:求出全部份量,把每一个份量当作一个点,统计每一个点的度,有一个度为一则cnt加1,答案为(cnt+1)/2;

b.有一些点,一些边,问最少多少个点单着。

大体方法:求出全部的份量便可,但要注意不一样的题可能有特殊要求(如圆桌骑士要求奇圈,要用到二分图断定)

c.各类变式问题

 

2.给出的图是连通图,如:

a.给定一个起点一个终点,求各类问题是否能实现。

大体方法:求出全部份量,并把每一个份量当成点,因而问题获得化简;

b.给一个图,而后有大量的离线回答。

大体方法:求出全部份量,再求出上下子树的信息;

c.各类变式问题;

 

强连通份量

1.给出的是非连通图,如:

a.有一些点,一些有向边,求至少加多少边使任意两个点可相互到达

大体方法:求出全部的份量,缩点,分别求出出度入度为0的点的数量,取多的为答案;

b.有一些点,一些有向边,求在这个图上走一条路最多能够通过多少个点

大体方法:求出全部的份量,缩点,造成一个或多个DAG图,而后作DAG上的dp

c.有一些点,一些有向边,给出一些特殊点,求终点是特殊点的最长的一条路

大体方法:求出全部份量,并标记哪些份量有特殊点,而后也是DAG的dp

 

2.给出的是连通图,比较少,有也比较简单

总结

1.遇到非连通图几乎能够确定是要求连通份量,不管是无向仍是有向图;(能够节约大量思考时间)

2.凡是对边、点的操做,在同一个份量内任意一个点效果相同的,几乎都是缩点解决问题;再粗暴点,几乎求了连通份量都要缩点;

3.必定要考虑特殊状况,如整个图是一个连通份量等(考虑到了就有10-20分);

4.对于双连通份量要分析是边仍是点双连通份量;

5.拿到题目要先搞清楚给的是连通图仍是非连通图。

 

POJ3694 Network

https://vjudge.net/problem/POJ-3694

problem

A network administrator manages a large network. The network consists of N computers and M links between pairs of computers. Any pair of computers are connected directly or indirectly by successive links, so data can be transformed between any two computers. The administrator finds that some links are vital to the network, because failure of any one of them can cause that data can't be transformed between some computers. He call such a link a bridge. He is planning to add some new links one by one to eliminate all bridges.

You are to help the administrator by reporting the number of bridges in the network after each new link is added.

Input

The input consists of multiple test cases. Each test case starts with a line containing two integers N(1 ≤ N ≤ 100,000) and M(N - 1 ≤ M ≤ 200,000).
Each of the following M lines contains two integers A and B ( 1≤ A ≠ B ≤ N), which indicates a link between computer A and B. Computers are numbered from 1 to N. It is guaranteed that any two computers are connected in the initial network.
The next line contains a single integer Q ( 1 ≤ Q ≤ 1,000), which is the number of new links the administrator plans to add to the network one by one.
The i-th line of the following Q lines contains two integer A and B (1 ≤ A ≠ B ≤ N), which is the i-th added new link connecting computer A and B.

The last test case is followed by a line containing two zeros.

Output

For each test case, print a line containing the test case number( beginning with 1) and Q lines, the i-th of which contains a integer indicating the number of bridges in the network after the first i new links are added. Print a blank line after the output for each test case.

Sample Input

 
3 2
1 2
2 3
2
1 2
1 3
4 4
1 2
2 1
2 3
1 4
2
1 2
3 4
0 0

Sample Output

 
Case 1:
1
0

Case 2:
2
0

大体翻译:给你N个点M条边的无向图,而且有Q次加边,问每次加边以后图中的桥的数量。

显然,若是加入的边的两个端点在同一个边双内,那么桥的数量不变。因此咱们先用Tarjan对原图进行边双连通份量缩点获得一棵树。

接着,对于两个端点不在一个边双的状况,显然桥的数量减小量等于两个端点的树上距离。咱们求出树上距离,而后把两端点之间的边标记起来,即边长由原来的1改为0。每次求树上距离时就先一个个地往上爬,顺便还能够标记边。时间复杂度为O(M+QN),能够经过本题,但显然不优。

既然边长变成0了,咱们之后都不必再管这些边了,因此咱们能够用缩树的办法,用并查集把两个端点之间的点合并到一个集合中去,而后下次爬到这两个端点处时直接跳到LCA的位置就行了。

 

题解

1.利用Tarjan算法,求出每一个边双联通份量,而且记录每一个点属于哪个份量。

2.将每个边双联通份量缩成一个点,最终获得一棵树。而咱们想要获得一棵有根树,怎么办?其实在执行Tarjan算法的时候,就已经造成了一个有根树。因此咱们只须要在Tarjan算法的基础上,再记录每个点的父节点以及深度就能够了。

3.每次询问的时候,若是两个点在同一个份量中,那么他们的相连不会减小桥的个数。若是两个点在不一样的份量中,那么u->LCA(u,v)和v->LCA(u,v)上路径上的桥,均可以减小,路径上的点均可以缩成一个点,即合并成一个份量。

 

对于缩点的处理:

  方法一:对于一个份量,能够设置一个点为实点,其他的点为虚点。实点即表明着这个份量的全部信息,虚点虽然属于这个份量的点,可是却对他视而不见。咱们要作的,就是在这个份量里选择一个点,去表明整个份量。

  方法二:一样地,咱们也须要为每个份量选出一个表明,以表示这个份量。与方法一的“视而不见”不一样的是,方法二对每个点都设置了一个归属集合,即表示这个点属于哪个集合。因为在处理的过程当中,一个集合可能又会被另外一个集合所包含,因此咱们能够利用并查集的路径压缩,很快地找到一个点的最终所属集合。

方法一:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <queue>
#include <stack>
#include <map>
#include <string>
#include <set>
#define ms(a,b) memset((a),(b),sizeof((a)))
using namespace std;
typedef long long LL;
const double EPS = 1e-8;
const int INF = 2e9;
const LL LNF = 2e18;
const int MAXN = 1e5+10;

struct Edge
{
    int to, next;
}edge[MAXN*8];
int tot, head[MAXN];

int index, dfn[MAXN], low[MAXN];
int isbridge[MAXN], sum_bridge;
int fa[MAXN], depth[MAXN];

void addedge(int u, int v)
{
    edge[tot].to = v;
    edge[tot].next = head[u];
    head[u] = tot++;
}

void Tarjan(int u, int pre)
{
    dfn[u] = low[u] = ++index;
    depth[u] = depth[pre] + 1;  //记录深度
    fa[u] = pre;        //记录父亲结点
    for(int i = head[u]; i!=-1; i = edge[i].next)
    {
        int v = edge[i].to;
        if(v==pre) continue;
        if(!dfn[v])
        {
            Tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if(low[v]>dfn[u])   //isbridge[v]表示在树中,以v为儿子结点的边是否为桥
                isbridge[v] = 1, sum_bridge++;
        }
        else
            low[u] = min(low[u], dfn[v]);
    }
}

void LCA(int u, int v)
{
    if(depth[u]<depth[v]) swap(u, v);
    while(depth[u]>depth[v])    //深度大的先往上爬。遇到桥,就把它删去。
    {
        if(isbridge[u]) sum_bridge--, isbridge[u] = 0;
        u = fa[u];
    }
    while(u!=v) //当深度同样时,一块儿爬。遇到桥,就把它删去。
    {
        if(isbridge[u]) sum_bridge--, isbridge[u] = 0;
        u = fa[u];
        if(isbridge[v]) sum_bridge--, isbridge[v] = 0;
        v = fa[v];
    }
}

void init()
{
    tot = 0;
    memset(head, -1, sizeof(head));

    index = 0;
    memset(dfn, 0, sizeof(dfn));
    memset(low, 0, sizeof(low));
    memset(isbridge, 0, sizeof(isbridge));

    sum_bridge = 0;
}

int main()
{
    int n, m, kase = 0;
    while(scanf("%d%d", &n, &m) && (n||m) )
    {
        init();
        for(int i = 1; i<=m; i++)
        {
            int u, v;
            scanf("%d%d", &u, &v);
            addedge(u, v);
            addedge(v, u);
        }

        depth[1] = 0;
        Tarjan(1, 1);
        int q, a, b;
        scanf("%d", &q);
        printf("Case %d:\n", ++kase);
        while(q--)
        {
            scanf("%d%d", &a, &b);
            LCA(a, b);
            printf("%d\n", sum_bridge);
        }
        printf("\n");
    }
}
View Code

方法二:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <vector>
#include <queue>
#include <stack>
#include <map>
#include <string>
#include <set>
#define ms(a,b) memset((a),(b),sizeof((a)))
using namespace std;
typedef long long LL;
const double EPS = 1e-8;
const int INF = 2e9;
const LL LNF = 2e18;
const int MAXN = 1e6+10;

struct Edge
{
    int to, next;
}edge[MAXN], edge0[MAXN];   //edge为初始图, edge0为重建图
int tot, head[MAXN], tot0, head0[MAXN];

int index, dfn[MAXN], low[MAXN];
int top, Stack[MAXN], instack[MAXN];
int belong[MAXN];
int fa[MAXN], depth[MAXN];  //fa用于重建图时记录当前节点的父亲节点,depth记录当前节点的深度
int sum_bridge;

//找到x最终所属的结合
int find(int x) { return belong[x]==x?x:belong[x]=find(belong[x]); }

void addedge(int u, int v, Edge edge[], int head[], int &tot)
{
    edge[tot].to = v;
    edge[tot].next = head[u];
    head[u] = tot++;
}

void Tarjan(int u, int pre)
{
    dfn[u] = low[u] = ++index;
    Stack[top++] = u;
    instack[u] = true;
    for(int i = head[u]; i!=-1; i = edge[i].next)
    {
        int v = edge[i].to;
        if(v==pre) continue;
        if(!dfn[v])
        {
            Tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if(low[v]>dfn[u]) sum_bridge++;
        }
        else if(instack[v])
            low[u] = min(low[u], dfn[v]);
    }

    if(dfn[u]==low[u])
    {
        int v;
        do
        {
            v = Stack[--top];
            instack[v] = false;
            belong[v] = u;  //把集合的编号设为联通份量的第一个点
        }while(v!=u);
    }
}

void build(int u, int pre)
{
    fa[u] = pre;    //记录父亲节点
    depth[u] = depth[pre] + 1;  //记录深度
    for(int i  = head0[u]; i!=-1; i=edge0[i].next)
        if(edge0[i].to!=pre)    //防止往回走
            build(edge0[i].to, u);
}


int LCA(int u, int v)   //左一步右一步地找LCA
{
    if(u==v) return u;  //由于两个结点必定有LCA, 因此必定有u==v的时候

    //可能爬一步就爬了几个深度,由于中间的结点已经往上缩点了
    if(depth[u]<depth[v]) swap(u, v);   //深度大的往上爬
    sum_bridge--;
    int lca = LCA(find(fa[u]), v);
    return belong[u] = lca;     //找到了LCA,在沿路返回的时候把当前节点的所属集合置为LCA的所属集合
}

void init()
{
    tot = tot0 = 0;
    memset(head, -1, sizeof(head));
    memset(head0, -1, sizeof(head0));

    index = top = 0;
    memset(dfn, 0, sizeof(dfn));
    memset(low, 0, sizeof(low));
    memset(instack, 0, sizeof(instack));

    sum_bridge = 0;
}

int main()
{
    int n, m, kase = 0;
    while(scanf("%d%d", &n, &m) && (n||m) )
    {
        init();
        for(int i = 1; i<=m; i++)
        {
            int u, v;
            scanf("%d%d", &u, &v);
            addedge(u, v, edge, head, tot);
            addedge(v, u, edge, head, tot);
        }

        Tarjan(1, 1);
        for(int u = 1; u<=n; u++)   //重建建图
        for(int i = head[u]; i!=-1; i = edge[i].next)
        {
            int tmpu = find(u);
            int tmpv = find(edge[i].to);
            if(tmpu!=tmpv)
                addedge(tmpu, tmpv, edge0, head0, tot0);
        }

        depth[find(1)] = 0;
        build(find(1), find(1));    //把无根树转为有根树

        int q, a, b;
        scanf("%d", &q);
        printf("Case %d:\n", ++kase);
        while(q--)
        {
            scanf("%d%d", &a, &b);
            LCA(find(a), find(b));
            printf("%d\n", sum_bridge);
        }
        printf("\n");
    }
}
View Code

 

题目大意:n个点的无向图 初始化有m条边

以后q次操做 每次表示在点a与点b间搭建一条边 输出对于q次操做 每次剩下的桥的条数

 

初始化能够用tarjan算法求出桥 对于不是割边的两个点 就能够算是在一个集合中 这样用并查集就能够进行缩点

最后生成的就是一棵树 树边就是图中的全部桥 q次询问中 每次加边<u,v> 若是u和v在一个集合中 说明新的边不会形成影响

若是u和v在两个集合中 两个集合间的边在添加<u,v>后就会失去桥的性质 这样经过LCA就能够遍历全部两个集合间的集合 在加上<u,v>这条边后 这两个集合间的集合其实就变成了一个环 也就是能够缩成一个点 在合并集合的过程当中 就能够把消失的桥从总和中减去了


以前一直在想为何要用LCA来作这道题,原来他们缩点以后会造成一棵树,而后由于已经通过缩点了,因此这些树上的边都是桥(终于理解为何他们说缩点以后的树边为桥了),那么若是加入的这条边是属于一个缩点的话(缩点里面的点算是一个集合)那么就对原图中的桥没有任何影响,可是若是加入的边是属于两个缩点的话,那么就会造成一个环,那么任意删除这个环里面的一条边,这棵树仍是互通的。ORZ终于理解了,那么就能够利用LCA的特性去算出到底减小了多少条桥了,由于是最近公共祖先,那么新加入的这条边的两个点经过LCA找到对方确定是走最短的路径(在树上走最小的边)那么就能够获得结果了,总桥数减去走LCA上的边就是题目要的答案了!!!!

#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <stack>
using namespace std;
#define N 100010
#define M 400010

struct edge{
    int v;
    int next;
}Edge[M];//边的集合

int node[N];//顶点集合
int DFN[N];//节点u搜索的序号(时间戳)
int LOW[N];//u或u的子树可以追溯到的最先的栈中节点的序号(时间戳)
int fa[N];//上一个节点 
int pre[N];//并查集父亲节点 
int n,m;//n:点的个数;m:边的条数
int cnt_edge;//边的计数器
int Index;//序号(时间戳)
int ans;//桥的个数 


void init()//初始化,注意不要把n初始为0 
{
    cnt_edge=0;
    Index=0;
    ans=0;
    memset(Edge,0,sizeof(Edge));
    memset(node,-1,sizeof(node));
    memset(DFN,0,sizeof(DFN));
    memset(LOW,0,sizeof(LOW));
    memset(fa,0,sizeof(fa));
    memset(pre,0,sizeof(pre));
    for(int i=1;i<=n;i++)
    {
        pre[i]=i;
    }
}

int Find(int x)
{
//    while(n!=pre[n])//写成这样会出错
//    {
//        n=pre[n];
//    }
//    return n;
    return pre[x] == x? pre[x]: (pre[x] = Find(pre[x]));
}

int Union(int u,int v)
{
    int uu,vv;
    uu=Find(u);
    vv=Find(v);
    if(vv==uu)
        return 0;
    pre[uu]=vv;
    return 1;
}

void add_edge(int u,int v)//邻接表存储
{
    Edge[cnt_edge].next=node[u];
    Edge[cnt_edge].v=v;
    node[u]=cnt_edge++;
}

void tarjan(int u)
{
    DFN[u]=LOW[u]=Index++;
    for(int i=node[u];i!=-1;i=Edge[i].next)
    {
        int v=Edge[i].v;
        if(v==fa[u]) //这个要写前面 
            continue;
        if(!DFN[v])//若是点v没被访问
        {
            fa[v]=u;
            tarjan(v);
            LOW[u]=min(LOW[u],LOW[v]);
            if(LOW[v]>DFN[u])
            {
                ans++;
            }
            else Union(v,u);
        }
        else //if(v!=fa[u]) //若是点v已经被访问过
            LOW[u]=min(LOW[u],DFN[v]);  
    }
}

void LCA(int u,int v)
{
    if(DFN[v]<DFN[u])
        swap(u,v);
    while(DFN[v]>DFN[u])
    {
        if(Union(v,fa[v]))
            ans--;
        v=fa[v];
    }
    while(v!=u)
    {
        if(Union(u,fa[u]))
            ans--;
        u=fa[u];
    }
}

int main()
{
    //freopen("sample.txt","r",stdin);
    int tot=0;
    while(~scanf("%d %d",&n,&m)&&(m+n))
    {
        init();
        while(m--)
        {
            int u,v;
            scanf("%d %d",&u,&v);
            add_edge(u,v);
            add_edge(v,u);
        }
        fa[1]=1;
        for(int i=1;i<=n;i++)
        {
            if(!DFN[i])
            {
                tarjan(i);
            }
        }
        int q;
        scanf("%d",&q);
        printf("Case %d:\n",++tot);
        while(q--)
        {
            int u,v;
            scanf("%d %d",&u,&v);
            LCA(u,v);
            printf("%d\n",ans);
            
        }
        printf("\n");
    }
    return  0;
}
View Code

 

 


 

 

【POJ 3177】Redundant Paths(Tarjan求桥、边双连通份量)

Description

In order to get from one of the F (1 <= F <= 5,000) grazing fields (which are numbered 1..F) to another field, Bessie and the rest of the herd are forced to cross near the Tree of Rotten Apples. The cows are now tired of often being forced to take a particular path and want to build some new paths so that they will always have a choice of at least two separate routes between any pair of fields. They currently have at least one route between each pair of fields and want to have at least two. Of course, they can only travel on Official Paths when they move from one field to another. 

Given a description of the current set of R (F-1 <= R <= 10,000) paths that each connect exactly two different fields, determine the minimum number of new paths (each of which connects exactly two fields) that must be built so that there are at least two separate routes between any pair of fields. Routes are considered separate if they use none of the same paths, even if they visit the same intermediate field along the way. 

There might already be more than one paths between the same pair of fields, and you may also build a new path that connects the same fields as some other path.

Input

Line 1: Two space-separated integers: F and R 

Lines 2..R+1: Each line contains two space-separated integers which are the fields at the endpoints of some path.

Output

Line 1: A single integer that is the number of new paths that must be built.

Sample Input

7 7
1 2
2 3
3 4
2 5
4 5
5 6
5 7

Sample Output

 2

Hint

Explanation of the sample: 

Check some of the routes: 
1 – 2: 1 –> 2 and 1 –> 6 –> 5 –> 2 
1 – 4: 1 –> 2 –> 3 –> 4 and 1 –> 6 –> 5 –> 4 
3 – 7: 3 –> 4 –> 7 and 3 –> 2 –> 5 –> 7 
Every pair of fields is, in fact, connected by two routes. 

It's possible that adding some other path will also solve the problem (like one from 6 to 7). Adding two paths, however, is the minimum.

 

  【题意】

[求一个无向图中还需加入多少条边能构成一个边双连通份量]

  【题解】

看起来很厉害的样子,其实仍是先用Tarjan缩点,而后,枚举每一条边,看左右两个端点缩点后是否在同一个点中,若是不在连边,其实只要更新每点的度便可,最后统计度为1的点的个数ans,由求“加入多少条边能构成一个边双连通份量”的方法可知,答案为(ans+1)/2。

相关文章
相关标签/搜索