정렬
페이지
미리보기
깊이 우선 탐색그래프 탐색너비 우선 탐색
자세히 보기
DFS/BFS (Graph Traversal)

들어가기 전 사전 지식

스택 (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번: 미로 탐색

구현
자세히 보기
구현 (Implementation)

개념

머릿속에 있는 알고리즘을 소스코드로 바꾸는 과정

  • 모든 문제에 포함되는 개념
  • 코딩테스트에서는 구현을 중심으로 하는 문제가 출제되어 따로 분류함
    • 풀이를 떠올리는 것은 쉽지만 소스코드로 옮기기 어려운 문제 유형을 의미함

코딩테스트에서의 구현

풀이를 떠올려도 소스코드로 옮기기 어려운 문제 유형

피지컬을 요구

<aside> 💡 코딩테스트의 피지컬

프로그래밍 언어 문법에 능숙하고 코드 작성 속도가 빠른 사람을 의미

</aside>

문자열 처리나 큰 수 처리등 구현에 어려움이 없는 파이썬이 유리

언어구현 난이도실행 시간
파이썬쉬운 편긴 편
PyPy쉬운 편다소 짧은 편
C/C++어려운 편짧은 편
자바어려운 편긴 편

대표적인 까다로운 구현 문제들

  • 알고리즘은 간단하지만 코드가 지나치게 길어지는 문제
  • 특정 소수점 자리까지 출력하는 문제
  • 문자열이 입력으로 주어졌을 때 한 문자 단위로 끊어서 리스트에 넣는(파싱) 문제
  • 완전 탐색
    • 모든 경우의 수를 주저 없이 다 계산하는 해결 방법
  • 시뮬레이션
    • 문제에서 제시한 알고리즘을 한 단계씩 차례로 직접 수행해야하는 문제

구현에서 고려해야 할 메모리 제약 사항

전통적 프로그래밍 언어의 정수형 - int(integer)

  • 4Byte
  • -2,147,483,648 ~ 2,147,483,647

큰 수를 처리하는 방법

C/C++

  • long long 사용
    • 9,223,372,036,854,775,807 까지 가능
    • 이보다 큰 수는 BigInteger클래스를 구현해야 함

Java

  • BigInteger 클래스 표준 라이브러리 사용

Python

  • 기본 자료형으로 지원

파이썬의 리스트 크기 - 1당 약 4바이트

리스트 길이메모리 사용량
1,000약 4KB
1,000,000약 4MB
10,000,000약 40MB
  • 크기가 1000만 이상인 리스트가 있다면 메모리 초과 가능성이 있음

채점 환경

  • 파이썬
    • C/C++ 에 비해 2배의 수행 시간 제한을 적용하기도 함
    • 3.7기준 1초에 2,000만번의 연산 수행을 가정하면 됨
      • 제한 시간 1초에 데이터의 갯수가 100만 개라면 시간 복잡도 O(NlogN) 이내 알고리즘을 이용해야함

구현 문제 접근 방법

구현 문제 특징

  • 사소한 입력 조건 등을 전부 명시
  • 문제의 길이가 길어짐

→ 고차원적인 사고력을 요구하는 문제는 나오지 않는 편이라 문제의 길이를 보고 지레 겁먹지 말고 알고 있는 문법을 이용해 조건을 만족시켜 해결한다

문제

상하좌우

외쪽 상단 (1, 1)에서 오른쪽 하단 (N, N) 좌표를 갖는 N * N 크기의 공간이 있을 때 다음 네가지 입력들에 대해 (1, 1)좌표에서 출발해 나중에 위치할 좌표를 출력 (단, 공간을 벗어나는 입력은 무시함)

  1. L : 왼쪽으로 이동
  2. R : 오른쪽으로 이동
  3. U : 위로 이동
  4. D : 아래로 이동

문제 해설

요구 사항 대로 구현한다면 연산 횟수는 이동 횟수에 비례 이동 횟수가 N번이라면 시간 복잡도는 O(N)으로 넉넉함

일련의 명령에 따라 개체를 차례로 이동시킨다는 점에서 시뮬레이션 유형으로 분류할 수 있음

입력 받은 이동 계획을 하나씩 확인하여 현재 위치를 그에 맞게 하나씩 바꿔준다. 단 (1, 1)에서 (n, n)까지의 범위 내에서만 수행한다.

코드

import sys
input = sys.stdin.readline

n = int(input())            # 공간 크기 입력
planer = input().split()    # 이동 계획 입력

# 이동 계획에 따라 (이동 축, 이동 방향)을 같는 딕셔너리
mapPlan = {'L':(1, -1), 'R':(1, 1), 'U':(0, -1), 'D':(0, 1)}
# 현재 좌표
coor = [1, 1]
# 이동 계획을 하나씩 수행
for i in planer:
    t = mapPlan[i]              # 이동 축, 이동 방향을 가져옴
    temp = coor[t[0]] + t[1]    # 이동 축에서 이동 방향으로 이동한 결과
    if temp > 0 and temp <= n:  # 범위를 벗어나지 않았다면
        coor[t[0]] = temp       # 현재 좌표에 적용
print(coor[0], coor[1])         # 최종 출력

시각

왕실의 나이트

게임 개발

백준

14503번: 로봇 청소기

16505번: 별