

알고리즘도 머슬메모리!
“모든 트리는 그래프이지만, 모든 그래프가 트리는 아니다.”
| 구분 | 트리 (Tree) | 그래프 (Graph) |
|---|---|---|
| 정의 | 사이클이 없는 연결된 비순환 그래프 | 노드와 엣지로 구성된 일반적인 자료구조 |
| 구조 | 계층적 구조 (부모-자식 관계) | 네트워크 구조 (모든 노드가 연결될 수 있음) |
| 경로 (Path) | 두 노드 간 유일한 경로 존재 | 두 노드 간 여러 경로 가능 |
| 사이클 (Cycle) | 존재하지 않음 (비순환) | 존재할 수 있음 (순환 허용) |
| 루트 노드 (Root Node) | 존재함 (트리의 시작점) | 존재하지 않을 수 있음 |
| 연결성 (Connectivity) | 모든 노드가 연결됨 (하나의 컴포넌트) | 다수의 컴포넌트로 나뉠 수 있음 |
| 엣지 수 (Edges) | 노드 개수 - 1 (N - 1) | 일반적으로 노드 개수보다 많거나 적을 수 있음 |
| 방향성 (Direction) | 일반적으로 방향이 있음 (단방향) | 방향 그래프 또는 무방향 그래프 모두 존재 가능 |
트리와 그래프의 가장 중요한 차이는 사이클의 존재 여부와 부모-자식 관계 유무이다.
트리는 사이클이 없고 계층 구조를 가지며, 모든 노드가 루트로부터 연결되어 있다.
반면, 그래프는 사이클을 허용하고 네트워크 형태로 모든 노드가 자유롭게 연결될 수 있다.
그래프를 구현할 때 사용하는 대표적인 방식은 인접 리스트와 인접 행렬이다. 두 방식은 그래프의 특성에 따라 성능 차이가 크게 발생한다.
arr[i][j]로 O(1)에 즉시 확인 가능. # 인접 행렬 방식
graph = [[0] * (N + 1) for _ in range(N + 1)]
for i in range(M):
a, b = map(int, input().split())
graph[a][b] = graph[b][a] = 1
# 인접 리스트 방식
graph = [[] for _ in range(N + 1)]
for i in range(M):
start, end = map(int, input().split())
graph[start].append(end)
graph[end].append(start)

인접 행렬 방식으로 구현한 그래프는 568ms가 소요된 반면,
인접 리스트 방식은 64ms로 훨씬 빠르게 동작하였다.
그래프 구현 방식의 차이에 따라 성능 차이가 크게 발생한다는 점이 인상적이다.
DFS는 루트 노드(또는 임의의 노드) 에서 시작하여 다음 분기로 넘어가기 전에 해당 분기를 완벽하게 탐색하는 방식입니다.
O(N + E) O(N^2) def dfs(graph, v, visited):
visited[v] = True # 방문 처리
print(v, end=' ') # 방문한 노드 출력
for i in graph[v]: # 인접 리스트 방식일 경우
if not visited[i]:
dfs(graph, i, visited)
visited = [False] * 9 # 방문 체크 리스트
dfs(graph, 1, visited)
def dfs_stack(graph, start, visited):
stack = [start] # 스택에 시작 노드를 추가
while stack:
v = stack.pop() # 스택의 최상단 노드 꺼내기
if not visited[v]:
print(v, end=' ') # 방문한 노드 출력
visited[v] = True # 방문 처리
# 연결된 노드를 스택에 추가 (역순으로 추가)
for i in reversed(graph[v]):
if not visited[i]:
stack.append(i)
BFS는 시작 정점으로부터 가까운 정점을 먼저 방문하고, 멀리 떨어진 정점을 나중에 방문하는 방식입니다.
O(N + E) O(N^2) def bfs(graph, start, visited):
# deque 모듈 활용한 큐 구현
queue = deque([start])
visited[start] = True # 현재 노드 방문 처리
while queue:
v = queue.popleft() # front 에서 원소 하나 뽑기
for i in graph[v]:
if not visited[i]:
queue.append(i)
visited[i] = True
위상 정렬은 비순환 방향 그래프 (DAG: Directed Acyclic Graph) 에서 정점들을 순서대로 나열하는 알고리즘이다. 보통 사건 간의 순서를 나타내는 문제에서 활용된다.
Topology_sort(G)
1. 각 정점 v에 대해 종료시간 v.finish 계산 위해 DFS(G) 호출
2. 각 정점이 종료될 때마다 연결리스트 맨 앞에 삽입
3. return 정점들의 연결 리스트
import sys
from collections import deque
input = sys.stdin.readline
# 노드의 개수와 간선의 개수 입력
v, e = map(int, input().split())
# 모든 노드에 대한 진입차수는 0으로 초기화
indegree = [0] * (v + 1)
# 각 노드에 연결된 간선 정보를 담기 위한 연결 리스트
graph = [[] for _ in range(v + 1)]
# 그래프 입력 받기
for _ in range(e):
a, b = map(int, input().split())
graph[a].append(b)
indegree[b] += 1 # 진입차수 증가
# 위상 정렬 함수
def topology_sort():
result = [] # 정렬 결과를 담을 리스트
q = deque() # 큐를 사용
# 처음 시작할 때 진입차수가 0인 노드를 큐에 삽입
for i in range(1, v + 1):
if indegree[i] == 0:
q.append(i)
# 큐가 빌 때까지 반복
while q:
now = q.popleft()
result.append(now)
# 해당 노드와 연결된 노드들의 진입차수에서 1 빼기
for g in graph[now]:
indegree[g] -= 1
if indegree[g] == 0:
q.append(g)
# 결과 출력
for node in result:
print(node, end=' ')
위상정렬의 과정을 직접 그려보며 공부했는데,
그것이 코드를 이해하는데 많은 도움이 되었던 것 같다.
트리의 후위 순회 순서가 여전히 헷갈리는데, 이것도 여러번 손으로 많이 써봐야할 것 같다.