http://poj.org/problem?id=3177
/*
大致题意:
为了保护放牧环境,避免牲畜过度啃咬同一个地方的草皮,牧场主决定利用不断迁移牲畜进行喂养的方法去保护牧草。然而牲畜在迁移过程中也会啃食路上的牧草,所以如果每次迁移都用同一条道路,那么该条道路同样会被啃咬过度而遭受破坏。
现在牧场主拥有F个农场,已知这些农场至少有一条路径连接起来(不一定是直接相连),但从某些农场去另外一些农场,至少有一条路可通行。为了保护道路上的牧草,农场主希望再建造若干条道路,使得每次迁移牲畜时,至少有2种迁移途径,避免重复走上次迁移的道路。已知当前有的R条道路,问农场主至少要新建造几条道路,才能满足要求?
解题思路:
“使得每次迁移牲畜时,至少有2种迁移途径,避免重复走上次迁移的道路。”就是说当吧F个农场看作点、路看作边构造一个无向图G时,图G不存在桥。
那么可以建立模型:
给定一个连通的无向图G,至少要添加几条边,才能使其变为双连通图。
2.求双连通分量以及构造双连通分量:
http://blog.csdn.net/lyy289065406/article/details/6762432
对于点双连通分支,实际上在求割点的过程中就能顺便把每个点双连通分支求出。建立一个栈,存储当前双连通分支,在搜索图时,每找到一条树枝边或后向边(非横叉边),就把这条边加入栈中。如果遇到某时满足DFS(u)<=Low(v),说明u是一个割点,同时把边从栈顶一个个取出,直到遇到了边(u,v),取出的这些边与其关联的点,组成一个点双连通分支。割点可以属于多个点双连通分支,其余点和每条边只属于且属于一个点双连通分支。
对于边双连通分支,求法更为简单。只需在求出所有的桥以后,把桥边删除,原图变成了多个连通块,则每个连通块就是一个边双连通分支。桥不属于任何一个边双连通分支,其余的边和每个顶点都属于且只属于一个边双连通分支。
一个有桥的连通图,如何把它通过加边变成边双连通图?方法为首先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1。
统计出树中度为1的节点的个数,即为叶节点的个数,记为leaf。则至少在树上添加(leaf+1)/2条边,就能使树达到边二连通,所以至少添加的边数就是(leaf+1)/2。具体方法为,首先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的。然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(leaf+1)/2次,把所有点收缩到了一起。
*/
#include<stdio.h>
#include<string.h>
#include<vector>
using namespace std;
const int maxn = 1005;
const int inf = 1<<30;
int n,m;
int time,ans;
int low[maxn],dfn[maxn],cut[maxn];
bool vis[maxn][maxn];
vector<int>map[maxn];
void tarjan( int u, int fa )
{
low[u] = dfn[u] = ++time;
for( int i = 0; i < map[u].size(); i ++ )
{
int v = map[u][i];
if( v == fa ) //判定回边
continue;
if( !dfn[v] )
{
tarjan( v,u );
low[u] = low[u] <= low[v] ? low[u]:low[v];
}
else
low[u] = low[u] <= dfn[v] ? low[u]:dfn[v];
}
}
void output()
{
memset( cut,0,sizeof(cut) );
int num = 0;
for( int i = 1; i <= n; i++ )
{
for( int j = 0; j < map[i].size(); j ++ ) //计算每个点的度数
{
int v = map[i][j];
if( low[v] != low[i] ) //不属于同一个块
{
cut[low[i]] ++;
}
}
}
for( int i = 0; i <= n; i ++ ) //计算度数为一的点数
{
if( cut[i] == 1 )
num ++;
}
printf("%d\n",(num+1)/2);
}
void init() //初始化
{
time = 0,ans = inf;
for( int i = 1; i <= n; i ++ )
{
map[i].clear();
}
memset( low,0,sizeof(low) );
memset( dfn,0,sizeof(dfn) );
}
int main()
{
int u,v,d;
while( scanf("%d%d",&n,&m) != EOF )
{
init();
for( int i = 1; i <= m; i ++ )
{
scanf("%d%d",&u,&v);
if( !vis[u][v] )
{
map[u].push_back(v);
map[v].push_back(u);
vis[u][v] = vis[v][u] = 1;
}
}
tarjan(1,1);
output();
}
return 0;
}
//并查集版
#include<stdio.h>
#include<string.h>
#include<vector>
using namespace std;
const int maxn = 1005;
const int inf = 1<<30;
int n,m;
int time;
int low[maxn],dfn[maxn],p[maxn];
int bridge[maxn][2],bridge_n;
int inq[maxn];
vector<int>map[maxn];
int find( int x )
{
return p[x] == x?x:p[x] = find(p[x]);
}
void merge( int a,int b )
{
int x = find(a);
int y = find(b);
if( x == y )
return;
p[y] = x;
}
void tarjan( int u, int fa )
{
int son = 0; //平行边
low[u] = dfn[u] = ++time;
for( int i = 0; i < map[u].size(); i ++ )
{
int v = map[u][i];
if( v == fa ) //判定回边
son ++;
if( !dfn[v] )
{
tarjan( v,u );
low[u] = low[u] <= low[v] ? low[u]:low[v];
if( !(low[v] > dfn[u]) ) //缩点
{
merge(v,u);
}
else
{
bridge[++bridge_n][0] = u; //存割边
bridge[bridge_n][1] = v;
}
}
else if( v != fa || son != 1 )
low[u] = low[u] <= dfn[v] ? low[u]:dfn[v];
}
}
void output()
{
memset( inq,0,sizeof(inq) );
int ans = 0;
for( int i = 1; i <= bridge_n; i++ ) //割边两端的块度数加加
{
int a = find( bridge[i][0] );
int b = find( bridge[i][1] );
inq[a] ++;
inq[b] ++;
}
for( int i = 1; i <= n; i ++ ) //计算度数为一的块数
{
if( inq[i] == 1 )
ans ++;
}
printf("%d\n",(ans+1)/2);
}
void init() //初始化
{
time = 0,bridge_n = 0;
for( int i = 1; i <= n; i ++ )
{
map[i].clear();
p[i] = i;
}
memset( bridge,0,sizeof(bridge) );
memset( low,0,sizeof(low) );
memset( dfn,0,sizeof(dfn) );
}
int main()
{
int u,v,d;
while( scanf("%d%d",&n,&m) != EOF )
{
init();
for( int i = 1; i <= m; i ++ )
{
scanf("%d%d",&u,&v);
map[u].push_back(v);
map[v].push_back(u);
}
tarjan(1,1);
output();
}
return 0;
}