Featured image of post [Algorithm] C++ 백준 25172번: 꼼꼼한 쿠기의 졸업여행

[Algorithm] C++ 백준 25172번: 꼼꼼한 쿠기의 졸업여행

정점이 순차적으로 삭제될 때마다 그래프가 비지 않으면서 연결인지 판별한다. 삭제를 역순 추가로 바꿔 DSU로 컴포넌트 수를 관리해 O((N+M)α(N))에 답을 구한다.

정점이 하나씩 영구적으로 삭제되는 과정마다, 남아있는 정점이 1개 이상 존재하고 그래프가 연결(모든 정점 쌍이 도달 가능)인지 빠르게 판별해야 한다.
삭제는 DSU로 직접 처리하기 까다로우므로, 삭제 순서를 역순으로 뒤집어 “추가” 문제로 바꾸는 오프라인 풀이가 핵심이다.

문제 정보

문제 링크: https://www.acmicpc.net/problem/25172

문제 요약:

  • \(N\)개의 관광지(정점)와 \(M\)개의 길(무방향 간선)로 이루어진 그래프가 주어진다.
  • 이후 정점이 \(N\)번에 걸쳐 하나씩 삭제되며, 삭제되는 정점과 연결된 모든 간선도 함께 사라진다(영구 삭제).
  • 각 시점(처음 포함)마다, 남은 정점이 비어있지 않고 그래프가 연결이면 CONNECT, 아니면 DISCONNECT를 출력한다.

제한 조건:

  • 시간 제한: 2초
  • 메모리 제한: 512MB
  • \(1 \le N \le 200,000\)
  • \(1 \le M \le 200,000\)

입출력 예제

입력 1:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
5 4
5 4
4 3
3 2
2 1
4
5
1
2
3

출력 1:

1
2
3
4
5
6
CONNECT
DISCONNECT
CONNECT
CONNECT
CONNECT
DISCONNECT

접근 방식

핵심 관찰 1: “삭제”를 “추가”로 바꾸면 DSU로 처리 가능

정점 삭제는 DSU가 지원하지 않지만, 삭제 순서를 역순으로 보면 정점이 하나씩 추가되는 과정이 된다.

  • 원래 문제: \(v_1, v_2, \ldots, v_N\) 순서로 정점 삭제
  • 역순 문제: \(v_N, v_{N-1}, \ldots, v_1\) 순서로 정점 추가
    • 정점 \(v\)를 추가할 때, 이미 활성화(active)된 이웃과만 간선을 “복원”해 DSU로 합친다.

핵심 관찰 2: 연결 여부는 “활성 컴포넌트 수”로 판정

각 시점에서

  • 활성 정점 수가 0이면 무조건 DISCONNECT
  • 활성 정점 수가 1 이상이고, 활성 컴포넌트 수가 정확히 1이면 CONNECT

정점 \(v\)를 새로 활성화하면 컴포넌트가 1 증가하고, 활성 이웃과 DSU로 합칠 때마다(서로 다른 컴포넌트였다면) 컴포넌트 수가 1 감소한다.

알고리즘 설계 (Mermaid Flowchart)

flowchart TD
    A["입력: N, M, 간선 목록, 삭제 순서 v[1..N]"] --> B["활성 배열 active 초기화 (모두 false)"]
    B --> C["DSU 초기화, activeCnt=0, comps=0"]
    C --> D["i = N..1 (역순)"]
    D --> E["v = v[i] 를 활성화"]
    E --> F["activeCnt += 1, comps += 1"]
    F --> G["v의 모든 이웃 u에 대해, active[u]면 DSU.union(v,u)"]
    G --> H["union 성공 시 comps -= 1"]
    H --> I{"activeCnt > 0 && comps == 1 ?"}
    I -- "yes" --> J["ok[t] = CONNECT 저장"]
    I -- "no" --> K["ok[t] = DISCONNECT 저장"]
    J --> L["다음 i"]
    K --> L
    L --> M["정방향 출력: k번 삭제 상태 = 역순에서 (N-k)개 추가 상태"]

단계별 로직

  1. 입력 파싱: 인접 리스트로 그래프 저장, 삭제 순서 배열 저장
  2. 역순 처리:
    • 정점을 하나 활성화하고(activeCnt++, comps++)
    • 활성 이웃과 DSU union하여 컴포넌트 수 감소
    • activeCnt>0 && comps==1이면 그 시점은 연결
  3. 정방향 매핑:
    • 정방향에서 \(k\)개 삭제한 상태 ↔ 역순에서 \(N-k\)개 추가한 상태
    • \(k=0..N\)에 대해 정답 출력(총 \(N+1\)줄)

복잡도 분석

항목복잡도비고
시간 복잡도\(O((N+M)\alpha(N))\)각 간선은 양 끝이 활성화될 때 최대 1번 union 시도
공간 복잡도\(O(N+M)\)인접 리스트 + DSU + 활성 배열

코너 케이스 및 실수 포인트

케이스설명처리 방법
모든 정점 삭제 후정점이 0개면 연결 개념 성립 불가activeCnt==0이면 무조건 DISCONNECT
처음부터 비연결초기 그래프가 이미 여러 컴포넌트역순 처리 후 ok[N]가 false가 될 수 있음
간선이 없는 그래프\(M=0\)정점 1개일 때만 CONNECT, 그 외 DISCONNECT
중복 union같은 컴포넌트끼리 합치기 시도union 성공 여부로 comps 감소 여부 결정
출력 줄 수처음 상태 + N번 삭제 결과총 \(N+1\)줄 출력

구현 코드 (C++)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 42jerrykim.github.io에서 더 많은 정보를 확인할 수 있다
#include <bits/stdc++.h>
using namespace std;

struct DSU {
    vector<int> p, sz;
    DSU(int n = 0) { init(n); }
    void init(int n) {
        p.resize(n + 1);
        sz.assign(n + 1, 1);
        iota(p.begin(), p.end(), 0);
    }
    int find(int x) {
        while (p[x] != x) {
            p[x] = p[p[x]];
            x = p[x];
        }
        return x;
    }
    bool unite(int a, int b) {
        a = find(a); b = find(b);
        if (a == b) return false;
        if (sz[a] < sz[b]) swap(a, b);
        p[b] = a;
        sz[a] += sz[b];
        return true;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int N, M;
    cin >> N >> M;

    vector<vector<int>> adj(N + 1);
    for (int i = 0; i < M; i++) {
        int a, b;
        cin >> a >> b;
        adj[a].push_back(b);
        adj[b].push_back(a);
    }

    vector<int> del(N + 1);
    for (int i = 1; i <= N; i++) cin >> del[i];

    DSU dsu(N);
    vector<char> active(N + 1, 0);

    long long activeCnt = 0;
    long long comps = 0;

    // ok[t] = reverse process after adding t vertices
    vector<char> ok(N + 1, 0);

    int t = 0;
    for (int i = N; i >= 1; i--) {
        int v = del[i];
        active[v] = 1;
        activeCnt++;
        comps++; // new vertex introduces one new component

        for (int u : adj[v]) {
            if (active[u]) {
                if (dsu.unite(v, u)) comps--;
            }
        }

        t++;
        ok[t] = (activeCnt > 0 && comps == 1);
    }

    // forward state after k deletions == reverse state after (N-k) additions
    for (int k = 0; k <= N; k++) {
        int added = N - k;
        cout << (ok[added] ? "CONNECT" : "DISCONNECT") << "\n";
    }

    return 0;
}

참고 문헌 및 출처