본문 바로가기
백준 알고리즘/Java

[백준] 2580. 스도쿠 - Java

by 리버🐦‍🔥 2025. 6. 12.

https://www.acmicpc.net/problem/2580

 

백트래킹 + 재귀를 활용한 문제

 

⭐️ 중요 포인트

1. blank의 가로, 세로, 해당 blank를 포함하는 정사각형 구역에서 주어진 num이 나오지 않았는지를 판별하는 메서드 구현

2. 재귀 내에서는 1~9(스도쿠에 들어갈 수 있는 수) 중 유효한 수만 선택하며 빈칸에 채워넣는 백트래킹 방식을 사용했다.

3. 만약 blankNum이 blanks.size()에 도달하지 못했다는 것은 중간에 잘못된 값을 선택해서 마지막 빈 칸 까지 채우지 못함을 의미한다.

4. blankNum == blanks.size()가 되면 모든 빈 칸이 유효한 값으로 채워졌고, 이후 최초로 나온 경우의 수에 대해서 board를 출력하면 된다. 이 때, System.exit(0);을 통해서 프로그램을 종료해도 되지만, 필자는 main 메서드에서 프로그램이 종료되는 형식을 선호하기 때문에 endFlag를 true로 변경하고 모든 호출된 재귀 메서드에 대해서 endFlag가 세워지면 탈출하는 조건을 넣어 첫 번째 경우의 수만 출력하도록 구현하였다.

 

필자는 해당 문제를 해결하며 시간초과 문제에 부딪혔다.

아래는 시간초과가 났던 코드와 개선하여 해결한 코드이다.

 

<각 blank별로 후보가 되는 수를 미리 찾고, 해당 수의 조합을 구하며 모든 칸이 채워질 때마다 성공했는지 판단하는 코드 - 시간초과>

1. blank별 후보를 찾는데 O((blanks.size() * 9) + (blanks.size() * 각 블랭크의 후보 숫자의 개수의 누적곱))...

딱 봐도 시간복잡도가 엄청 커보인다.. 그래서 후보들을 구하지 않고 바로바로 가능한 숫자를 고르며 빈 칸을 채워넣는 형식으로 변경하였다.

import java.util.*;
import java.io.*;

public class Main {

    static class Node {
        int x;
        int y;
        Set<Integer> candidates;

        Node(int x, int y) {
            this.x = x;
            this.y = y;
            this.candidates = new HashSet<>();

            for (int i = 1; i <= 9; i++) {
                candidates.add(i);
            }
        }
    }

    static BufferedReader br;
    static BufferedWriter bw;
    static int[][] board;
    static List<Node> blanks;
    static boolean endFlag;


    static void init() throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        bw = new BufferedWriter(new OutputStreamWriter(System.out));
        board = new int[9][9];
        blanks = new ArrayList<>();
        endFlag = false;

        for (int y = 0; y < 9; y++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int x = 0; x < 9; x++) {
                board[y][x] = Integer.parseInt(st.nextToken());

                if (board[y][x] == 0) {
                    blanks.add(new Node(x, y));
                }
            }
        }
    }

    static void solve() throws IOException {
        for (int i = 0; i < blanks.size(); i++) {
            checkCandidate(blanks.get(i));
//            System.out.println(blanks.get(i).x + " " + blanks.get(i).y + " " + blanks.get(i).candidates);
        }

        recursion(0);
    }

    static void recursion(int blankNum) throws IOException {
        if (endFlag) {
            return;
        }

        if (blankNum >= blanks.size()) {
            if (checkBlanks()) {
                printBoard();
                endFlag = true;

                bw.flush();
                bw.close();
                br.close();
                System.exit(0);
            }
            return;
        }

        Node blank = blanks.get(blankNum);

        for (Integer num: blank.candidates) {
            board[blank.y][blank.x] = num;
            recursion(blankNum + 1);
            board[blank.y][blank.x] = 0;
        }
    }

    static void printBoard() throws IOException {
        for (int y = 0; y < 9; y++) {
            for (int x = 0; x < 9; x++) {
                bw.write(board[y][x] + " ");
//                if (x < 8) {
//                    bw.write(" ");
//                }
            }
            bw.newLine();
//            if (y < 8) {
//                System.out.println();
//            }
        }
    }

    static void checkCandidate(Node node) throws IOException {
        for (int i = 0; i < 9; i++) {
            node.candidates.remove(board[node.y][i]);
            node.candidates.remove(board[i][node.x]);
        }
        int startX = (node.x / 3) * 3;
        int startY = (node.y / 3) * 3;

        for (int dy = 0 ; dy < 3; dy++) {
            for (int dx = 0; dx < 3; dx++) {
                node.candidates.remove(board[startY + dy][startX + dx]);
            }
        }
    }

    static boolean checkBlanks() throws IOException {
        for (int i = 0; i < blanks.size(); i++) {
            Node node = blanks.get(i);
            boolean[] isVisited = new boolean[10];
            for (int j = 0; j < 9; j++) {
                if (!isVisited[board[node.y][j]]) {
                    isVisited[board[node.y][j]] = true;
                } else {
                    return false;
                }
            }

            isVisited = new boolean[10];
            for (int j = 0; j < 9; j++) {
                if (!isVisited[board[j][node.x]]) {
                    isVisited[board[j][node.x]] = true;
                } else {
                    return false;
                }
            }

            int startX = (node.x / 3) * 3;
            int startY = (node.y / 3) * 3;

            isVisited = new boolean[10];
            for (int dy = 0 ; dy < 3; dy++) {
                for (int dx = 0; dx < 3; dx++) {
                    if (!isVisited[board[startY + dy][startX + dx]]) {
                        isVisited[board[startY + dy][startX + dx]] = true;
                    } else {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    public static void main(String[] args) throws IOException {
        init();
        solve();

        bw.flush();
        bw.close();
        br.close();
    }
}

 

 

<정답 코드>

그때그때마다 빈칸에 유효한 숫자를 선택하여 채워넣고, 다음 빈칸으로 이동하는 재귀를 구현했다.

import java.util.*;
import java.io.*;

public class Main {

    static class Node {
        int x;
        int y;

        Node(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    static BufferedReader br;
    static BufferedWriter bw;
    static int[][] board;
    static List<Node> blanks;
    static boolean endFlag;


    static void init() throws IOException {
        br = new BufferedReader(new InputStreamReader(System.in));
        bw = new BufferedWriter(new OutputStreamWriter(System.out));
        board = new int[9][9];
        blanks = new ArrayList<>();
        endFlag = false;

        for (int y = 0; y < 9; y++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int x = 0; x < 9; x++) {
                board[y][x] = Integer.parseInt(st.nextToken());

                if (board[y][x] == 0) {
                    blanks.add(new Node(x, y));
                }
            }
        }
    }

    static void solve() throws IOException {
        recursion(0);
    }

    static void recursion(int blankNum) throws IOException {
        if (endFlag) {
            return;
        }
        
        // blankNum이 blanks.size()와 같아졌다는 것은 모든 blank에 유효한 값이 제대로 들어갔음을 의미
        // 여러 경우의 수 중 첫 번째 경우의 수에 걸려서 들어왔을 것임.
        if (blankNum == blanks.size()) {
            printBoard();
            
            // 최초로 나오는 경우의 수만 출력하면 되므로 endFlag를 활성화시켜 이후 경우의 수를 따지지 않고 recursion을 모두 종료하도록 하자
            // System.exit(0); 을 통해서 시스템을 종료할 수도 있지만 기존에 내가 사용하던 정해진 형식을 지키기 위해
            // endFlag를 활성화시키고, main 메서드에서 종료하도록 하기 위해 이렇게 구현했다.
            endFlag = true;
            return;
        }

        Node blank = blanks.get(blankNum);

        // 1~9까지 숫자중에 나오지 않은 수만 선택하며 빈 칸에 값을 채워넣어간다.
        // blankNum을 1씩 더해가며 마지막 blankNum을 채울 때까지 재귀를 반복한다.
        // 앞에 선택된 blank에 들어갈 num은 다음 blank에 채워질 num에 계속 누적하여 영향을 끼친다.
        for (int i = 1; i <= 9; i++) {
            if (checkValidNum(blank.x, blank.y, i)) {
                board[blank.y][blank.x] = i;
                recursion(blankNum + 1);
                board[blank.y][blank.x] = 0;
            }
        }
    }

    // 스도쿠 보드를 출력하는 메서드.
    static void printBoard() throws IOException {
        for (int y = 0; y < 9; y++) {
            for (int x = 0; x < 9; x++) {
                bw.write(board[y][x] + " ");
            }
            bw.newLine();
        }
    }

    // blank의 가로, 세로, 해당 blank를 포함하는 정사각형 구역에서 주어진 num이 나오지 않았는지를 판별하는 메서드
    static boolean checkValidNum(int x, int y, int num) throws IOException {
        for (int i = 0; i < 9; i++) {
            if (board[y][i] == num || board[i][x] == num) {
                return false;
            }
        }

        int startX = (x / 3) * 3;
        int startY = (y / 3) * 3;

        for (int dy = 0 ; dy < 3; dy++) {
            for (int dx = 0; dx < 3; dx++) {
                if (board[startY + dy][startX + dx] == num) {
                    return false;
                }
            }
        }
        return true;
    }

    public static void main(String[] args) throws IOException {
        init();
        solve();

        bw.flush();
        bw.close();
        br.close();
    }
}

'백준 알고리즘 > Java' 카테고리의 다른 글

[백준] 2239. 스도쿠 - Java  (0) 2025.06.13
[백준] 13023. ABCDE - Java  (0) 2025.06.13
[백준] 1987. 알파벳 - Java  (0) 2025.06.11
[백준] 1759. 암호 만들기 - Java  (0) 2025.06.11
[백준] 15654. N과 M (5) - Java  (0) 2025.06.11