SCC (Strongly Connected Component)

2025. 2. 16. 20:58C++ study

아이디어!

  • 유향 그래프에서 모든 노드가 서로 도달 가능한 최대 크기의 부분 그래프를 의미한다.

BOJ 2150

  • 위의 그래프에서 1-4-5는 서로 도달은 가능하지만 외부에서는 도달할 수 없는 형태를 가지고 있다!
  • 위와 같이 1-4-5, 2-3-7, 6과 같은 집합을 SCC라고 할 수 있다.

특징!

  • SCC는 내부의 노드들은 서로 이동할 수 있는 경로가 존재해야 한다.
  • SCC를 압축한다면 DAG가 된다. (DAG: Directed Acyclic Graph(비순환 그래프))

알고리즘!

  • SCC를 찾는 알고리즘은 Kosaraju 알고리즘과 Tarjan 알고리즘이 있다.

Kosaraju Algorithm

과정
1. DFS를 실행
-그래프에서 DFS를 수행하며 종료 순서대로 정점을 스택에 저장해 준다!

2. 그래프를 반대로 뒤집기
-스택에 넣은 정점들을 꺼내가면서 반대로 dfs 탐색을 시작해 준다.

최종 코드(BOJ 2150)
#include <bits/stdc++.h>
using namespace std;

int n, m;
vector<int> v[100004], rev_v[100004];
stack<int> stk;
vector<bool> visited(100004);
vector<vector<int>> SCC;

void dfs(int x) {
    visited[x] = 1;
    for (auto i : v[x]) {
        if (!visited[i]) dfs(i);
    }
    stk.push(x);
}

void rev_dfs(int x, vector<int>& com) { 
    visited[x] = 1;
    com.push_back(x);
    for (auto i : rev_v[x]) {
        if (!visited[i]) rev_dfs(i, com);
    }
}

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);

    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int x, y;
        cin >> x >> y;
        v[x].push_back(y);
        rev_v[y].push_back(x);
    }

    fill(visited.begin(), visited.end(), 0);

    for (int i = 1; i <= n; i++) {
        if (!visited[i]) dfs(i);
    }

    fill(visited.begin(), visited.end(), 0);

    while (!stk.empty()) {
        int x = stk.top();
        stk.pop();

        if (!visited[x]) {
            vector<int> com;
            rev_dfs(x, com);
            sort(com.begin(), com.end());
            SCC.push_back(com);
        }
    }

    sort(SCC.begin(), SCC.end());

    cout << SCC.size() << '\n';
    for (auto i : SCC) {
        for (auto x : i) cout << x << ' ';
        cout << "-1\n";
    }
}​


dfs 탐색

void dfs(int x) {
    visited[x] = 1;
    for (auto i : v[x]) {
        if (!visited[i]) dfs(i);
    }
    stk.push(x);
}


-dfs탐색을 수행해 주는데 수행이 종료되었을 때 정점을 스택에 넣어준다.

역 방향 탐색

void rev_dfs(int x, vector<int> &component) {
    visited[x] = 1;
    component.push_back(x);
    for (auto i : rev_v[x]) {
        if (!visited[i]) rev_dfs(i, component);
    }
}

while (!stk.empty()) {
        int node = stk.top();
        stk.pop();

        if (!visited[node]) {
            vector<int> component;
            rev_dfs(node, component);
            sort(component.begin(), component.end());
            SCC.push_back(component);
        }
    }


-스택의 top()부터 탐색(역방향 탐색)을 하면서 SCC를 찾아준다.
-만약 정점이 방문되지 않았다면 그 정점부터 시작하여서 방문을 해주고 dfs방문을 하면서 방문한 정점은 com 백터에 넣어둔다.

 

Tarjan Algorithm

과정
1. DFS탐색을 하면서 탐색 순서와 고유 번호를 지정해 준다.
-한 배열에는 dfs탐색의 순서를 지정해 주고 또 다른 배열에는 최소 방문 번호를 지정해 준다.

2. 역방향 간건을 이용해서 최소 방문 번호를 갱신해 준다.
-만약 SCC가 발견된다면 그 내부에 있는 모든 정점의 최소 방문 번호를 SCC내부에서 가장 작은 번호로 변경해 준다!

3. 스택에서 저장되어 있는 값들을 꺼내서 SCC를 구한다!
-SCC의 정점들은 모두 방문처리를 해준다.

최종 코드(BOJ 2150)
#include <bits/stdc++.h>
using namespace std;

int n, m, id; 
vector<int> adj[100004], SCC[100004];
int d[100004], low[100004], sccIndex;
bool finished[100004]; 
stack<int> stk;

void dfs(int x) {
    d[x] = low[x] = ++id; 
    stk.push(x); 

    for (int next : adj[x]) {
        if (d[next] == 0) { 
            dfs(next);
            low[x] = min(low[x], low[next]);
        } 
        else if (!finished[next]) { 
            low[x] = min(low[x], d[next]);
        }
    }

    if (low[x] == d[x]) { 
        vector<int> scc;
        while (true) {
            int node = stk.top();
            stk.pop();
            scc.push_back(node);
            finished[node] = true;
            if (node == x) break;
        }
        sort(scc.begin(), scc.end());
        SCC[sccIndex++] = scc;
    }
}

int main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);

    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        adj[u].push_back(v);
    }

    for (int i = 1; i <= n; i++) {
        if (d[i] == 0) dfs(i);
    }

    cout << sccIndex << '\n';
    sort(SCC, SCC + sccIndex);
    for (int i = 0; i < sccIndex; i++) {
        for (int x : SCC[i]) cout << x << " ";
        cout << "-1\n";
    }
}​

 


주요 변수들

vector<int> adj[100004], SCC[100004]; 
int d[100004], low[100004], sccIndex; //d: 방문 순서, low: 최소 방문 순서, sccIndex: scc개수
bool finished[100004]; //finished: 이미 SCC그룹화 된 정점은 방문처리
stack<int> stk; //정점들을 역방향으로 탐색​

-low 같은 경우는 처음에는 d와 동일하게 방문 순서를 처리해 주다가 SCC가 발견된다면 SCC 내에서 제일 작은 방문순서로 모든 정점을 업데이트해준다.

dfs 탐색
void dfs(int x) {
    d[x] = low[x] = ++id;//방문 순서를 저장해줌 d,low동일
    stk.push(x); //현제 방문 정점은 스택에 넣어줌

    for (int next : adj[x]) { 
        if (d[next] == 0) { //방문 처리 안되어 있다면 다음 정점을 탐색해줌
            dfs(next);
            low[x] = min(low[x], low[next]); //방문을 완료한 이후에는 low값을 업데이트
        } 
        else if (!finished[next]) { //SCC그룹화 하지 않았다면 low업데이트
            low[x] = min(low[x], d[next]);
        }
    }

    if (low[x] == d[x]) { //low[x]==d[x]: 이미 방문하였고 SCC그룹화 안된 점을 발견한 경우!
        vector<int> scc;
        while (true) {
            int node = stk.top(); 
            stk.pop();
            scc.push_back(node);
            finished[node] = true; //SCC그룹화 한 경우 방문 처리해주기!
            if (node == x) break; //만약 현제 노드, 즉 SCC그룹 까지 SCC처리 해주었으면 종료
        }
        sort(scc.begin(), scc.end());
        SCC[sccIndex++] = scc;
    }
}​


-low는 다음 정점을 방문을 마친다음에 low를 업데이트하거나 SCC를 그룹화한 다음에 low를 업데이트해준다.

-low[x]==d[x] 의 경우는 이미 방문을 했다는 의미와 가장 작은 방문 순서라는 의미이다!

BOJ 2150번의 문제를 예제로 예시를 들어서 low의 양상을 보면 다음과 같다. 
BOJ 2150

DFS 진행 순서 현재 정점 탐색 순서(d[x]) low[x] 스택 내부
1 1 1 1 {1}
2 4 2 2 {1,4}
3 5 3 3 {1,4,5}
4 1 (방문한 정점)   min(3,1)=1 {1,4,5}
5 (되돌아감) 5   min(3,1)=1 {1,4,5} 
6 (되돌아감) 4   min(2,1)=1 {1,4,5}
7 (되돌아감) 1   min(1,1)=1 {1,4,5}
{1,4,5}: SCC       {} (SCC 탐색 후)
8 6 4 4 {6}
9 7 5 5 {6,7}
10 3 6 6 {6,7,3}
11 7   min(6,5)=5 {6,7,3,2}
12 2 7 7 {6,7,3,2}
13 7   min(7,5)=5 {6,7,3,2}
{2,3,7}: SCC       {6}
{6}: SCC       {}
이렇게 표로 본다면 쉽게 확인할 수 있다.

 

'C++ study' 카테고리의 다른 글

이분 매칭(Bipartite Matching)  (0) 2025.03.10
2-SAT  (0) 2025.03.01
외접원의 성질  (0) 2025.02.15
트리 순회 (Tree Traversal)  (0) 2025.02.14
Meet In The Middle  (0) 2025.02.12