N-Queens II
체스판의 퀸은 아무리 멀리 떨어져 있어도 같은 행과 열, 그리고 두 대각선 위의 모든 칸을 공격합니다. 정수 n이 주어집니다. 서로 공격하지 않도록 n × n 체스판에 n개의 퀸을 놓는 방법의 수를 반환하세요.
한 방법에서는 어떤 칸에 퀸이 있고 다른 방법에서는 그 칸이 비어 있다면 두 방법은 서로 다릅니다. 따라서 체스판과 그 거울상은 서로 비슷해 보여도 두 가지 방법으로 셉니다.
함수
- ninteger
- 보드의 크기와 퀸의 수
- 반환값integer
- 어느 퀸도 다른 퀸을 공격하지 않도록 퀸을 배치하는 방법의 수
제약 조건
1 ≤ n ≤ 12n = 12의 답은 14,200이므로 32비트 정수에 들어갑니다.
예제
- 입력
- n = 4
- 출력
- 2
- 설명
- 각 행의 퀸이 있는 열을 위에서 아래로 적으면 두 보드는
1, 3, 0, 2와2, 0, 3, 1입니다. 각 보드는 서로의 거울상이며, 두 가지 경우로 셉니다. 다른 모든 선택에서는 두 퀸이 같은 열이나 대각선에 놓입니다.
- 입력
- n = 3
- 출력
- 0
- 설명
- 왼쪽 위 모서리에 퀸을 놓으면 가운데 행의 오른쪽 끝 칸만 남고, 그러면 아래쪽 행에는 안전한 칸이 없습니다. 오른쪽 위 모서리도 같은 이유로 불가능하며, 위쪽 가운데에 퀸을 놓으면 가운데 행의 세 칸을 모두 공격합니다. 따라서 가능한 보드는 없습니다.
제출 시 숨은 테스트 +10개
후속 질문
보드를 회전하거나 거울 대칭해도 서로 다른 보드만 셀 수 있나요? n = 8일 때, 92개의 보드는 이러한 12개의 그룹으로 나뉩니다.
힌트
하나씩 열어 보세요. 열 때마다 조금 더 알려 줍니다.
같은 행에 있는 두 퀸은 서로를 공격하므로, 각 행에는 정확히 하나의 퀸이 놓입니다. 이 사실을 알고 나면 무엇을 선택하면 될까요?
보드를 위에서부터 한 번에 한 행씩 채우세요. 새 퀸이 공격받는 즉시 그 부분 보드를 포기하세요. 아래에 무엇을 더 추가해도 이를 해결할 수 없기 때문입니다. 보드 전체를 살펴보지 않고도 칸을 확인하려면, 이미 퀸이 있는 열과 대각선을 기억하세요. 한쪽 대각선 방향에서는 모든 칸의
row + col값이 같고, 다른 쪽에서는row - col값이 같습니다.place(row)를 작성하세요. 이 함수는 현재 상태에서 완성할 수 있는 전체 보드의 수를 반환합니다.row == n일 때는 1을 반환합니다. 그렇지 않으면 열,row + c대각선,row - c대각선이 모두 비어 있는 모든 열c를 시도합니다. 세 곳을 표시하고, 누적 합계에place(row + 1)을 더한 다음 표시를 해제합니다. 정답은place(0)입니다.
풀이
각 행에서 하나의 열을 선택하면 배치가 정해집니다. 같은 행에 있는 두 퀸은 항상 서로를 공격하기 때문입니다. 그래도 선택지는 n^n개이며, n = 12일 때 약 8.9 × 10^12개이므로 모두 나열할 수는 없습니다. 두 가지 아이디어로 이 문제를 해결할 수 있습니다. 행마다 보드를 구성하면서 퀸이 공격받는 순간 부분 보드를 포기하면, n = 12일 때 탐색 대상이 백만 개 미만의 부분 보드로 줄어듭니다. 그리고 어떤 열과 대각선이 차지되었는지 기록하면, 한 칸을 확인할 때 지금까지 놓은 모든 퀸을 훑는 대신 세 번 조회하면 됩니다.
각 행에 퀸을 하나씩 배치하는 모든 경우를 시도해 보세요
정답이지만 가장 큰 테스트에서는 끝나지 않음
핵심 아이디어
각 행에는 퀸이 정확히 하나 있어야 하므로, 배치는 cols 목록으로 나타내며 cols[r]는 r번째 행에 있는 퀸의 열입니다. 각 항목은 n개의 열 중 어느 것이든 될 수 있으므로 목록은 n^n개입니다. 주행 거리계가 세는 방식으로 모든 경우를 차례로 살펴봅니다. 마지막 항목을 1씩 늘리고, n-1을 넘으면 0으로 재설정한 뒤 그 앞 항목에 올림을 합니다.
각 목록에 대해 모든 행 쌍 i < j를 비교합니다. 두 퀸은 같은 열에 있거나(cols[i] == cols[j]) 대각선에 있을 때 서로 공격합니다. 대각선에서는 한 행 아래로 이동할 때 열이 왼쪽이나 오른쪽으로 하나 이동하므로, 두 퀸이 같은 대각선에 있는 조건은 열 차이가 행 차이와 같은 경우입니다. 즉, |cols[i] - cols[j]| == j - i입니다. 모든 쌍 검사에 통과한 목록은 유효한 보드 하나입니다. 모든 목록을 확인하므로 누락되거나 두 번 계산되는 경우는 없습니다.
이 방법은 일찍 멈추지 않기 때문에 느립니다. 처음 두 행의 퀸이 같은 대각선에 있으면 보드는 이미 실패한 것이지만, 주행 거리계는 나머지 행을 채우는 n^(n-2)가지 방법을 모두 시도합니다. n = 8이면 보드 92개를 찾기 위해 목록 16,777,216개를 검사합니다. n = 12이면 목록이 약 8.9 × 10^12개입니다. 목록 하나를 검사하는 데 1나노초가 걸린다고 해도 약 2.5시간이 걸립니다.
알고리즘
cols를 모두 0으로 시작합니다. 모든 퀸이 열 0에 있습니다.- 모든 행 쌍
i < j를 확인합니다.cols[i] == cols[j]이거나|cols[i] - cols[j]| == j - i이면 목록은 유효하지 않습니다. - 서로 충돌하는 쌍이 없으면 개수를 1 증가시킵니다.
- 계수기처럼
cols를 다음 상태로 진행합니다. 마지막 행부터 거슬러 올라가며n-1인 각 항목을 0으로 재설정한 다음, 그렇지 않은 첫 항목을 1 증가시킵니다. - 모든 항목이
n-1이었다면n^n개의 목록을 모두 확인한 것이므로 개수를 반환합니다.
def totalNQueens(n):
def is_valid(cols):
# cols[r] is the column of the queen in row r, so rows never clash.
for i in range(n):
for j in range(i + 1, n):
if cols[i] == cols[j] or abs(cols[i] - cols[j]) == j - i:
return False
return True
cols = [0] * n
count = 0
while True:
if is_valid(cols):
count += 1
# Move to the next placement, like an odometer with n digits in base n.
row = n - 1
while row >= 0 and cols[row] == n - 1:
cols[row] = 0
row -= 1
if row < 0:
return count
cols[row] += 1열 및 대각선 집합을 사용한 백트래킹
핵심 아이디어
퀸을 맨 위에서부터 행마다 배치하고, 새 퀸을 놓는 즉시 확인합니다. 공격받는다면 아래 행을 어떻게 채워도 해결할 수 없으므로 그 칸은 바로 건너뜁니다. 안전하다면 다음 행으로 재귀 호출하고, 그 호출이 반환되면 퀸을 제거한 뒤 다음 열을 시도합니다. 행 n에 도달하는 호출은 안전한 퀸 n개를 배치한 것이므로 보드 하나로 셉니다. 이것이 백트래킹이며, 탐색 공간을 크게 가지치기합니다. n = 12일 때 전체 배치 8.9 × 10^12개 대신 부분 보드 856,189개를 방문합니다.
나머지 절반은 칸을 빠르게 검사하는 것입니다. 아래 행은 비어 있고 새 퀸이 놓인 행에도 다른 퀸이 없으므로, 칸 (row, c)를 공격할 수 있는 선은 세 개뿐입니다. 열, / 대각선, \ 대각선입니다. 하나의 / 대각선에 있는 모든 칸은 0부터 2n-2까지 같은 row + c 값을 가집니다. 하나의 \ 대각선에 있는 모든 칸은 -(n-1)부터 n-1까지 같은 row - c 값을 가지므로, n-1을 더해 0부터 2n-2까지의 인덱스를 얻습니다. 플래그 배열 세 개를 유지합니다. 크기가 n인 cols와 크기가 2n-1인 diag, anti입니다. 세 플래그가 모두 꺼져 있을 때만 해당 칸은 안전합니다. 조회 세 번으로 O(1)이며, 지금까지 배치한 퀸을 하나씩 모두 비교하면 O(n)이 듭니다.
한 선에는 퀸을 최대 하나만 둘 수 있으므로, 퀸을 놓을 때 세 플래그를 켜고 제거할 때 다시 끄면 배열은 원래 상태 그대로 유지됩니다. 4 × 4 보드에서 (0, 0)에 있는 퀸은 cols[0], diag[0], anti[3]을 설정합니다. 1행의 1열은 anti[3]에 놓이므로 퀸 자체를 확인하지 않고 건너뜁니다.
첫 번째 행에는 n개의 열을 시도할 수 있고, 두 번째 행에는 많아야 n-1개를 시도할 수 있으며, 이후에도 이와 같이 줄어듭니다. 따라서 탐색은 O(n!)으로 제한되며, 대각선 조건으로 실제 탐색량은 이보다 훨씬 줄어듭니다. n = 12일 때 반복문은 총 10,103,868개의 칸을 검사합니다. 재귀 깊이는 n번 호출이며 배열에는 약 5n개의 플래그가 들어가므로, 공간 복잡도는 O(n)입니다.
알고리즘
- 모든 플래그가 꺼진 배열 세 개를 만듭니다.
n개의 항목이 있는cols와 각각2n-1개의 항목이 있는diag,anti입니다. place(row)를 작성합니다.row == n이면 1을 반환합니다. 모든 행에 안전한 퀸이 놓였다는 뜻입니다.- 그렇지 않으면 각 열
c에 대해cols[c],diag[row + c]또는anti[row - c + n - 1]가 켜져 있으면 해당 열을 건너뜁니다. - 안전한 열에서는 세 플래그를 켜고,
place(row + 1)을 합계에 더한 다음 플래그를 끕니다. - 합계를 반환합니다. 정답은
place(0)입니다.
def totalNQueens(n):
cols = [False] * n # cols[c]: column c holds a queen
diag = [False] * (2 * n - 1) # diag[r + c]: that / diagonal holds a queen
anti = [False] * (2 * n - 1) # anti[r - c + n - 1]: that \ diagonal holds a queen
def place(row):
if row == n:
return 1 # a queen in every row: one complete board
count = 0
for c in range(n):
if cols[c] or diag[row + c] or anti[row - c + n - 1]:
continue # attacked: three lookups, no scan of the board
cols[c] = diag[row + c] = anti[row - c + n - 1] = True
count += place(row + 1)
cols[c] = diag[row + c] = anti[row - c + n - 1] = False # take it back
return count
return place(0)비트마스크를 사용한 백트래킹
핵심 아이디어
집합을 사용하는 탐색은 빠르지만, 각 행에서 여전히 n개의 열을 모두 검사하며, 그중 대부분은 공격받는 칸입니다. 비트마스크를 사용하면 비어 있는 칸으로 바로 이동할 수 있습니다. 정수의 비트 c가 채우려는 행의 열 c를 나타내도록 하고, 세 가지 마스크를 유지합니다. cols는 이미 차지한 열, left는 이 행에서 한 대각선 방향으로 공격받는 칸, right는 다른 대각선 방향으로 공격받는 칸입니다.
그러면 비어 있는 칸은 하나의 식으로 구할 수 있습니다. free = ~(cols | left | right) & full이며, 여기서 full은 하위 n개 비트를 1로 설정한 값입니다. free & -free는 가장 낮은 위치의 비어 있는 칸만 남기고, 이를 빼면 다음 칸으로 이동합니다. bit 위치에 퀸을 놓고 한 행 아래로 내려가면, 해당 열은 계속 차지된 상태이고 각 대각선 공격은 한 열 옆으로 이동합니다. 따라서 다음 행에는 cols | bit, ((left | bit) << 1) & full, (right | bit) >> 1이 전달됩니다. 되돌릴 것은 없습니다. 각 호출은 자체적인 세 정수를 사용합니다. cols == full이면 n개의 퀸을 모두 놓은 것입니다.
n = 4이고 첫 번째 퀸을 열 1에 놓는다고 합시다. 열 0을 가장 오른쪽 비트로 표기하면 bit = 0010입니다. 그러면 행 1에는 cols = 0010, left = 0100, right = 0001이 전달되므로 free = 1000입니다. 열 3만 선택할 수 있으며, 열 0, 1, 2를 검사하지 않고도 찾을 수 있습니다.
탐색은 집합 버전과 동일한 부분 보드를 방문하지만, 이제 루프의 각 단계에서 퀸을 하나 놓습니다. n = 12일 때 정수 연산을 몇 번씩 수행하며 10,103,868번의 칸 검사 대신 856,188번의 단계만 거칩니다. 시간 복잡도는 여전히 O(n!)으로 제한되고, 재귀 깊이는 n번의 호출입니다. R 코드는 재귀 없이 동일한 마스크를 실행합니다. 한 행의 모든 부분 보드를 벡터에 보관하고 매번 한 행씩 모두 확장하므로, n번의 호출 대신 보드의 한 단계 전체를 메모리에 보관합니다.
알고리즘
full = (1 << n) - 1을 설정합니다. 이는 모든n개 열의 마스크입니다.count(cols, left, right)를 작성합니다.cols == full이면 1을 반환합니다.free = ~(cols | left | right) & full을 계산합니다.free가 0이 아닐 동안bit = free & -free를 구하고, 이를free에서 제거한 다음, 전체 합계에count(cols | bit, ((left | bit) << 1) & full, (right | bit) >> 1)을 더합니다.- 전체 합계를 반환합니다. 답은
count(0, 0, 0)입니다.
def totalNQueens(n):
full = (1 << n) - 1 # bit c stands for column c of the current row
def count(cols, left, right):
# cols: columns taken. left, right: squares of this row hit along a diagonal.
if cols == full:
return 1 # every column used: n queens placed
total = 0
free = full & ~(cols | left | right)
while free:
bit = free & -free # the lowest free square
free -= bit
# Moving down a row shifts each diagonal attack one column over.
total += count(cols | bit, ((left | bit) << 1) & full, (right | bit) >> 1)
return total
return count(0, 0, 0)
함정과 경계 사례
탐색 자체는 짧습니다. 대부분의 버그는 대각선 계산과 되돌리기 단계에서 발생합니다.
n-1을 더하지 않고row - c를 인덱스로 사용하는 경우입니다. Java에서는 예외가 발생하고, C에서는 배열 밖의 메모리를 읽으며, Python에서는anti[-2]가 조용히 다른 대각선의 플래그를 읽기 때문에 오류 없이 개수가 잘못 계산됩니다.- 대각선 배열의 크기를
n개 항목으로 지정하는 경우입니다.n × n보드에는 각 방향마다2n-1개의 대각선이 있습니다. - 한 방향의 대각선만 확인하거나 열만 확인하는 경우입니다. 두 방향의 대각선 모두 공격합니다.
- 재귀 호출이 반환된 후 플래그를 끄는 것을 잊는 경우입니다. 그러면 이후의 모든 분기에서 더 이상 보드에 없는 퀸을 보게 되어 개수가 줄어듭니다.
free를 계산할 때& full을 빼먹는 경우입니다.~x는 열n-1위의 모든 비트도 설정하므로 반복문이 보드 밖의 칸을 선택합니다. 또한 정수의 너비가 고정되어 있지 않은 Python이나 Ruby에서는free가 음수가 되어 반복문이 끝나지 않습니다.- 대칭인 보드를 하나의 보드로 취급하는 경우입니다. 이 문제에서는 대칭인 보드도 각각 따로 셉니다.
n = 4에는 보드가 2개 있으며, 서로 대칭입니다. - 작은 보드를 잘못 특수 처리하는 경우입니다.
n = 1에는 보드가 1개 있고,n = 2와n = 3에는 보드가 없습니다. 탐색은 특수 처리 없이도 세 경우 모두 올바르게 처리합니다.
자주 묻는 질문4
N-Queens II의 시간 복잡도는 무엇인가요?
백트래킹의 시간 복잡도는 O(n!)으로 제한됩니다. 첫 번째 행에는 n개의 선택지가 있고, 다음 행에는 최대 n-1개가 있으며, 이런 식으로 이어집니다. 대각선 검사는 이 상한보다 훨씬 적은 856,189개의 부분 보드만 탐색하도록 가지치기합니다(n = 12일 때). 해의 개수를 세는 다항 시간 방법은 알려져 있지 않으므로, 이와 같은 탐색이 표준적인 해법입니다. 공간 복잡도는 O(n)입니다.
정사각형이 어느 대각선 위에 있는지 어떻게 알 수 있나요?
/ 대각선을 따라 한 칸 이동하면 행에 1을 더하고 열에서 1을 빼므로 row + col은 변하지 않습니다. \ 대각선을 따라 이동하면 행과 열 모두에 1을 더하므로 row - col은 변하지 않습니다. 각 합은 하나의 대각선을 나타내며, 차이에 n-1을 더하면 0부터 2n-2까지의 배열 인덱스로 바뀝니다.
N-Queens와 N-Queens II의 차이점은 무엇인가요?
N-Queens는 텍스트 행으로 그린 모든 보드를 요구합니다. N-Queens II는 보드가 몇 개인지만 묻습니다. 탐색에는 동일한 백트래킹을 사용하지만, 개수를 세는 데는 보드를 메모리에 저장할 필요 없이 열 집합과 대각선 집합만 있으면 되므로 더 빠르고 가볍습니다. 따라서 여기서는 비트마스크 버전이 자연스럽습니다.
대칭성을 활용해 N-Queens II의 속도를 높일 수 있을까요?
맞습니다. 보드를 좌우로 대칭시키면 또 다른 유효한 보드가 되므로, 첫 번째 퀸이 왼쪽 절반에 있는 보드의 수는 첫 번째 퀸이 오른쪽 절반에 있는 보드의 수와 같습니다. 첫 번째 퀸이 0부터 n/2 - 1 열에 있는 보드의 수를 세고 두 배로 계산하세요. n이 홀수이면 첫 번째 퀸이 가운데 열에 있는 보드를 한 번 더하세요. 이렇게 하면 탐색량이 절반으로 줄어듭니다.
비슷한 문제
같은 아이디어를 쓰는 문제입니다. 두세 개를 풀면 패턴이 몸에 익습니다.
Python
def totalNQueens(n):
# 여기에 코드를 작성하세요케이스 1
케이스 2
입력
n = 4
기대값
2