Prerequisite

Biconnected Component

이중연결요소는 무방향 그래프에서 정의되는 그래프에서의 정점 분할 테크닉이다.

BCC의 그룹은 다음과 같이 정의된다.

그룹의 정점들 중 하나를 제거해도 여전히 모든 정점이 연결되어 있고, 그런 정점들을 최대로 많이 모아둔 그룹

예시를 보자. 같은 색상의 정점들은 같은 BCC 그룹이다.

엄밀히 말하면 간선을 기준으로 그룹핑을 한 것이라고 볼 수 있다.

image.png

활용

필자도 BCC관련된 문제들을 많이 풀어보지 않아서 잘 모르겠지만,

그래프 문제들 자체를 이해하게 되는데도 도움이 되며, 선인장이나 단절점, 단절선과 관련된 키워드들에서 흔히 활용된다.

성질

하나의 정점이 $2$개 이상의 BCC에 속할 수 있다.

간선은 하나의 BCC에만 속하지만, 정점은 위 그림에서 볼 수 있듯이 여러 색을 가질 수 있다.

여러 색을 가지는 정점들은 여러 BCC에 속해있다.

$2$개 이상의 BCC에 속하는 정점은 단절점이다.

그렇다. 위 그림에서 $2$개 이상의 색을 가지는 정점들이 단절점이라는 것을 쉽게 확인할 수 있다.

BCC 내부에 간선이 하나라면 그 간선은 단절선이다.

BCC 내부에 간선이 하나라면 두 정점이 연결되어 있다.

따라서 그 간선이 제거되면 두 정점은 분리되므로 단절선이다.

BCC의 정점 개수가 $3$개 이상이면 모든 정점을 써서 싸이클을 이루게 할 수 있다.

아래 연습 문제에서 살펴보자.

알고리즘

여러 알고리즘이 있을 수 있으나 그 중 하나를 소개한다.

구현에서 우리가 얻으려는 것은,

  • 각 정점들이 속해있는 BCC 개수
  • BCC 마다 어떤 정점들이 속해있는지
  • BCC 마다 어떤 간선들이 속해있는지

이다. 이 세 가지를 모두 구할 필요는 없고, 필요한 것만 적절히 구현해서 구하면 된다.

코드에선 세 개를 다 구하는 식으로 해보자.


알고리즘은 DFS 기반으로 선형 시간 $O(N)$에 구할 수 있고, SCC를 구하는 타잔 알고리즘과 유사하다.

DFS 함수는 DFS 스패닝 트리에서 부모로 가는 간선을 사용하지 않고, 현재 정점에서 도달할 수 있는 가장 이르게 방문된 간선의 dfsn을 반환한다.

현재 노드가 $u$라면, $u$에서 인접한 간선들 중, 한 번도 검사되지 않은 간선은 스택에 $(u, v)$ 형태로 삽입한다.

만약 $v$가 이미 방문된 정점이라면 $v$의 DFS 순서만 결과값에 $Min$ 연산으로 갱신해주고, 방문되지 않았다면 DFS 함수를 호출한다.

만약 $v$의 DFS 결과 방문순서가 $u$의 방문순서 이상이라면 $u$는 단절점이란 의미이므로 하나의 BCC를 찾은 것이다.

각 정점들이 몇개에 속해있는지를 쉽게 구하는 법은,

  • 단절점이라면 BCC가 검출될 때, $1$을 더해준다.
  • DFS를 처음 시작할 때 루트 노드가 아니라면 $1$을 더해준다.

로 쉽게 구할 수 있다.

코드

struct BCC {
   int N, next_dfsn = 0;
   vvi edges, bcc_v;
   vi dfsn, included;
   stack<pi> s;
   vector<vector<pi>> bcc_e;
   BCC(int N) : N(N) {
      edges.resize(N);
      dfsn.resize(N, -1);
      included.resize(N);
   }
   void add_edge(int u, int v) { edges[u].pb(v), edges[v].pb(u); }
   int dfs(int cur, int p) {
      dfsn[cur] = next_dfsn++;
      int ret = dfsn[cur];
      for (int to: edges[cur]) {
         if (to == p) continue;
         if (dfsn[to] < dfsn[cur]) s.push({cur, to});
         if (~dfsn[to]) {
            ret = min(ret, dfsn[to]);
         } else {
            int tmp = dfs(to, cur);
            ret = min(ret, tmp);
            if (tmp >= dfsn[cur]) {
               included[cur]++;
               vector<pi> bcc_tmp;
               bcc_v.emplace_back();
               while (1) {
                  bcc_tmp.pb(s.top());
                  bcc_v.back().pb(bcc_tmp.back().fi);
                  bcc_v.back().pb(bcc_tmp.back().se);
                  s.pop();
                  if (bcc_tmp.back() == mp(cur, to)) break;
               }
               uniq(bcc_v.back());
               bcc_e.pb(bcc_tmp);
            }
         }
      }
      return ret;
   }
   void build() {
      for (int i = 0; i < N; i++) if (dfsn[i] == -1) dfs(i, -1); else included[i]++;
   }
};

선인장

선인장 그래프란, 정의에 따라 정점이나 간선이 최대 $1$개의 싸이클에만 포함되는 연결 그래프이다.

이걸 정점 선인장과 간선 선인장이라 하자.

예를 들어, 다음과 같은 그래프는 간선 선인장이고,

image.png

이런 그래프는 정점 선인장이다.

image.png

정점 선인장은 항상 간선 선인장이다. 역은 성립하지 않는다.

간선이 $2$개의 싸이클에 포함된다고 하자. 그럼 그 간선을 이루는 두 정점 $\overline{u,v}$ 는 $2$개의 싸이클에 포함이 되어야해서 모순이다. $\square$

BOJ 10891 - Cactus? Not cactus?

이 문제는 정점 선인장 판별이다.

BCC들을 모두 찾고, 그 BCC의 정점 개수가 $3$개 이상이라 싸이클이 생긴다면 간선의 개수가 정점의 개수와 동일한지 확인해주자.

이후 $2$번 예제를 살펴보면, 다음과 같은 그래프이다.

image.png

보면 $3$번 정점이 두 개의 싸이클에 포함이 되어있음을 알 수 있다.

앞선 검사로는 이 케이스를 검사하지 못하므로, 단절점들 중, 싸이클을 이루는 두 개 이상의 BCC에 속하는 단절점이 있다면 선인장이 아니다.

문제의 입력이 연결 그래프이므로 그 외의 경우는 모두 선인장이다.

void solve() {  
   int n, m;  
   cin >> n >> m;  
   BCC bcc(n);  
   while (m--) {  
      int u, v;  
      cin >> u >> v;  
      u--, v--;  
      bcc.add_edge(u, v);  
   }  
   bcc.build();  
  
   auto fail = [&]() {  
      cout << "Not cactus";  
      exit(0);  
   };  
  
   int B = sz(bcc.bcc_v);  
   vi is_cycle(B);  
   for (int i = 0; i < B; i++) {  
      if (bcc.bcc_v[i].size() >= 3) {  
         if (bcc.bcc_e[i].size() != bcc.bcc_v[i].size()) fail();  
         is_cycle[i] = 1;  
      }  
   }  
   for (int i = 0; i < n; i++) {  
      int cycle_cnt = 0;  
      for (int j: bcc.bcc_indice[i]) {  
         cycle_cnt += is_cycle[j];  
      }  
      if (cycle_cnt >= 2) fail();  
   }  
   cout << "Cactus";  
}

bcc_indice 는 각 정점이 어떤 bcc 에 속하는지 역으로 인덱스를 저장하고 있는 배열이다.

BOJ 2111 - 선인장

이 문제는 간선 선인장 문제이다.

간선 선인장은 정점 선인장에서 각 단절점이 싸이클 $2$개 이상에 속해있으면 안된다는 검사를 제거하면 된다.

그저 정점이 $3$개 이상인 각 BCC의 간선 수와 정점 수가 동일한지만 판별하면 된다.

지문이 뭐라뭐라 하는데, 결국 스패닝 서브선인장을 만들기 위해서는 싸이클이 있는(정점이 $3$개 이상인) BCC 내부에 정점이 $v$개라면, $(v+1)$ 개를 정답에 곱해주는 것으로 경우의 수를 셀 수 있다.

연결 그래프가 아니거나 간선 선인장이 아니라면 $0$을 출력하고, 그렇지 않다면 위와 같이 정답을 구해서 출력한다.

정답에 큰 수 연산을 써야한다 -.-

연습 문제

BOJ 11266 - 단절점

정점들 중 $2$개 이상의 BCC에 속한 것들이 단절점이다.

void solve() {  
   int n, m;  
   cin >> n >> m;  
   BCC bcc(n);  
   while (m--) {  
      int u, v;  
      cin >> u >> v;  
      u--, v--;  
      bcc.add_edge(u, v);  
   }  
   bcc.build();  
   vi ans;  
   for (int i = 0; i < n; i++) if (bcc.included[i] > 1) ans.pb(i + 1);  
   cout << sz(ans) << endl;  
   sort(all(ans));  
   for (int i: ans) cout << i << ' ';  
}

BOJ 11400 - 단절선

BCC중, 한 개의 간선만 포함하는($=2$개의 정점만 가지는) BCC들에 있는 간선이 단절선이다.

void solve() {  
   int n, m;  
   cin >> n >> m;  
   BCC bcc(n);  
   while (m--) {  
      int u, v;  
      cin >> u >> v;  
      u--, v--;  
      bcc.add_edge(u, v);  
   }  
   bcc.build();  
   vector<pi> ans;  
   for (int i = 0; i < sz(bcc.bcc_v); i++) {  
      if (sz(bcc.bcc_v[i]) == 2) {  
         ans.pb({bcc.bcc_v[i][0] + 1, bcc.bcc_v[i][1] + 1});  
         if (ans.back().fi > ans.back().se)  
            swap(ans.back().fi, ans.back().se);  
      }  
   }  
   cout << sz(ans) << endl;  
   sort(all(ans));  
   for (auto [a, b]: ans) cout << a << ' ' << b << endl;  
}

BOJ 20264 - Critical Structures

$n$개의 노드와 $m$개의 간선이 있고 두 정점 $(i, j)$ 사이엔 최대 하나의 간선만 있고, 자신에게 가는 간선은 없다.

그래프 $G$의 선형 배열 구조는 모든 서로 다른 두 정점이 항상 연속된 간선으로 이어지는 경우이다.

각 테스트 케이스에 대해 다음과 같은 것들을 찾아라.

  • 제거하면 $G$ 가 분리되는 정점의 개수
  • 제거하면 $G$ 가 분리되는 간선의 개수
  • BCC 의 개수
  • BCC 내 간선 개수의 최대

BOJ 23314 - Minimum Spanning Cactus

블로그 해설


참고

Comments