들어가기 전 사전 지식

스택 (Stack)

  • FILO(선입후출) / LIFO(후입선출) 구조
  • 파이썬에서의 사용

리스트 사용

  • push : append()
  • pop : pop()

collections 모듈의 deque 자료구조 사용

<aside> 💡 deque (양방향 큐) - 스택과 큐의 장점을 모두 채택

  • 데이터를 넣고 빼는 속도가 리스트 자료형에 비해 빠름
  • push : append()
  • pop : pop()

큐 (Queue)

  • FIFO(선입선출) 구조
  • 파이썬에서의 사용
    • 리스트 사용
      • enqueue : append()
      • dequeue : pop(0)
    • collections 모듈의 deque 자료구조 사용
      • enqueue : append()
      • dequeue : popleft()

재귀 함수 (Recursive Function)

  • 자기 자신을 다시 호출하는 함수
  • 스택을 이용해 쌓이고 다음 함수 호출
  • 스택 구조를 이용해야 하는 상당 수의 알고리즘은 재귀로 풀릴 수 있음
  • 반복문을 통해 해결하는 것과 비교해 코드가 간결해지는 장점을 가짐
  • 수학에서의 점화식과 같음

그래프 (Graph)

노드(node) or **정점(vertex)**과 **간선(edge)**으로 이루어짐

  • G = (V, E)

두 노드가 인접(adjacent)한 형태

  • 두 노드가 간선을 통해 이어져 있는 형태

그래프의 표현 방법 2가지 ( 방향 / 무방향 그래프 모두 적용 가능)

예시

Untitled

인접 행렬 (Adjacency Matrix)

  • 2차원 배열로 그래프의 연결 관계를 표현
  • 높은 밀도 그래프( |E| ~= |V|^2 ) 나 두 정점을 연결하는 간선이 있는지를 빠르게 확인할 필요가 있을 때 효율적

인접 리스트 (Adjacency List)

  • 연결 리스트로 그래프의 연결 관계를 표현
    • 파이썬에서는 리스트의 append()를 이용하면 됨
  • 작은 밀도 그래프( |E| << |V|^2 )에 대에 효율적

인접행렬과 리스트 방식의 차이점 정리

  • 공간적 측면 : 리스트 > 행렬
  • 시간적 측면 : 행렬 > 리스트

2차원 배열 형태와 그래프

  • 이차원 배열 형태의 탐색 문제는 그래프로 생각하여 풀 때 쉽게 풀 수 있을 때가 많이 있다

DFS (Depth-First Search; 깊이 우선 탐색)

  • 그래프의 깊은 곳으로의 탐색을 우선적으로 실행하는 탐색
  • 스택 자료구조를 이용
  • 시간 복잡도
    • 세타(V + E)
      • 재귀 호출 루프 수행에 걸리는 시간 세타(V)
      • 재귀 내 인접 정점 검사 시간 세타(E)
    • 재귀로 구현한 DFS는 컴퓨터 시스템 특성상 실제 수행 시간은 느려 질 수 있음

동작 과정

  1. 탐색 시작 노드를 스택에 push하고 방문 처리.
  2. 스택의 최상단 노드에 방문하지 않은 인접 노드가 있다면 그 인접 노드를 스택에 push하고 방문 처리. 방문하지 않은 인접 노드가 없다면, 스택의 최상단 노드를 꺼냄.
  3. 2번 과정을 더 이상 수행할 수 없을 때까지 반복 수행

<aside> 💡 방문 처리란?

  • 스택에 중복 삽입되지 않도록 스택에 삽입된 노드를 체크하는 처리

</aside>

  • 일반적으로 인접한 노드 중 방문하지 않은 노드가 여러개라면 번호가 낮은 순서부터 처리함
  • 노드의 탐색 순서 : 스택에 들어간 순서

의사 코드

time = 0                 // 시간 기록을 위한 전역 변수

DFS(G)                   // G: 무방향 그래프
	for u in [각 정점들]     // 각 정점들 초기화
		u.color = WHITE      // 탐색 상태, WHITE: 탐색 전
		u.p = NIL            // 직전 원소 속성

	for u in [각 정점들]     // *이 정점의 순서에 따라 방문 순서가 달라질 수 있다! 
		if u.color = WHITE   // 아직 방문하지 않은 정점에 대해
			DFS-VISIT(G, u)    // 탐색 실행

DFS-VISIT(G, u)
	time += 1
	u.d = time             // u에 방문 했을 때 시간 기록
	u.color = GRAY         // GRAY: 방문 처리
	for v in [u의 인접 정점들]
		if v.color == WHITE  // 방문하지 않은 정점이면
			v.p = u            // 현재 정점을 직전 원소 속성으로 지정
			DFS-VISIT(G, v)    // 탐색 실행
	u.color = BLACK        // BLACK: 인접 정점까지 탐색 완전 종료
	time += 1
	u.f = time             // u의 인접 정점 조사가 끝났을 때 시간 기록
  • 직전원소 부분 그래프는 여러 개의 깊이 우선 트리로 구성될 수 있음
    • 즉, 깊이 우선 포레스트를 형성
  • 특성
    • 깊이 우선 트리의 구조가 재귀 호출 구조를 따라 직전원소 부분 그래프가 트리들의 포레스트를 형성함
      • 재귀 호출이 해당 정점의 인접 정점들을 검색하는 동안 호출되면 u = v.p고 이 역도 성립함
    • 발견 시간과 종료 시간이 괄호 구조를 가짐 - (v)
      • 정점 발견 : ‘(’
      • 정점 탐색 종료 : ‘)’
    • 그래프의 간선을 분류하는데 사용할 수 있음

어떤 방향 그래프가 비순환이면, 깊이 우선 검색이 ‘역행‘ 간선을 만들지 않으며 그 역도 성립

네 가지 간선 정의

  • 트리 간선(tree edge)
    • 깊이 우선 포레스트에 있는 일반적인 간선
  • 역행 간선(back edge)
    • 깊이 우선 트리에서 정점을 조상에 연결하는 간선
    • 자기 순환(self-loop) 간선은 역행 간선으로 간주함
  • 순행 간선(forward edge)
    • 깊이 우선 트리에서 정점을 자손에 연결하는 간선
  • 교차 간선(cross edge)
    • 나머지 모든 간선
    • 하나의 정점이 또 다른 정점의 조상이 아니어야 함
    • 서로 다른 깊이 우선 트리에 있는 두 정점 사이에 있을 수도 있음

분류

  • WHITE는 트리 간선을 나타냄
  • GRAY는 역행 간선을 나타냄
  • BLACK은 순행 또는 교차 간선을 나타냄

구현

# 깊이 우선 탐색 함수 정의
def dfs(graph, node, vidited):
	# 현재 노드를 방문 처리
	visited[node] = True
	# 탐색한 노드 출력
	print(node, end=' ')
	# 현재 노드와 연결된 다른 노드를 재귀적으로 방문
	for i in graph[node]:
		if not visited[i]:
			dfs(graph, i, visited)

# 각 노드가 연결된 정보를 리스트 자료형으로 표현(2차원 리스트)
graph = [
	[],
	[2, 3, 8],
	[1, 7],
	[1, 4, 5],
	[3, 5],
	[3, 4],
	[7],
	[2, 6, 8],
	[1, 7]
]

# 각 노드가 방문한 정보
visited = [False] * 9

# DFS 실행
dfs(graph, 1, visited)
	

BFS (Breadth-First Search; 너비 우선 탐색)

  • 가장 단순한 그래프 탐색 알고리즘. 그래프 탐색 알고리즘의 원형.
    • BFS 기반 알고리즘들
      • Prim의 최소 신장 트리 알고리즘
      • Dijkstra의 단일 출발 최단 경로 알고리즘
  • 그래프의 탐색 시작 노드에서 가까운 노드를 우선적으로 탐색
  • 큐 자료구조 이용
  • O(V + E)
    • 초기화 하는데 필요한 시간 O(V)
    • 인접 리스트 스캔에 필요한 시간 O(E)

동작 과정

  1. 탐색 시작 노드를 큐에 삽입하고 방문 처리.
  2. 큐에서 노드를 꺼내 해당 노드의 인접 노드 중 방문하지 않은 노드를 모두 큐에 삽입하고 방문 처리
  3. 2번 과정을 더 이상 수행할 수 없을 때까지 반복
  • 노드의 탐색 순서 : 큐에 들어간 순서 (혹은 큐에서 나오는 순서)

의사 코드

BFS(G, s) 
	// 정점들 속성 초기화
	for u in [s를 제외한 각각의 정점들]
		u.color = WHITE               // 탐색 상태. WHITE: 탐색 전
		u.d = INF                     // 거리
		u.p = NIL                     // 직전 원소 속성
	
	// 시작 정점 속성 설정
	s.color = GRAY                  // GRAY: 방문 처리
	s.d = 0 .
	s.p = NIL 
	
	Q = 0                           // 큐 생성
	ENQUEUE(Q.s)                   // 큐에 시작 정점 삽입
	
	// 큐가 빌 때까지 반복 (탐색)
	while Q != 0
		u = DEQUEUE(Q)               // 큐에서 정점 하나를 가져옴
		for v in [u의 인접 정점들]       // 해당 정점의 인접 정점들에 대해
			if v.color == WHITE         // 방문하지 않았다면
				v.color = GRAY            // 방문 처리
				v.d = u.d + 1             // 거리 해당 정점 + 1
				v.p = u                   // 부모 속성으로 해당 정점 속성 추가
				ENQUEUE(v)                // 큐에 삽입
		u.color = BLACK               // BLACK: 인접 정점까지 탐색 완전 종료
  • 직전원소 부분 그래프는 너비 우선 트리가 된다
    • 직전원소 부분 그래프는 하나의 트리를 형성한다.

구현

  • deque 라이브러리를 사용하는 것이 좋음
from collections import deque

#BFS 메서드 정의
def bfs(graph, start_node, visited):
	# 큐 구현을 위해 deque 라이브러리 사용
	q = deque([start_node])
	# 현재 노드 방문 처리
	visited[start_node] = True
	# 큐에 아무것도 없을 때까지 반복
	while q:
		# 큐에서 원소 하나 뽑기
		node = q.popleft()
		# 탐색한 노드 출력
		print(node, end=' ')
		# 해당 노드와 연결된, 아직 방문 안한 원고들을 큐에 삽입
		for i in graph[node]:
			if not visited[i]:
				q.append(i)
				visited[i] = True

# 각 노드가 연결된 정보를 리스트 자료형으로 표현(2차원 리스트)
graph = [
	[],
	[2, 3, 8],
	[1, 7],
	[1, 4, 5],
	[3, 5],
	[3, 4],
	[7],
	[2, 6, 8],
	[1, 7]
]

# 각 노드가 방문한 정보
visited = [False] * 9

# BFS 실행
bfs(graph, 1, visited)

문제 풀이

음료수 얼려 먹기

N * M 크기의 얼음 틀이 있음.

  • 구멍 뚤린 부분 : 0
  • 칸막이가 있는 부분 : 1

구멍이 뚫혀 있는 부분끼리 상, 하, 좌, 우로 붙어있는 경우 서로 연결된 것으로 간주

이 때, 얼음 틀의 모양이 주어졌을 때 생성되는 총 아이스크림의 개 수는?

예시

8E11A2DE-491F-497E-956A-1819C94CFD01.jpeg

  • 총 3개 아이스크림 생성

입력 조건

  • 첫 번째 줄에 얼음 틀의 세로 길이 N과 가로 길이 M이 주어짐 (1 ≤ N, M ≤ 1,000)
  • 두 번째 줄부터 N+1번 줄까지 얼음 틀의 형태가 주어짐

출력 조건

  • 한 번에 만들 수 있는 아이스크림의 개수를 출력

풀이

# 얼음틀 크기 입력 받음
n, m = map(int, input().split())

# 얼음틀 입력 받음
iceFrame=[]
for i in range(n):
	iceFrame.append(list(map(int, input())))

# dfs
def dfs(graph, x, y):
	# 탐색 범위를 벗어났다면 종료
	if x < 0 or x >= n or y < 0 or y >= m:
		return False
	if graph[x][y] != 1:    # 해당 노드에 방문하지 않았으면
		graph[x][y] = 1       # 해당 공간을 막음으로 방문 처리

		dfs(graph, x+1, y)    # 상, 하, 좌, 우 재귀로 체크 연결 체크
		dfs(graph, x, y+1)
		dfs(graph, x-1, y)
		dfs(graph, x, y-1)
		return True           # 아이스크림 공간 있음
	return False            # 아이스크림 공간 없음 (막힘)

# 가능한 아이스크림 공간 갯수 세기
cnt = 0
for i in range(n):
	for j in range(m):
		if dfs(iceFrame, i, j):
			cnt += 1
print(cnt)                # 갯수 출력

미로 탈출

N * M 크기의 직사각형 미로에서 괴물을 피해 탈출해야 한다.

  • 현재 위치 : (1, 1)
  • 출구 : (N, M)
  • 한 번에 한 칸 이동
  • 괴물 칸 : 0
  • 괴물 없는 칸 : 1

미로를 탈출할 수 있는 최단 거리를 구해라. (시작 칸과 마지막 칸 갯수에 포함)

입력 조건

  • 첫째 줄에 두 정수 N, M (4 ≤ N, M ≤ 200)이 주어짐
  • 다음 N개 줄에 각각 M개의 정수로 미로의 정보가 주어짐. (공백 없이)
  • 시작 칸과 마지막 칸은 항상 1임

출력 조건

  • 최소 이동 칸의 갯수 출력

풀이

from collections import deque

# 미로 크기 입력
n, m = map(int, input().split())
# 미로 정보 입력
maze = []
for i in range(n):
	maze.append(list(map(int, input())))

# 방향 정보 딕셔너리
dir = {'u': (-1, 0), 'd':(1, 0), 'l':(0, -1), 'r':(0, +1)}

# bfs
def bfs(graph, x, y):
	q = deque()
	q.append((x, y))        # 시작 위치 큐에 삽입
	graph[x][y] = 1         # 괴물 위치로 바꿔 방문 처리
	while q:                # 큐가 빌 때까지
		x, y = q.popleft()    # 큐에서 현재 위치 빼기
		for d in dir:         # 상, 하, 좌, 우 확인
			dx, dy = dir[d]     # 다음 위치 지정
			rx, ry = x + dx, y + dy
			# 미로 안에 없다면 다음 조건 진행
			if (rx < 0 or rx >=n or ry < 0 or ry >= m):
				continue
			# 값이 1이라면 (처음 위치는 재방문 하지만 상관없으므로 진행)		
			if graph[rx][ry] == 1:
				q.append((rx, ry))                # 큐에 다음 위치로 넣기
				graph[rx][ry] = graph[x][y] + 1   # 다음 위치의 거리로 현재 위치 거리에 + 1
	return graph[n-1][m-1]  # 마지막 위치에 거리를 반환

# 실행
print(bfs(maze, 0, 0))

마무리 정리

 DFSBFS
동작 원리StackQueue
구현 방법재귀 함수deque 라이브러리
탐색 위치스택에 넣을 때큐에서 빼고 나서
  • 수행 속도 측면에서 BFS > DFS이다
  • DFS 스택 / BFS 큐를 외워보자
    • 디스하면 뻐큐다~
  • 기본적인 BFS / DFS 함수는 안보고도 구현할 정도로 암기하자
    • 왜냐? 코테에 기본적으로 활용해야 하는 경우가 많거든~

DFS / BFS의 사용

  • BFS는 주어진 출발점으로부터 최단 경로들을 발견하게 해줌
  • DFS는 다른 알고리즘의 서브 루틴으로 많이 활용됨

금주의 문제

1260번: DFS와 BFS

2178번: 미로 탐색