强连通分+双连通分量+圆方树

强连通分量

求所有割边(桥)

割边定义:对于一个无向图,如果删掉一条边后图中的连通分量数增加了,这条边就是割边。

模板:https://www.luogu.com.cn/problem/P1656

#include <algorithm>
#include <iostream>
#include <vector>

using namespace std;
using PII = pair<int, int>;

const int MAXN = 150 + 3, MAXM = 5000 + 3;

struct Edge {
  int nxt, id;
};

int n, m, ANS_len = 0;
int dep[MAXN], low[MAXN];
PII _eg[MAXM], ANS[MAXM];
vector<Edge> eg[MAXN];

int dfs(int x, int dad, int e) {
  if(dep[x] > 0) return dep[x]; // 这里也可以返回 low
  dep[x] = low[x] = dep[dad] + 1; // 这里也可以 depth 进行累加
  for(Edge E : eg[x]){
    if(E.id != e) low[x] = min(low[x], dfs(E.nxt, x, E.id));
  }
  if(e > 0 && low[x] == dep[x]) ANS[++ANS_len] = _eg[e];
  return low[x];
}

int main() {
  cin >> n >> m;
  for (int i = 1, U, V; i <= m; i++) {
    cin >> U >> V;
    if (U > V) swap(U, V);  // U <= V
    _eg[i] = {U, V}, eg[U].push_back({V, i}), eg[V].push_back({U, i});
  }
  dfs(1, 0, 0);
  sort(ANS + 1, ANS + 1 + ANS_len); // pair 自带比较规则:先比较 first 再比较 second
  for (int i = 1; i <= ANS_len; i++) {
    cout << ANS[i].first << " " << ANS[i].second << "\n";
  }
  return 0;
}

求所有割点

割点定义:对于一个无向图,如果把一个点删除后这个图的极大连通分量数增加了,那么这个点就是这个图的割点(又称割顶)。

模板:https://www.luogu.com.cn/problem/P3388

参考题解:https://www.luogu.com.cn/article/pqth396e

无向图 DFS 树的性质(非常重要):

  • 祖先后代性:任意非树边两端具有祖先后代关系。
  • 子树独立性:结点的每个儿子的子树之间没有边(和上一条性质等价)。
  • 时间戳区间性:子树时间戳为一段区间。
  • 时间戳单调性:结点的时间戳小于其子树内结点的时间戳。

\(T(x)\) 表示树上 \(x\) 子树(含 \(x\)),设 \(T'(x)\) 表示除 \(T(x)\) 以外的所有节点。

因为子树独立性,对于 \(T(x)\) 如果删除 \(x\) 可能会分裂成多个联通块。但是 \(T'(x)\) 始终是联通的一个联通块。

所以 \(T(y)\) 不会被孤立的唯一希望就是于 \(T'(x)\) 有连边。

根据割点定义,我们可以转化为以下问题:若 \(x\) 是割点,等价于存在 \(u \in T(x)\) 不经过 \(x\) 可以一步到达 \(v \in T'(x)\)

\((u,v)\) 是树边,则 \(u\) 必须等于 \(x\),矛盾。所以 \((u,v)\) 为非树边。

我们只需要判断 \(x\) 是否存在一个儿子 \(y\) ,满足存在 \(u\in T(y)\) 可以可以通过一步到达 \(v \in T'(x)\)

\(f_y\) 表示 \(u \in T(y)\) 可以通过返祖边到达的最大时间戳。

如果 \(x\) 是割点,也就是:存在一个 \(x\) 的儿子 \(y\) 满足 \(f_y \ge d_x\)

但是实现的时候,对于根节点,需要特别处理,看代码。

#include <algorithm>
#include <iostream>
#include <vector>

using namespace std;
using LL = long long;
using PII = pair<int, int>;

const int MAXN = 2e4 + 3, MAXM = 1e5 + 3;

struct Edge {
  int nxt, id;
};

int n, m, ANS_len = 0;
int dep[MAXN], low[MAXN], cnt[MAXN], ANS[MAXN], nd_vis[MAXN];
vector<Edge> eg[MAXN];

int dfs(int x, int dad) {
  if(dep[x] > 0) return dep[x]; 
  dep[x] = low[x] = dep[dad] + 1; // 这里也可以 depth 进行累加
  for(Edge E : eg[x]){
    low[x] = min(low[x], dfs(E.nxt, x));
  }
  if(cnt[x] >= 1 + (dad == 0)){
    ANS[++ANS_len] = x;
  }
  cnt[dad] += (dep[dad] <= low[x]);
  return low[x];
}

int main() {
  cin >> n >> m;
  for (int i = 1, U, V; i <= m; i++) {
    cin >> U >> V, eg[U].push_back({V, i}), eg[V].push_back({U, i});
  }
  for(int i = 1; i <= n; i++) dfs(i, 0);
  sort(ANS + 1, ANS + 1 + ANS_len);
  cout << ANS_len << "\n";
  for (int i = 1; i <= ANS_len; i++) {
    cout << ANS[i] << " ";
  }
  return 0;
}

缩点

若两点之间可以互相到达,则两点是 强连通

若一个有向图任意两点之间强连通,则该有向图是 强联通图

有向图的一个极大强连通子图,为 强连通分量 (Strongly Connected Component,SCC)。

注意到每个 强连通分量 都没有交。

缩点就是缩每个强联通分量。

模板:https://www.luogu.com.cn/problem/P3387

参考题解:https://www.cnblogs.com/alex-wei/p/basic_graph_theory.html 的 4.3 部分

注意有向图的 DFS 序有额外的两种边:

  • 从祖先指向后代的非树边,称为 前向边。
  • 从后代指向祖先的非树边,称为 返祖边。
  • 两端无祖先后代关系的非树边,称为 横叉边。

考虑在最浅的结点处求出包含它的 SCC。称一个点是关键点,当且仅当它是某个 SCC 的最浅结点。

考虑何时 \(x\)\(fa\) 两点强连通(看上面的图)

  • \(x\)\(u1\) 然后再走返祖边到达 \(fa\)
  • \(x\) 走横插边到 \(v2\),且 \(v2\) 可以到达 \(fa\)

\(g_x\) 表示 \(x\) 可以 通过返祖边 和 通过横插边 到达的最小时间戳。

结论:\(x\) 是关键点,当且仅当 \(g_x \ge d_x\)。证明看 https://www.cnblogs.com/alex-wei/p/basic_graph_theory.html 的 4.3 部分。

考虑如何求 \(g\)

当找到了一个强联通分量时,则这个强联通分量不会对外贡献。

然后就是代码那种实现了。

#include <algorithm>
#include <iostream>
#include <vector>
#include <cmath>

using namespace std;
using LL = long long;

const int MAXN = 1e4 + 3, MAXM = 1e5 + 3;

int n, m, depth = 0, ANS = 0;
int s[MAXN], l = 0; // dfs序
int a[MAXN], dp[MAXN];
int dep[MAXN], low[MAXN], root[MAXN];
vector<int> eg[MAXN];

int dfs(int x){
  if(dep[x]) return (root[x] > 0 ? n : low[x]); // can dep too
  depth++, dep[x] = low[x] = depth;
  s[++l] = x;
  for(int E : eg[x]){
    low[x] = min(low[x], dfs(E));
  }
  if(low[x] == dep[x]){
    for( ; l > 0 && s[l] != x; l--){
      for(int E : eg[s[l]]) eg[x].push_back(E);
      root[s[l]] = x, a[x] += a[s[l]];
    }
    root[x] = x, l--;
  }
  return low[x];
}

int a_dfs(int x){
  if(dp[x] == -1){
    dp[x] = 0;
    for(int E : eg[x]){
      dp[x] = max(dp[x], a_dfs(root[E]));
    }
    dp[x] += a[x];
  }
  return dp[x];
}

int main(){
  cin >> n >> m;
  for(int i = 1; i <= n; i++) cin >> a[i];
  for(int i = 1, U, V; i <= m; i++){
    cin >> U >> V;
    eg[U].push_back(V);
  }
  for(int i = 1; i <= n; i++) dfs(i);
  fill(dp + 1, dp + 1 + n, -1);
  for(int i = 1; i <= n; i++){
    ANS = max(ANS, a_dfs(root[i]));
  }
  cout << ANS;
  return 0;
}

双连通分量

定义

在一张连通的无向图中,对于两个点 \(u\)\(v\),如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 \(u\)\(v\) 边双连通

在一张连通的无向图中,对于两个点 \(u\)\(v\),如果无论删去哪个点(只能删去一个)都不能使它们不连通,我们就说 \(u\)\(v\) 点双连通

  • 点双连通图:不存在割点的无向连通图称为 点双连通图。根据割点的定义,孤立点和孤立边均为点双连通图。
  • 边双连通图:不存在割边的无向连通图称为 边双连通图。根据割边的定义,孤立点是边双连通图,但孤立边不是。
  • 点双连通分量:一张图的极大点双连通子图称为 点双连通分量(V-BCC),简称 点双。
  • 边双连通分量:一张图的极大边双连通子图称为 边双连通分量(E-BCC),简称 边双。

边双连通分量

一个割边可以将图分为 \(2\) 个连通块,\(s\) 割边可以将图分为 \(s+1\) 个连通块。断开所有割边,我们就得到了 \(s+1\) 个边双连通分量。

性质:

  • 边双连通的传递性
    • \(a\)\(b\) 边双连通,\(b\)\(c\) 边双连通,则有 \(a\)\(c\) 边双连通。

模板题:https://www.luogu.com.cn/problem/P8436

点击查看代码
#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int MAXN = 5e5 + 3, MAXM = 2e6 + 3;

struct Edge{
  int to, id;
};

int n, m, k = 0;
vector<int> ans[MAXN];
vector<Edge> eg[MAXN];

int depth = 0, dep[MAXN], low[MAXN];
vector<int> stk;
int dfs(int x, int fe){
  if(dep[x]) return dep[x];
  dep[x] = low[x] = ++depth;
  stk.push_back(x);
  for(Edge e : eg[x]){
    if(e.id == fe) continue;
    low[x] = min(low[x], dfs(e.to, e.id));
  }
  if(dep[x] == low[x]){
    k++;
    while(stk.size() > 0 && stk.back() != x){
      ans[k].push_back(stk.back()), stk.pop_back();
    }
    ans[k].push_back(stk.back()), stk.pop_back();
  }
  return low[x];
}

int main(){
  cin >> n >> m;
  for(int i = 1, U, V; i <= m; i++){
    cin >> U >> V;
    eg[U].push_back({V, i});
    eg[V].push_back({U, i});
  }
  for(int i = 1; i <= n; i++) dfs(i, 0);
  cout << k << "\n";
  for(int i = 1; i <= k; i++){
    cout << ans[i].size() << " ";
    for(int x : ans[i]) cout << x << " ";
    cout << "\n";
  }
  return 0;
}

点双连通分量

性质:

  • 若两点双有交,那么交点一定是割点。
  • 一个点是割点当且仅当它属于超过一个点双。

模板题:https://www.luogu.com.cn/problem/P8435

实现:

点击查看代码
#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int MAXN = 5e5 + 3, MAXM = 2e6 + 3;

struct Edge{
  int to, id;
};

int n, m, k = 0;
int vis[MAXN], check[MAXN];
vector<int> ans[MAXM];
vector<Edge> eg[MAXN];

int depth = 0, dep[MAXN], low[MAXN], cnt[MAXN];
vector<int> stk;
void dfs(int x, int fx){
  dep[x] = low[x] = ++depth;
  stk.push_back(x);
  bool ooo = 0;
  for(Edge e : eg[x]){
    if(dep[e.to]){
      low[x] = min(low[x], dep[e.to]);
      continue;
    }
    ooo = 1, dfs(e.to, x), low[x] = min(low[x], low[e.to]);
    if(dep[x] <= low[e.to]){
      k++;
      while(stk.size() > 0){
        ans[k].push_back(stk.back()), stk.pop_back(); 
        if(ans[k].back() == e.to) break;
      }
      ans[k].push_back(x);
    }
  }
  if(fx == 0 && ooo == 0) ans[++k].push_back(x);
}

int main(){
  cin >> n >> m;
  for(int i = 1, U, V; i <= m; i++){
    cin >> U >> V;
    if(U == V) continue;
    eg[U].push_back({V, i});
    eg[V].push_back({U, i});
  }
  for(int i = 1; i <= n; i++){
    if(dep[i] == 0) stk.clear(), dfs(i, 0);
  }
  cout << k << "\n";
  for(int i = 1; i <= k; i++){
    cout << ans[i].size() << " ";
    for(int x : ans[i]) cout << x << " ";
    cout << "\n";
  }
  return 0;
}

圆方树

点双连通图:有两种定义,两种是等价的

  • 图中任意两不同点之间都有至少两条点不重复的路径。
  • 图中不存在割点

点双连通分量 则是一个 极大点双连通子图。

点双联通图有的性质:

  • P4630 [APIO2018] 铁人两项: 对于一个点双中的两点,它们之间简单路径的并集,恰好完全等于这个点双。即两点之间,对于任意点双内的点 \(w\),一定有一条路径经过它。(证明可以看 oi-Wiki)
    • 它告诉了我们:考虑两圆点在圆方树上的路径,与路径上经过的方点相邻的圆点的集合,就等于原图中两点简单路径上的点集。

应为割点数量小于等于 \(n\),所以圆方树节点个数小于等于 \(2n\),所以所有数组都要开两倍。

oi-wiki 上讲的圆方树:https://oi-wiki.org/graph/block-forest/

点击查看 铁人两项 代码
#include <bits/stdc++.h>

using namespace std;
using LL = long long;

const int MAXN = 2e5 + 3;

int n, m, k = 0, w[MAXN];
vector<int> _eg[MAXN], eg[MAXN];

int depth = 0, dfn[MAXN], low[MAXN];
vector<int> stk;
void dfs(int x){
  low[x] = dfn[x] = ++depth, stk.push_back(x);
  for(int nxt : _eg[x]){
    if(!dfn[nxt]){
      dfs(nxt);
      low[x] = min(low[x], low[nxt]); // 细节,挂了一次 
      if(low[nxt] == dfn[x]){
        k++;
        int back = 0; 
        while(back != nxt){ // 细节,挂了一次 
          back = stk.back(); 
          w[k]++, eg[k].push_back(stk.back()), eg[stk.back()].push_back(k);
          stk.pop_back();
        }
        w[k]++, eg[k].push_back(x), eg[x].push_back(k);
      }
    }else low[x] = min(low[x], dfn[nxt]);
  }
}

void init(){
  cin >> n >> m, k = n;
  for(int i = 1, U, V; i <= m; i++){
    cin >> U >> V, _eg[U].push_back(V), _eg[V].push_back(U);
  }
  for(int i = 1; i <= n; i++){
    w[i] = -1;
    if(!dfn[i]) stk.clear(), dfs(i);
  }
  swap(n, k);
}

int vis[MAXN], sz[MAXN];
LL ans = 0;
vector<int> vt; 

void dfs2(int x){
  vt.push_back(x);
  int op = (x <= k);
  sz[x] = op, vis[x] = 1;
  for(int nxt : eg[x]){
    if(!vis[nxt]){
      dfs2(nxt);
      ans += 2ll * w[x] * sz[x] * sz[nxt];
      sz[x] += sz[nxt];
    }
  }
}

int main(){
  ios::sync_with_stdio(0), cin.tie(0);
  init();
  for(int i = 1; i <= n; i++){
    if(!vis[i]){
      vt.clear(), dfs2(i);
      for(int x : vt){
        ans += 2ll * w[x] * sz[x] * (sz[i] - sz[x]); // 细节,挂了一次(不能将 sz[i] 改为 k) 
      }
    }
  }
  cout << ans;
  return 0;
}

练习:https://www.luogu.com.cn/problem/CF487E

练习

https://www.cnblogs.com/huangqixuan/p/18446558#luogu---p11022-laoi-6yet-another-graph-coloration-problem

posted @ 2023-10-17 21:07  hhhqx  阅读(7)  评论(0编辑  收藏  举报