D 华华和月月逛公园 || tarjin算法 割点割边

链接:https://ac.nowcoder.com/acm/contest/5713/D
来源:牛客网

华华和月月逛公园

题目描述
月月和华华一起去逛公园了。公园很大,为了方便,可以抽象的看成一个N个点M条边的无向连通图(点是景点,边是道路)。公园唯一的入口在1号点,月月和华华要从这里出发,并打算参观所有的景点。因为他们感情很好,走多远都不会觉得无聊,所以所有景点和道路都可以无数次的重复经过。月月发现,有些路可走可不走,有些路则必须要走,否则就无法参观所有的景点。现在月月想知道,有几条路是不一定要经过的。因为这是个很正常的公园,所以没有重边和自环。
输入描述:
第一行两个正整数N和M,表示点数和边数。
接下来M行,每行两个正整数U和V表示一条无向边。
保证给定的图是连通的。
输出描述:
输出一行一个非负整数表示不一定要经过的边有几条。

思路:先求出有几条边是桥,然后再减去就可以。用Tarjan算法。

首先什么是Tarjan算法。这篇文章(感谢大佬的文章)

/-------------------------------------------------/

割点:无向连通图中,去掉一个顶点及和它相邻的所有边,图中的连通分量数增加,则该顶点称为割点。

桥(割边):无向联通图中,去掉一条边,图中的连通分量数增加,则这条边,称为桥或者割边。

割点与桥(割边)的关系:

1)有割点不一定有桥,有桥一定存在割点

2)桥一定是割点依附的边。

Tarjan算法的原理
判断一个顶点是不是割点除了从定义,还可以从DFS(深度优先遍历)的角度出发。我们先通过DFS定义两个概念。

假设DFS中我们从顶点U访问到了顶点V(此时顶点V还未被访问过),那么我们称顶点U为顶点V的父顶点,V为U的孩子顶点。在顶点U之前被访问过的顶点,我们就称之为U的祖先顶点。

显然如果顶点U的所有孩子顶点可以不通过父顶点U而访问到U的祖先顶点,那么说明此时去掉顶点U不影响图的连通性,U就不是割点。相反,如果顶点U至少存在一个孩子顶点,必须通过父顶点U才能访问到U的祖先顶点,那么去掉顶点U后,顶点U的祖先顶点和孩子顶点就不连通了,说明U是一个割点。

Tarjan算法的实现细节
在具体实现Tarjan算法上,我们需要在DFS(深度优先遍历)中,额外定义三个数组dfn[],low[],parent[]

4.1 dfn数组

dnf数组的下标表示顶点的编号,数组中的值表示该顶点在DFS中的遍历顺序(或者说时间戳),每访问到一个未访问过的顶点,访问顺序的值(时间戳)就增加1。子顶点的dfn值一定比父顶点的dfn值大(但不一定恰好大1,比如父顶点有两个及两个以上分支的情况)。在访问一个顶点后,它的dfn的值就确定下来了,不会再改变。

4.2 low数组

low数组的下标表示顶点的编号,数组中的值表示DFS中该顶点不通过父顶点能访问到的祖先顶点中最小的顺序值(或者说时间戳)。

每个顶点初始的low值和dfn值应该一样,在DFS中,我们根据情况不断更新low的值。

假设由顶点U访问到顶点V。当从顶点V回溯到顶点U时,

如果

dfn[v] < low[u]

那么

low[u] = dfn[v]

如果顶点U还有它分支,每个分支回溯时都进行上述操作,那么顶点low[u]就表示了不通过顶点U的父节点所能访问到的最早祖先节点。

4.3 parent数组

parent[]:下标表示顶点的编号,数组中的值表示该顶点的父顶点编号,它主要用于更新low值的时候排除父顶点,当然也可以其它的办法实现相同的功能。

4.5 割点及桥的判定方法

割点:判断顶点U是否为割点,用U顶点的dnf值和它的所有的孩子顶点的low值进行比较,如果存在至少一个孩子顶点V满足low[v] >= dnf[u],就说明顶点V访问顶点U的祖先顶点,必须通过顶点U,而不存在顶点V到顶点U祖先顶点的其它路径,所以顶点U就是一个割点。对于没有孩子顶点的顶点,显然不会是割点。

桥(割边):low[v] > dnf[u] 就说明V-U是桥

需要说明的是,Tarjan算法从图的任意顶点进行DFS都可以得出割点集和割边集。

在这里插入图片描述
附上代码:

#include <bits/stdc++.h>
#define MAX_INT  ((unsigned)(-1)>>1)
#define MIN_INT  (~MAX_INT)
#define db printf("where!\n");
#define pb push_back
using namespace std;
#define ll long long
#define MP std::make_pair
ll gcd(ll x,ll y){return y ? gcd(y,x%y) : x;}
template<class T>inline void read(T &res){
char c;T flag=1;
while((c=getchar())<'0'||c>'9')if(c=='-')flag=-1;res=c-'0';
while((c=getchar())>='0'&&c<='9')res=res*10+c-'0';res*=flag;
}
struct A
{
    int a,b;
};
vector<int> mp[100005];
int n,m;
int low[100005],dfn[100005],id;
vector<A> ans;
A e;
void tarjin(int now,int pre)
{
    dfn[now]=low[now]=++id;
    for(auto i:mp[now]){
        if(i==pre) continue;
        if(!dfn[i]){
            tarjin(i,now);
            low[now]=min(low[now],low[i]);//取最小的low
            if(low[i]>dfn[now]){//确定是桥了
                e.a=now;
                e.b=i;
                ans.pb(e);
            }
        }
        else
            low[now]=min(low[now],dfn[i]);
    }
}

int main()
{
    read(n),read(m);
    for(int i=1;i<=m;i++){
        int a,b;
        read(a),read(b);
        mp[a].pb(b),mp[b].pb(a);//建图
    }
    tarjin(1,1);
    cout<<m-ans.size();
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值