图的连通性算法
Posted quinn18
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了图的连通性算法相关的知识,希望对你有一定的参考价值。
概念
割点:无向连通图,删除这个点和这个点关联的边,图不连通
点双连通图:无向连通图,没有割点出现
桥:无向连通图,删除某条边,图不连通
边双连通图:无向连通图,没有桥
时间戳:对一个图做深度优先搜索的时候,第一次访问某个点的时间
强连通分量:有向图任意两点都可互相到达
求割点和点连通分量
int times = 0;
int dfn[maxn], low[maxn]; // dfn记录时间戳
// low(u)来表示 u 以及其后代 **最多经过** 一条反向边能回到的最早的点的时间戳
int bcc_cnt = 0; // 点双连通分量数量
bool iscut[maxn]; // 标记是否是割点
bool vis[maxn<<1]; // 记录这条边是不是访问过了处理重编
set<int> bcc[maxn]; // 记录每个点双连通分量里面的点
stack<edge> S; // 记录连通分量的点集
void dfs (int u, int fa) {
dfn[u] = low[u] = ++times;
int child = 0; // 用来处理根结点子结点数
for (int i = p[u]; i != -1; i = E[i].next) {
int v = E[i].v;
if(vis[i])continue;
vis[i]=vis[i^1]=1;//
if (dfn[v] == 0) { // v 没有被访问过,u, v 是树边
S.push(E[i]);
++child;
dfs(v, u);
low[u] = min(low[u], low[v]);
/*if(low[v]>dfn[u]) 桥++;*/
if (low[v] >= dfn[u]) {
// 当且仅当 u 存在一个子结点 v,使得 v 及其所有后代都没有反向边连回 u 的**祖先**(不包括 u)
iscut[u] = true;
++bcc_cnt; // 增加一个点双连通分量
while (true) {
edge x = S.top();
S.pop();
bcc[bcc_cnt].insert(x.u);
bcc[bcc_cnt].insert(x.v);
if (x.u == u && x.v == v) {
break;
}
}
}
} else { // 反向边,注意 v == fa 的时候,是访问重复的边
S.push(E[i]);
low[u] = min(low[u], dfn[v]);// 初始的时候,low(u) = dfn(u)low(u)=dfn(u),我们认为自己当然可以回到自己
}
}
if (fa < 0 && child == 1) {
// fa < 0 表示根结点,之前根结点一定被标记为割点, 取消之
//对于树根来说当且仅当它有两个或者更多的子结点的时候,它才是割点
iscut[u] = false;
}
}
//普通建图输出点霜
memset(dfn, 0, sizeof(dfn));
times=bcc_cnt=0;
dfs(1, -1);
cout<<bcc_cnt<<endl;
for(int i=1; i<=bcc_cnt; i++) {
for(set<int>::iterator it=bcc[i].begin(); it!=bcc[i].end(); it++) {
cout<<(*it)<<" ";
}
cout<<endl;
}
emm
例题
铁路
一直t不知道为啥
55看队长的题解
桥和边连通分量
int times = 0;
int dfn[maxn], low[maxn];
int bcc_cnt = 0; // 边双连通分量数量
vector<int> bcc[maxn]; // 记录每个点双连通分量里面的点 因为就经过一次就装在vector里
stack<int> S;
void dfs(int u, int fa) {
dfn[u] = low[u] = ++times;
S.push(u);
for (int i = p[u]; i != -1; i = E[i].next) {
int v = E[i].v;
if (dfn[v] == 0) { // v 没有被访问过,u, v 是树边
dfs(v, u);
low[u] = min(low[u], low[v]);// 根据low数组的含义
/*if(low[v]>dfn[u]) 桥++;*/
} else if (dfn[v] < dfn[u] && v != fa) { // 反向边,注意 v == fa 的时候,是访问重复的边
low[u] = min(low[u], dfn[v]);
}
}
if (low[u] == dfn[u]) { // 此时 u 是根结点或者 fa -> u 是桥
++bcc_cnt; // 增加一个边双连通分量
while (!S.empty()) { //从栈中弹出 u 及 u 之后的顶点
int x = S.top();
S.pop();
bcc[bcc_cnt].push_back(x);
if (x == u) break;
}
}
}
int main() {
init();
int n, m;
cin >> n >> m;
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
insert(u, v);
insert(v, u);
}
memset(dfn, 0, sizeof(dfn));
times=bcc_cnt=0;
dfs(1, -1);
cout << bcc_cnt << endl;
for (int i = 1; i <= bcc_cnt; ++i) {
for (int j = 0; j < bcc[i].size(); j++) {
cout << bcc[i][j] << " ";// 输出边双
}
cout << endl;
}
return 0;
}
例题
Redundant Paths
题意: 添加最少的边使这棵树上所有的链都变成环
缩点后是一棵树 贪心 注意重边标记
对于无向图的缩点,由于是无向图,所以要从u到v建一条边,又要从v到u建一条边,但是,在tarjan时会有两条边重复,这是一个麻烦,而且,还不得不建两条边,这该怎么办呢?
解决的方法就是,当同一条无向边的两条有向边的其中一条走过时,把另一条同时赋值为走过,这就要用到一个神奇的公式,^ 1。
举例来说,
0 ^ 1=1, 1 ^ 1=0;
2 ^ 1=3, 3 ^ 1=2;
4 ^ 1=5, 5 ^ 1=4;
而建边的时候,一条无向边的两条有向边刚好相差1
#include <cstring>
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
const int maxm = 1010;
const int maxn = 110;
struct edge {
int u, v;
int next;
} E[maxm];
int p[maxn], eid = 0;
void init() {
memset(p, -1, sizeof(p));
eid = 0;
}
void insert(int u, int v) {
E[eid].u = u;
E[eid].v = v;
E[eid].next = p[u];
p[u] = eid++;
}
int times = 0;
int dfn[maxn], low[maxn];
int bc[maxm], in[maxn], chu[maxn];
int bcc_cnt = 0;
vector<int> bcc[maxn];
stack<int> S;
bool vis[maxm<<1];
void dfs(int u) {
dfn[u] = low[u] = ++times;
S.push(u);
for (int i = p[u]; i != -1; i = E[i].next) {
if(!vis[i]) {// 拿vis数组来标记是否经过 解决重边问题 然后缩点处理
vis[i]=vis[i^1]=1;
int v = E[i].v;
if (dfn[v] == 0) {
dfs(v);
low[u] = min(low[u], low[v]);
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
if (low[u] == dfn[u]) {
++bcc_cnt;
while (!S.empty()) {
int x = S.top();
S.pop();
bc[x]=bcc_cnt; // 标记这个点是那个边双里的
bcc[bcc_cnt].push_back(x);
if (x == u) break;
}
}
}
int main() {
init();
int n, m;
cin >> n >> m;
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
insert(u, v);
insert(v, u);
}
memset(dfn, 0, sizeof(dfn));
times=bcc_cnt=0;
for (int i=1; i<=n; ++i) if (!dfn[i]) dfs(i);
for(int u=1; u<=n; u++) {
for(int i=p[u]; i!=-1; i=E[i].next) {
int v=E[i].v;
if(bc[v]!=bc[u]) {
in[bc[v]]++;
}
}
}int ans=0;
for(int i=1; i<=bcc_cnt; i++) {
if(in[i]==1) ans++;
}
cout<<(ans+1)/2<<endl;// 最后
return 0;
}
强连通分量
复杂度(V+E)
#include <iostream>
#include <stack>
#include <set>
#include <cstring>
using namespace std;
const int maxm = 1010;
const int maxn = 110;
struct edge {
int v;
int next;
} E[maxm];
int p[maxn], eid = 0;
void init() {
memset(p, -1, sizeof(p));
eid = 0;
}
void insert(int u, int v) {
E[eid].v = v;
E[eid].next = p[u];
p[u] = eid++;
}
int times = 0;
int dfn[maxn], low[maxn];
int scc_cnt = 0; // 强连通分量数量
int sccno[maxn]; // 记录每个点属于的强连通分量的编号
set<int> scc[maxn];
stack<int> S;// 把点压入栈中 因为每个点只属于一个强连通分量
void dfs (int u) {
dfn[u] = low[u] = ++times;
S.push(u);
for (int i = p[u]; i != -1; i = E[i].next) {
int v = E[i].v;
if (dfn[v] == 0) { // v 没有被访问过,u, v 是树边
dfs(v);
low[u] = min(low[u], low[v]);
} else if (!sccno[v]) { // 对于已经求出 scc 的点,直接删除
low[u] = min(low[u], dfn[v]);
}
}
if (low[u] == dfn[u]) { // 说明 u 是第一个被探测到的点
++scc_cnt;
while (true) {
int x = S.top();
S.pop();
sccno[x] = scc_cnt;// 记录这个点在哪个连通分量
scc[scc_cnt].insert(x);
if (x == u) {
break;
}
}
}
}
/*缩点
edge new_E[maxm];
int new_p[maxn], new_eid=0;
void new_init() {
memset(new_p, -1, sizeof(new_p));
new_eid=0;
}
void new_insert(int u, int v) {
new_E[new_eid].v=v;
new_E[new_eid].next=new_p[u];
new_p[u]=new_eid++;
}*/
int main() {
init();
int n, m;
cin >> n >> m;
for (int i = 0; i < m; ++i) {
int u, v;
cin >> u >> v;
insert(u, v);
}
memset(dfn, 0, sizeof(dfn));
memset(sccno, 0, sizeof(sccno));
times = scc_cnt = 0;
for (int i = 1; i <= n; ++i) {
if (!dfn[i]) { // 每个点都要尝试 dfs 一次
dfs(i);
}
}
/* 缩点
new_init();
for(int u=1; u<=n以上是关于图的连通性算法的主要内容,如果未能解决你的问题,请参考以下文章