인접 행렬
정의
인접 행렬은 그래프의 연결 관계를 이차원 배열로 나타내는 방식.
adj[i][j]: 노드 i에서 노드 j로 가는 간선이 있으면 1, 아니면 0
간선에 방향이 있는 유향 그래프가 아닌, 간선에 방향이 없는 무향 그래프의 경우에는, 노드 i -> 노드 j가 존재하면 노드 j -> 노드 i도 존재하는 것이다. 따라서 대각 성분(adj[i][j]에서 i == j)을 기준으로 대칭인 성질을 가지게 된다.
장점
- 구현이 쉽다
- 노드 i와 노드 j가 연결되어 있는지 확인하고 싶을 때, indexing으로 접근하기 때문에 O(1)의 시간 복잡도를 가진다
단점
- 전체 노드의 개수를 V개, 간선의 개수를 E개라고 하면, 노드 i에 연결된 모든 노드들에 방문하고 싶은 경우 adj[i][1]부터 adj[i][V]를 모두 확인해봐야 하기 때문에 총 O(V)의 시간이 걸린다.
- 노드의 수에 비해 간선의 개수가 훨씬 적은 그래프면 적절하지 않다.
구현
그래프에 대한 정보는
- 노드의 개수(n)
- 간선의 개수(e)
- 각 간선의 양 끝 노드(nodes)
로 주어진다.
인접 행렬로 저장하기 위해서 작성해야 하는 코드
- 노드의 개수와 간선의 개수를 입력받은 후, 간선의 개수만큼 for문을 돌면서 양 끝 노드를 입력받으면 된다.
- 아래 예시는 방향이 없는 무향 그래프를 인접 행렬으로 구현하는 코드이다
n = 4
e = 5
nodes = [[1, 2], [1, 3], [1, 4], [2, 3], [3, 4]]
adj = [[0 for _ in range(n)] for _ in range(n)]
for src, dst in nodes:
adj[src-1][dst-1] = 1
adj[dst-1][src-1] = 1
인접 리스트
정의
각각의 노드에 연결된 노드들을 원소로 갖는 리스트들의 배열을 의미한다
adj[i]: i번째 노드에 연결된 노드들을 원소로 갖는 리스트
장점
- 실제 연결된 노드에 대한 정보만 저장하기 때문에, 모든 원소의 개수의 합이 간선의 개수와 동일하다
- 즉, 간선의 개수에 비례하는 메모리만 차지하여 구현이 가능하다
- 각 노드에 연결된 모든 노드들을 방문해 보아야 하는 경우, 인접 리스트로 연결 관계를 저장하는 것이 시간상 큰 이점을 가진다
단점
- 노드 i와 노드 j의 연결 여부를 알고 싶을 때, adj[i] 리스트를 순회하며 j 원소가 존재하는지 확인해야 한다 - 시간복잡도 O(V)
- 인접 행렬은 adj[i][j]가 1인지 0인지만 확인하면 i와 v 노드의 연결 여부를 O(1)로 확인 가능하다
구현
n = 4
e = 5
nodes = [[1, 2], [1, 3], [1, 4], [2, 3], [3, 4]]
adj = [[] for _ in range(n)]
for src, dst in nodes:
adj[src-1].append(dst-1)
adj[dst-1].append(src-1)
그래프 탐색하기
- 노드 개수에 비해 엣지 개수가 훨씬 적인 그래프라면 인접 행렬 < 인접 리스트 -> 전체 노드가 아닌 연결된 노드만 살펴보면 되기 때문이다
- 두 노드의 연결 관계를 알고싶을 때는 인접 행렬이 효율적이다
# undirected graph
graph = {'A': set(['B', 'C']),
'B': set(['A', 'D', 'E']),
'C': set(['A', 'F']),
'D': set(['B']),
'E': set(['B', 'F']),
'F': set(['C', 'E'])}
너비 우선 탐색(BFS)
- 그래프 내 모든 노드를 방문하고 싶을 때
- 찾는 것을 발견할 때까지 모든 노드를 적어도 한 번은 방문하고 싶을 때
- 시작 노드로부터 차례대로 인접 노드들을 queue에 추가하는 방식을 사용해 구현할 수 있다
def bfs(graph, start):
visited = []
queue = [start]
while queue:
n = queue.pop(0)
if n not in visited:
visited.append(n)
queue += graph[n] - set(visited)
return visited
## result
> bfs(graph, 'A')
['A', 'B', 'C', 'D', 'E', 'F']
두 노드 간 경로 탐색
위의 코드를 조금 수정해 두 노드 간 가능한 모든 경로를 찾아보자
def bfs_paths(graph, start, end):
queue = [(start, [start])]
result = []
while queue:
n, path = queue.pop(0)
if n == end:
result.append(path)
else:
for m in graph[n] - set(path):
queue.append((m, path + [m]))
return result
너비 우선 경로 탐색을 진행하면 가장 먼저 찾은 경로가 최단 경로가 된다
>> bfs_paths(graph, 'A', 'F')
[['A', 'C', 'F'], ['A', 'B', 'E', 'F']]
>> bfs_paths(graph, 'D', 'F')
[['D', 'B', 'E', 'F'], ['D', 'B', 'A', 'C', 'F']]
깊이 우선 탐색(DFS)
- 매우 큰 그래프에서, 탐색을 시작한 노드로부터 너무 멀어지게 되면 즉시 그만두고 싶을 때 사용하면 효과적이다
- 트리 순회 기법은 전부 DFS이다
- 과거 위치의 인접 노드보다 현재 위치의 인접 노드를 먼저 방문한다는 특징을 가지므로, stack을 사용해 구현
def dfs(graph, start):
visited = []
stack = [start]
while stack:
n = stack.pop()
if n not in visited:
visited.append(n)
stack += graph[n] - set(visited)
return visited
>> dfs(graph, 'A')
['A', 'B', 'E', 'F', 'C', 'D']
두 노드 간 경로 탐색
위의 코드를 조금 수정하여 두 노드 간 가능한 모든 경로를 찾아보자
def dfs_paths(graph, start, end):
stack = [(start, [start])]
result = []
while stack:
n, path = stack.pop()
if n == end:
result.append(path)
else:
for m in graph[n] - set(path):
stack.append((m, path + [m]))
return result
>> dfs_paths(graph, 'A', 'F')
[['A', 'C', 'F'], ['A', 'B', 'E', 'F']]
>> dfs_paths(graph, 'D', 'F')
[['D', 'B', 'A', 'C', 'F'], ['D', 'B', 'E', 'F']]
깊이 우선 탐색은 너비 우선 탐색과는 달리 최단 경로를 가장 먼저 찾지 못할 수 있다
출처: [그래프] 인접 행렬과 인접 리스트, [Python] 인접 행렬과 인접 리스트, 파이썬을 사용한 그래프의 너비 우선 탐색과 깊이 우선 탐색 구현
'Coding > 알고리즘' 카테고리의 다른 글
Kruskal, Prim 알고리즘 (0) | 2020.10.10 |
---|---|
Greedy 알고리즘이란 (0) | 2020.10.04 |
Binary Search란 (0) | 2020.10.03 |
Dynamic Programming이란 (0) | 2020.10.01 |