Problem
acm.hdu.edu.cn/showproblem.php?pid=4612
vjudge.net/contest/129990#problem/E
Reference
Tarjan三大算法之双连通分量(割点,桥)
Tarjan三大算法之双连通分量(双连通分量)
hdu 4612 Warm up(无向图Tarjan+树的直径)
HDU-4612-Warm up(无向图缩点+直径)
Meaning
一幅 n 个点、m 条无向边的联通图,现要加一条边,使得图中桥的数量最少,求这个最少的桥数。
Analysis
把无向图中成环的(边双连同分量?)缩成一个点,就得到一棵树,树上的每一条边都对应原图的一座桥。加一条边把树上任意两个点连起来,就会成环,环上的边就都变成不是桥了,所以要减少最多的桥,就把树上最长路径(即树的直径)上的两个端点连起来。答案就是树的边数(即原图的桥数) - 树的直径长
。
无项图的缩点跟有向图不同,因为如果 f 有边指向 t,那 t 必然有边指回 f,而在有向图中未必。所以在 Tarjan 深搜的时候,不能让子结点直接通过反向边找到父结点(第一个参考博客里求桥时就有判是否为父结点)。但由于这题有重边,子结点虽不能通过反向边找到父结点,但允许通过重边找到父结点。比如数据:
4 4
1 2
1 3
1 4
2 1
是应该只有 2 座桥,缩点后只有 3 个点。所以用到了一个 vis 数组,在搜边时,同时标记它的反向边,防止子结点通过反向边找父结点,但同时又不影响重边。
Code
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
#include <stack>
using namespace std;
const int N = 200000, M = 1000000;
int head[N+1], from[M<<1], to[M<<1], nxt[M<<1];
bool vis[M<<1];
void add_edge(int f, int t, int sz)
{
from[sz] = f;
to[sz] = t;
nxt[sz] = head[f];
head[f] = sz;
vis[sz] = false;
}
int bridge, tm, num;
int dfn[N+1], low[N+1], belong[N+1];
bool instk[N+1];
stack<int> stk;
void tarjan(int now)
{
dfn[now] = low[now] = ++tm;
stk.push(now);
instk[now] = true;
for(int i = head[now]; ~i; i = nxt[i])
{
// 当搜到已搜过的边的反向边时
// 就应跳过
if(vis[i])
continue;
// 同时标记反向边(主要是用来标记反向边)
vis[i] = vis[i^1] = true;
if(!dfn[to[i]])
{
tarjan(to[i]);
low[now] = min(low[now], low[to[i]]);
if(low[to[i]] > dfn[now])
++bridge;
}
else if(instk[to[i]])
low[now] = min(low[now], dfn[to[i]]);
}
if(dfn[now] == low[now])
{
++num;
int t;
do
{
t = stk.top();
stk.pop();
instk[t] = false;
belong[t] = num;
} while(t != now);
}
}
queue<int> que;
int dis[N+1];
void bfs(int s)
{
memset(dis, 0, sizeof dis);
dis[s] = 1;
que.push(s);
for(int tp; !que.empty(); que.pop())
{
tp = que.front();
for(int i = head[tp]; ~i; i = nxt[i])
if(!dis[to[i]])
{
dis[to[i]] = dis[tp] + 1;
que.push(to[i]);
}
}
}
int main()
{
int n, m;
while(scanf("%d%d", &n, &m), n || m)
{
memset(head, -1, sizeof head);
for(int i = 0, f, t, sz = 0; i < m; ++i)
{
scanf("%d%d", &f, &t);
add_edge(f, t, sz++);
add_edge(t, f, sz++);
}
// Tarjan 找桥、缩点
memset(dfn, 0, sizeof dfn);
memset(instk, false, sizeof instk);
tm = num = bridge = 0;
tarjan(1);
// 重新建图 -> 缩点后的树
// 直接建在原来的前向星数组就行
memset(head, -1, sizeof head);
for(int i = 0, f, t, sz = 0; i < m << 1; i += 2)
{
// 注意要先读出 from[i] 和 to[i]
// 因为它们有可能被新边的数据覆盖
f = from[i], t = to[i];
if(belong[f] != belong[t])
{
add_edge(belong[f], belong[t], sz++);
add_edge(belong[t], belong[f], sz++);
}
}
// 找树的直径
bfs(1);
int a = 1, b = 0;
for(int i = 1; i <= num; ++i)
if(dis[i] > dis[a])
a = i;
bfs(a);
for(int i = 1; i <= num; ++i)
if(dis[i] > b)
b = dis[i];
printf("%d\n", bridge - b + 1);
}
return 0;
}