💻 백준 9663번: N-Queen
- 문제 링크: https://www.acmicpc.net/problem/9663
- 알고리즘 분류: 백트래킹
문제 소개 🧐
N-Queen 문제는 크기가 N × N인 체스판 위에 퀸 N개를 서로 공격할 수 없게 놓는 문제다. N이 주어졌을 때, 퀸을 놓는 방법의 수를 구하는 프로그램을 작성해야 했다.
입력 📝
- 첫째 줄에 N이 주어진다. (1 ≤ N < 15)
출력 📤
- 첫째 줄에 퀸 N개를 서로 공격할 수 없게 놓는 경우의 수를 출력한다.
예제 ⌨️
입력
8
출력
92
첫 번째 시도: 클래식 백트래킹 👊
Idea
전통적인 백트래킹 방식으로 문제를 해결하려 했다. 각 행마다 퀸을 하나씩 놓으면서, 다음 행으로 이동할 때 이전에 놓인 퀸들과 충돌하는지 확인하는 방식이었다.
cols
: 특정 열에 퀸이 이미 놓였는지 확인했다.diag1
:row + col
값이 같은 대각선 라인을 확인했다.diag2
:row - col
값이 같은 대각선 라인을 확인했다.
이 세 가지 조건을 모두 만족하는 경우에만 퀸을 놓고, 다음 행으로 재귀 호출을 진행했다.
Code
import sys
n = int(sys.stdin.readline().rstrip())
cols = set() # 사용 중인 열
diag1 = set() # 아래 대각선 (row+col)
diag2 = set() # 위 대각선 (row-col)
def backtrack(row, N):
if row == N:
return 1
count = 0
for col in range(N):
if col in cols or (row + col) in diag1 or (row - col) in diag2: # 행, 열, 대각선 검사
continue
cols.add(col)
diag1.add(row + col)
diag2.add(row - col)
count += backtrack(row + 1, N)
# 백트랙 한번이 성공적으로 끝났으니깐 상태 복원
cols.remove(col)
diag1.remove(row + col)
diag2.remove(row - col)
return count
print(backtrack(0, N=n))
Result
- 결과: 시간 초과 😭
- 분석: Python의 실행 속도와 재귀 호출의 오버헤드로 인해 N이 커질수록 시간이 기하급수적으로 늘어나는 것 같았다.
두 번째 시도: 비트마스킹 최적화 ✨
Idea
시간 초과 문제를 해결하기 위해 AI에게 조언을 구해 비트마스킹을 활용한 최적화 방법을 적용했다. Python도 비트 연산은 C언어 수준으로 빠르다는 점을 이용했다.
cols
,diag1
,diag2
를 정수(비트마스크)로 사용하여 퀸의 위치를 기록했다.available
변수는 현재 행에서 퀸을 놓을 수 있는 모든 위치를 비트마스크로 나타냈다.pos = available & -available
연산으로 가장 오른쪽에 있는 ‘1’ 비트(놓을 수 있는 가장 낮은 인덱스의 열)를 찾아냈다.- 재귀 호출 시, 대각선 비트마스크는
<< 1
과>> 1
시프트 연산을 통해 다음 행의 공격 위치를 효율적으로 계산했다.
Code
import sys
# 표준 입력으로부터 N 읽기
n = int(sys.stdin.readline().rstrip())
# 백트래킹 함수
# row: 현재 퀸을 놓을 행
# cols: 현재 점유된 열 비트마스크
# diag1: ↘ 대각선 점유 비트마스크
# diag2: ↗ 대각선 점유 비트마스크
# N: 체스판 크기
def backtrack(row, cols, diag1, diag2, N):
# 종료 조건: 모든 행에 퀸 배치 완료
if row == N:
return 1 # 하나의 해답 완성
count = 0
# 가능한 위치 계산:
# 1 << N - 1 => N개의 1비트 생성 (0~N-1 열 모두 가능)
# ~(cols | diag1 | diag2) => 이미 공격받는 위치는 제외
available = ((1 << N) - 1) & ~(cols | diag1 | diag2)
# 가능한 위치가 남아 있는 동안 반복
while available:
# 가장 오른쪽 1비트 선택 (최하위 비트)
# pos는 현재 행에서 퀸을 놓을 위치를 나타내는 비트마스크
pos = available & -available
# 선택한 위치 제거 → 다음 반복에서 다른 위치 시도
available -= pos
# 재귀 호출: 다음 행으로 이동
# cols | pos: 현재 열 점유 상태 업데이트
# (diag1 | pos) << 1: 아래 대각선 ↘ 점유 상태 업데이트 (행 증가하면 좌우 이동)
# (diag2 | pos) >> 1: 위 대각선 ↗ 점유 상태 업데이트 (행 증가하면 좌우 이동)
count += backtrack(row + 1,
cols | pos,
(diag1 | pos) << 1,
(diag2 | pos) >> 1,
N)
return count # 현재 분기에서 나온 해답 개수 반환
# 함수 호출 및 결과 출력
print(backtrack(0, 0, 0, 0, n))
Result
- 결과: 성공! 🎉
- 분석: 비트 연산을 통해
set
자료구조를 사용할 때보다 훨씬 빠른 속도로 충돌 검사를 수행할 수 있었다.
개선할 부분 및 회고 🤔
- 비트마스킹을 통해 위치 관련 연산을 처리하는 아이디어가 매우 신선했다. 시간 복잡도가 너무 커지는 문제에서 이 방법을 고려해봐야겠다.
- 같은 로직이라도 자료구조와 구현 방식에 따라 성능이 크게 달라질 수 있다는 점을 다시 한번 깨달았다.
- 솔직히 AI 에게 벽을 느꼈다…
- 관련해서 따로 조사 후 포스팅을 올렸다. (click)
⁂