DFS를 다음과 같이 알아보겠다:
- 지난 시간 BFS를 알아보았다. BFS와 차이점은?
- DFS는 어떤 때 쓰고 예시 코드는?
2025.02.14 - [개발 관련 기타/알고리즘] - DSA (데이터구조와 알고리즘) 면접 질문 - "BFS가 뭔가요?"
BFS와 차이점은?
DFS는 Depth-First Search를 뜻한다. 깊이 우선 탐색이다. 이 방식으로 인해 어떤 차이점이 생기는지 알아보았다.
- 특징으로는 백트래킹 알고리즘에 사용되는 알고리즘이다.
- 한 곳을 해보고, 그 한 곳을 토대로 파생되는 곳 해보고. 그 파생되는 곳 해보고 또 다시 파생되는 곳 해보고. 만약, 더 이상 파생되는게 없다면, 그 전 단계에서 파생된 곳 해보고 하는 식이다.
graph:
A
/ | \
B C D
/ \ |
E F G
graph = {
'A': ['B', 'C', 'D'],
'B': ['E', 'F'],
'C': ['G'],
'D': [],
'E': [],
'F': [],
'G': []
}
# graph = dict of lists
DFS 방식: LIFO (후입선출) 을 활용한다
1. start 노드로 간다. start 노드를 갔다고 표시한다.
2. 노드에서 갈 수 있는 첫번째 것을 간다. 이때, 간 곳이라면 안간다.
3. 다음 노드에 갔으면 갔다고 표시한다. 그리고 그 노드에서 갈 수 있는 다음 노드로 간다
4. 갈 곳이 없으면 전 노드에서 2를 한다.
DFS 사용 자료구조:
- stack
from collections import deque
stack = [] # python에서 stack은 list 사용 가능
stack = deque() # stack을 위해 deque를 사용해도 무방함
# insert
stack.append('A')
# delete
node = stack.pop()
- recursion: 내부적으로 stack으로 동작하는 방식을 사용
def recursion(parameters):
condition = True
if condition:
return
recursion(modified_parameters)
BFS 비교 메모리 사용량:
- 더 적다: BFS 비교 하였을 때 queue에 집어넣지 않으므로
stack과 recursion 비교:
- recursion이 함수 호출 스택에 의해 메모리 사용량이 더 많다
- stack이 함수 호출 오버헤드가 없어 더 빠르다
DFS 코드:
- stack: stack을 사용 할 때는 전 단계 노드로 실행 흐름이 직접 가지 않는다. 전 단계 노드에서 이미 방문할 노드들을 stack에 넣었기 때문에, stack 순으로 하나씩 꺼내서 방문하는 방법을 사용한다.
from collections import deque
graph = {
'A': ['B', 'C', 'D'],
'B': ['E', 'F'],
'C': ['G'],
'D': [],
'E': [],
'F': [],
'G': []
}
stack = deque()
visited = set()
start = 'A'
stack.append(start)
visited.add(start)
while stack:
node = stack.pop()
print(node)
for neighbor in reversed(graph[node]):# B부터 먼저 가려고. 보통 왼쪽에서 오른쪽으로 가므로.
if neighbor not in visited:
stack.append(neighbor)
visited.add(neighbor)
- recursion: 갈 수 있는 node 만큼 루프를 돈다
graph = {
'A': ['B', 'C', 'D'],
'B': ['E', 'F'],
'C': ['G'],
'D': [],
'E': [],
'F': [],
'G': []
}
def dfs(node, visited):
visited.add(node)
print(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor, visited)
start = 'A'
visited = set()
dfs('A', visited)
DFS 사용하는 때와 예시 코드
특징:
- 최단거리 보장 안됨
사용하는 때:
- 순환탐색
- 백트래킹
- 한 곳이 안되면 다시 돌아가 다른 곳을 하는데 최적임
- BFS는 메모리가 비효율적으로 사용됨:
- DFS 어차피 안되는 경로를 따로 저장할 필요가 없음
- BFS는 추후에 갈 녀석들에 대한 개별 상태들을 기록하고 있어야함. DFS는 상태를 relative하게만 가지고 있으면 됨
- BFS는 메모리가 비효율적으로 사용됨:
- 한 곳이 안되면 다시 돌아가 다른 곳을 하는데 최적임
- 순열:
- 정의:
- nPr:
- n개의 자리가 있을 때 r 명의 사람을 앉힐 수 있는 경우의 수
- ABC가 앉아 있더라도, ABC와 BCA는 다르다. 옆에 있는 사람이 달라지거나 없너 질 수 있기 때문에.
- nPr:
- graph:
- node: 깊이에 따른 완성된 경우의 수 한개
- edge: 가능한 선택지. visited를 활용.
- traverse:
- path: 현재까지 걸었던 길
- stack: stack에 추가하는 것은 path + 선택지 및 visited 에 대한 상태
- recursion: 한 단계 들어 갈 때 path + 선택지
- visited: 이미 순열 만들 때 사용한 것
- end: 타겟 r에 대한 순열 경우의 수 1개가 만들어진 경우
- 시간 복잡도:
- O(n^r): n * (n-1) * (n-2) * (n-r+1) 번 만큼 계산이 일어난다
- python (recursion and stack):
nums = ['A', 'B', 'C', 'D']
r = 2
# recursion
def dfs_permutation_recursion(nums, r, path, visited, result):
if len(path) == r:
result.append(path)
else:
for num in nums:
if num not in visited:
next_visited = visited.copy()
next_visited.add(num)
next_path = path + [num]
dfs_permutation_recursion(nums, r, next_path, next_visited, result)
visited = set()
path = []
result = []
dfs_permutation_recursion(nums, r, path, visited, result)
print(len(result))
# stack
from collections import deque
def dfs_permutation_stack(nums, r):
stack = deque()
visited = set()
result = []
init = ([], visited)
stack.append(init)
while stack:
path, visited = stack.pop()
if len(path) == r:
result.append(path)
else:
for num in reversed(nums):
if num not in visited:
next_visited = visited.copy()
next_visited.add(num)
next_path = path + [num]
next_node = (next_path, next_visited)
stack.append(next_node)
return result
result = dfs_permutation_stack(nums, r)
print(len(result))
- 조합:
- 정의:
- nCr: n개 중 r개를 선택 할 수 있는 경우의 수
- graph:
- node: 깊이에 따른 완성된 조합의 경우의 수 한 개
- edge: 가능한 선택지. index를 활용.
- traverse: 공집합 (nC0) node에서 출발 해서 r의 증가에 따라
- stack: 다음 이어만들 path와 봐야할 index를 넣은 것
- recursion: 다음 이어만들 path와 봐야할 index를 인수로 넣어서 호출
- index: 조합은 현재 자신의 index 다음번부터 path를 이어만드는 방식이다
- end 조건: 타겟 r에 대한 조합 경우 수의 1개가 만들어진 경우
- 시간복잡도:
- O(n^r): 최악의 경우 n * (n-1) * (n-2) * (n-r+1) 번 만큼 계산이 일어난다
- 최적의 경우: 6C1 = 6
- 최악의 경우: 6C6 = 6 * 5 * 4 * 3 * 2 * 1
- O(n^r): 최악의 경우 n * (n-1) * (n-2) * (n-r+1) 번 만큼 계산이 일어난다
- python (recursion and stack):
- 정의:
# recursion
def dfs_combination_recursion(nums, path, r, index):
if len(path) == r:
print(path)
else:
for i in range(index, len(nums)):
dfs_combination_recursion(nums, path + [nums[i]], r, i+1)
nums = ['A', 'B', 'C', 'D']
index = 0
r = 4
path = []
dfs_combination_recursion(nums, path, r, index)
# stack
from collections import deque
def dfs_combination_stack(nums, r):
stack = deque([])
init = (0, [])
stack.append(init)
while stack:
node = stack.pop()
index, path = node
if len(path) == r:
print(path)
else:
for i in range(index, len(nums)):
stack.append((i + 1, path + [nums[i]]))
nums = ['A', 'B', 'C', 'D']
dfs_combination_stack(nums, 3)
- N-Queens:
- 정의:
- n 크기의 체스판이 주어졌을 때, n 개의 queen을 서로 공격하지 않도록 각 queen을 어디에 둘 수 있는지 계산하는 알고리즘
- tree traversal:
- level: placing another queen as going deeper level
- branch: placing another queen at different places on a chess board
- pruning: 기존에 올려둔 queens 를 공격하지 않는지 확인
- for each queen, 해당 row에 한해서
- 1: 상하 - queen col 에 의해 안되는 곳
- 2: 대각선 1 - -45도 대각선 에 의해 안되는 곳: row + col 값이 같다
- 3: 대각선 2 - 45도 대각선 에 의해 안되는 곳: row - col 값이 같다
- 좌우 - queen row는 신경안써도 된다. 왜냐하면 row 당 1개만 올리기 때문에.
- 위 안되는 것들은 저장해서 가져감 (set): queens, diagonals_1, diagonals_2
- 저장하는 이유: 기존에 계산한 값을 사용, set 사용 시 O(1) 가능함. 매번 queens 루프 돌면서 확인하면 O(n)
- for each queen, 해당 row에 한해서
- pruning: 기존에 올려둔 queens 를 공격하지 않는지 확인
- node: 현시점의 queens 좌표들
- leaf: 최종적인 n queen의 각 y 좌표
- end:
- row index의 size 벗어남: n개의 queen 이기 때문에, 각 row 별 특정 col에는무조건 올려져야 하므로
- 그리고 한 곳에 올릴수록 row 을 1식 증가 시킬 텐데, index의 size 벗어남은 모든 곳에 올릴 때까지 pruning에 거쳐서 살아남았다는 뜻이므로 성공적인 결과라고 볼 수 있다
- 시간복잡도:
- recursion:
- O(n!): level (row) 당 queen 을 어디에 둘지 선택한다. 이미 둔 곳 제외.
- level 1: n 자리, level 2: n-1 자리, level 3: n-2 자리
- 단, 실제 실행 속도는 pruning에 의해 줄어든다.
- pruning 시간복잡도:
- 기존 queens의 col 비교: O(1)
- 기존 queens에 의한 대각선 비교: O(1) + O(1)
- pruning 시간복잡도:
- O(n!): level (row) 당 queen 을 어디에 둘지 선택한다. 이미 둔 곳 제외.
- stack:
- O(n!): recursion과 같다.
- 단, 실제 실행 속도는 pruning에 의해 줄어든다.
- recursion:
- python :
- 정의:
# recursion
# level: number of queens
# edge: possible queen placement
# node: states
# end: n queens placed. queen is placed at either each row or each col.
# pruning: create checkboard with queens. check availability.
# row
# col
# diagonal
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
results = [] # columns
queens = []
cols = set()
diagonals_1 = set()
diagonals_2 = set()
start = 0
self.backtracking_recursion(n, queens, cols, diagonals_1, diagonals_2, start, results)
answer = []
for result in results:
board = []
for col in result:
row = []
for i in range(n):
if col != i:
row.append('.')
else:
row.append('Q')
board.append(''.join(row))
answer.append(board)
return answer
def backtracking_recursion(self, n, queens, cols, diagonals_1, diagonals_2, row, results) -> List[int]:
if row == n:
results.append(queens.copy()) # queens must be copied, or else it will get changed after
return
for col in range(n):
# place queen
if col not in cols:
diagonal_1 = row + col
if diagonal_1 not in diagonals_1:
diagonal_2 = row - col
if diagonal_2 not in diagonals_2:
queens.append(col)
cols.add(col)
diagonals_1.add(diagonal_1)
diagonals_2.add(diagonal_2)
self.backtracking_recursion(n, queens, cols, diagonals_1, diagonals_2, row + 1, results)
queens.pop()
cols.remove(col)
diagonals_1.remove(diagonal_1)
diagonals_2.remove(diagonal_2)
# stack
# edge: next possible queen
# node: queens cols list
# stack: next step을 저장
from collections import deque
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
results = self.dfs_stack(n)
answer = []
for result in results:
board = []
for col in result:
row = []
for i in range(n):
if col != i:
row.append('.')
else:
row.append('Q')
board.append(''.join(row))
answer.append(board)
return answer
def dfs_stack(self, n: int):
stack = deque()
result = []
queens = []
cols = set()
diagonals_1 = set()
diagonals_2 = set()
row = 0
start = (queens, cols, diagonals_1, diagonals_2, row)
stack.append(start)
while stack:
node = stack.pop()
queens, cols, diagonals_1, diagonals_2, row = node
if row == n:
result.append(queens.copy())
continue
for col in range(n):
if col not in cols:
diagonal_1 = row + col
if diagonal_1 not in diagonals_1:
diagonal_2 = row - col
if diagonal_2 not in diagonals_2:
queens.append(col)
cols.add(col)
diagonals_1.add(diagonal_1)
diagonals_2.add(diagonal_2)
next = (queens.copy(), cols.copy(), diagonals_1.copy(), diagonals_2.copy(), row + 1)
stack.append(next)
queens.pop()
cols.remove(col)
diagonals_1.remove(diagonal_1)
diagonals_2.remove(diagonal_2)
return result
'알고리즘' 카테고리의 다른 글
외국계 코딩 테스트 후기 (0) | 2025.03.05 |
---|---|
DSA (데이터구조와 알고리즘) 면접 질문 - "BFS가 뭔가요?" (0) | 2025.02.14 |
[백준 BOJ] C++) 1605번: 반복 부분 문자열 (0) | 2020.02.09 |
[프로그래머스] (C++) 야근지수 (0) | 2020.01.09 |
[프로그래머스] (C++) 이중우선순위큐 (0) | 2020.01.03 |