algorithm/UnionFind

[백준7511/골드5] 소셜 네트워킹 어플리케이션 - Python (클래스 리팩토링)

ayeongjin 2025. 5. 3. 01:37

https://www.acmicpc.net/problem/7511

 

 

💡 리팩토링 이유

알고리즘 문제를 풀면서 여러 테스트 케이스가 반복되는 문제를 풀 때 특히 가독성과 유지보수 측면, 재사용성 관점에서 코드를 클래스화하면 더 깔끔하고 독립적인 코드가 될 수 있다고 생각했다.

그래서 이번에는 유니온파인드 문제를 풀이하면서 클래스로 변경해봤다.

 

✅ 1. 초기 구현 – 전역 변수와 함수 기반

 

처음에는 단순히 문제를 푸는 데 집중해서 parent, rank, find, union 전부 전역에 두고 구현했다.

import sys
input = sys.stdin.readline

T = int(input())

for t in range(T):
    print(f'Scenario {t+1}:')
    n = int(input()) # 유저의 수
    k = int(input()) # 친구 관계 수

    parent = [i for i in range(n)]
    rank = [1] * n

    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])
        return parent[x]

    def union(x, y):
        rootX = find(x)
        rootY = find(y)

        if rootX != rootY:
            if rank[rootX] < rank[rootY]:
                rank[rootY] += rank[rootX]
                parent[rootX] = rootY
            else:
                rank[rootX] += rank[rootY]
                parent[rootY] = rootX


    # 친구 관계 (a, b는 친구)
    for _ in range(k):
        a, b = map(int, input().split())
        union(a, b)

    m = int(input()) # 미리 구할 쌍의 수

    # 구해야 하는 쌍의 수
    for _ in range(m):
        u, v = map(int, input().split())
        result = find(u) == find(v)
        print(1 if result else 0)

    if t != T-1:
        print()

 

이 방식은 빠르게 구현할 수 있다는 장점이 있지만, 다음과 같은 문제가 생긴다.

  • 함수 간의 의존성이 암시적이다 (외부에서 parent, rank를 가져옴)
  • 스코프 오염으로 다른 코드와 충돌 가능성 존재
  • 재사용성과 테스트가 어렵다

 

✅ 2. 클래스 도입 – 관련 데이터와 함수를 하나로 묶기

 

이후, 문제 로직과 분리하여 이 데이터 구조 자체를 하나의 객체로 만들어보기로 했다.

class SNA:
    def __init__(self, size):
        self.parent = list(range(size))
        self.rank = [1] * size

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        rootX = self.find(x)
        rootY = self.find(y)

        if rootX != rootY:
            if self.rank[rootX] < self.rank[rootY]:
                self.rank[rootY] += self.rank[rootX]
                self.parent[rootX] = rootY
            else:
                self.rank[rootX] += self.rank[rootY]
                self.parent[rootY] = rootX

 

이제 parent와 rank는 SNA 클래스의 내부 상태가 되었고, find, union은 이 상태를 다루는 인스턴스 메서드로 구조화되었다.

이 구조는 이러한 장점을 가진다.

  • 상태 (parent, rank)가 외부로 노출되지 않음 → 캡슐화
  • 동일한 문제에 재사용할 수 있음 → 재사용성 향상
  • 메인 로직에서 객체에 메시지를 보내는 형태로 구조화 가능 → 책임 분리

 

✅ 3. 메인 로직 리팩토링 – 구조 분리 및 함수 단위화

 

기존에는 모든 로직이 한 곳에 몰려 있었다. 이를 solve() 함수로 분리하여 테스트 가능성과 가독성을 높였다

def solve():
    n = int(input())
    k = int(input())
    sna = SNA(n)

    for _ in range(k):
        a, b = map(int, input().split())
        sna.union(a, b)

    m = int(input())
    for _ in range(m):
        u, v = map(int, input().split())
        print(1 if sna.find(u) == sna.find(v) else 0)

 

 

그리고 반복되는 시나리오를 처리하는 메인 함수도 별도로 분리했다.

def main():
    T = int(input())
    for t in range(T):
        print(f'Scenario {t+1}:')
        solve()
        if t != T - 1:
            print()

 

✅ 정답 코드

import sys
input = sys.stdin.readline

class SNA:
    def __init__(self, size):
        self.parent = list(range(size))
        self.rank = [1] * size

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        rootX = self.find(x)
        rootY = self.find(y)

        if rootX != rootY:
            if self.rank[rootX] < self.rank[rootY]:
                self.rank[rootY] += self.rank[rootX]
                self.parent[rootX] = rootY
            else:
                self.rank[rootX] += self.rank[rootY]
                self.parent[rootY] = rootX

def solve():
    n = int(input())
    k = int(input())
    sna = SNA(n)

    for _ in range(k):
        a, b = map(int, input().split())
        sna.union(a, b)

    m = int(input())
    for _ in range(m):
        u, v = map(int, input().split())
        print(1 if sna.find(u) == sna.find(v) else 0)

def main():
    T = int(input())
    for t in range(T):
        print(f'Scenario {t+1}:')
        solve()
        if t != T - 1:
            print()

main()

 

 

 

 

✅ 리팩토링 요약

번경 전 변경 후
전역 변수, 전역 함수 객체 내 상태와 메서드로 분리
모든 로직이 한 함수에 시나리오 처리 함수 분리
재사용 불가, 테스트 어려움 테스트 가능, 재사용 가능 구조
변수와 로직의 의도 명확하지 않음 책임이 분명한 클래스 중심 구조