有向图的强连通分量 scc

基本概念:

  1. 树枝边(x,y):x是y的父亲

  2. 前向边(x,y):x是y的祖先结点

  3. 后向边(x,y):y是x的祖先结点

  4. 横叉边(x,y):在对有向图进行dfs遍历时,x是已经搜过的图的分支(不是前向边),现在在搜的点是y,y到x的有向边是横叉边

image

连通分量作用

  1. 通过缩点,将图变成有向无环图

缩点步骤:

for i=1;i<=n;i++
 for i的所有邻点j
   if i和j不在同一个scc中:
    加一条新边id[i]→id[j]

tarjin算法求强连通分量

原文:

引入时间戳的概念:在dfs遍历的过程中,按照每个结点第一次被访问的时间顺序,依次给予图中N个结点1~N的整数标记,该标记被称为时间戳,记为dfn[x].

对每个点定义两个时间戳:

  1. dfn[u] 表示遍历到u的时间戳;
  2. low[u] 表示从u开始走,所能遍历到的最小时间戳是什么。

我们在求强连通分量的时候,求的是每个强连通分量最上面的那个点,也就是最高点。
若u是所在的强连通分量的最高点 等价于 dfn[u]== low[u],
因为low[u]表示的是从u开始能够遍历到的最小的时间戳,若正好等于自己的时间戳,则就是说明u是最高点。

tarjan模板

背模板的思路

  1. 加时间戳;
  2. 放入栈中,做好标记;
  3. 遍历邻点

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

题目:Kattis - watchyourstep

原文
题意
给定有向图的邻接矩阵,现在需要给该图增加边,使得如果两点可达必直接可达,求需要加边的最大数量。

思路:
通过强连通分量缩点。

  1. 对每个联通块内部
    将其中的点全部连接起来需要n*(n-1)条边(建完全图)
  2. 对每个联通块之间
    将其全部链接起来需要\(v[i] × v[j]\)条边(v[i]为每个块中的点数),为避免重复建边,需要对每个联通块排序。

因此对整个图进行两次dfs

  1. 第一次 对原图跑dfs 用栈记录连通分量缩后的点访问顺序
  2. 第二次 对反向图根据出栈顺序跑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;
}
posted @ 2022-07-11 11:34  kingwzun  阅读(30)  评论(0编辑  收藏  举报