
이에 대해 알기 전에 먼저 반드시 알아야 하는 내용들에 대해 먼저 짚고 넘어가자.

출처: https://wikidocs.net/198491
파이썬에서는 다른 라이브러리를 사용하는 것이 아닌 기본 제공 객체인 List를 이용해 Stack으로 사용할 수 있다.
Java나 C++의 경우는 Stack을 지원하며 각각 스택 최상단 원소를 출력하기위해 .top(), .peek() 메서드를 사용한다.
삽입(Push)과 삭제(Pop)의 두 연산으로 계산됨
stack=[]
stack.append(0)
stack.append(1)
stack.append(2)
stack.append(3)
stack.pop()
stack.append(4)
print(stack) # [0, 1, 2, 4]
print(stack[::-1]) # [4, 2, 1, 0] # 역순
append()나 pop()은 맨 위에 데이터를 삽입하거나 삭제하기 때문에 시간복잡도는 O(1) O(n)
출처: https://wikidocs.net/198491
O(k)의 시간복잡도가 걸리기 때문에 collection 모듈의 deque 를 사용할 수 있다. from collections import deque
# deque 객체 생성
queue=deque()
queue.append(0)
queue.append(1)
queue.append(2)
queue.append(3)
queue.popleft()
queue.append(4)
print(queue) # deque([1, 2, 3, 4])
queue.reverse()
print(queue) # deque([4, 3, 2, 1])
append()를 통해 입력하고 제거할 때는 popleft()를 사용하는 모습이다. from collections import deque
# deque 객체 생성
queue=deque()
queue.append(0)
queue.append(1)
queue.append(2)
queue.append(3)
queue.popleft() # 0 제거
queue.append(4)
queue.pop() # 마지막에 쌓인 4 제거
queue.appendleft(5) # 가장 앞에 5 삽입
queue.insert(0, 3) # 0번 자리(가장 앞)에 3 삽입
queue.insert(9999, 3) # 9999번 자리(없으니까 가장 뒤)에 3 삽입
queue.remove(3) # 같은 원소 존재 -> 왼쪽부터 삭제
queue[2] = 7 # 인덱스로 원소 수정
print(queue) # deque([5, 1, 7, 3, 3])
queue.reverse()
print(queue) # deque([3, 3, 7, 1, 5])
그리고 Queue를 검색해보면 보통 왼쪽에서 입력되어 오른쪽으로 나오는 그림을 자주 볼 수 있는데 여기서는 반대 방향임을 꼭 기억하고 popleft()를 사용해야 한다.
자기 자신을 다시 호출하는 함수
이후 다루게 되는 DFS 를 실질적으로 구현하고자 할 때 자주 사용되는 것이므로 반드시 알아야 한다.
아래 코드는 간단한 재귀 함수 호출 코드다.
def recursive_func():
print("재귀 함수 호출!")
recursive_func()
recursive_func()

이를 잘 보면 메인에서 함수를 호출하고 해당 함수에서 자기 자신을 호출하기에 무한히 반복되어야 하는 코드인데, 이 파이썬에서는 최대 재귀 깊이 라는 것이 존재하기 때문에 별다른 설정없이 해당 코드를 실행하면 어느 정도 출력하다가 최대 재귀 깊이가 초과되었다는 메세지와 함께 프로그램이 종료된다.
여기서 잘 알아둬야하는 내용이 있다.
실제 컴퓨터 상에서 함수가 재귀적으로 호출이 되면, 컴퓨터 시스템의 스택 프레임(Stack Frame)에 이러한 함수가 반복적으로 쌓여서 가장 마지막에 호출되는 함수가 처리가 된 이후, 그 함수를 불렀던 함수까지 처리가 되는 방식이다.
즉 스택 자료구조 안에 함수에 대한 정보가 차례대로 담겨서 컴퓨터 메모리에 올라간다는 것이다.
컴퓨터 메모리는 당연히 한정된 크기의 자원을 가지기에 함수가 종료되지 않고 계속해서 쌓아올려서 재귀적으로 호출만 하게되면 빠르게 메모리가 가득차서 문제가 발생할 수 있다. 따라서 재귀 깊이 제한을 걸어둘 수 있다.
만약 제한 없이 재귀함수를 호출하고자 한다면, 이러한 재귀 제한을 느슨하게 만드는 방법을 이용하거나 별도로 스택 자료구조를 이용해서 스택 객체를 따로 만들어서 이용하는 방법이 있다.
코딩테스트에서는 이와같이 일반적인 재귀함수를 사용해도 통과할 수 있도록 출제되는 경우가 많다.
그래서 간단히 재귀함수를 이용해서 무한히 어떤 내용을 반복하도록 만들어 보았던 것이다.
이렇게 한다면 while이나 for문을 이용하지 않고도 어떤 내용을 반복적으로 수행할 수 있다.
다만 일반적인 경우 무한루프를 이용할 것이 아니라면 일반적인 코딩테스트에서 이 재귀함수를 문제풀이에 사용할 때는 재귀함수의 종료 조건을 반드시 명시해야 한다.
그렇지 않다면 함수가 무한히 호출될 수 있고 오류 또는 예기치 못한 결과가 발생할 수 있다.
이제 코드로 보면 앞서 말한 스택 프레임에 쌓인다는 뜻도 이해라 수 있을 것이다.


이와 같이 모든 재귀 함수는 반복문을 이용하여 동일한 기능을 구현할 수 있다.



깊이 우선 탐색

간선에 방향이 없는 무방향 그래프
DFS는 인접한 노드가 여러 개 일수 있다.
이럴 때는 어떤 노드부터 방문할지를 위한 기준이 필요하다.
기준은 문제에서 요구하는 기준에 따라서 조금씩 달라질 수 있고, 어떤 노드부터 방문하더라도 상관없는 경우도 있다.
본 예시에서는..
시작 노드: 1
방문 기준: 번호가 낮은 인접 노드부터 방문
동작 과정 (1->2->7->6->8->3->4->5)

‼️여기서
스택의 최상단 노드인 6에 방문하지 않은 인접 노드 X
따라서 스택에서 노드 6 제거
그 다음 다시 최상단 노드가 된 7에, 방문하지 않은 인접 노드 8부터 다시 반복

실제로 이렇게 깊게 들어가는 형태로 동작하기 때문에 스택 자료구조 대신에 재귀함수를 이용해서 구현할 수 있다.
자세한 내용들은 주석으로 걸어두었기에 반드시 읽어보자!
# DFS 메서드 정의
# graph: 그래프 정보 리스트 / visited: 방문 처리 여부 리스트
def dfs(graph, current_v, visited):
# 현재 노드 방문 처리
visited[current_v] = True # 방문 했음 표시 -> True로
print(current_v, end=" ") # 현재 해당 노드 번호 출력
# 현재 노드(스택 최상단 원소)와 연결된 다른 노드들을 하나씩 확인하면서
for i in graph[current_v]:
# 만약 그 인접한 노드가 방문되지 않은 상태(False)라면,
if not visited[i]:
# 그 노드에 대해서도 재귀함수를 이용해 방문 진행.
"""
재귀적으로 방문하지 않은 노드들을 계속해서 방문한다는 점에서
깊이 우선으로 최대한 깊게 그래프를 탐색할 수 있게 됨
"""
dfs(graph, i, visited)
# 2D List를 이용해 그래프에서 각 노드가 연결된 정보들을 표현
"""
그래프 문제가 출제되면
노드 번호가 보통 1번 부터 시작하는 경우가 많기 때문에 인덱스 0에 대한 내용은 비워두는 것이 일반적임
그리고 나서 인덱스 1부터 해당 노드에 인접한 노드가 무엇인지 리스트 형태로 담아 줄 수 있음
따라서 1번 노드와 연결되어 있는 건 2, 3, 8. 2번 노드와 연결되어 있는 건 1, 7.
"""
graph = [
[], # 0번 인덱스 (사용 안 함)
[2, 3, 8], # 1번 노드와 연결된 노드들의 리스트
[1, 7], # 2번 노드와 연결된 노드들의 리스트
[1, 4, 5], # ...
[3, 5],
[3, 4],
[7], # ...
[2, 6, 8], # 7번 노드와 연결된 노드들의 리스트
[1, 7] # 8번 노드와 연결된 노드들의 리스트
]
# 1D List를 만들어 방문 처리 기록 (방문 T / 미방문 F)
# 1번~8번 노드는 8개지만 인덱스 0은 사용하지 않기 위해서 하나 더 큰 크기 9로 설정
visited = [False]*9
# DFS 함수 호출
dfs(graph, 1, visited)
너비 우선 탐색
깊은 부분이 아닌 자신에게 가까운 노드부터 우선적으로 탐색하는 알고리즘이며 큐 자료구조를 이용한다.
특정 조건에서의 최단 경로를 해결하기 위한 목적으로 사용되기도 한다.
큐 자료구조가 사용되기 때문에 각 프로그래밍 언어마다 큐 자료구조를 어떻게 사용할 수 있는가에 대한 내용도 숙지할 필요가 있다.
동작 과정

앞서 DBF에서 사용했던 간선에 방향이 없는 무방향 그래프
DFS와 조건 동일
시작 노드: 1
방문 기준: 번호가 낮은 인접 노드부터 방문
특정 노드에 대해서 인접한 노드가 여러 개일 경우
인접한 노드들 중 번호가 낮은 노드부터 방문할 수 있도록 처리
동작 과정 (1->2->3->8->7->4->5->6)

‼️여기서
큐에서 8번 노드를 꺼내고 방문하지 않은 인접 노드는 없기 때문에 무시하고 다음 노드 번호로 이어간다.

시작 노드부터 가까운 노드를 우선적으로 탐색하는 모습을 볼 수 있다. 그래서 다시 살펴보면 시작 노드인 1번 노드에서부터...
BFS는 이러한 특성 때문에 각 간선의 비용이 동일한 상황에서 최단거리 문제를 해결하기 위한 목적으로도 사용될 수 있다는 것을 기억하자.
코드로 보면 다음과 같다.
from collections import deque
# BFS 메서드 정의
def bfs(graph, start, visited):
# 시작 노드를 큐 객체에 삽입
queue = deque([start])
# 현재 노드를 방문(True) 처리
visited[start] = True
# 큐가 비워질 때까지 반복
while queue:
# 큐에서 하나의 원소 뽑을 때: popleft()
current_v = queue.popleft()
print(current_v, end=" ")
"""
꺼낸 노드와 아직 방문하지 않은 인접한 노드들을 확인하면서
아직 방문하지 않은 노드들이 있다면 모두 큐에 삽입한다.
그 다음 방문처리한다.
"""
for i in graph[current_v]:
if not visited[i]:
queue.append(i)
visited[i] = True
graph = [
[],
[2, 3, 8],
[1, 7],
[1, 4, 5],
[3, 5],
[3, 4],
[7],
[2, 6, 8],
[1, 7]
]
visited = [False]*9
bfs(graph, 1, visited)

5 5
00010
11111
11101
00000
00110
3
# DFS로 특정 노드를 방문하고 연결된 모든 노드들도 방문
def dfs(x, y):
# 주어진 범위를 벗어나는 경우 종료
if x <= -1 or x >= N or y <= -1 or y >= M:
return False
# 현재 노드를 아직 방문하지 않았다면 이제 방문 처리를 진행한다.
if graph[x][y] == 0:
# 해당 노드 방문 처리
graph[x][y] = 1
"""
상하좌우의 연결된 모든 노드들에서
방문 처리를 진행할 수 있도록 하기위해서 모두 재귀적으로 dfs 호출
여기서는 리턴값이 없이 연결된 모든 노드들에 대해서 방문처리만 진행한다.
"""
dfs(x-1, y) # 상
dfs(x+1, y) # 하
dfs(x, y-1) # 좌
dfs(x, y+1) # 우
return True
return False
N, M = map(int, input().split()) # 5 5
graph = []
for i in range(N):
# 00010
# 11111
# 11101
# 00000
# 00110
graph.append(list(map(int, input())))
count = 0
# 모든 노드에 얼음 채우기
for i in range(N):
for j in range(M):
"""
이 dfs가 한번 수행되면 현재 해당 노드와 연결된 모든 노드들에 방문 처리할 수 있도록 하고
그 시작점, 해당 노드가 방문처리가 되었다면(return True를 받았다면),
즉 처음 방문한 것이라면 그때만 count를 증가시킨다.
"""
if dfs(i, j) == True:
count += 1
print(count) # 3
5 6
100110
111111
110001
110011
011111
10

(1, 1)에서 BFS를 수행한다.
인접한 노드 중에서 값이 1인 노드로만 이동이 가능하다고 판단하여 queue에 원소를 넣어서 방문 처리할 때 초기 원소의 값이 1인 경우에 한해서만 BFS를 진행한다.

상하좌우로 탐색을 진행하면서 옆의 노드로 방문하게 되고 이제 해당 노드까지의 거리는 2라고(본 문제에서 시작위치와 마지막 위치를 포함해서 거리를 잰다고 했으니) 기록한다.
이어서 2도 queue에 담기게 되고 다시 2 노드를 꺼낸 다음에 마찬가지로 상하좌우로 연결되어 있는 노드를 확인해서 인접한 노드까지 방문을 수행한다.

그래서 해당 노드를 queue에 넣을 때 방문 처리를 수행한 다음에 이 노드까지는 거리가 3인 것으로 기록하는 방식으로 매번 새로운 노드를 방문할 때 그 이전 노드까지의 최단 거리에 +1 한 값을 기록하도록 한다.
시작 위치에서 BFS를 수행했을 때 다음과 같이 최단 경로의 값들이 거리가 멀어질 수록 +1 씩 증가하는 형태로 변경되고, 결과적으로 마지막 위치에 기록되어 있는 최단 거리 값을 출력하도록 만들면 정답이 된다.

from collections import deque
def bfs(x, y):
queue = deque()
# (x, y) 튜플 데이터 담기
queue.append((x, y))
# 큐가 비어질 때 까지 bfs를 수행하는데,
while queue:
# 반복할때마다 큐에서 하나의 원소를 꺼내서
x, y = queue.popleft()
# 현재 위치에서 상하좌우 확인한다.
for i in range(4):
nx = x+dx[i]
ny = y+dy[i]
# 이제 연결된 위치가 공간 벗어난 경우는 제외하고
if nx < 0 or nx >= N or ny < 0 or ny >= M:
continue
# 또한 몬스터(0)가 존재하는 경우도 제외하도록 한다.
if graph[nx][ny] == 0:
continue
"""
그래서 결과적으로 해당 노드를 처음 방문할 때만 최단 거리를 기록하도록 한다.
바로 직전 노드 위치에서의 최단 거리 값에 +1 만큼을 더한 값을 넣어줄 수 있도록 한다.
다음으로 이동할 위치까지는 +1 만큼 더 먼 곳이기 때문에
queue에 데이터를 넣으면서 거리 값을 +1 증가시킬 수 있다.
"""
if graph[nx][ny] == 1:
graph[nx][ny] = graph[x][y]+1
queue.append((nx, ny))
# 그리고서 bfs를 수행한 뒤에 가장 오른쪽 아래까지의 최단 거리 반환하면 정답 판정
return graph[N-1][M-1]
N, M = map(int, input().split())
graph = []
for i in range(N):
graph.append(list(map(int, input())))
# 상하좌우 방향 벡터
dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]
# BFS를 수행한 결과 출력
print(bfs(0, 0))