알고리즘

BFS, DFS 다시 공부

gogi masidda 2024. 2. 19. 21:03

참고


그래프 탐색 알고리즘 (그래프 : 여러 개체들이 연결되어 있는 자료구조)

 

대표적인 문제 유형

  • 경로 탐색 유형(최단거리, 시간)
  • 네트워크 유형(연결)
    • 연결되어 있는 그룹의 개수
    • 두 개체가 같은 네트워크 안에 있는지 
  • 조합 유형(모든 조합 만들기)

스택

먼저 들어온 데이터가 나중에 나가는 형식(선입후출)의 자료구조. 입구와 출구가 동일한 형태로 스택을 시각화할 수 있다.

파이썬에서 스택은 단순히 리스트와 append(), pop()으로 구현 가능하다.

stack = []
stack.append(5)
stack.pop()
...

print(stack[::-1]) #최상단 원소부터 출력
print(stack) #최하단 원소부터 출력

 


먼저 들어온 데이터가 먼저 나가는 형식(선입선출)의 자료구조. 입구와 출구가 모두 뚫려있는 터널과 같은 형태로 시각화할 수 있다.

from collections import deque

queue = deque()

queue.append(5)
queue.append(3)
queue.popleft()
queue.append(2)
queue.popleft()

print(queue) #먼저 들어온 순서대로 출력
queue.reverse() #역순으로 바꾸기
print(queue) #나중에 들어온 순서대로 출력

리스트를 사용하고, pop() 메서드의 특정 위치를 지정해서 원소를 빼게되면 위치 조정도 필요하므로 O(n) 만큼의 시간복잡도가 들게된다. 그래서 리스트로도 구현할 수 있지만 비용적으로 deque 라이브러리를 사용하는 것이 낫다.


재귀함수

자기 자신을 다시 호출하는 함수.

재귀 함수를 문제 풀이에서 사용할 때는 재귀함수의 종료 조건을 반드시 명시해야 함.

def recursive_function(i):
	if i == 100:
    	return
    print(i, '번째 재귀함수에서', i + 1, '번째 재귀함수를 호출합니다.')
    recursive_function(i+1)
    print(i, '번째 재귀함수를 종료합니다.')
    
recursive_function(1)
#팩토리얼 구현

#반복
def factorial_iterative(n):
	result = 1
    for i in range(1, n+1):
    	result *= i
    return result
    
#재귀
def factorial_recursive(n):
	if n <= 1:
    	return 1
    return n * factorial_recursive(n-1)

 

유클리드 호제법: 두 개의 자연수에 대한 최대공약수를 구하는 알고리즘

  1. 두 자연수 A, B에 대하여 (A>B) A를 B로 나눈 나머지를 R이라 한다.
  2. A와 B의 최대공약수는 B와 R의 최대공약수와 같다.

GCD(192, 162) = GCD(162, 30) = GCD(30, 12) = GCD(12, 6) = 6

def gcd(a, b):
	if a % b == 0:
    	return b
    else:
    	return gcd(b, a % b)
    
print(gcd(192, 162))

 

  • 모든 재귀 함수는 반복문을 이용하여 동일한 기능을 구현할 수 있다.
  • 재귀 함수가 반복문보다 유리한 경우도 있고 불리한 경우도 있다.
  • 컴퓨터가 함수를 연속적으로 호출하면 컴퓨터 메모리 내부의 스택 프레임에 쌓인다.
    • 그래서 스택을 사용해야 할 때 구현상 스택 라이브러리 대신에 재귀 함수를 이용하는 경우가 많다.

DFS (깊이 우선 탐색)

  • 스택 자료 구조나 재귀 함수를 사용
  • 깊은 부분을 우선적으로 탐색
  1. 탐색 시작 노드를 스택에 삽입하고 방문 처리
  2. 스택의 최상단 노드에 방문하지 않은 인접한 노드가 하나라도 있으면 그 노드를 스택에 넣고 방문 처리. 방문하지 않은 인접 노드가 없으면 스택에서 최상단 노드를 꺼냄.
  3. 더 이상 2번 과정을 수행할 수 없을 때까지 반복한다.
def dfs(graph, v, visited):
	#현재 노드 방문 처리
    visited[v] = True
    print(v, end= ' ')
    #현재 노드와 인접한 다른 노드를 재귀적으로 방문
    for i in graph[v]:
    	if not visited[i]:
        	dfs(graph, i, visited)
            
graph = [
	[],        # 인덱스 0은 사용하지 않음
    [2, 3, 8], # 1번 노드와 연결
    [1, 7],	   # 2번 노드와 연결
    [1, 4, 5], # 3번 노드와 연결
    [3, 5],    # 4번 노드와 연결
    [3, 4],    # 5번 노드와 연결
    [7],       # 6번 노드와 연결
    [2, 6, 8], # 7번 노드와 연결
    [1, 7]     # 8번 노드와 연결
]

visited = [False] * 9 #인덱스 0은 사용하지 않음

dfs(graph, 1, visited)

# 1 2 7 6 8 3 4 5

BFS (너비 우선 탐색)

  • 순서가 보장되어야 하기 때문에 Queue나 LinkedList를 사용하는 것이 일반적
  • 그래프에서 가까운 노드부터 우선적으로 탐색하는 알고리즘
  1. 탐색 시작 노드를 큐에 삽입하고 방문 처리를 합니다.
  2. 큐에서 노드를 꺼낸 뒤에 해당 노드의 인접 노드 중에서 방문하지 않은 노드를 모두 큐에 삽입하고 방문 처리
  3. 더 이상 2번의 과정을 수행할 수 없을 때까지 반복
from collections import deque

def bfs(graph, start, visited):
	#큐 구현
    queue = deque([start])
    #현재 노드를 방문 처리
    visited[start] = True
    #큐가 빌 때까지 반복
    while queue:
    	#큐에서 하나 원소 뽑아서 출력
        v = queue.popleft()
        print(v, end = ' ')
        #아직 방문하지 않은 인접한 원소들을 큐에 삽입
        for i in graph[v]:
        	if not visited[v]:
            	queue.append(i)
                visited[v] = True
                
 graph = [
	[],        # 인덱스 0은 사용하지 않음
    [2, 3, 8], # 1번 노드와 연결
    [1, 7],	   # 2번 노드와 연결
    [1, 4, 5], # 3번 노드와 연결
    [3, 5],    # 4번 노드와 연결
    [3, 4],    # 5번 노드와 연결
    [7],       # 6번 노드와 연결
    [2, 6, 8], # 7번 노드와 연결
    [1, 7]     # 8번 노드와 연결
]

visited = [False] * 9 #인덱스 0은 사용하지 않음

bfs(graph, 1, visited)

# 1 2 3 8 7 4 5 6

 


예제1. 음료수 얼려 먹기

N x M 크기의 얼음틀. 구멍이 뚫린 부분은 0, 칸막이가 존재하는 부분은 1

구멍이 뚫려있는 부분끼리 상, 하, 좌, 우로 붙어 있는 경우 서로 연결되있는 것.

얼음 틀의 모양이 주어졌을 때 생성되는 총 아이스크림의 개수를 구하는 프로그램

 

입력 예시
4 5
00110
00011
11111
00000

출력 예시
3

연결 요소 찾기 문제 유형

 

문제 풀이 아이디어: 상하좌우가 연결된 형태의 그래프로 생각. 특정 지점에서 bfs나 dfs 수행하며 이동가능한 모든 경로에 대해서 모두 방문 처리를 한다. 

  1. 특정한 지점의 주변 상하좌우를 살펴본 뒤에 주변 지점 중에서 값이 0이면서 아직 방문하지 않은 지점이 있으면 해당 지점 방문
  2. 방문한 지점에서 다시 상하좌우를 살펴보면서 방문을 진행하는 과정을 반복하면 연결된 모든 지점을 방문할 수 있다.
  3. 모든 노드에 대해 1~2번의 과정을 반복하며 방문하지 않은 지점의 수를 카운트.
#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(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())

graph = []
for i in range(n):
	graph.append(list(map(int,input()))
    
result = 0
for i in range(n):
	for j in range(m):
    	#현재 위치에서 DFS 수행
        if dfs(i, j) == True:
        	result += 1
            
print(result)

dfs 함수에서 상하좌우 위치에 대해서 재귀적으로 호출하는 것은 return을 하지 않기 때문에 그냥 연결된 모든 노드에 대해서 방문처리를 위한 것이다. 특정 지점에서 처음 재귀가 호출될 때만 'return True'가 되는 것으로 한 덩이의 구멍이 있는 것을 체크한다.


예제 2.  미로 탈출

N x M 크기의 직사각형 형태의 미로. 미로에는 여러 마리의 괴물이 있어 이를 피해 탈출해야 한다.

시작 위치는 (1, 1)이고 미로의 출구는 (N, M)이며, 한 번에 한 칸씩 이동할 수 있다. 괴물이 있는 부분은 0으로, 괴물이 없는 부분은 1로 표시되어 있다.

이때 탈출하기 위해 움직여야 하는 최소 칸의 개수를 구해라. 시작 칸과 마지막 칸을 모두 포함해서 칸 수를 계산한다. 시작 칸과 마지막 칸은 항상 1이다.

입력 예시
5 6
101010
111111
000001
111111
111111

출력 예시
10

 

문제 해결 아이디어

  1. BFS는 시작 지점에서 가까운 노드부터 차례대로 그래프의 모든 노드를 탐색한다.
  2. 상하좌우로 연결된 모든 노드로의 거리가 1로 동일하다.
    1. 따라서 (1, 1) 지점부터 BFS를 수행하여 모든 노드의 최단 거리 값을 기록하면 해결할 수 있다.
#BFS 코드 구현
from collections import deque

def bfs(x, y):
	queue = deque()
    queue.append((x,y))
    #큐가 빌 때까지 반복
    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
            #괴물인 경우 무시
            if graph[nx][ny] == 0:
            	continue
            #해당 노드를 처음 방문하는 경우에만 최단 거리 기록
            if graph[nx][ny] == 1:
            	graph[nx][ny] = graph[x][y] + 1
                queue.append((nx, ny))
    #도착 지점인 가장 오른쪽 아래까지의 최단거리 반환
	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]

print(bfs(0,0))
728x90