[PS] BOJ 4056 / 스-스-스도쿠
문제 링크: https://www.acmicpc.net/problem/4056
Thumbnail: Photo by Luna Lee (Unsplash)
스도쿠를 풀어봅시다. 백트래킹으로요.
풀이
스도쿠의 규칙은 다음과 같습니다.
- 같은 행에는 1~9까지의 수가 1개씩만 포함됩니다.
- 같은 열에는 1~9까지의 수가 1개씩만 포함됩니다.
- 같은 칸(3x3)에는 1~9까지의 수가 1개씩만 포함됩니다.
이 규칙을 기반으로, 백트래킹을 진행하면서 스도쿠를 채웁니다.
스도쿠 입력
문제 입력에 대해 주의해야 할 점이 몇가지 있습니다.
- 항상 빈칸이 5개인 스도쿠만 입력으로 주어진다.
- 잘못된 스도쿠 (숫자 중복) 또는 풀 수 없는 스도쿠가 주어질 수 있다.
- 반드시 정답이 유일한 스도쿠만 입력으로 주어진다.
따라서, 백트래킹을 진행하기 전에 먼저 입력 받은 스도쿠 데이터를 검증해야 합니다.
sudoku = [[0 for _ in range(9)] for _ in range(9)]
rows = [[False for _ in range(10)] for _ in range(9)]
cols = [[False for _ in range(10)] for _ in range(9)]
grids = [[False for _ in range(10)] for _ in range(9)]
blanks = [None] * 5 # [(r, c)] 형태로 빈칸의 좌표를 저장한다.
def handle_input():
"""입력값을 읽어 sudoku, rows, cols, grids, blanks를 초기화합니다."""
data = [input().strip() for _ in range(9)] # 반드시 입력을 모두 받아둬야 한다.
# 스도쿠 입력 및 상태변수 갱신
blank_cnt = 0
for r in range(9):
for c, v in enumerate(map(int, data[r])):
sudoku[r][c] = v
if v == 0:
blanks[blank_cnt] = (r, c)
blank_cnt += 1
else:
grids_idx = (r // 3) * 3 + (c // 3)
if rows[r][v] or cols[c][v] or grids[grids_idx][v]:
return False # 잘못된 입력 처리
rows[r][v] = True
cols[c][v] = True
grids[grids_idx][v] = True
return True입력 데이터 검증하기
입력 데이터 검증시 주의할 것
이전에는 아래와 같이 입력 데이터를 검증했습니다. 위 코드와 차이점이라면, 입력을 미리 받지 않고 그때그때 받으면서 검사한 것인데, 만약 잘못된 입력이 발견될 경우 즉시 return되면서 이전 테스트 케이스의 입력 데이터가 다 처리되지 못하고 남아있게 됩니다.
이 경우, 남아 있는 입력 데이터로 인해 전체 입력이 밀려서 처리되어 정상적으로 동작하지 않게 됩니다. 따라서, 위 코드처럼 입력 데이터를 반드시 다 읽은 뒤 함수를 종료하도록 해야 합니다.
sudoku = [[0 for _ in range(9)] for _ in range(9)]
rows = [[False for _ in range(10)] for _ in range(9)]
cols = [[False for _ in range(10)] for _ in range(9)]
grids = [[False for _ in range(10)] for _ in range(9)]
blanks = [None] * 5 # [(r, c)] 형태로 빈칸의 좌표를 저장한다.
def handle_input():
"""입력값을 읽어 sudoku, rows, cols, grids, blanks를 초기화합니다."""
# 스도쿠 입력 및 상태변수 갱신
blank_cnt = 0
for r in range(9):
for c, v in enumerate(map(int, input().strip())):
sudoku[r][c] = v
if v == 0:
blanks[blank_cnt] = (r, c)
blank_cnt += 1
else:
grids_idx = (r // 3) * 3 + (c // 3)
if rows[r][v] or cols[c][v] or grids[grids_idx][v]:
return False # 잘못된 입력 처리
rows[r][v] = True
cols[c][v] = True
grids[grids_idx][v] = True
return True잘못된 입력 데이터 검증 방식
백트래킹으로 스도쿠 풀기
이제 백트래킹으로 입력 받은 스도쿠를 풀어야 합니다.
기본적인 풀이는 [PS] BOJ 2239 / 스도쿠와 유사하니, 자세한 설명은 이 글을 참고해주세요 😉
백트래킹을 통해 5개의 빈칸을 순서대로 채우는데, 언제나 정답이 유일한 스도쿠만 입력으로 주어지므로 정답을 찾는 즉시 탐색을 종료해도 됩니다.
이를 위해서, found라는 상태 변수를 도입해 백트래킹 전반에 적용했습니다. 또, 탐색 결과는 그 즉시 sudoku배열에 채우도록 해 불필요한 배열 사용을 줄였습니다.
def backtracking(depth):
global found
if found: # 이미 정답을 찾았으므로 더 이상 탐색하지 않습니다.
return
if depth == 5:
found = True
return
r, c = blanks[depth]
grids_idx = (r // 3) * 3 + (c // 3)
for v in range(1, 10):
if not found and not rows[r][v] and not cols[c][v] and not grids[grids_idx][v]:
# 분기 변수 설정
rows[r][v] = True
cols[c][v] = True
grids[grids_idx][v] = True
sudoku[r][c] = v
# DFS (백트래킹)
backtracking(depth + 1)
if found:
return
# 분기 변수 초기화
rows[r][v] = False
cols[c][v] = False
grids[grids_idx][v] = False
sudoku[r][c] = 0백트래킹으로 스도쿠 풀기
전체 테스트 케이스 처리
이 과정을 $T$개의 테스트 케이스에 대해 반복해야 합니다. 따라서, 매 테스트 케이스마다 상태 변수들을 초기화 해줘야 합니다. 또, 각 테스트 케이스의 답 사이에는 개행을 두고 출력해야 합니다.
for _ in range(int(input())):
# 상태 변수 초기화
found = False
for idx in range(9):
for number in range(1, 10):
rows[idx][number] = False
cols[idx][number] = False
grids[idx][number] = False
# 초기 입력 검증
is_correct = handle_input()
if not is_correct:
print("Could not complete this grid.\n")
continue
# 백트래킹으로 스도쿠 풀기
backtracking(0)
# 정답에 따라 결과 처리하기
if found:
# 결과 출력하기
print("\n".join("".join(map(str, row)) for row in sudoku))
else:
print("Could not complete this grid.")
print()전체 코드
input = open(0).readline
sudoku = [[0 for _ in range(9)] for _ in range(9)]
rows = [[False for _ in range(10)] for _ in range(9)]
cols = [[False for _ in range(10)] for _ in range(9)]
grids = [[False for _ in range(10)] for _ in range(9)]
blanks = [None] * 5 # [(r, c)] 형태로 빈칸의 좌표를 저장한다.
def handle_input():
"""입력값을 읽어 sudoku, rows, cols, grids, blanks를 초기화합니다."""
blank_cnt = 0
data = [input().strip() for _ in range(9)]
# 스도쿠 입력 및 상태변수 갱신
for r in range(9):
for c, v in enumerate(map(int, data[r])):
sudoku[r][c] = v
if v == 0:
blanks[blank_cnt] = (r, c)
blank_cnt += 1
else:
grids_idx = (r // 3) * 3 + (c // 3)
if rows[r][v] or cols[c][v] or grids[grids_idx][v]:
return False # 잘못된 입력 처리
rows[r][v] = True
cols[c][v] = True
grids[grids_idx][v] = True
return True
def backtracking(depth):
global found
if found: # 이미 정답을 찾았으므로 더 이상 탐색하지 않습니다.
return
if depth == 5:
found = True
return
r, c = blanks[depth]
grids_idx = (r // 3) * 3 + (c // 3)
for v in range(1, 10):
if not found and not rows[r][v] and not cols[c][v] and not grids[grids_idx][v]:
# 분기 변수 설정
rows[r][v] = True
cols[c][v] = True
grids[grids_idx][v] = True
sudoku[r][c] = v
# DFS (백트래킹)
backtracking(depth + 1)
if found:
return
# 분기 변수 초기화
rows[r][v] = False
cols[c][v] = False
grids[grids_idx][v] = False
sudoku[r][c] = 0
for _ in range(int(input())):
# 상태 변수 초기화
found = False
for idx in range(9):
for number in range(1, 10):
rows[idx][number] = False
cols[idx][number] = False
grids[idx][number] = False
# 초기 입력 검증
is_correct = handle_input()
if not is_correct:
print("Could not complete this grid.\n")
continue
# 백트래킹으로 스도쿠 풀기
backtracking(0)
# 정답에 따라 결과 처리하기
if found:
# 결과 출력하기
print("\n".join("".join(map(str, row)) for row in sudoku))
else:
print("Could not complete this grid.")
print()
solution.py