有向图的强连通分量 scc
基本概念:
-
树枝边(x,y):x是y的父亲
-
前向边(x,y):x是y的祖先结点
-
后向边(x,y):y是x的祖先结点
-
横叉边(x,y):在对有向图进行dfs遍历时,x是已经搜过的图的分支(不是前向边),现在在搜的点是y,y到x的有向边是横叉边
连通分量作用
- 通过缩点,将图变成有向无环图
缩点步骤:
for i=1;i<=n;i++
for i的所有邻点j
if i和j不在同一个scc中:
加一条新边id[i]→id[j]
tarjin算法求强连通分量
引入时间戳的概念:在dfs遍历的过程中,按照每个结点第一次被访问的时间顺序,依次给予图中N个结点1~N的整数标记,该标记被称为时间戳,记为dfn[x].
对每个点定义两个时间戳:
dfn[u]
表示遍历到u的时间戳;low[u]
表示从u开始走,所能遍历到的最小时间戳是什么。
我们在求强连通分量的时候,求的是每个强连通分量最上面的那个点,也就是最高点。
且若u是所在的强连通分量的最高点 等价于 dfn[u]== low[u],
因为low[u]表示的是从u开始能够遍历到的最小的时间戳,若正好等于自己的时间戳,则就是说明u是最高点。
tarjan模板
背模板的思路
- 加时间戳;
- 放入栈中,做好标记;
- 遍历邻点
1)如果没遍历过,tarjan一遍,用low[j]更新最小值low
2) 如果在栈中,用dfn[j]更新最小值low
4.找到最高点
1)scc个数++
2)do-while循环:
从栈中取出每个元素;标志为出栈;
对元素做好属于哪个scc;该scc中点的数量++
具体模板代码
// tarjan 算法求强连通分量
// 时间复杂度O(n+ m)
void tarjan(int u){
// 初始化自己的时间戳
dfn[u] = low[u] = ++ timestamp;
//将该点放入栈中
stk[++ top] = u, in_stk[u] = true;
// 遍历和u连通的点
for(int i = h[u]; ~i; i = ne[i]){
int j = e[i];
if(!dfn[j]){
tarjan(j);
// 更新u所能遍历到的时间戳的最小值
low[u] = min(low[u], low[j]);
}
// 如果当前点在栈中
//注意栈中存的可能是树中几个不同分支的点,因为有横叉边存在
// 栈中存的所有点,是还没搜完的点,同时都不是强连通分量的最高点
// 这里表示当前强连通分量还没有遍历完,即栈中有值
else if(in_stk[j])
//更新一下u点所能到的最小的时间戳
//此时j要么是u的祖先,要么是横叉边的点,时间戳小于u
low[u] = min(low[u], dfn[j]);
}
// 找到该连通分量的最高点
if(dfn[u] == low[u]){
int y;
++ scc_cnt; // 强连通分量的个数++
do{// 取出来该连通分量的所有点
y = stk[top --];
in_stk[y] = false;
id[y] = scc_cnt; // 标记点属于哪个连通分量
size_scc[scc_cnt] ++;
} while(y != u);
}
}
题目
1174. 受欢迎的牛
思路:
先找到强连通分量,缩点。
- 假如存在两及以上个出度=0的牛(强连通分量) 则必然有一头牛(强连通分量)不被所有牛欢迎。
因为出度都为零的牛(强连通分量)之间一点不会喜欢。 - 当只有一个牛(强连通分量)的出度为0,则该强连通分量中的所有点都被其他强连通分量的牛欢迎
#include <bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
typedef pair<int, int> pii;
const int N = 5e5 + 10;
int n, m;
vector<int> g[N];
int dfn[N], low[N], timestemp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, scc_size[N];
int dout[N]; // 记录新图中每个点(也就是原图每个连通分量)的出度
void tarjan(int u)
{
dfn[u] = low[u] = ++timestemp;
stk[++top] = u, in_stk[u] = true;
for (int i = 0; i < g[u].size(); i++)
{
int k = g[u][i];
// cout<<k<<endl;
if (!dfn[k])
{
tarjan(k);
low[u] = min(low[u], low[k]);
}
else if (in_stk[k])
{
low[u] = min(low[u], dfn[k]);
}
}
if (low[u] == dfn[u])
{
int k;
scc_cnt++;
do
{
k = stk[top--];
scc_size[scc_cnt]++;
id[k] = scc_cnt;
} while (k != u);
}
}
void slove()
{
cin >> n >> m;
for (int i = 0; i < m; i++)
{
int a, b;
cin >> a >> b;
g[a].push_back(b);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i])
{
tarjan(i);
}
}
for (int i = 1; i <= n; i++)
{
for (int j : g[i])
{
if (id[i] != id[j])
{
dout[id[i]]++;
}
}
}
// 和本题有关的部分:
// zeros是统计在新图中,出度为0的点的个数
// sum表示满足条件的点(最受欢迎的奶牛)的个数
int zeros = 0, sum = 0;
for (int i = 1; i <= scc_cnt; i++)
{
if (!dout[i])
{
zeros++;
sum += scc_size[i];
if (zeros > 1)
{
sum = 0;
break;
}
}
}
cout << sum << endl;
}
signed main()
{
slove();
return 0;
}
练习
J - Watch Where You Step
原文
题意
给定有向图的邻接矩阵,现在需要给该图增加边,使得如果两点可达必直接可达,求需要加边的最大数量。
思路:
通过强连通分量缩点。
- 对每个联通块内部
将其中的点全部连接起来需要n*(n-1)条边(建完全图) - 对每个联通块之间
将其全部链接起来需要\(v[i] × v[j]\)条边(v[i]为每个块中的点数),为避免重复建边,需要对每个联通块排序。
因此对整个图进行两次dfs
- 第一次 对原图跑dfs 用栈记录连通分量缩后的点访问顺序
- 第二次 对反向图根据出栈顺序跑dfs 获得每个联通块中的点的个数;
最后对每个块按序处理即可。
#include <bits/stdc++.h>
#define endl '\n'
#define int long long
using namespace std;
typedef pair<int, int> pii;
const int N = 1e6 + 10;
const int INF = 0x3f3f3f3f3f;
int n;
int a[N];
int cnt[N],st[N];
vector<int> g[N];
vector<int> gg[N];
int vis[N];
int vis2[N];
stack<int> stk;
vector<int> vec;
int sum_bian;
void dfs(int u){
vis[u]=1;
for(int v: g[u]){
if(!vis[v]) dfs(v);
}
stk.push(u);
}
int dfs2(int u){
vis2[u]=1;
int res=1;
for(int v:gg[u]){
if(!vis2[v]) res+=dfs2(v);
}
return res;
}
void slove()
{
int n;cin>>n;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
int t;cin>>t;
if(t)
sum_bian++, g[i].push_back(j),gg[j].push_back(i);
}
}
for(int i=0;i<n;i++){
if(!vis[i]) dfs(i);
}
while(stk.size()){
int k=stk.top();
stk.pop();
if(!vis2[k]) {
int cnt=dfs2(k);
vec.push_back(cnt);
}
}
int ans=0;
for(int i = 0; i < vec.size(); ++i) {
ans += vec[i] * (vec[i] - 1);
for(int j = i + 1; j < vec.size(); ++j) {
ans += vec[i] * vec[j];
}
}
cout<<ans-sum_bian<<endl;
}
signed main()
{
slove();
return 0;
}