class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
#중첩함수
def dfs(i, j):
#값이 '1'이 아니거나 범위를 벗어나면 종료
if (i < 0 or i >= len(grid)) or (j < 0 or j >= len(grid[0])) or grid[i][j] != '1':
return
grid[i][j] = '0' #다시 방문하지 않도록 값 변경
#동서남북으로 재귀 호출
dfs(i-1, j)
dfs(i+1, j)
dfs(i, j-1)
dfs(i, j+1)
cnt = 0
for i in range(len(grid)):
for j in range(len(grid[0])):
if grid[i][j] == '1':
dfs(i, j)
cnt += 1
return cnt
📝 재귀함수 종료조건에서 grid[i][j] != '1'가 (i < 0 or i >= len(grid)) or (j < 0 or j >= len(grid[0])) 보다 앞에 있으면 IndexError: list out of range(Runtime Error) 발생
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
#중첩함수
def dfs(index, path):
#재귀 종료 조건 -> 끝까지 탐색하면 백트래킹
if len(path) == len(digits):
answer.append(path)
return
for char in num_dic[digits[index]]:
dfs(index + 1, path + char)
#예외처리-> input이 digits = ""일 경우
if not digits: #빈 문자열이면 if, while같은 문의 조건에서 거짓으로 간주
return []
num_dic = {'2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl',
'6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz'}
answer = []
dfs(0, "")
return answer
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
answer, prev_e = [], []
def dfs(elements):
#재귀함수 종료조건
if not elements:
answer.append(prev_e[:])
return
for e in elements:
#elements을 prev_e와 next_e로 나눔
next_e = elements[:] #파이썬은 객체를 참조함에 유의
next_e.remove(e) #현재값을 제외한 요소들을 next_e에
prev_e.append(e) #현재값을 prev_e에 위치하게 함
dfs(next_e)
prev_e.pop() #재귀함수 종료휴 prev_e = []로 만들기 위해 하나 씩 pop
dfs(nums)
return answer
📝 next_e = elements 안하고 next_e = elements[:] 한 이유는 파이썬은 모든 객체를 참조하는 형태로 처리되기 때문임
=>next_e = elements 하면 elements가 변경될때마다 next_e의 값이 변경됨.
next_e = elements[:]하면 elements.copy()와 같이 값은 같지만 다른 객체를 가리킴
from itertools import permutations
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
return list(map(list, permutations(nums)))
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
answer, prev_e = [], []
def dfs(elements, cnt):
#재귀함수 종료조건
if not cnt:
answer.append(prev_e[:])
return
for idx, e in enumerate(elements):
next_e = elements[idx + 1:]
prev_e.append(e)
dfs(next_e, cnt - 1)
prev_e.pop()
dfs([num for num in range(1, n + 1)], k)
return answer
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
answer = []
def dfs(elements, start, cnt):
#재귀함수 종료조건
if not cnt:
answer.append(elements[:])
return
for i in range(start, n + 1):
elements.append(i)
dfs(elements, i + 1, cnt - 1)
elements.pop()
dfs([], 1, k)
return answer
from itertools import combinations
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
return list(map(list, combinations([num for num in range(1, n+1)], k)))
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
answer = []
#재귀 함수
def dfs(c_sum, idx, path):
#재귀함수 종료조건 => target값 초과했을 경우
if c_sum < 0:
return
#재귀함수 종료조건 => target값 일 경우
if c_sum == 0:
answer.append(path)
return
for i in range(idx, len(candidates)):
dfs(c_sum - candidates[i], i, path + [candidates[i]])
dfs(target, 0, [])
return answer
📝 입력값중 0이 포함되어 있으면? => 종료조건을 만족할 수 없기 때문에 무한히 깊이 탐색을 시도
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
answer = []
def dfs(idx, path):
answer.append(path) #매번 추가
for i in range(idx, len(nums)):
dfs(i + 1, path + [nums[i]])
dfs(0, [])
return answer
from collections import defaultdict
class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
#그래프 구성
#pop()하면 사전어휘순으로 나올 수 있도록 내림차순 정렬
graph = defaultdict(list)
for start, end in sorted(tickets, reverse = True):
graph[start].append(end)
#재귀dfs (백트래킹 하여 경로 역순으로 추가)
route = []
def dfs(start):
while graph[start]:
dfs(graph[start].pop())
route.append(start)
dfs('JFK')
#경로를 역순으로 추가했으므로 다시 역순으로 바꾸어 return
return route[::-1]
from collections import defaultdict
class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
#그래프 구성
graph = defaultdict(list)
for start, end in sorted(tickets, reverse = True):
graph[start].append(end)
#dfs
stack, route = ['JFK'], []
while stack:
while graph[stack[-1]]:
#어휘순서가 빠른 자식노드 stack추가 => 더 이상 자식노드가 없을때까지 탐색
stack.append(graph[stack[-1]].pop())
route.append(stack.pop()) #빠져나오면서 경로 추가
return route[::-1]
그래프가 Cyclic(순환구조)인지 판별하는 문제
from collections import defaultdict
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
graph = defaultdict(list)
for after, before in prerequisites:
graph[after].append(before)
#dfs
traced = [] #이전에 방문했던 노드
def dfs(node):
if node in traced: #이전에 방문한 노드일경우
return False
traced.append(node)
for v in graph[node]:
if not dfs(v): #자식노드가 이전에 방문한 노드일경우
return False
#탐색 종료 후 순환 노드 삭제
traced.pop()
return True
#각 노드에서 출발하는 모든경우를 따지는 이유
#-> 그래프가 끊어져 있는 경우에서 cyclic 판별하기 위해!
for node in list(graph): #dict를 list로 변경하면 key만 list의 요소로 들어감
if not dfs(node): #dfs에서 False return될 경우
return False
return True
from collections import defaultdict
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
graph = defaultdict(list)
for after, before in prerequisites:
graph[after].append(before)
#dfs
visited, traced = [], [] #순환구조 판별 완료한 노드, 이전에 방문했던 노드
def dfs(node):
if node in traced: #이전에 방문한 노드일경우
return False
if node in visited:
return True
traced.append(node)
for v in graph[node]:
if not dfs(v): #자식노드가 이전에 방문한 노드일경우
return False
traced.pop() #탐색 종료 후 순환 노드 삭제
visited.append(node) #순환구조 판별 완료
return True
#각 노드에서 출발하는 모든경우를 따지는 이유
#-> 그래프가 끊어져 있는 경우에서 cyclic 판별하기 위해!
for node in list(graph): #dict를 list로 변경하면 key만 list의 요소로 들어감
if not dfs(node): #dfs에서 False return될 경우
return False
return True
📝 풀이1은 순환이 발견될 때까지 모든 자식 노드를 탐색하는 구조 -> 불필요하게 동일한 그래프를 여러번 탐색
풀이2는 한번 방문했던 그래프는 두 번 이상 방문하지 않도록 무조건 True로 return하는 구조로 개선 -> 가지치기로 최적화한 풀이
📝 for node in list(graph)에서 dict를 list로 형변환하는 이유?
하지 않으면 RuntimeError: dictionary changed size buring iteration 발생
-> graph = defaultdict(list)로 선언하여 빈 값 조회시 오류를 내지 않기 위해 항상 디폴트를 생성하는 구조로 되어 있으므로 graph값이 변경된다는 오류가 발생
-> list()로 묶을 경우 새로운 복사본이 생성되면서 다른 ID를 갖게 됨