
그래프(Graph)는 정점(vertex, node)과 간선(edge)으로 구성된 비선형 자료구조다. 간선은 두 정점을 연결하며, 네트워크와 같은 관계 구조를 표현할 수 있다.
그래프는 크게 두 가지로 나뉜다.
또한 간선에 가중치가 있을 수도, 없을 수도 있다.
그래프는 네트워크 모델링, 웹 페이지 링크 구조, 최단 경로 탐색 등 다양한 분야에서 활용된다.
방향 그래프는 간선이 특정 방향을 가진다. 예를 들어 도시를 정점으로 두고, 일방통행 도로를 간선으로 표현하면 방향 그래프가 된다.
# 방향 그래프 구현 예시 (인접 리스트)
graph = {
"A": ["B", "C"],
"B": ["D"],
"C": ["D"],
"D": []
}
print(graph)
무방향 그래프는 간선이 특정 방향을 가지지 않는다. 즉, 간선은 단순히 두 정점을 연결하며, 양쪽으로 자유롭게 이동할 수 있다.
# 무방향 그래프 구현 예시
graph = {
"A": ["B", "C"],
"B": ["A", "D"],
"C": ["A", "D"],
"D": ["B", "C"]
}
print(graph)
BFS는 가까운 정점부터 차례대로 탐색하는 알고리즘이다. 큐(queue)를 활용한다.
from collections import deque
def bfs(graph, start):
visited = set()
queue = deque([start])
while queue:
node = queue.popleft()
if node not in visited:
print(node, end=" ")
visited.add(node)
queue.extend(graph[node])
graph = {
"A": ["B", "C"],
"B": ["D", "E"],
"C": ["F"],
"D": [],
"E": ["F"],
"F": []
}
bfs(graph, "A") # 출력: A B C D E F
DFS는 한 경로를 끝까지 탐색한 후, 막다른 길에서 되돌아오는 방식으로 진행된다. 스택이나 재귀를 이용한다.
def dfs(graph, start, visited=None):
if visited is None:
visited = set()
visited.add(start)
print(start, end=" ")
for neighbor in graph[start]:
if neighbor not in visited:
dfs(graph, neighbor, visited)
dfs(graph, "A") # 출력: A B D E F C
다익스트라 알고리즘은 음수 가중치가 없는 그래프에서 최단 경로를 찾는 알고리즘이다.
import heapq
def dijkstra(graph, start):
distances = {node: float("inf") for node in graph}
distances[start] = 0
queue = [(0, start)]
while queue:
current_dist, current_node = heapq.heappop(queue)
if current_dist > distances[current_node]:
continue
for neighbor, weight in graph[current_node]:
distance = current_dist + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
heapq.heappush(queue, (distance, neighbor))
return distances
graph = {
"A": [("B", 1), ("C", 4)],
"B": [("C", 2), ("D", 5)],
"C": [("D", 1)],
"D": []
}
print(dijkstra(graph, "A")) # {'A': 0, 'B': 1, 'C': 3, 'D': 4}
벨만-포드는 음수 가중치가 있는 그래프도 처리 가능하다. 단, 음수 사이클이 존재하면 무한 루프에 빠질 수 있다.
def bellman_ford(edges, num_vertices, start):
dist = [float("inf")] * num_vertices
dist[start] = 0
for _ in range(num_vertices - 1):
for u, v, w in edges:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
# 음수 사이클 검출
for u, v, w in edges:
if dist[u] + w < dist[v]:
raise ValueError("Negative cycle detected")
return dist
edges = [(0, 1, 4), (0, 2, 5), (1, 2, -3), (2, 3, 4)]
print(bellman_ford(edges, 4, 0)) # [0, 4, 1, 5]
프림 알고리즘은 최소 신장 트리(MST)를 찾는 탐욕적 방법이다.
import heapq
def prim(graph, start):
visited = set()
mst = []
edges = [(0, start, start)]
while edges:
cost, u, v = heapq.heappop(edges)
if v not in visited:
visited.add(v)
mst.append((u, v, cost))
for neighbor, weight in graph[v]:
if neighbor not in visited:
heapq.heappush(edges, (weight, v, neighbor))
return mst
graph = {
"A": [("B", 1), ("C", 3)],
"B": [("A", 1), ("C", 1), ("D", 4)],
"C": [("A", 3), ("B", 1), ("D", 2)],
"D": [("B", 4), ("C", 2)]
}
print(prim(graph, "A"))
# [('A', 'A', 0), ('A', 'B', 1), ('B', 'C', 1), ('C', 'D', 2)]
크루스칼 알고리즘은 간선을 정렬한 후, 사이클이 생기지 않게 최소 비용 간선을 추가해 MST를 만든다.
class UnionFind:
def __init__(self, n):
self.parent = list(range(n))
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):
rx, ry = self.find(x), self.find(y)
if rx != ry:
self.parent[ry] = rx
def kruskal(num_vertices, edges):
uf = UnionFind(num_vertices)
mst = []
edges.sort(key=lambda x: x[2])
for u, v, w in edges:
if uf.find(u) != uf.find(v):
uf.union(u, v)
mst.append((u, v, w))
return mst
edges = [(0, 1, 1), (1, 2, 2), (0, 2, 3)]
print(kruskal(3, edges)) # [(0, 1, 1), (1, 2, 2)]
그래프는 현실 세계의 문제를 모델링할 때 굉장히 많이 활용되므로, 각 알고리즘의 특징과 구현 방식을 익혀두는 것이 중요하다.