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

双连通分量(biconnected component, 简称bcc)

概念:

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

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

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

点双连通和边双连通

  • 连通的概念:在无向图中,所有点能互相到达
  • 连通分量:互相联通的子图
  • 点双连通:删掉一个点之后,图仍联通
  • 边双连通:删掉一条边之后,图仍联通

概述

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

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

性质

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

算法

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

(说实话我觉得存点不比存边难理解和实现啊……下面会解释)

理解

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

如下图:

我猜想可能是因为割点的定义,此图中两个点均不为割点,所以此图也属于BCC?

总之做题时注意题面要求,若要求的不含此种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),但会有很多无用操作。个人觉得存点也是个不错的选择。

模板

 1 #include<cstdio>
 2 #include<cctype>
 3 #include<vector>
 4 using namespace std;
 5 struct edge
 6 {
 7     int to,pre;
 8 }edges[1000001];
 9 int head[1000001],dfn[1000001],dfs_clock,tot; 10 int num;//BCC数量 11 int stack[1000001],top;//栈 12 vector<int>bcc[1000001]; 13 int tarjan(int u,int fa) 14 { 15 int lowu=dfn[u]=++dfs_clock; 16 for(int i=head[u];i;i=edges[i].pre) 17 if(!dfn[edges[i].to]) 18  { 19 stack[++top]=edges[i].to;//搜索到的点入栈 20 int lowv=tarjan(edges[i].to,u); 21 lowu=min(lowu,lowv); 22 if(lowv>=dfn[u])//是割点或根 23  { 24 num++; 25 while(stack[top]!=edges[i].to)//将点出栈直到目标点 26 bcc[num].push_back(stack[top--]); 27 bcc[num].push_back(stack[top--]);//目标点出栈 28 bcc[num].push_back(u);//不要忘了将当前点存入bcc 29  } 30  } 31 else if(edges[i].to!=fa) 32 lowu=min(lowu,dfn[edges[i].to]); 33 return lowu; 34 } 35 void add(int x,int y)//邻接表存边 36 { 37 edges[++tot].to=y; 38 edges[tot].pre=head[x]; 39 head[x]=tot; 40 } 41 int main() 42 { 43 int n,m; 44 scanf("%d%d",&n,&m); 45 for(int i=1;i<=m;i++) 46  { 47 int x,y; 48 scanf("%d%d",&x,&y); 49  add(x,y),add(y,x); 50  } 51 for(int i=1;i<=n;i++)//遍历n个点tarjan 52 if(!dfn[i]) 53  { 54 stack[top=1]=i; 55  tarjan(i,i); 56  } 57 for(int i=1;i<=num;i++) 58  { 59 printf("BCC#%d: ",i); 60 for(int j=0;j<bcc[i].size();j++) 61 printf("%d ",bcc[i][j]); 62 printf("\n"); 63  } 64 return 0; 65 }

form:https://www.cnblogs.com/LiHaozhe/p/9527136.html


【双连通分量】

一、边双连通分量定义

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

二、点双连通分量的定义

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

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

边双连通分量:

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

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

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

上述几点如下:

 1 void DFS(int i,int fd)//fd是父边 
 2 {
 3     low[i]=dfn[i]=++dfs_clock;
 4     vis[i]=1;
 5     stk[++top]=i;//栈存节点 
 6     for(int p=last[i];p;p=E[p].pre)
 7     {
 8         int j=E[p].to,id=E[p].id;
 9         if(vis[j])
10         {
11             if(dfn[j]<dfn[i]&&fd!=id) low[i]=min(low[i],dfn[j]);
12             continue;
13         }
14         DFS1(j,id);
15         low[i]=min(low[i],low[j]); 
16     }
17     
18     //所有儿子遍历完再求 
19     if(low[i]==dfn[i])
20     {
21         cc++;
22         int x;
23         while(1)
24         {
25             x=stk[top--];
26             belong[x]=cc;
27             size[cc]++;
28             if(x==i) break;//注意是等于i才跳出,也就是i只能属于一个边连通分量 
29         }
30         maxcc=max(maxcc,size[cc]);
31     }
32 }

点双连通分量:

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

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

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

上述几点如下:

 1 void DFS(int i,int fd)//fd是父边 
 2 {
 3     low[i]=dfn[i]=++dfs_clock;
 4     stk[++top]=i;//栈存节点 
 5     int chd=0;//统计儿子数 
 6     
 7     for(int p=last[i];p;p=E[p].pre)
 8     {
 9         
10         int j=E[p].to,id=E[p].id;
11         if(dfn[j])
12         {
13             if(dfn[j]<dfn[i]&&id!=fd) low[i]=min(low[i],dfn[j]);
14             continue;
15         }
16         
17         
18         chd++;
19         DFS(j,id);
20         low[i]=min(low[i],low[j]);
21         
22         
23         if(low[j]>=dfn[i])//遍历完一个儿子就看是否有连通分量 
24         {
25             cut[i]=1;//初步判断i是割顶(还不一定,要看最后的条件) 
26             bcc_cnt++;
27             bcc[bcc_cnt].push_back(i);//只是把i给存进去,而不存i属于哪个分量,因为i是割顶,可能也属于别的分量 
28             int x;
29             while(1)
30             {
31                 x=stk[top--];
32                 bcc[bcc_cnt].push_back(x); 
33                 if(x==j) break;//注意到j结束 
34             }
35         }
36         
37     }
38     
39     
40     if(fd==0&&chd==1) cut[i]=0;//这个结论应该都知道 
41 }

【强连通分量】

一、定义

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

二、模板代码注意的地方

1.每个点所有儿子遍历完才开始求分量;(类似边双连通分量)

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

 1 void DFS(int i)
 2 {
 3     low[i]=dfn[i]=++dfs_clock;
 4     stk[++top]=i;
 5     for(int p=last[i];p;p=E[p].pre)
 6     {
 7         int j=E[p].v;
 8         if(dfn[j])
 9         {
10             if(!belong[j]) low[i]=min(low[i],dfn[j]);
11             continue;
12         }
13         
14         DFS(j);
15         low[i]=min(low[i],low[j]); 
16     }
17     
18     if(dfn[i]==low[i])
19     {
20         scc++;
21         while(1)
22         {
23             int x=stk[top--];
24             belong[x]=scc;
25             size[scc]++;
26             if(x==i) break;
27         }
28     }
29 }


 

【强连通分量和双连通分量常见的模型和问法】(水平有限,后续会尽力更新)

双连通分量

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.拿到题目要先搞清楚给的是连通图还是非连通图。

原文:https://blog.csdn.net/WWWengine/article/details/80616779 


 

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)上路径上的桥,都可以减少,路径上的点都可以缩成一个点,即合并成一个分量。

对于缩点的处理:

  方法一:对于一个分量,可以设置一个点为实点,其余的点为虚点。实点即代表着这个分量的所有信息,虚点虽然属于这个分量的点,但是却对他视而不见。我们要做的,就是在这个分量里选择一个点,去代表整个分量。

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

方法一:

  1 #include <iostream>
  2 #include <cstdio>
  3 #include <cstring>
  4 #include <cmath>
  5 #include <algorithm>
  6 #include <vector>
  7 #include <queue>
  8 #include <stack>
  9 #include <map>
 10 #include <string>
 11 #include <set>
 12 #define ms(a,b) memset((a),(b),sizeof((a)))
 13 using namespace std;
 14 typedef long long LL;
 15 const double EPS = 1e-8;
 16 const int INF = 2e9;
 17 const LL LNF = 2e18;
 18 const int MAXN = 1e5+10;
 19 
 20 struct Edge
 21 {
 22     int to, next;
 23 }edge[MAXN*8];
 24 int tot, head[MAXN];
 25 
 26 int index, dfn[MAXN], low[MAXN];
 27 int isbridge[MAXN], sum_bridge;
 28 int fa[MAXN], depth[MAXN];
 29 
 30 void addedge(int u, int v)
 31 {
 32     edge[tot].to = v;
 33     edge[tot].next = head[u];
 34     head[u] = tot++;
 35 }
 36 
 37 void Tarjan(int u, int pre)
 38 {
 39     dfn[u] = low[u] = ++index;
 40     depth[u] = depth[pre] + 1;  //记录深度
 41     fa[u] = pre;        //记录父亲结点
 42     for(int i = head[u]; i!=-1; i = edge[i].next)
 43     {
 44         int v = edge[i].to;
 45         if(v==pre) continue;
 46         if(!dfn[v])
 47         {
 48             Tarjan(v, u);
 49             low[u] = min(low[u], low[v]);
 50             if(low[v]>dfn[u])   //isbridge[v]表示在树中,以v为儿子结点的边是否为桥
 51                 isbridge[v] = 1, sum_bridge++;
 52         }
 53         else
 54             low[u] = min(low[u], dfn[v]);
 55     }
 56 }
 57 
 58 void LCA(int u, int v)
 59 {
 60     if(depth[u]<depth[v]) swap(u, v);
 61     while(depth[u]>depth[v])    //深度大的先往上爬。遇到桥,就把它删去。
 62     {
 63         if(isbridge[u]) sum_bridge--, isbridge[u] = 0;
 64         u = fa[u];
 65     }
 66     while(u!=v) //当深度一样时,一起爬。遇到桥,就把它删去。
 67     {
 68         if(isbridge[u]) sum_bridge--, isbridge[u] = 0;
 69         u = fa[u];
 70         if(isbridge[v]) sum_bridge--, isbridge[v] = 0;
 71         v = fa[v];
 72     }
 73 }
 74 
 75 void init()
 76 {
 77     tot = 0;
 78     memset(head, -1, sizeof(head));
 79 
 80     index = 0;
 81     memset(dfn, 0, sizeof(dfn));
 82     memset(low, 0, sizeof(low));
 83     memset(isbridge, 0, sizeof(isbridge));
 84 
 85     sum_bridge = 0;
 86 }
 87 
 88 int main()
 89 {
 90     int n, m, kase = 0;
 91     while(scanf("%d%d", &n, &m) && (n||m) )
 92     {
 93         init();
 94         for(int i = 1; i<=m; i++)
 95         {
 96             int u, v;
 97             scanf("%d%d", &u, &v);
 98             addedge(u, v);
 99             addedge(v, u);
100         }
101 
102         depth[1] = 0;
103         Tarjan(1, 1);
104         int q, a, b;
105         scanf("%d", &q);
106         printf("Case %d:\n", ++kase);
107         while(q--)
108         {
109             scanf("%d%d", &a, &b);
110             LCA(a, b);
111             printf("%d\n", sum_bridge);
112         }
113         printf("\n");
114     }
115 }
View Code

方法二:

  1 #include <iostream>
  2 #include <cstdio>
  3 #include <cstring>
  4 #include <cmath>
  5 #include <algorithm>
  6 #include <vector>
  7 #include <queue>
  8 #include <stack>
  9 #include <map>
 10 #include <string>
 11 #include <set>
 12 #define ms(a,b) memset((a),(b),sizeof((a)))
 13 using namespace std;
 14 typedef long long LL;
 15 const double EPS = 1e-8;
 16 const int INF = 2e9;
 17 const LL LNF = 2e18;
 18 const int MAXN = 1e6+10;
 19 
 20 struct Edge
 21 {
 22     int to, next;
 23 }edge[MAXN], edge0[MAXN];   //edge为初始图, edge0为重建图
 24 int tot, head[MAXN], tot0, head0[MAXN];
 25 
 26 int index, dfn[MAXN], low[MAXN];
 27 int top, Stack[MAXN], instack[MAXN];
 28 int belong[MAXN];
 29 int fa[MAXN], depth[MAXN];  //fa用于重建图时记录当前节点的父亲节点,depth记录当前节点的深度
 30 int sum_bridge;
 31 
 32 //找到x最终所属的结合
 33 int find(int x) { return belong[x]==x?x:belong[x]=find(belong[x]); }
 34 
 35 void addedge(int u, int v, Edge edge[], int head[], int &tot)
 36 {
 37     edge[tot].to = v;
 38     edge[tot].next = head[u];
 39     head[u] = tot++;
 40 }
 41 
 42 void Tarjan(int u, int pre)
 43 {
 44     dfn[u] = low[u] = ++index;
 45     Stack[top++] = u;
 46     instack[u] = true;
 47     for(int i = head[u]; i!=-1; i = edge[i].next)
 48     {
 49         int v = edge[i].to;
 50         if(v==pre) continue;
 51         if(!dfn[v])
 52         {
 53             Tarjan(v, u);
 54             low[u] = min(low[u], low[v]);
 55             if(low[v]>dfn[u]) sum_bridge++;
 56         }
 57         else if(instack[v])
 58             low[u] = min(low[u], dfn[v]);
 59     }
 60 
 61     if(dfn[u]==low[u])
 62     {
 63         int v;
 64         do
 65         {
 66             v = Stack[--top];
 67             instack[v] = false;
 68             belong[v] = u;  //把集合的编号设为联通分量的第一个点
 69         }while(v!=u);
 70     }
 71 }
 72 
 73 void build(int u, int pre)
 74 {
 75     fa[u] = pre;    //记录父亲节点
 76     depth[u] = depth[pre] + 1;  //记录深度
 77     for(int i  = head0[u]; i!=-1; i=edge0[i].next)
 78         if(edge0[i].to!=pre)    //防止往回走
 79             build(edge0[i].to, u);
 80 }
 81 
 82 
 83 int LCA(int u, int v)   //左一步右一步地找LCA
 84 {
 85     if(u==v) return u;  //因为两个结点一定有LCA, 所以一定有u==v的时候
 86 
 87     //可能爬一步就爬了几个深度,因为中间的结点已经往上缩点了
 88     if(depth[u]<depth[v]) swap(u, v);   //深度大的往上爬
 89     sum_bridge--;
 90     int lca = LCA(find(fa[u]), v);
 91     return belong[u] = lca;     //找到了LCA,在沿路返回的时候把当前节点的所属集合置为LCA的所属集合
 92 }
 93 
 94 void init()
 95 {
 96     tot = tot0 = 0;
 97     memset(head, -1, sizeof(head));
 98     memset(head0, -1, sizeof(head0));
 99 
100     index = top = 0;
101     memset(dfn, 0, sizeof(dfn));
102     memset(low, 0, sizeof(low));
103     memset(instack, 0, sizeof(instack));
104 
105     sum_bridge = 0;
106 }
107 
108 int main()
109 {
110     int n, m, kase = 0;
111     while(scanf("%d%d", &n, &m) && (n||m) )
112     {
113         init();
114         for(int i = 1; i<=m; i++)
115         {
116             int u, v;
117             scanf("%d%d", &u, &v);
118             addedge(u, v, edge, head, tot);
119             addedge(v, u, edge, head, tot);
120         }
121 
122         Tarjan(1, 1);
123         for(int u = 1; u<=n; u++)   //重建建图
124         for(int i = head[u]; i!=-1; i = edge[i].next)
125         {
126             int tmpu = find(u);
127             int tmpv = find(edge[i].to);
128             if(tmpu!=tmpv)
129                 addedge(tmpu, tmpv, edge0, head0, tot0);
130         }
131 
132         depth[find(1)] = 0;
133         build(find(1), find(1));    //把无根树转为有根树
134 
135         int q, a, b;
136         scanf("%d", &q);
137         printf("Case %d:\n", ++kase);
138         while(q--)
139         {
140             scanf("%d%d", &a, &b);
141             LCA(find(a), find(b));
142             printf("%d\n", sum_bridge);
143         }
144         printf("\n");
145     }
146 }
View Code

题解:tarjan+lca。求一遍桥,用fa[v]表示v的父节点,br[v]表示从v的父节点到v这条边是否为桥,为1表示是。得到fa[]和br[]之后,若添加u和v这条边,则u和v的最近公共祖和u、v之间会形成一个环,在这个环的为桥的边将不再是桥,记录个数即可。

  1 #include   
  2 #include   
  3 #include   
  4 #include   
  5 #include   
  6 #include   
  7 #include   
  8 #include   
  9 #include   
 10 #include   
 11 #include   
 12 #include   
 13 using namespace std;  
 14   
 15 typedef long long LL;  
 16 #define mem(a, n) memset(a, n, sizeof(a))  
 17 #define rep(i, n) for(int i = 0; i < (n); i ++)  
 18 #define REP(i, t, n) for(int i = (t); i < (n); i ++)  
 19 #define FOR(i, t, n) for(int i = (t); i <= (n); i ++)  
 20 #define ALL(v) v.begin(), v.end()  
 21 #define si(a) scanf("%d", &a)  
 22 #define sii(a, b) scanf("%d%d", &a, &b)  
 23 #define siii(a, b, c) scanf("%d%d%d", &a, &b, &c)  
 24 #define pb push_back  
 25 const int inf = 0x3f3f3f3f, N = 1e5 + 5, MOD = 1e9 + 7;  
 26   
 27 int T, cas = 0;  
 28 int n, m, q;  
 29 int dfn[N], low[N], fa[N], head[N];  
 30 bool brige[N], vis[N];  
 31 struct edge {  
 32     int v, next;  
 33 }e[4 * N];  
 34 int brigeNum = 0, ne = 0, dfsNum = 0;  
 35 void addEdge(int u, int v) {  
 36     e[ne].v = v;  
 37     e[ne].next = head[u];  
 38     head[u] = ne ++;  
 39 }  
 40 // Imp  
 41 void init() {  
 42     mem(dfn, 0);  
 43     mem(low, 0);  
 44     mem(fa, -1);  
 45     mem(vis, 0);  
 46     mem(brige, 0);  
 47     mem(head, -1);  
 48     dfsNum = ne = brigeNum = 0;  
 49 }  
 50   
 51 void Tarjan(int u, int f) {  
 52     dfn[u] = low[u] = ++ dfsNum;  
 53     for(int i = head[u]; i != -1; i = e[i].next) {  
 54         int v = e[i].v;  
 55         if(!dfn[v]) {  
 56             vis[v] = 1;  
 57             fa[v] = u;  
 58             Tarjan(v, u);  
 59             low[u] = min(low[u], low[v]);  
 60             if(dfn[u] < low[v]) {  
 61                 brigeNum ++;  
 62                 brige[v] = 1;  
 63             }  
 64         } else if(v != f) low[u] = min(low[u], dfn[v]);  
 65     }  
 66 }  
 67   
 68 void work(int u) {  
 69     if(brige[u]) brigeNum --, brige[u] = 0;  
 70 }  
 71   
 72 void LCA(int u, int v) {  
 73     if(dfn[u] > dfn[v]) swap(u, v);  
 74     while(dfn[u] < dfn[v]) {  
 75         work(v);  
 76         v = fa[v];  
 77     }  
 78     while(v != u) {  
 79         work(u);  
 80         work(v);  
 81         u = fa[u], v = fa[v];  
 82     }  
 83 }  
 84   
 85 int main(){  
 86 #ifdef LOCAL  
 87     freopen("/Users/apple/input.txt", "r", stdin);  
 88 //  freopen("/Users/apple/out.txt", "w", stdout);  
 89 #endif  
 90       
 91     while(sii(n, m) != EOF, n + m) {  
 92         init();  
 93         int u, v;  
 94         rep(i, m) {  
 95             sii(u, v);  
 96             addEdge(u, v), addEdge(v, u);  
 97         }  
 98         FOR(i, 1, n + 1) fa[i] = i;  
 99         Tarjan(1, -1);  
100         printf("Case %d:\n", ++ cas);  
101         si(q);  
102         rep(i, q) {  
103             sii(u, v);  
104             LCA(u, v);  
105             printf("%d\n", brigeNum);  
106         } puts("");  
107   
108     }  
109       
110     return 0;  
111 }  
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上的边就是题目要的答案了!!!!

  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <algorithm>
  4 #include <stack>
  5 using namespace std;
  6 #define N 100010
  7 #define M 400010
  8 
  9 struct edge{
 10     int v;
 11     int next;
 12 }Edge[M];//边的集合
 13 
 14 int node[N];//顶点集合
 15 int DFN[N];//节点u搜索的序号(时间戳)
 16 int LOW[N];//u或u的子树能够追溯到的最早的栈中节点的序号(时间戳)
 17 int fa[N];//上一个节点 
 18 int pre[N];//并查集父亲节点 
 19 int n,m;//n:点的个数;m:边的条数
 20 int cnt_edge;//边的计数器
 21 int Index;//序号(时间戳)
 22 int ans;//桥的个数 
 23 
 24 
 25 void init()//初始化,注意不要把n初始为0 
 26 {
 27     cnt_edge=0;
 28     Index=0;
 29     ans=0;
 30     memset(Edge,0,sizeof(Edge));
 31     memset(node,-1,sizeof(node));
 32     memset(DFN,0,sizeof(DFN));
 33     memset(LOW,0,sizeof(LOW));
 34     memset(fa,0,sizeof(fa));
 35     memset(pre,0,sizeof(pre));
 36     for(int i=1;i<=n;i++)
 37     {
 38         pre[i]=i;
 39     }
 40 }
 41 
 42 int Find(int x)
 43 {
 44 //    while(n!=pre[n])//写成这样会出错
 45 //    {
 46 //        n=pre[n];
 47 //    }
 48 //    return n;
 49     return pre[x] == x? pre[x]: (pre[x] = Find(pre[x]));
 50 }
 51 
 52 int Union(int u,int v)
 53 {
 54     int uu,vv;
 55     uu=Find(u);
 56     vv=Find(v);
 57     if(vv==uu)
 58         return 0;
 59     pre[uu]=vv;
 60     return 1;
 61 }
 62 
 63 void add_edge(int u,int v)//邻接表存储
 64 {
 65     Edge[cnt_edge].next=node[u];
 66     Edge[cnt_edge].v=v;
 67     node[u]=cnt_edge++;
 68 }
 69 
 70 void tarjan(int u)
 71 {
 72     DFN[u]=LOW[u]=Index++;
 73     for(int i=node[u];i!=-1;i=Edge[i].next)
 74     {
 75         int v=Edge[i].v;
 76         if(v==fa[u]) //这个要写前面 
 77             continue;
 78         if(!DFN[v])//如果点v没被访问
 79         {
 80             fa[v]=u;
 81             tarjan(v);
 82             LOW[u]=min(LOW[u],LOW[v]);
 83             if(LOW[v]>DFN[u])
 84             {
 85                 ans++;
 86             }
 87             else Union(v,u);
 88         }
 89         else //if(v!=fa[u]) //如果点v已经被访问过
 90             LOW[u]=min(LOW[u],DFN[v]);  
 91     }
 92 }
 93 
 94 void LCA(int u,int v)
 95 {
 96     if(DFN[v]<DFN[u])
 97         swap(u,v);
 98     while(DFN[v]>DFN[u])
 99     {
100         if(Union(v,fa[v]))
101             ans--;
102         v=fa[v];
103     }
104     while(v!=u)
105     {
106         if(Union(u,fa[u]))
107             ans--;
108         u=fa[u];
109     }
110 }
111 
112 int main()
113 {
114     //freopen("sample.txt","r",stdin);
115     int tot=0;
116     while(~scanf("%d %d",&n,&m)&&(m+n))
117     {
118         init();
119         while(m--)
120         {
121             int u,v;
122             scanf("%d %d",&u,&v);
123             add_edge(u,v);
124             add_edge(v,u);
125         }
126         fa[1]=1;
127         for(int i=1;i<=n;i++)
128         {
129             if(!DFN[i])
130             {
131                 tarjan(i);
132             }
133         }
134         int q;
135         scanf("%d",&q);
136         printf("Case %d:\n",++tot);
137         while(q--)
138         {
139             int u,v;
140             scanf("%d %d",&u,&v);
141             LCA(u,v);
142             printf("%d\n",ans);
143             
144         }
145         printf("\n");
146     }
147     return  0;
148 }
View Code



猜你喜欢

转载自www.cnblogs.com/jiamian/p/11202189.html
今日推荐