对于强连通分量的求法可以参考这个博客https://blog.csdn.net/qq_34374664/article/details/77488976
很多大佬已经对tarjan的手动模拟描述的很清楚了,作为一个蒟蒻我就不再做过多解释
tarjan算法基于dfs的搜图
首先对于dfs搜图请参考以下代码
void dfs(ll u)
{
for(int i = 0; i < a[u].size(); i ++)
{
ll v = a[u][i];
dfs(v);
}
}
在搜索的过程中可以用flag标记
1 强连通分量
在这里我们引进两个数组作为标记 一个是low,另外一个是dfn,通俗的说这两个数组的作用、
dfn是搜索到这个点的顺序,1 2 3 4 5 。。。。low是能通过当前点走到那个已经出现的点
我们在搜图的过程中需要跟新low数组。我们可以在刚开始的时候给两个数组赋值dfs[u] = low[u] = ++ cnt;cnt是搜索的顺序;
在搜索的过程中如果这个点没有进行访问我们就访问他,当此时的这个点的搜索结束的时候跟新u的low看谁是谁的儿子;
对于已经访问过的点我们要更新low是需要去找dfn[v];
那么问题来了我们是么时候才能知道必须更新low(解决方法我们定义一个栈就好了2333)当一个元素已经出栈我们就将他标记为0,所以在需引进一个数组那就是flag
当low == dfn的时候说明这个点十强联通分量我们就将它以及它的儿子都弹出栈
//解说完毕这个算法就完美的出现了,下面是蒟蒻的实现代码
ll n,m,x,y,cnt,col;
//在找强连通分量的时候需要进栈出栈
stack<ll>sta;
//图形的建立
vector<ll> mp[maxn];
//color将强连通分量染色,flag是标记数组
ll color[maxn],flag[maxn];
//dfn代表的是走到的点的顺序,low是回溯的时候这个点祖宗的顺序;
ll dfn[maxn],low[maxn];
//u代表的是出发点
void tarjan(ll u)
{
dfn[u] = low[u] = ++cnt;
flag[u] = 1;//表示在栈里面
sta.push(u);
for(int i = 0; i < mp[u].size(); i ++)
{
ll v = mp[u][i];
if(!dfn[v])//没有被访问
{
tarjan(v);//每次寻找能走的边
low[u] = min(low[u],low[v]);//递归结束过来比较看谁是谁儿子
}
else if(flag[v])
{
low[u] = min(low[u],dfn[v]) ;//又回到一个走过的路了233看谁是谁的儿子
}
}
if(low[u] == dfn[u])//我是强连通美滋滋
{
ll k = sta.top();
if(k != u)col++;
flag[k] = 0;
while(k != u)
{
flag[k] = 0;//出栈了,标记结束
sta.pop();
k = sta.top();
}
sta.pop();
}
}
习题练习:P2863 [USACO06JAN]牛的舞会The Cow Promhttps://www.luogu.org/problemnew/show/P2863
习题ac代码
#include <bits/stdc++.h>
typedef long long ll;
typedef long double ld;
using namespace std;
const ll maxn = 3e5 + 7;
ll n,m,x,y,cnt,col;
//在找强连通分量的时候需要进栈出栈
stack<ll>sta;
//图形的建立
vector<ll> mp[maxn];
//color将强连通分量染色,flag是标记数组
ll color[maxn],flag[maxn];
//dfn代表的是走到的点的顺序,low是回溯的时候这个点祖宗的顺序;
ll dfn[maxn],low[maxn];
//u代表的是出发点
void tarjan(ll u)
{
dfn[u] = low[u] = ++cnt;
flag[u] = 1;//表示在栈里面
sta.push(u);
for(int i = 0; i < mp[u].size(); i ++)
{
ll v = mp[u][i];
if(!dfn[v])//没有被访问
{
tarjan(v);//每次寻找能走的边
low[u] = min(low[u],low[v]);//递归结束过来比较看谁是谁儿子
}
else if(flag[v])
{
low[u] = min(low[u],dfn[v]) ;//又回到一个走过的路了233看谁是谁的儿子
}
}
if(low[u] == dfn[u])//我是强连通美滋滋
{
ll k = sta.top();
if(k != u)col++;
flag[k] = 0;
while(k != u)
{
flag[k] = 0;//出栈了,标记结束
sta.pop();
k = sta.top();
}
sta.pop();
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
ll n,m,x,y;
cin >> n >> m;
for(int i = 1; i <= m; i ++)
{
cin >> x >> y;
mp[x].push_back(y);
}
for(int i = 1; i <= n; i ++)
{
//没有被访问过继续访问
if(!dfn[i])
tarjan(i);
}
cout << col << endl;
return 0;
}
2 :缩点
缩点就是运用了上面的强连通分量,我们就可以将一个强连通分量全部染色标记成为一个新的点
习题练习:poj Popular Cows http://poj.org/problem?id=2186
#include <iostream>
#include <algorithm>
#include <vector>
#include <stack>
typedef long long ll;
typedef long double ld;
using namespace std;
const ll maxn = 1e5 + 7;
ll n,m,x,y,cnt,col;
//在找强连通分量的时候需要进栈出栈
stack<ll>sta;
//图形的建立
vector<ll> mp[maxn];
//color将强连通分量染色,flag是标记数组,du代表一个点的出度,cnnt强连通分量有几个点
ll color[maxn],flag[maxn],du[maxn],cnnt[maxn];
//dfn代表的是走到的点的顺序,low是回溯的时候这个点祖宗的顺序;
ll dfn[maxn],low[maxn];
//u代表的是出发点
void tarjan(ll u)
{
dfn[u] = low[u] = ++cnt;
flag[u] = 1;//表示在栈里面
sta.push(u);
for(int i = 0; i < mp[u].size(); i ++)
{
ll v = mp[u][i];
if(!dfn[v])//没有被访问
{
tarjan(v);//每次寻找能走的边
low[u] = min(low[u],low[v]);//递归结束过来比较看谁是谁儿子
}
else if(flag[v])
{
low[u] = min(low[u],dfn[v]) ;//又回到一个走过的路了233看谁是谁的儿子
}
}
if(low[u] == dfn[u])//我是强连通美滋滋
{
ll k = sta.top();
flag[k] = 0;
color[u] = ++col;
while(k != u)
{
flag[k] = 0;//出栈了,标记结束
color[k] = col;//将强连通分量标记颜色
sta.pop();
k = sta.top();
}
sta.pop();
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
ll n,m,x,y;
cin >> n >> m;
for(int i = 1; i <= m; i ++)
{
cin >> x >> y;
mp[x].push_back(y);
}
for(int i = 1; i <= n; i ++)
{
//没有被访问过继续访问
if(!dfn[i])
tarjan(i);
}
for(int i = 1; i <= n; i ++)
{
ll mm = mp[i].size();
for(int j = 0; j < mm; j ++)
{
ll v = mp[i][j];
if(color[v] != color[i])//不是同一个强连通分量
{
du[color[i]]++;//i点的出度为多少
}
}
cnnt[color[i]]++;//强连通分量中有多少个点
}
ll temp = 0,ans = 0;
for(int i = 1; i <= col; i ++)
{
if(du[i] == 0)//如果有一个度为0那么这个点就是答案,两个就没有
{
temp++;
ans = cnnt[i];
}
}
if(temp == 0)
{
cout << 0 << endl;
}
else
{
if(temp > 1)
{
cout << 0 << endl;
}
else
{
cout << ans << endl;
}
}
return 0;
}
3:割点:参考博客:https://www.cnblogs.com/collectionne/p/6847240.html
割点以后这个图不连贯那么这个点就是重要的点,我们割掉它2333;
当我们发现这个点是根的话,我们就去看他有几个儿子,超过两个我们就可以割掉它;
如果是非根节点我们就看他的low[v] >= dfn[u],如果大于我们割掉它
对于更新low =min(low,low)和low = min(low,dfn)我们要考虑到当他找祖宗的时候还能找到祖宗
就像这个图一样5的祖宗是3,3的祖宗是1,我们如果更新low =min(low,low)就会发现没有割点;
讲解完毕
习题练习 P3388 【模板】割点(割顶)https://www.luogu.org/problemnew/show/P3388
ac代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <stack>
typedef long long ll;
typedef long double ld;
using namespace std;
const ll maxn = 1e5 + 7;
ll n,m,x,y,cnt;
//在找强连通分量的时候需要进栈出栈
stack<ll>sta;
//图形的建立
vector<ll> mp[maxn];
//color将强连通分量染色,flag是标记数组,du代表一个点的出度,cnnt强连通分量有几个点
ll color[maxn],flag[maxn],du[maxn],cnnt[maxn];
//dfn代表的是走到的点的顺序,low是回溯的时候这个点祖宗的顺序;
ll dfn[maxn],low[maxn];
//u代表的是出发点 Fa是父亲
void tarjan(ll u,ll fa)
{
dfn[u] = low[u] = ++cnt;
ll col = 0;
//flag[u] = 1;//表示在栈里面
//sta.push(u);
for(int i = 0; i < mp[u].size(); i ++)
{
ll v = mp[u][i];
if(!dfn[v])//没有被访问
{
tarjan(v,u);//每次寻找能走的边
low[u] = min(low[u],low[v]);//递归结束过来比较看谁是谁儿子
//col表示孩子的数量
if(u == fa)col++;
//他不是根节点,当他的儿子回溯也找不到更早的点说明这个儿子只能通过他父亲才能找祖宗
if(low[v] >= dfn[u] && u != fa)color[u] = 1;
}
low[u] = min(low[u],dfn[v]);
}
//说明他是根节点他是自己的父亲
if(col >= 2 && u == fa)color[u] = 1;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i ++)
{
cin >> x >> y;
mp[x].push_back(y);
mp[y].push_back(x);
}
for(int i = 1; i <= n; i ++)
{
//没有被访问过继续访问
if(!dfn[i])
tarjan(i,i);
}
ll ans = 0;
for(int i = 1; i <= n; i ++)
{
if(color[i] == 1)
{
ans++;
}
}
cout << ans << endl;
for(int i = 1; i <= n; i ++)
{
if(color[i] == 1)
{
cout << i << " ";
}
}
cout << endl;
return 0;
}
4割边:
判断这个地方low[v] > dfn[u]从而进行割边 因为是是无向图所以可能每一个点都是自己的父亲,所以我们在v!=fa时候更新low = min(low,dfn) ;
习题练习 https://ac.nowcoder.com/acm/contest/392/I
ac代码如下
#include <iostream>
#include <algorithm>
#include <vector>
#include <stack>
typedef long long ll;
typedef long double ld;
using namespace std;
const ll maxn = 3e5 + 7;
ll n,m,x,y,cnt;
//在找强连通分量的时候需要进栈出栈
stack<ll>sta;
//图形的建立
vector<pair<ll,ll> > mp[maxn];
//color将强连通分量染色,flag是标记数组,du代表一个点的出度,cnnt强连通分量有几个点
ll color[maxn],flag[maxn],du[maxn],cnnt[maxn];
//dfn代表的是走到的点的顺序,low是回溯的时候这个点祖宗的顺序;
ll dfn[maxn],low[maxn];
//u代表的是出发点 Fa是父亲
void tarjan(ll u,ll fa)
{
dfn[u] = low[u] = ++cnt;
ll col = 0;
//flag[u] = 1;//表示在栈里面
//sta.push(u);
for(int i = 0; i < mp[u].size(); i ++)
{
ll v = mp[u][i].first;
ll id = mp[u][i].second;
if(!dfn[v])//没有被访问
{
tarjan(v,u);//每次寻找能走的边
low[u] = min(low[u],low[v]);//递归结束过来比较看谁是谁儿子
//col表示孩子的数量
//if(u == fa)col++;
//他不是根节点,当他的儿子回溯也找不到更早的点说明这个儿子只能通过他父亲才能找祖宗
if(low[v] > dfn[u])color[id] = 1;
}
else if(v != fa)
low[u] = min(low[u],dfn[v]);
}
//说明他是根节点他是自己的父亲
//if(col >= 2 && u == fa)color[u] = 1;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= m; i ++)
{
cin >> x >> y;
mp[x].push_back(make_pair(y,i));
mp[y].push_back(make_pair(x,i));
}
for(int i = 1; i <= n; i ++)
{
//没有被访问过继续访问
if(!dfn[i])
tarjan(i,i);
}
ll ans = 0;
for(int i = 1; i <= n; i ++)
{
if(color[i] == 1)
{
ans++;
}
}
cout << m - ans << endl;
return 0;
}