연결되어 있는 정점들 간의 관계를 표현할 수 있는 자료구조.
자료구조 구분:
1. 선형구조: 자료 삽입/추출에 초점
2. 비선형구조: 표현에 초점
그래프는 연결 관계에 초점이 맞춰져 있음.
예시 그래프:
인접 행렬 표현:
/ 0123
0xoxx
1oxox
2xoxo
3xxox
배열로 표현(코드):
graph = [
[ False, True, False, False ],
[ True, False, True, False ],
[ False, True, False, True ],
[ False, False, True, False ]
인접 리스트 표현:
0 -> 1
1 -> 0 -> 2
2 -> 1 -> 3
3 -> 2
딕셔너리로 표현(코드):
graph = {
0: [1],
1: [0, 2],
2: [1, 3],
3: [2]
}
주로 리스트로 많이 표현하는데, 그 이유는 공간 복잡도가 더 효율적이기 때문이다. 하지만 때로는 행렬로 표현할 때도 있다(노드 간 연결 되어있는 지 확인할 때 행렬로 표현하면 접근법을 쓰면 되서 더 빠름)
DFS(Depth First Search): 깊이 우선 탐색. 내려갈 수 있을 때까지(깊이) 내려가다가 다시 돌아가는 방식.

두 가지 구현 방법이 있다:
1. 재귀. (1, 2, 5, 6, 7, 3, 4)
2. 스택. (1, 4, 3, 5, 7, 6, 2)
예시 그래프)

위 그래프를 예시로 DFS를 구현해보기.
알아야 할 핵심 포인트:
1. 반복적으로 발생하는 일
2. 종료 조건
이번 문제는 단순 방문이기 때문에 종료 조건은 자식이 없는 경우이다. 반복적으로 실행하는 것은 현재 노드에서 인접 노드를 차례대로 방문하는 것이다.
방법:
1. 현재 노드 방문 처리 (visited.append(node))
2. 인접 노드를 순회하면서 아직 방문하지 않은 노드라면 해당 노드를 기준으로 1~2 반복
stack: 방문할 노드를 담은 리스트
visited: 방문한 노드를 담은 리스트
방법:
1. 방문할 노드 리스트가 비어있지 않다면 반복
2. 가장 최근에 삽입된 노드를 꺼내고 방문 처리
3. 꺼낸 노드의 인접 노드를 방문하면서 방문하지 않은 인접 노드는 stack에 추가
graph = {
1: [2, 3, 4],
2: [5],
3: [5],
4: [],
5: [6, 7],
6: [],
7: [3],
}
# node: 현재 방문한 노드
# visited: 방문한 노드의 리스트
def dfs_recursive(node, visited):
# 방문처리
visited.append(node)
# 인접 노드 방문
for adj in graph[node]:
if adj not in visited:
dfs_recursive(adj, visited)
return visited
def dfs_stack(start):
visited = []
# 방문할 순서를 담아두는 용도
stack = [start]
# 방문할 노드가 남아있는 한 아래 로직을 반복한다.
while stack:
# 제일 최근에 삽입된 노드를 꺼내고 방문처리한다.
top = stack.pop()
visited.append(top)
# 인접 노드를 방문한다.
for adj in graph[top]:
if adj not in visited:
stack.append(adj)
return visited
BFS(Breadth First Search): 너비 우선 탐색. 옆으로 갈 수 있을 때까지(너비) 가다가 다시 돌아가는 방식.

def bfs_queue(start):
visited = [start]
q = deque([start])
while q:
node = q.popleft()
for adj in graph[node]:
if adj not in visited:
q.append(adj)
visited.append(adj)
return visited
필요없는 경우를 가지치기(pruning)하므로써 시간 복잡도를 줄이는 방법.
DFS, BFS와 같은 완전 탐색 기법들을 효율적으로 만들어주는 기법.
DFS와 BFS는 기본적으로 전체 탐색(brute force)이다. 모든 경우의 수를 다 탐색한다는 뜻이다. 하지만 일반적으로 모든 경우의 수를 다 탐색하는 건 가능은 하지만 오래 걸리는 일이다.
백트래킹은 전체 탐색보다 필요가 없는 수는 탐색하지 않는 효율적인 탐색을 하는 것이다.
강의에서 코드를 제공해주고 설명을 해주어서 대충 이해는 했지만 직접 구현은 아직 많이 어려운 것 같다. 내일 다시 처음부터 코드를 잘 살펴보고 새로 구현해보기로 하자.
def island_dfs_stack(grid):
# dx, dy: 상하좌우의 인덱스
dx = [0, 0, 1, -1]
dy = [1, -1, 0, 0]
# grid의 길이와 높이
rows, cols = len(grid), len(grid[0])
# 섬의 개수
count = 0
for row in range(rows):
for col in range(cols):
# 각 요소를 순회하며 1이 아닐 때는 다음 col로 넘어감
if grid[row][col] != "1":
continue
# 1이라면 무조건 섬의 개수 증가
count += 1
# 해당 인덱스를 stack에 저장
stack = [(row, col)]
while stack:
# 가장 최근에 삽입된 인덱스짝 pop
x, y = stack.pop()
# 해당 인덱스를 0으로 수정
grid[x][y] = "0"
# 상,하,좌,우 총 4개의 위치 확인
for i in range(4):
# nx, ny 상하좌우의 인덱스짝
nx = x + dx[i]
ny = y + dy[i]
# nx나 ny가 범위 밖이거나 해당 인덱스의 값이 1이 아니면 다음 i로 넘어감
if nx < 0 or nx >= rows or ny < 0 or ny >= cols or grid[nx][ny] != "1":
continue
# nx, ny를 stack (grid[nx][ny]는 1임)
stack.append((nx, ny))
return count
def island_dfs_recursive(grid):
pass
def island_dfs_recursive(grid):
# dx, dy: 상하좌우의 인덱스
dx = [0, 0, 1, -1]
dy = [1, -1, 0, 0]
m = len(grid)
n = len(grid[0])
count = 0
def dfs_recursive(r, c):
# 재귀의 종료 조건: r과 c가 grid의 범위를 벗어나거나 해당 인덱스의 값이 1이 아닌 경우
if r < 0 or r >= m or c < 0 or c >= n or grid[r][c] != "1":
return
# 해당 인덱스의 값을 0으로 바꿈
grid[r][c] = "0"
# 현재 인덱스의 상,하,좌,우 인덱스들에 대해 재귀 실행
for i in range(4):
dfs_recursive(r + dx[i], c + dy[i])
return
for r in range(m):
for c in range(n):
if grid[r][c] == "1":
continue
count += 1
dfs_recursive(r, c)
return count
def island_bfs_queue(grid):
# dx, dy: 상하좌우의 인덱스
dx = [0, 0, 1, -1]
dy = [1, -1, 0, 0]
# grid의 길이와 높이
rows, cols = len(grid), len(grid[0])
# 섬의 개수
count = 0
for row in range(rows):
for col in range(cols):
# 각 요소를 순회하며 1이 아닐 때는 다음 col로 넘어감
if grid[row][col] != "1":
continue
# 1이라면 무조건 섬의 개수 증가
count += 1
# 해당 인덱스를 stack에 저장
q = deque([(row, col)])
while q:
# 맨 처음에 삽입된 인덱스짝 pop
x, y = q.popleft()
# 해당 인덱스를 0으로 수정
grid[x][y] = "0"
# 상,하,좌,우 총 4개의 위치 확인
for i in range(4):
# nx, ny 상하좌우의 인덱스짝
nx = x + dx[i]
ny = y + dy[i]
# nx나 ny가 범위 밖이거나 해당 인덱스의 값이 1이 아니면 다음 i로 넘어감
if nx < 0 or nx >= rows or ny < 0 or ny >= cols or grid[nx][ny] != "1":
continue
# nx, ny를 q (grid[nx][ny]는 1임)
q.append((nx, ny))
return count
내가 생각한 방법과 페어 분이 생각한 방법이 달랐다. 나는 드라이버 역할을 하기로 해서 페어 분의 방법대로 진행했다.
N: 동전의 종류
K: 목표 금액
풀이:
1. 받은 입력들을 역순으로 리스트에 저장 (내림차순)
2. 리스트의 요소들을 순회하면서
2-1. 요소가 K와 같거나 작을 동안 K에서 요소만큼 빼고 count 증가
2-2. K가 0이 되면 반복문 종료
import sys
input = sys.stdin.readline
N, K = map(int, input().split())
inputs = []
for i in range(N):
inputs.append(int(input()))
coins = []
for i in range(len(inputs)):
coins.append(inputs[len(inputs) - 1 - i])
count = 0
for i in coins:
while i <= K:
count += 1
K -= i
if K == 0:
break
print(count)
이항 계수라는 수학개념을 이해하지 못해도 이항 계수를 구하는 공식만 알면 풀 수 있는 문제다.
이항 정리
위 그림을 코드로 옮기기만 하면 끝이다.
def factorial(n):
f = 1
for i in range(n):
f = f * (i+1)
return f
N, K = map(int, input().split())
print(int(factorial(N) / (factorial(K) * factorial(N-K))))
이 문제 역시 간단하긴 한데 페어 분 덕분에 더 빨리 풀 수 있었다.
exps: 입력받은 문자열
result: 적절한 괄호를 추가해 식의 값을 최소로 만든 값
예시) exps = "55-50+40-30+20-20-20"
풀이:
1. '-'를 기준으로 입력받은 문자열을 자른다. ==> exps = [55, 50+40, 30+20, 20, 20]
2. result에 첫 번째 요소(숫자 혹은 +로 나뉘어진 숫자 리스트)를 넣어준다. ==> result = 55
3. 그 외의 요소(숫자 혹은 +로 나뉘어진 숫자 리스트)는 전부 result에서 빼준다. ==> result = 55 - (50+40) - (30+20) - 20 - 20
exps = input().split('-')
result = sum(list(map(int, exps[0].split('+'))))
for i in range(1, len(exps)):
result -= sum(list(map(int, exps[i].split('+'))))
print(result)