[X, X, O, O, O] 형태의 경계선이 생기는가?"가장 먼저 눈에 띄는 1차 신호입니다.
for문으로 하나씩 다 검사(선형 탐색)하려면 입력값이 (10만) 내외여야 안전합니다.이분 탐색을 적용하기 위한 가장 결정적인 필수 조건입니다.
코딩 테스트 이분 탐색 문제의 90%는 이 유형입니다. 질문의 관점을 뒤집는 것입니다.
| 원래 문제의 질문 | 이분 탐색으로 바꾸는 질문 (O/X 검사) |
|---|---|
| "제한시간 내에 풀어내는 최소 숙련도는 얼마인가?" | "숙련도가 K일 때, 제한시간 내에 풀 수 있는가? (O / X)" |
| "나무를 M미터 가져가기 위한 톱날 높이의 최댓값은?" | "톱날 높이가 K일 때, 나무 M미터를 얻을 수 있는가? (O / X)" |
isValid(K))를 만들기는 훨씬 쉽습니다.[X, X, X, O, O, O]처럼 경계선을 기준으로 나뉜다면 무조건 이분 탐색 문제입니다."조건을 만족하는 최댓값 또는 최솟값"을 찾을 때 사용하는 템플릿입니다.
def solution(target_limit, data):
# 1. 탐색 범위 설정 (문제에 맞춰 최소/최대 범위 지정)
left = 1
right = max(data) # 또는 10**9 등 문제의 최대 범위
answer = 0
# 2. O/X 판정 함수 (mid 값으로 조건 만족 여부 확인)
def is_valid(mid):
# [문제 조건에 맞게 계산 로직 작성]
total = 0
for val in data:
total += val // mid # 예시 계산
return total >= target_limit # 조건을 만족하면 True, 아니면 False
# 3. 이분 탐색 수행
while left <= right:
mid = (left + right) // 2
if is_valid(mid):
answer = mid # 조건 만족 시 정답 기록!
# [최댓값을 찾을 때]: 더 큰 값도 가능한지 오른쪽 탐색
left = mid + 1
# [최솟값을 찾을 때]: 더 작은 값도 가능한지 왼쪽 탐색
# right = mid - 1
else:
# 조건 불만족 시 반대쪽 범위를 줄임
right = mid - 1 # (최대 찾기일 때)
# left = mid + 1 # (최소 찾기일 때)
return answer
is_valid(mid) 함수: 이 부분이 문제의 핵심입니다. "숙련도가 mid일 때 시간 내에 풀 수 있는가?", "톱날 높이가 mid일 때 나무를 충분히 가져갈 수 있는가?"처럼 mid를 인자로 받아 True/False를 반환하는 함수를 구현하면 됩니다.answer = mid, 더 크게 해보기 위해 left = mid + 1answer = mid, 더 작게 해보기 위해 right = mid - 1이미 정렬된 배열에서 '특정 값 Target이 어디에 있는지' 찾을 때 쓰는 기본 형태입니다.
def binary_search(arr, target):
left = 0
right = len(arr) - 1
while left <= right:
mid = (left + right) // 2
if arr[mid] == target:
return mid # 찾았을 때의 인덱스 반환
elif arr[mid] < target:
left = mid + 1 # target이 오른쪽 구역에 있음
else:
right = mid - 1 # target이 왼쪽 구역에 있음
return -1 # 찾지 못했을 때
left <= right: while문 조건식에 <= (등호) 꼭 챙기기mid = (left + right) // 2: 중간값 계산left = mid + 1 / `right = mid - 1: 이동 시 반드시 +1, -1`을 해주어야 무한 루프에 빠지지 않음is_valid(mid) 판정 함수 작성: 파라메트릭 서치 풀이의 핵심 로직| 구분 | BFS (너비 우선 탐색) |
|---|---|
| 핵심 질문 | "A에서 B까지 가는 최단 경로 / 최소 시간은?" |
| 데이터 구조 | 2차원 격자, 그래프, 상태 트리의 연결 관계 |
| 탐색 방식 | deque(큐)를 써서 한 단계씩 동심원으로 확장 |
| 주요 키워드 | 상하좌우, 퍼짐, 미로 탈출, 최단 거리, 덩어리 |
BFS의 가장 강력한 특징은 가장 먼저 목적지에 도달하는 경로가 무조건 최단 경로라는 점입니다.
2차원 배열 상에서 물리적으로 무언가가 퍼져나가는 상황은 99% BFS 문제입니다.
deque에 시작점들을 한 번에 다 넣어두고 BFS를 돌리면 깔끔하게 해결됩니다.그래프나 2차원 배열에서 서로 붙어있는 무리를 찾는 문제(Connected Components)입니다.
꼭 2차원 지도가 아니더라도 "어떤 상태 A에서 상태 B로 바꾸는 최소 연산 횟수"를 물어볼 때도 BFS입니다.
최단 거리, 최소 시간, 퍼짐 문제를 풀 때 사용하는 템플릿입니다.
from collections import deque
def bfs(start_r, start_c, board):
n = len(board)
m = len(board[0])
# 1. 방문 여부 체크 배열 (필요시 숫자로 거리를 기록: visited[r][c] = distance)
visited = [[False] * m for _ in range(n)]
# 2. 큐 생성 및 시작점 삽입
queue = deque([(start_r, start_c)])
visited[start_r][start_c] = True
# 3. 4방향 이동 (상, 하, 좌, 우)
dr = [-1, 1, 0, 0]
dc = [0, 0, -1, 1]
# 4. 큐가 빌 때까지 탐색
while queue:
r, c = queue.popleft()
# [문제에 따른 처리 지점]: 현재 위치에서 작업 수행 (예: cnt += 1)
for i in range(4):
nr = r + dr[i]
nc = c + dc[i]
# 격자 범위 내인지 확인
if 0 <= nr < n and 0 <= nc < m:
# 이동 가능 조건 (벽이 아니고 + 방문 안 함)
if board[nr][nc] != 0 and not visited[nr][nc]:
visited[nr][nc] = True
queue.append((nr, nc))
visited를 False 대신 0으로 초기화하고, visited[nr][nc] = visited[r][c] + 1로 거리를 1씩 늘려가며 저장합니다.while문 실행 전에 큐에 시작점들을 for문으로 한 번에 다 쏟아 넣고 시작합니다.dr = [-1, 1, 0, 0], `dc = [0, 0, -1, 1]`: 상하좌우 방향 배열.0 <= nr < n and 0 <= nc < m: 인덱스 에러 방지용 범위 체크 조건문.visited 처리: 큐에 넣을 때(BFS) 또는 함수에 들어갈 때(DFS) 방문 체크.| 구분 | DFS (깊이 우선 탐색) | BFS (너비 우선 탐색) |
|---|---|---|
| 구현 방식 | 재귀 함수 또는 스택(Stack) | 큐(Queue, deque) |
| 탐색 방식 | 막힐 때까지 끝까지 깊게 탐색 | 시작점에서 동심원(넓게)으로 확장 |
| 대표 목적 | 모든 경우의 수 탐색, 백트래킹, 경로의 조건 검사 | 최단 거리 / 최소 시간 구하기 |
| 메모리 특징 | 트리의 높이(깊이)만큼 메모리 사용 | 큐에 넣는 동시 방문 수만큼 메모리 사용 |
BFS는 최단 경로를 찾으면 탐색을 중간에 끝내버리지만, DFS는 모든 가짓수를 다 확인해야 하는 문제에 적합합니다.
"A에서 B로 가는데, 지나온 칸들의 합이 K여야 한다"처럼 지나온 경로의 기록(상태)이 중요한 문제입니다.
2차원 격자판이 아니라 트리(Tree)나 순일성이 없는 그래프 구조를 탐색할 때 DFS가 훨씬 직관적입니다.
석유 시추, 섬의 개수, 단지 번호 붙이기처럼 "연결되어 있는 영역 하나를 싹 지우거나 카운트"할 때는 DFS와 BFS 둘 다 사용 가능합니다.
sys.setrecursionlimit)을 늘려주지 않으면 에러가 날 수 있어 BFS를 더 선호하기도 합니다.모든 경로 탐색, 연결된 영역 지우기, 백트래킹에 사용하는 템플릿입니다.
import sys
# 파이썬 재귀 깊이 제한 해제 (DFS 필수!)
sys.setrecursionlimit(10**6)
def dfs(r, c, board, visited):
n = len(board)
m = len(board[0])
# 1. 현재 위치 방문 처리
visited[r][c] = True
# [문제에 따른 처리 지점]: 현재 위치에서 작업 수행
# 2. 4방향 이동
dr = [-1, 1, 0, 0]
dc = [0, 0, -1, 1]
for i in range(4):
nr = r + dr[i]
nc = c + dc[i]
# 격자 범위 내인지 확인
if 0 <= nr < n and 0 <= nc < m:
# 이동 가능 조건
if board[nr][nc] != 0 and not visited[nr][nc]:
dfs(nr, nc, board, visited)
dfs()를 호출한 직후에 visited[nr][nc] = False로 다시 방문을 해제해 주는 한 줄을 추가합니다.dr = [-1, 1, 0, 0], `dc = [0, 0, -1, 1]`: 상하좌우 방향 배열.0 <= nr < n and 0 <= nc < m: 인덱스 에러 방지용 범위 체크 조건문.visited 처리: 큐에 넣을 때(BFS) 또는 함수에 들어갈 때(DFS) 방문 체크.코딩 테스트에서 날짜, 시간, 요일 관련 문제가 나오면 "가장 작은 단위(초, 분, 일)로 통일해서 단일 숫자로 바꾸는 것"이 정석이자 가장 안전한 풀이법입니다.
복잡하게 연/월/일, 시/분/초를 따로 가지고 다니면서 if문으로 60초가 넘는지, 12달이 넘는지 분기 처리를 하면 오답이 나기 매우 쉽기 때문입니다.
아날로그 시계, 기차 시간표, 영상 재생 구간(프로그래머스 '볼드 타임' 등) 문제에 적용됩니다.
def to_seconds(time_str):
h, m, s = map(int, time_str.split(':'))
return h * 3600 + m * 60 + s
주차 요금 계산, 영화 상영 시간, 대중교통 배차 간격 문제에 적용됩니다.
def to_minutes(time_str):
h, m = map(int, time_str.split(':'))
return h * 60 + m
개인정보 유효기간, D-Day 계산, 2016년 요일 맞추기 문제 등에 적용됩니다.
months = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]을 만든 뒤, 누적 합으로 구해줍니다.총 일수 % 7 (나머지 연산)을 쓰면 요일이 바로 나옵니다.total_time)로 바꾸기끝 시간 - 시작 시간), 더하기(유효기간), 비교(>=) 수행Hour = total_seconds // 3600Minute = (total_seconds % 3600) // 60Second = total_seconds % 60