홈 LeetCode. 37. Sudoku Solver
글
취소

LeetCode. 37. Sudoku Solver

image

유효한 스도쿠 보드에는 해가 하나만 있습니다. 각 행, 열, 3 × 3 박스에 1부터 9까지의 숫자가 정확히 한 번씩 들어가도록 빈 칸을 제자리에서 채웁니다.

문제 링크

접근 방법

각 행, 열, 박스마다 이미 사용 중인 숫자를 9비트 마스크로 기록합니다. 비트 d는 숫자 d + 1을 나타내며, 비트가 설정되어 있으면 해당 칸에 그 숫자를 놓을 수 없습니다. 따라서 빈 칸에 놓을 수 있는 숫자는 전체 마스크 중 해당 행·열·박스의 마스크에 모두 포함되지 않은 비트입니다.

재귀 단계마다 남은 빈 칸을 살펴보고 놓을 수 있는 숫자가 가장 적은 칸을 선택합니다(최소 잔여값, MRV). 제약이 강한 칸부터 선택하면 분기 수를 일찍 줄일 수 있습니다. 가능한 숫자가 없는 칸이 있으면 현재 탐색 경로는 실패입니다. 그렇지 않으면 각 후보를 차례로 시도합니다. 보드에 숫자를 쓰고 해당 행·열·박스의 마스크를 설정한 뒤 재귀 호출합니다. 경로가 실패하면 비트를 지우고 칸을 '.'로 되돌립니다. 성공하면 즉시 반환하므로 완성된 해가 입력 보드에 그대로 남습니다.

빈 칸은 최대 81개이며, 최악의 경우 탐색은 지수 시간입니다(각 빈 칸에서 최대 9개 선택). MRV는 모순을 일찍 찾고 분기 수를 줄여 실제 탐색을 효율적으로 만듭니다. 보조 공간은 재귀 스택과 고정 크기 마스크를 포함해 O(81)이고, 보드는 제자리에서 수정합니다.

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class Solution {
    private static final int FULL = (1 << 9) - 1;

    private char[][] board;
    private final int[] rows = new int[9];
    private final int[] columns = new int[9];
    private final int[] boxes = new int[9];

    public void solveSudoku(char[][] board) {
        for (int i = 0; i < 9; i++) {
            rows[i] = 0;
            columns[i] = 0;
            boxes[i] = 0;
        }
        this.board = board;
        for (int row = 0; row < 9; row++) {
            for (int column = 0; column < 9; column++) {
                char cell = board[row][column];
                if (cell != '.') {
                    int bit = 1 << (cell - '1');
                    int box = (row / 3) * 3 + column / 3;
                    rows[row] |= bit;
                    columns[column] |= bit;
                    boxes[box] |= bit;
                }
            }
        }
        solve();
    }

    private boolean solve() {
        int bestRow = -1;
        int bestColumn = -1;
        int bestCandidates = 0;
        int fewestChoices = 10;

        for (int row = 0; row < 9; row++) {
            for (int column = 0; column < 9; column++) {
                if (board[row][column] != '.') {
                    continue;
                }

                int box = (row / 3) * 3 + column / 3;
                int candidates = FULL & ~(rows[row] | columns[column] | boxes[box]);
                int choices = Integer.bitCount(candidates);
                if (choices < fewestChoices) {
                    bestRow = row;
                    bestColumn = column;
                    bestCandidates = candidates;
                    fewestChoices = choices;
                    if (choices <= 1) {
                        break;
                    }
                }
            }
            if (fewestChoices <= 1) {
                break;
            }
        }

        if (bestRow == -1) {
            return true;
        }
        if (bestCandidates == 0) {
            return false;
        }

        int box = (bestRow / 3) * 3 + bestColumn / 3;
        while (bestCandidates != 0) {
            int bit = bestCandidates & -bestCandidates;
            bestCandidates -= bit;
            int digit = Integer.numberOfTrailingZeros(bit);

            board[bestRow][bestColumn] = (char) ('1' + digit);
            rows[bestRow] |= bit;
            columns[bestColumn] |= bit;
            boxes[box] |= bit;

            if (solve()) {
                return true;
            }

            rows[bestRow] &= ~bit;
            columns[bestColumn] &= ~bit;
            boxes[box] &= ~bit;
            board[bestRow][bestColumn] = '.';
        }
        return false;
    }
}
이 글은 저자가 CC BY 4.0 라이선스로 배포합니다.