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()
✅ 리팩토링 요약
번경 전 | 변경 후 |
전역 변수, 전역 함수 | 객체 내 상태와 메서드로 분리 |
모든 로직이 한 함수에 | 시나리오 처리 함수 분리 |
재사용 불가, 테스트 어려움 | 테스트 가능, 재사용 가능 구조 |
변수와 로직의 의도 명확하지 않음 | 책임이 분명한 클래스 중심 구조 |