오늘은 탐색 문제를 풀면서
단순히 DFS, BFS를 외우는 것보다
문제를 어떤 구조로 바꿔서 볼지, 그리고 그걸 어떤 코드 형태로 구현할지를 더 많이 고민한 날이었다.
특히 오늘은 이런 흐름이 계속 이어졌다.
- BFS에서는 왜 리스트 대신 deque를 써야 하는지
- K거리 도시 문제에서 왜 DFS가 아니라 BFS인지
- 미로 탐색에서는 왜 도시 번호가 아니라 좌표로 생각해야 하는지
- 얼음 문제에서는 왜 탐색을 한 번 더 “포장”해야 하는지
오늘은 정답만 보는 날이 아니라,
기존에 배운 탐색 아이디어를 문제에 맞게 튜닝하는 연습을 한 느낌이 더 강했다.
1. deque: BFS에서 왜 중요한지 다시 정리
오늘 제일 먼저 정리한 건 deque였다.
메모에도 적어둔 것처럼 deque는 double ended queue다.
from collections import deque
q = deque()
q.append(10) # 오른쪽 추가
q.appendleft(5) # 왼쪽 추가
print(q) # deque([5, 10])
print(q.popleft()) # 왼쪽에서 꺼내기
print(q.pop()) # 오른쪽에서 꺼내기
핵심은 이거였다.
- append left
- append
- pop left
- pop
즉, 양쪽 끝을 다 빠르게 다룰 수 있는 구조라는 점이다.
오늘 정리한 텍스트에서도
파이썬에서 BFS를 구현할 때 리스트의 pop(0)은 비효율적이고,
앞에서 꺼내는 작업이 많은 BFS에서는 collections.deque를 쓰는 게 자연스럽다고 설명돼 있었다. 특히 deque는 양쪽 끝을 뚫어 둔 구조라 앞쪽 제거가 빠르다는 점이 강조돼 있었다.
예전에는 그냥
“큐니까 리스트 써도 되지 않나?”
싶었던 적도 있었는데,
오늘은 이게 단순 문법 차이가 아니라 자료구조 선택 문제라는 게 더 잘 들어왔다.
리스트로도 이렇게는 쓸 수 있다.
q = []
q.append(1)
q.append(2)
q.append(3)
print(q.pop(0))
그런데 BFS는 앞에서 계속 꺼내야 하니까
문제가 커지면 이 방식이 점점 부담이 된다.
💡 그래서 오늘 남은 건 아주 단순했다.
BFS를 파이썬으로 구현할 때는, 큐를 “deque”로 생각하는 게 거의 기본이다.
2. K거리 도시: 왜 이 문제는 BFS인가
🔗 문제 링크
백준 18352 - 특정 거리의 도시 찾기
오늘 K거리 도시 문제는
처음부터 “이건 BFS인가 DFS인가”를 생각하게 했던 문제였다.
정리된 텍스트에서도
이 문제는 모든 도로의 거리가 1이라는 점이 핵심이고,
이런 경우 출발점에서부터 점진적으로 퍼져 나가는 BFS가 최단거리 추적에 더 적합하다고 설명돼 있었다. DFS는 왔다 갔다 하며 깊게 들어가기 때문에 거리 추적이 상대적으로 불편하지만, BFS는 레벨 단위로 진행되기 때문에 이런 문제와 잘 맞는다는 흐름이었다.
즉, 오늘 이 문제를 보면서 다시 정리된 건 이거였다.
- 이 문제는 거리 문제가 맞다
- 그런데 모든 간선 가중치가 동일하다
- 그러면 BFS의 레벨 순회가 최단거리 개념과 자연스럽게 연결된다
이게 오늘 꽤 중요했다.
예전에는 탐색 문제를 보면
“DFS도 되나? BFS도 되나?”
정도로만 생각했는데,
오늘은 적어도
최단거리 + 모든 간선 길이 1
이 조합이 나오면 BFS를 먼저 떠올리는 게 맞겠다고 느꼈다.
3. K거리 도시: 내가 처음 세운 아이디어
처음에는 대략 이런 식으로 접근했다.
# 일단 ? BFS로 진행
# 입력을 받는건 도시개수N, 도로개수 M, 거리정보 K, 출발도시번호 X =>def(N,M,K,X)
# 처음에 도시 세팅 -> 인접 리스트 길이 N
# 거리 정보를 넣어줘야함 K 넣기 -> for문으로 거리 정보 넣기 -> 0번은 도시, 1번 연최조결된거 -> 0번을 기준으로 1번을 리스트 요소에 추가
# 한일 리스트
# 출발점에서 할일 방문 할일 추가 -> 바로 가면 흠 1인거고 , 들려서 가면 그거만큼 추가 인데
# 아 pop이 되면 +1 추가
# 근데 이제 반복할때마다 +1 추가하고, 최종 pop(0)에서는 그걸 반영해서 dep 추가 !
# 흠 dep가 k와 같다면
# 그 리스트의 번호를 출력하면 될듯 ?
# 업ㅅ으면 -1출력
지금 다시 보면,
방향 자체는 아주 엉뚱하지 않았다.
적어도
- 이건 BFS 쪽이다
- 인접 리스트를 써야 한다
- 거리 기록이 필요하다
여기까지는 맞게 보고 있었다.
그런데 구현으로 들어가면서
“거리”를 정확히 어떻게 기록할지,
방문 여부와 거리를 어떤 식으로 함께 관리할지를 잘 못 잡았다.
4. K거리 도시: 처음 코드와 문제점
내 첫 코드는 이런 느낌이었다.
n,m,k,x = map(int,input("도시수 도로수 기준거리 출발도시 : ").split())
graph=[[]for i in range(n+1)]
for i in range(m):
s,e = map(int,input("출발도시 도착도시 : ").split())
graph[s].append(e)
graph
dis = [f'inf({i})' for i in range(n)] #길이
todo =[]
did = []
todo.append(x)
while todo:
now = todo.pop(0)
if dis[now]!=now:
did.append(now)
todo.extend(graph[now])
dis[now]=now+1
print(did)
지금 다시 보면 꼬인 부분이 꽤 선명하다.
⚠️ 1) 거리 배열이 문자열이었다
dis = [f'inf({i})' for i in range(n)]
거리 배열은 숫자로 비교하고 갱신해야 하는데,
문자열처럼 만들어버려서 구조 자체가 어색해졌다.
거리 배열은 결국
- 아직 방문하지 않았는지
- 현재까지 최단거리가 얼마인지
를 저장해야 하므로 숫자 기반이어야 했다.
⚠️ 2) 리스트의 pop(0)을 그대로 썼다
now = todo.pop(0)
오늘 deque를 따로 정리한 이유가 이거였다.
BFS에서 앞에서 계속 꺼내는 작업이 필요하니까,
이건 deque.popleft()로 가는 게 맞았다.
텍스트에서도 리스트보다 deque가 BFS에 적합하다고 분명히 설명돼 있었다.
⚠️ 3) 거리 갱신 방식이 틀렸다
dis[now] = now + 1
이건 지금 보면
도시 번호에 1을 더한 것뿐이다.
문제에서 필요한 건
도시 번호가 아니라 거리였다.
즉, 다음 도시 nxt의 거리는
현재 도시 now의 거리에서 1을 더한 값이어야 했다.
distance[nxt] = distance[now] + 1
⚠️ 4) 방문 여부와 거리 관리를 따로따로 생각했다
처음에는 did 리스트도 따로 두고,
거리 배열도 따로 두고,
큐도 따로 두고 하다 보니 구조가 오히려 헷갈렸다.
그런데 오늘 정리된 흐름에서는
이 문제의 distance 배열이 단순한 방문 여부 기록이 아니라
정답지 자체처럼 쓰인다는 점이 중요했다.
즉, 방문했는지 확인하는 용도와, 최단거리를 저장하는 용도를 같이 맡는 구조였다.
5. K거리 도시: 오늘 정리한 정답 구조
오늘 정리한 최종 구조는 이쪽이었다.
- 인접 리스트로 그래프 만들기
- distance 배열을 1로 초기화
- 출발점 x의 거리만 0으로 설정
- BFS 진행
- 아직 방문하지 않은 도시만 큐에 넣고 거리 갱신
- 마지막에 거리 k인 도시만 출력
코드로는 이렇게 정리할 수 있었다.
from collections import deque
n, m, k, x = map(int, input().split())
graph = [[] for _ in range(n + 1)]
for _ in range(m):
a, b = map(int, input().split())
graph[a].append(b)
distance = [-1] * (n + 1)
distance[x] = 0
q = deque([x])
while q:
now = q.popleft()
for nxt in graph[now]:
if distance[nxt] == -1:
distance[nxt] = distance[now] + 1
q.append(nxt)
result = []
for city in range(1, n + 1):
if distance[city] == k:
result.append(city)
if result:
for city in result:
print(city)
else:
print(-1)
이 문제를 통해 오늘 남은 핵심은 이거였다.
💡 BFS는 단순히 큐를 쓰는 문제가 아니라,distance 배열까지 같이 설계해서 “방문 + 최단거리”를 함께 관리하는 문제다.
6. 미로 탐색: 이번엔 도시 번호가 아니라 좌표였다
🔗 문제 링크
미로 탐색은 앞 문제랑 같은 BFS 계열인데,
문제를 보는 단위가 완전히 달랐다.
K거리 도시는
노드 = 도시 번호였다.
그런데 미로 탐색은
노드 = 좌표 (x, y) 였다.
정리한 텍스트에서도
이 문제는 도시 번호가 아니라 “칸의 위치”가 중요하고,
따라서 큐에도 번호 하나가 아니라 좌표 두 개를 묶어서 넣어야 한다고 설명돼 있었다.
또 상하좌우 이동을 위해 dx, dy 같은 방향 벡터를 쓰는 구조가 핵심이라고 정리돼 있었다.
즉, 오늘 여기서 가장 중요했던 건
알고리즘 이름보다 노드를 무엇으로 볼 것인가였다.
- 그래프 문제 → 도시 번호
- 격자 문제 → 좌표
- 미로 문제 → 좌표 + 상하좌우 인접
이걸 문제마다 먼저 바꿔서 볼 수 있어야 했다.
7. 미로 탐색: 좌표형 BFS 코드로 정리
이 문제에서는 입력부터 격자를 2차원 리스트로 받아야 했다.
from collections import deque
n, m = map(int, input().split())
graph = [list(map(int, input().strip())) for _ in range(n)]
dx = [-1, 1, 0, 0]
dy = [0, 0, -1, 1]
q = deque()
q.append((0, 0))
while q:
x, y = q.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
q.append((nx, ny))
print(graph[n - 1][m - 1])
여기서 오늘 크게 남은 건 두 가지였다.
1) 큐에 (x, y)를 넣는다
더 이상 도시 번호 하나가 아니라,
좌표 두 개가 하나의 위치 정보를 이룬다.
2) 지도 자체를 거리 기록용으로 쓸 수 있다
새 칸을 발견할 때마다
graph[nx][ny] = graph[x][y] + 1
이렇게 바로 값을 덮어쓰면서
거리까지 함께 기록한다.
즉, 이 문제에서는 별도의 distance 배열을 만들지 않고
지도를 그대로 재활용할 수도 있었다.
이게 K거리 도시랑 또 닮아 있었다.
- K거리 도시 → distance 배열 따로 둠
- 미로 탐색 → graph를 거리 기록용으로 활용
결국 같은 BFS라도
정답을 어디에 기록할지는 문제마다 달라질 수 있었다.
8. 얼음 얼려 먹기: 이번엔 탐색을 한 번 더 포장해야 했다
🔗 참고 링크
오늘 얼음 문제는 진짜 인상적이었다.
앞에 있던 문제들은 비교적 출발점이 명확했다.
- K거리 도시 → 시작 도시 x
- 미로 탐색 → (0, 0)
그런데 얼음 문제는 그렇지 않았다.
출발점을 하나만 정해서 끝나는 문제가 아니었다.
정리된 텍스트에서도
이 문제는 기존 탐색을 그대로 쓰는 게 아니라,
격자 전체를 돌면서 모든 칸을 시작점 후보로 보고 탐색을 포장해야 한다고 설명돼 있었다.
또 이 문제는 거리 누적이 필요한 게 아니라
연결된 0들을 1로 바꾸는 식으로 흔적만 남기면 되기 때문에, DFS든 BFS든 크게 상관없다고 정리돼 있었다.
이게 오늘 제일 크게 남은 부분이었다.
얼음 문제에서 핵심적으로 달라진 점
- 모든 칸을 시작점 후보로 본다
- 값이 0인 칸에서만 탐색 시작 가능
- 연결된 0들을 전부 1로 뒤집는다
- 탐색 하나가 끝날 때마다 카운트 +1
즉, 이 문제는
탐색 알고리즘을 한 번 더 큰 반복문으로 감싸는 문제였다.
9. 얼음 문제: 내가 이해한 방식대로 정리한 DFS 코드
이 문제는 DFS로 정리하면 이런 식이었다.
n, m = map(int, input().split())
graph = [list(map(int, input().strip())) for _ in range(n)]
def dfs(x, y):
if x < 0 or x >= n or y < 0 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
result = 0
for i in range(n):
for j in range(m):
if dfs(i, j):
result += 1
print(result)
이 코드가 왜 중요했냐면,
앞 문제들과 구조가 확실히 달랐기 때문이다.
앞 문제들과 다른 점
1) 출발점이 고정되지 않았다
그래서 전체 격자를 다 순회하면서
“여기서 시작 가능한가?”를 먼저 본다.
2) 거리를 누적할 필요가 없다
그냥 방문 흔적만 남기면 된다.
즉, 0을 1로 바꾸는 것으로 충분하다.
3) 탐색이 하나 끝날 때마다 카운트한다
한 번의 DFS가 얼음 한 덩어리를 끝까지 처리하므로
True를 리턴하면 결과를 +1 한다.
이 문제는 진짜로
“탐색을 포장한다”는 표현이 딱 맞았다.
10. 오늘 느낀 점
오늘은 탐색 문제를 보면서
이제 조금씩 이런 기준이 생기는 것 같다.
1) 탐색 문제는 DFS/BFS를 고르는 것부터가 아니다
먼저 이걸 생각해야 한다.
- 노드는 무엇인가?
- 출발점은 하나인가, 여러 개인가?
- 거리까지 기록해야 하나?
- 방문 흔적만 남기면 되나?
2) BFS는 거리 / 레벨 / 최단거리와 잘 연결된다
K거리 도시가 딱 그랬다.
모든 간선 길이가 1이면 BFS의 레벨 순회가 곧 최단거리 개념이 된다.
3) 격자 문제는 좌표 문제로 바뀐다
미로 탐색은 결국
번호 탐색이 아니라 좌표 탐색이었다.
4) 어떤 문제는 탐색을 한 번 더 포장해야 한다
얼음 문제는
탐색 하나로 끝나는 게 아니라,
모든 칸에 대해 탐색 시작 여부를 판단해야 했다.
결국 오늘 세 문제를 관통한 핵심은 이거였다.
💡 탐색은 DFS/BFS를 외우는 게 아니라,문제를 그 알고리즘으로 표현 가능한 구조로 바꾸는 연습이다.
그리고 개인적으로 오늘 제일 크게 남은 건 이것도 있었다.
처음 코드가 틀렸다고 해서 문제를 완전히 못 읽은 건 아닐 수 있다.
K거리 도시처럼 방향은 맞았는데,
거리 배열 설계나 자료구조 선택이 틀어져서 무너지는 경우도 많다.
이런 날이 오히려 더 오래 남는다.
탐색은 DFS, BFS를 외우는 게 아니라, 문제를 그 방식으로 바꿔서 구현하는 연습에 가깝다.
'[SK플래닛] ASAC 빅데이터전문가 11기 > 학습기록' 카테고리의 다른 글
| [SK플래닛] ASAC 빅데이터전문가 11기 | 10일차 (2) | 2026.04.24 |
|---|---|
| [SK플래닛] ASAC 빅데이터전문가 11기 | 09일차 (2) | 2026.04.23 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 07일차 (0) | 2026.04.21 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 06일차 (0) | 2026.04.20 |
| [SK플래닛] ASAC 빅데이터전문가 11기 | 05일차 (2) | 2026.04.17 |
