


문제 분석
- 스토쿠 칸을 채우는 방법 :
1. 각 가로, 세로 줄에는 1부터 9까지의 숫자가 한 번씩만 나타난다.
2. 굵은 선으로 구분된 3x3 정사각형 안에도 1부터 9까지의 숫자가 한 번씩만 나타난다.
풀이
- 연산 횟수는 약 1초 → 연산은 약 1억 번
- 0인 값들을 찾고, 해당 값들에 대해 1~9까지 검사
- 1~9 중 없는 값에 대해 입력하는데
- 만약 입력할 수 있는 값이 여러 개이면, 다음 턴에 다시 검사
- 종료 조건
- 스도쿠 안에 0이 없으면 종료
정답 코드
1차 - 시간 초과
import sys
input = sys.stdin.readline
sudoku = [list(map(int, input().split())) for _ in range(9)]
def check(r, c, check_num) :
# 가로, 세로
for i in range(9):
if check_num == sudoku[i][c]:
return False
if check_num == sudoku[r][i] :
return False
for i in range((r // 3)*3, (r // 3)*3 + 3) :
for j in range((c // 3)*3, (c // 3)*3 + 3) :
if check_num == sudoku[i][j] :
return False
return True
# 0의 위치가 담긴 리스트 생성
sum_total = 0
while True :
# for i in range(9) :
# sum_total += sum(sudoku[i])
# if sum_total == 405 :
# break
li_0 = []
for i in range(9):
for j in range(9):
if sudoku[i][j] == 0:
li_0.append([i, j])
if len(li_0) == 0 :
break
for r,c in li_0 :
can_put_num = []
for ck_num in range(1, 10) :
if check(r, c, ck_num) :
can_put_num.append(ck_num)
if len(can_put_num) == 1:
sudoku[r][c] = can_put_num[0]
# 정답 출력
for i in range(9) :
print(' '.join(map(str, sudoku[i])))
2차 - 시간초과
import sys
input = sys.stdin.readline
sudoku = [list(map(int, input().split())) for _ in range(9)]
def check(r, c, check_num) :
# 가로, 세로
for i in range(9):
if check_num == sudoku[i][c]:
return False
if check_num == sudoku[r][i] :
return False
# 3x3
for i in range((r // 3)*3, (r // 3)*3 + 3) :
for j in range((c // 3)*3, (c // 3)*3 + 3) :
if check_num == sudoku[i][j] :
return False
return True
li_0 = []
for i in range(9):
for j in range(9):
if sudoku[i][j] == 0:
li_0.append([i, j])
while len(li_0) != 0 :
for r,c in li_0 :
can_put_num = []
for ck_num in range(1, 10) :
if check(r, c, ck_num) :
can_put_num.append(ck_num)
if len(can_put_num) == 1:
sudoku[r][c] = can_put_num[0]
del li_0[li_0.index([r,c])]
# 정답 출력
for i in range(9) :
print(' '.join(map(str, sudoku[i])))
현재 방식은 백트래킹이 아닌 단순한 가능 숫자 리스트를 채우는 방식
여러번 리스트를 순회하며 가능한 숫자를 찾고 제거하는 방식으로 비효율적이다.
- 불필요한 방식 줄이기
- 따라서 현재 코드에서 li_0 리스트를 여러 번 순회하면서 가능한 숫자를 찾고, 그 중 유일한 값만을 채워 넣고 다시 반복하는 방식 사용
- 하지만 한 번의 루프에서 li_0을 모두 탐색하고 단일 숫자가 확정된 경우를 찾는 방식은 불필요한 연산을 증가시킨다.
- 백트래킹 도입
- 스도쿠 문제는 일반적으로 백트래킹을 이용하여 해결하는 것이 가장 효율적이다.
- 모든 경우를 탐색하는 것이 아니라 가능한 숫자를 하나씩 채워보면서 유망하지 않은 경우를 즉시 제거할 수 있다.
→ 백트래킹을 활용하여 DFS 재귀 방식 적용
3차 - 시간 초과
import sys
input = sys.stdin.readline
sudoku = [list(map(int, input().split())) for _ in range(9)]
blank = []
for i in range(9) :
for j in range(9) :
if sudoku[i][j] == 0 :
blank.append((i,j))
def check(r, c, ch_num) :
# 가로, 세로
for i in range(9) :
if sudoku[r][i] == ch_num or sudoku[i][c] == ch_num :
return False
# 3x3
x = (r//3) * 3
y = (c//3) * 3
for i in range(x, x+3) :
for j in range(y, y+3) :
if sudoku[i][j] == ch_num :
return False
return True
# dfs & 백트래킹 : 한 칸씩 숫자를 채워가면서 유효한 경우만 탐색 - 불가능하면 되돌림
def solve(idx) :
if idx == len(blank) : # 모든 빈 칸을 다 채우면 종료
# 스도쿠 안의 0이 더 이상 없으면
for i in range(9) :
print(' '.join(map(str, sudoku[i])))
sys.exit(0)
r,c = blank[idx]
for num in range(1,10) :
if check(r, c, num) : # 가로, 세로, 3x3 통과
sudoku[r][c] = num
solve(idx+1)
sudoku[r][c] = 0
solve(0)
check 부분을 쪼개볼까..
4차 - 시간 초과 : 쪼개는게 의미 없긴함..
import sys
input = sys.stdin.readline
sudoku = [list(map(int, input().split())) for _ in range(9)]
blank = []
for i in range(9) :
for j in range(9) :
if sudoku[i][j] == 0 :
blank.append((i,j))
# def check(r, c, ch_num) :
# # 가로, 세로
# for i in range(9) :
# if sudoku[r][i] == ch_num or sudoku[i][c] == ch_num :
# return False
# # 3x3
# x = (r//3) * 3
# y = (c//3) * 3
# for i in range(x, x+3) :
# for j in range(y, y+3) :
# if sudoku[i][j] == ch_num :
# return False
# return True
def ch_row(r, ch_num) :
for i in range(9) :
if sudoku[r][i] == ch_num :
return False
return True
def ch_col(c, ch_num) :
for i in range(9) :
if sudoku[i][c] == ch_num :
return False
return True
def ch_rect(r, c, ch_num) :
x = (r//3) * 3
y = (c//3) * 3
for i in range(x, x+3) :
for j in range(y, y+3) :
if sudoku[i][j] == ch_num :
return False
return True
# dfs & 백트래킹 : 한 칸씩 숫자를 채워가면서 유효한 경우만 탐색 - 불가능하면 되돌림
def solve(idx) :
if idx == len(blank) : # 모든 빈 칸을 다 채우면 종료
# 스도쿠 안의 0이 더 이상 없으면
for i in range(9) :
print(' '.join(map(str, sudoku[i])))
sys.exit(0)
r,c = blank[idx]
for num in range(1,10) :
if ch_row(r, num) and ch_col(c, num) and ch_rect(r,c,num) : # 가로, 세로, 3x3 통과
sudoku[r][c] = num
solve(idx+1)
sudoku[r][c] = 0
solve(0)
5차 - pypy 로 제출하니까 통과됨
import sys
input = sys.stdin.readline
sudoku = [list(map(int, input().split())) for _ in range(9)]
blank = []
for i in range(9) :
for j in range(9) :
if sudoku[i][j] == 0 :
blank.append((i,j))
def check(r, c, ch_num) :
# 가로, 세로
for i in range(9) :
if sudoku[r][i] == ch_num or sudoku[i][c] == ch_num :
return False
# 3x3
x = (r//3) * 3
y = (c//3) * 3
for i in range(x, x+3) :
for j in range(y, y+3) :
if sudoku[i][j] == ch_num :
return False
return True
# def ch_row(r, ch_num) :
# for i in range(9) :
# if sudoku[r][i] == ch_num :
# return False
# return True
#
# def ch_col(c, ch_num) :
# for i in range(9) :
# if sudoku[i][c] == ch_num :
# return False
# return True
#
# def ch_rect(r, c, ch_num) :
# x = (r//3) * 3
# y = (c//3) * 3
# for i in range(x, x+3) :
# for j in range(y, y+3) :
# if sudoku[i][j] == ch_num :
# return False
# return True
# dfs & 백트래킹 : 한 칸씩 숫자를 채워가면서 유효한 경우만 탐색 - 불가능하면 되돌림
def dfs(n) :
if n == len(blank) : # 모든 빈 칸을 다 채우면 종료
# 스도쿠 안의 0이 더 이상 없으면
for i in range(9) :
print(' '.join(map(str, sudoku[i])))
sys.exit(0)
r,c = blank[n]
for num in range(1,10) :
if check(r, c, num) : # 가로, 세로, 3x3 통과
sudoku[r][c] = num
dfs(n+1)
sudoku[r][c] = 0
dfs(0)
2회차 - 25.02.19
1차 - 시간초과
import sys
input = sys.stdin.readline
sudoku = [list(map(int, input().split())) for _ in range(9)]
# 0인 값들 찾기
lo_zero = []
for i in range(9) :
for j in range(9) :
if sudoku[i][j] == 0 :
lo_zero.append([i,j])
def check(r, c, ch_num) : # 현재 0의 위치 & 체크할 숫자
for i in range(9) :
# 가로
if sudoku[r][i] == ch_num :
return False
# 세로
if sudoku[i][c] == ch_num :
return False
# 3x3
nr = (r//3) * 3
nc = (c//3) * 3
for i in range(nr, nr+3) :
for j in range(nc, nc+3) :
if sudoku[i][j] == ch_num :
return False
return True
def dfs(n) :
if n == len(lo_zero) : # 도든 0의 자리를 다 체크
for i in range(9) : # 출력 및 종료
print(' '.join(map(str, sudoku[i])))
exit(0)
r, c = lo_zero[n] # 아직 다 체크하지 않았다면 현재
for num in range(1, 10) :
if check(r, c, num) :
sudoku[r][c] = num # 숫자를 채우고
dfs(n+1) # 다음 칸으로 이동
sudoku[r][c] = 0 # 선택한 숫자가 답이 아니라면 되돌리기 (0으로 원상복구) - 백트래킹
dfs(0) # lo_zero[0] 부터 검사
2차
시간 초과 수정 1
0의 위치를 저장(리스트 초기화)할 때
리스트를 사용하여 초기화 했더니 시간 초과
튜플 vs 리스트
튜플 ((i, j)): 불변(immutable) → 수정할 필요 없는 데이터 저장에 적합
리스트 ([i, j]): 가변(mutable) → 이후 값을 변경할 가능성이 있는 경우 유리
따라서 값 변경이 없으므로 튜플을 사용하는 것이 메모리적으로 더 효율적이다.
시간 초과 수정 2
sys.exit(0) vs exit(0)
- 코드 1: sys.exit(0)
- 코드 2: exit(0)
- sys.exit(0)과 exit(0)의 차이:
- sys.exit(0): sys 모듈의 함수를 사용하여 프로그램을 종료 → 일반적으로 더 권장됨
- exit(0): 내장 함수지만, sys.exit()와 다르게 인터프리터 환경에서는 SystemExit 예외를 발생시키지 않음
→ 스크립트가 아닌 인터프리터 환경에서는 exit(0)이 동작하지 않을 수 있음
시간 초과 수정 3
세로 & 가로 한 번에 체크
애초에 3x3 계산하는 부분이 잘못되었음..
import sys
input = sys.stdin.readline
sudoku = [list(map(int, input().split())) for _ in range(9)]
blank = []
for i in range(9):
for j in range(9):
if sudoku[i][j] == 0:
blank.append((i, j))
def check(r, c, ch_num): # 현재 0의 위치 & 체크할 숫자
for i in range(9):
if sudoku[r][i] == ch_num or sudoku[i][c] == ch_num:
return False
# 3x3
x = (r // 3) * 3
y = (c // 3) * 3
for i in range(x, x + 3):
for j in range(y, y + 3):
if sudoku[i][j] == ch_num:
return False
return True
def dfs(n):
if n == len(blank): # 도든 0의 자리를 다 체크
for i in range(9): # 출력 및 종료
print(' '.join(map(str, sudoku[i])))
sys.exit(0)
r, c = blank[n] # 아직 다 체크하지 않았다면 현재
for num in range(1, 10):
if check(r, c, num):
sudoku[r][c] = num # 숫자를 채우고
dfs(n + 1) # 다음 칸으로 이동
sudoku[r][c] = 0 # 선택한 숫자가 답이 아니라면 되돌리기 (0으로 원상복구) - 백트래킹
dfs(0) # lo_zero[0] 부터 검사
백트래킹 핵심
각 단계에서 가능한 숫자를 하나 넣고, 다음 빈칸을 재귀적으로 탐색하면서 답을 찾는 방식이다.
만약 현재 선택한 숫자가 정답이 아니라면, 되돌리기(백트래킹) 해서 다른 숫자 시도
🔥 동작 원리 (백트래킹)
for num in range(1, 10): # 1부터 9까지 넣을 수 있는 숫자 후보 탐색
if check(r, c, num): # 현재 숫자가 유효한지 확인
chess[r][c] = n # 숫자 채우기
dfs(n + 1) # 다음 빈칸으로 이동 (재귀 호출)
chess[r][c] = 0 # 되돌리기 (백트래킹)
🔍 동작 흐름 (예제)
빈칸이 아래와 같은 위치에 있다고 가정하자.
5 3 0 | 0 7 0 | 0 0 0
6 0 0 | 1 9 5 | 0 0 0
0 9 8 | 0 0 0 | 0 6 0
---------------------
8 0 0 | 0 6 0 | 0 0 3
4 0 0 | 8 0 3 | 0 0 1
7 0 0 | 0 2 0 | 0 0 6
---------------------
0 6 0 | 0 0 0 | 2 8 0
0 0 0 | 4 1 9 | 0 0 5
0 0 0 | 0 8 0 | 0 7 9
🚀 백트래킹 탐색 과정
- 첫 번째 빈칸 (0,2)에 1을 넣고 진행
- 다음 빈칸 (0,3)에 1을 넣었더니 규칙을 위반하면 되돌아감 (chess[0][3] = 0)
- (0,3)에 2를 넣고 진행, 다시 다음 칸으로 이동
- 만약 나중에 틀린 숫자가 발견되면 되돌아가서 다른 숫자를 시도
- 마지막 빈칸까지 채워지면 종료
💡 백트래킹을 왜 하는 걸까?
- 스도쿠는 정답이 하나일 수도 있지만, 없을 수도 있고, 여러 개일 수도 있다.
- 모든 가능한 경우를 탐색하면서 정답을 찾아야 하기 때문에 백트래킹 필요
- 현재 숫자가 맞다고 가정하고 진행했다가, 나중에 문제가 생기면 되돌아가서 다시 다른 숫자로 시도하는 방식
🔥 백트래킹 동작을 코드로 표현하면?
아래 그림처럼 재귀적으로 진행하다가, 틀린 경우 되돌아가면서 다시 다른 숫자로 채우는 방식이야.
1을 넣음 → 다음 칸 이동 → 2를 넣음 → 다음 칸 이동 → 만약 안 맞으면 되돌아감
↓ ↓ ↓
(0,2)=1 → (0,3)=2 → (0,4)=3 → 만약 틀림? 되돌아가기
↑ ↑
(0,3) 되돌리고 3 넣어보기 → (0,4)도 되돌리고 4 넣어보기
✅ 핵심 정리
- 빈칸을 채울 수 있는 숫자를 하나씩 넣고 (chess[r][c] = n)
- 다음 빈칸을 dfs()로 재귀 호출해서 탐색
- 만약 나중에 규칙을 위반하면 chess[r][c] = 0 으로 되돌려서 다른 숫자로 시도
- 모든 빈칸을 채우면 정답이므로 출력하고 종료 (exit(0))
- 결국 "가능한 모든 경우를 탐색하되, 중간에 잘못된 경우는 빨리 되돌리는" 백트래킹 방식! 🚀🔥
'코딩 테스트 > 백준' 카테고리의 다른 글
[Python] 백준 2839 설탕 배달 (0) | 2025.02.20 |
---|---|
[Python] 백준 2178 미로 탐색 (0) | 2025.02.19 |
[Python] 백준 11725 트리의 부모 찾기 (0) | 2025.02.14 |
[Python] 백준 1991 트리 순회 (0) | 2025.02.14 |
[Python] 백준 15989 1,2,3 더하기 4 (0) | 2025.02.13 |