C++
 Computer >> コンピューター >  >> プログラミング >> C++

【C++】グラフ内の橋(ブリッジエッジ)の数を検出するプログラムの解説

ブリッジエッジ(橋)とは?

重みなし無向グラフにおけるブリッジエッジ(橋)とは、その辺を取り除いたときにグラフが非連結(複数の連結成分に分断される)となるような辺のことです。本記事では、n個の頂点とm個の辺からなるグラフが与えられたとき、その中に含まれるブリッジの数を求めるC++プログラムを紹介します。なお、対象となるグラフには平行辺や自己ループは含まれないものとします。

問題の例

例として、n = 5、m = 6、edges = {{1, 2}, {1, 3}, {2, 3}, {2, 4}, {2, 5}, {3, 5}} という入力が与えられた場合を考えてみましょう。この場合の出力は 1 となります。

【C++】グラフ内の橋(ブリッジエッジ)の数を検出するプログラムの解説

このグラフには、ブリッジエッジ {2, 4} がちょうど1つだけ存在します。辺 {2, 4} を取り除くと、頂点4が他の頂点から切り離されてしまうためです。

解法のアプローチ

この問題は、深さ優先探索(DFS)を利用したTarjanの橋検出アルゴリズムによって効率的に解くことができます。探索の過程で各頂点に対して次の2つの値を記録します。

  • vk[v]:頂点vを訪問した順番(タイムスタンプ)
  • l[v]:頂点vから後退辺を経由しても到達できる、最も早いタイムスタンプ

そして、木構造の辺 (v, x) について l[x] > vk[v] が成り立つ場合、部分グラフx側からvより上の頂点へ戻る経路が存在しないことを意味するため、その辺はブリッジであると判定できます。

アルゴリズムの手順

mSize := 100
サイズmSizeの隣接リスト配列Gを定義
2次元配列bridgeを定義
サイズmSizeの配列visitedを定義
サイズmSizeの配列vkおよびlを定義
整数ペアを格納する配列edgesを定義

関数depthSearch(v, p = -1):
   visited[v] := 1
   vk[v] := l[v] := t++
   G[v]内の各xに対して:
      x == p の場合は次の反復へスキップ
      visited[x] が非ゼロの場合:
         l[v] := min(l[v], vk[x])
      それ以外の場合:
         depthSearch(x, v) を再帰呼び出し
         l[v] := min(l[v], l[x])
         l[x] > vk[v] であれば:
            bridge[v][x] := 1

関数bridgeSearch():
   t := 0
   i := 1 から n まで:
      visited[i] が偽であれば depthSearch(i) を呼び出す

メイン処理:
   i := 0 から m 未満まで:
      a := edges[i] の始点、b := edges[i] の終点
      G[a] に b を追加、G[b] に a を追加(無向グラフなので両方向)
   bridgeSearch() を呼び出す
   ans := 0
   i, j の全組み合わせについて:
      i != j かつ bridge[i][j] が真なら ans をインクリメント
   ans を返す

C++実装例

以下に、上記アルゴリズムの実際のC++コードを示します。

#include <bits/stdc++.h>
using namespace std;

const int mSize = 100;
vector<int> G[mSize];
int n, m, t;
vector<vector<int>> bridge(mSize, vector<int>(mSize));
vector<int> visited(mSize);
vector<int> vk(mSize, -1), l(mSize, -1);
vector<pair<int, int>> edges;

void depthSearch(int v, int p = -1) {
    visited[v] = 1;
    vk[v] = l[v] = t++;
    for (auto x : G[v]) {
        if (x == p) {
            continue;
        }
        if (visited[x]) {
            l[v] = min(l[v], vk[x]);
        } else {
            depthSearch(x, v);
            l[v] = min(l[v], l[x]);
            if (l[x] > vk[v]) {
                bridge[v][x] = 1;
            }
        }
    }
}

void bridgeSearch() {
    t = 0;
    for (int i = 1; i <= n; ++i) {
        if (!visited[i]) {
            depthSearch(i);
        }
    }
}

int solve() {
    for (int i = 0; i < m; ++i) {
        int a, b;
        a = edges[i].first;
        b = edges[i].second;
        G[a].push_back(b);
        G[b].push_back(a);
    }
    bridgeSearch();
    int ans = 0;
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            if (i != j and bridge[i][j]) ans++;
        }
    }
    return ans;
}

int main() {
    n = 5, m = 6;
    edges = {{1, 2}, {1, 3}, {2, 3}, {2, 4}, {2, 5}, {3, 5}};
    cout << solve();
    return 0;
}

入力

5, 6, {{1, 2}, {1, 3}, {2, 3}, {2, 4}, {2, 5}, {3, 5}}

出力

1

計算量の考察

DFSを1回実行するだけで済むため、探索自体の計算量は O(V + E)(Vは頂点数、Eは辺数)と非常に効率的です。ただし、上記のコードでは最後に隣接行列bridgeを二重ループで走査してブリッジをカウントしているため、全体としてはO(V² + E)となります。大規模なグラフを扱う場合は、ブリッジを検出した時点で直接カウントするように変更すれば、計算量をO(V + E)に抑えることが可能です。

  1. グラフ内のスーパー頂点を見つけるC++プログラムの解説

    問題の概要n個の頂点を持つグラフが与えられていると仮定しましょう。頂点には1からnまでの番号が付けられており、配列「edges」に含まれる辺によって互いに接続されています。さらに、各頂点は1からnの範囲の数値である「x」という値を持ち、その値は配列「values」で与えられます。このとき、グラフの中から「スーパー頂点(super vertex)」と呼ばれる特別な頂点を見つけ出す必要があります。頂点iがスーパー頂点であるとは、頂点1から頂点iへの最短経路上に、i番目の頂点と同じ「x」の値を持つ頂点が存在しないことを意味します。この条件を満たすすべての頂点を出力してください。たとえば、入力が n

  2. グリッド内で照らされているセルの数を求めるC++プログラム

    問題の概要 ここでは、縦 h × 横 w のサイズを持つグリッドが与えられたとき、光で照らされているセルの数を求めるC++プログラムを紹介します。グリッドのセルには「電球」または「障害物」が置かれています。電球のあるセルは、そのセル自身と上下左右のセルを照らし、光は障害物に遮られない限りまっすぐ伝わっていきます。一方、障害物のあるセルは照らされることがなく、電球の光を遮って他のセルへ光が届かないようにします。電球の位置を配列 bulb、障害物の位置を配列 obstacles として受け取り、グリッド全体で照らされているセルの合計数を求めます。 たとえば、入力が h = 4、w = 4、bulb