无向图的双联通分量
基础知识
割点:把这个点去掉,连通块数量增加
桥:把这条边删掉,连通块数量增加
割点之间的边不一定是桥,桥两边不一定是割点
边双联通:
边双连通分量:对于一个无向图的子图,当删除其中任意一条边后,不改变图内点的连通性,这样的子图叫做边的双连通子图。而当子图的边数达到最大时,叫做边的双连通分量。
数组:dfn[n],low[n]
1如何判断这条边数桥(u->v)dfn[u] < low[v]
2,如何找边双联通分量(1)将所有桥删掉,(2)Tarjan算法stackdfn[n] == low[n]
边双连通:任意x,y,x和y之间至少存在两条相互分离的路径
模板
#include <iostream>
#include <cstdio>
#include <stack>
#include <vector>
#include <map>
#include <cstring>
#include <algorithm>
#define IOS std::ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
#define INF 0x3f3f3f3f
#define str first
#define blu second
using namespace std;
const int N = 2e5 + 10;
typedef pair<int,int> PII;
int dfn[N], low[N], ef[N], bcc[N], in[N], out[N];
stack<int> str;
int n, m, br;
struct node
{
int to, next;
}e[N];
int head[N], cnt, idx, bccnum;
inline void add(int to,int from)
{
e[cnt] = {to,head[from]};
head[from] = cnt ++;
}
void Tarjan(int rt)
{
dfn[rt] = low[rt] = ++ idx;
str.push(rt);
for(int i = head[rt]; ~i; i = e[i].next)
{
int v = e[i].to;
if(ef[i]) continue;//标记访问的边
ef[i] = ef[i ^ 1] = 1;//无向图双向边
if(!dfn[v])
{
Tarjan(v);
low[rt] = min(low[rt],low[v]);
// if(dfn[rt] < low[v])
//(说明他们不是在一个环,那么他们就这条边就是桥)
// br ++;
}
else low[rt] = min(low[rt],dfn[v]);
}
if(low[rt] == dfn[rt])
{
bccnum ++;
while(1)
{
int top = str.top();
str.pop();
bcc[top] = bccnum;
if(top == rt) break;
}
}
return;
}
int main()
{
IOS;
memset(head,-1, sizeof head);
cin >> n >> m;
while(m --)
{
int l, r;
cin >> l >> r;
add(l, r);
add(r, l);
}
Tarjan(1);
for(int i = 1; i <= n; ++ i)
for(int j = head[i]; ~j; j = e[j].next)
if(bcc[i] != bcc[e[j].to])
out[bcc[i]] ++, in[bcc[e[j].to]] ++;
int ans = 0;
for(int i = 1; i <= bccnum ; ++ i)
if((in[i] + out[i]) / 2 == 1)
ans ++;
cout << (ans + 1) / 2<< endl;
return 0;
}
点双联通
点双连通图:该无向图中的任意两个顶点间都存在两条点不相交的路径
(或是说该图中去掉任意一个点都不会影响其联通性)
dfn[n], low[n]
1.如何判断割点
求割点模板题
**解题实录:1.求联通块数量2.枚举联通块 **
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010, M = 30010;
int n, m;
int h[N], e[M], ne[M], idx;
int dfn[N], low[N], timestamp;
int root, ans;
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
int cnt = 0;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
if (low[j] >= dfn[u]) cnt ++ ;
}
else low[u] = min(low[u], dfn[j]);
}
//如果不是根节点的的话连通块的点数再加一个
if (u != root && cnt) cnt ++ ;
ans = max(ans, cnt);
}
int main()
{
while (scanf("%d%d", &n, &m), n || m)
{
memset(dfn, 0, sizeof dfn);
memset(h, -1, sizeof h);
idx = timestamp = 0;
while (m -- )
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b), add(b, a);
}
ans = 0;
int cnt = 0;
for (root = 0; root < n; root ++ )
if (!dfn[root])
{
cnt ++ ;
tarjan(root);
}
printf("%d\n", ans + cnt - 1);
}
return 0;
}
2.如何求点双连通分量
可以使用Tarjan算法求割点(注意,还有一个求连通分量的算法也叫Tarjan算法,与此算法类似)。(Tarjan,全名Robert Tarjan,美国计算机科学家。)
首先选定一个根节点,从该根节点开始遍历整个图(使用DFS)。
1)对于根节点,判断是不是割点很简单——计算其子树数量,如果有2棵即以上的子树,就是割点。因为如果去掉这个点,这两棵子树就不能互相到达。
2)对于非根节点,判断是不是割点就有些麻烦了。我们维护两个数组dfn[]和low[],dfn[u]表示顶点u第几个被(首次)访问,low[u]表示顶点u及其子树中的点,通过非父子边(回边),能够回溯到的最早的点(dfn最小)的dfn值(但不能通过连接u与其父节点的边)。对于边(u, v),如果low[v]>=dfn[u],此时u就是割点。
但这里也出现一个问题:怎么计算low[u]。
假设当前顶点为u,则默认low[u]=dfn[u],即最早只能回溯到自身。
有一条边(u, v),如果v未访问过,继续DFS,DFS完之后,low[u]=min(low[u], low[v]);
如果v访问过(且u不是v的父亲),就不需要继续DFS了,一定有dfn[v]<dfn[u],low[u]=min(low[u], dfn[v])。
396. 矿场搭建
一个割点至少在两个点双联通分量
孤立点要特判(点双联通)
缩点之后是一颗树,因为题目说我们只坍塌一个点,那么我们就对不同的连通块进行讨论
1.加入说这个连通块的度数为1(就是割点个数是1)的话那么这个联通块内一定要设置1个出口(非割点的出口)因为,如果出口塌了他们可以通过割点跑到其他出口而且如果是割点塌了那么这个块内没有的话就会被困。所以综上对于每个度为1(也就是叶子节点都要设置1个出口)
2.如果说这个连通块度数为0那么一定要设置两个出口
3,如果这个联通块的度数大过二的话不过塌哪个点他都可以跑到预估叶子节点。
那么我们就要统计每个联通块有哪些点并且有多少个割点
代码
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ULL;
const int N = 2e4 + 10;
struct node {
int next, to;
}e[N];
int n, m;
int head[N], cnt;
int dfn[N], low[N];
vector<int> dcc[N];int dcc_cnt, idx;
bool cut[N];//判断是否是割点
stack <int> str;
int root;
inline void add(int from, int to)
{
e[cnt] = {head[from],to};
head[from] = cnt ++;
}
inline void init()
{
memset(cut,0,sizeof(cut));
memset(head,-1,sizeof(head));
memset(dfn,0,sizeof(dfn));
while(!str.empty()) str.pop();
for(int i = 0; i <= dcc_cnt; ++ i) dcc[i].clear();
idx = cnt = n = dcc_cnt = 0;
}
inline void tarjan(int u)
{
dfn[u] = low[u] = ++ idx;
str.push(u);
if(root == u && head[u] == -1)
{
dcc_cnt ++;
dcc[dcc_cnt].push_back(u);
return;
}
int cnt = 0;
for(int i = head[u]; ~i; i = e[i].next)
{
int v = e[i].to;
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u],low[v]);
if(dfn[u] <= low[v])//说明子节点只能戳到父节点前面或者是父节点
{
cnt ++;
//如果是根节点的话子树大过1就是割点。
if(u != root || cnt > 1) cut[u] = true;
++ dcc_cnt;
int top;
do
{
top = str.top();
str.pop();
dcc[dcc_cnt].push_back(top);
}
while(top != v);//注意这里只能弹到v这点
dcc[dcc_cnt].push_back(u);
}
}
else low[u] = min(low[u],dfn[v]);
}
}
int main()
{
int T = 1;
while(cin >> m && m)
{
init();
while(m --)
{
int l, r;
cin >> l >> r;
n = max(n,max(l,r));
add(l,r);
add(r,l);
}
for(root = 1; root <= n; ++ root)
if(!dfn[root])
tarjan(root);
int res = 0;
ULL num = 1;
for (int i = 1; i <= dcc_cnt; i ++ )
{
int cnt = 0;
for (int j = 0; j < dcc[i].size(); j ++ )
if (cut[dcc[i][j]])
cnt ++ ;
if (cnt == 0)
{
if (dcc[i].size() > 1) res += 2, num *= dcc[i].size() * (dcc[i].size() - 1) / 2;
else res ++ ;
}
else if (cnt == 1) res ++, num *= dcc[i].size() - 1;
}
printf("Case %d: %d %llu\n", T ++, res, num);
}
return 0;
}