BOJ 4574

스도미노쿠

문제출처

문제 해석

  • 스도쿠와 도미노를 혼합한 스도미노쿠를 완성시키는 문제입니다.
  • 기본 규칙은 스도쿠의 규칙을 따릅니다.
  • 그리드에 1~9의 숫자가 쓰여있고, 나머지 72칸은 도미노 타일 36개로 채워야 합니다.
  • 초기에 채워진 도미노 타일(10<=N<=35)이 주어졌을 때, 나머지 빈칸에 알맞은 도미노를 배치해야 합니다.

설계(20)

  • 스도쿠의 응용버전으로 백트래킹을 이용하는 문제입니다.
  • 기존 스도쿠와 다른 점은 도미노를 배치해야 한다는 점입니다.
  • 기존 스도쿠 규칙을 따르기 때문에, row[행의 index][숫자], col[열의 index][숫자], cross[그리드 index][숫자]배열을 이용하여 상태를 체크합니다.
  • 먼저, 어떤 도미노가 먼저 채워졌는지 확인하기 위해서 전처리가 필요합니다.
    • 도미노 정보의 입력을 받는 동시에 각 도미노(num1,num2)의 방문체크(visited[num1][num2],visited[num2][num1])를 합니다.
    • 이후, state에 각 숫자를 배치하고 각 row,col,cross 상태를 체크합니다.
    • 1~9가 들어있는 좌표의 상태 또한 갱신하도록 합니다.
    • 1+2~8+9의 가능한 도미노(36개)에서, 먼저 사용한(visited로 체크가 가능합니다)도미노가 아닌 것들만 벡터에 저장합니다. 벡터의 길이 즉, 남은 도미노를 모두 채워야 스도미노쿠가 완성됩니다.
  • 다음으로 생각해봐야 할 점은, 도미노가 배치되는 모양의 경우입니다.
    • 각 칸에 도미노를 배치할 수 있는 모양은 가로 또는 세로입니다. 문제에서 1+2와 2+1은 동일 취급된다고 하였으므로, 1+2의 도미노를 flip하는 연산 또한 고려해야 합니다(가지고 있는 도미노는 2+1이 없기 때문입니다).
    • 따라서, 한 칸에서 도미노를 배치할 수 있는 방법은 가로2가지,세로 2가지입니다.
    • 구현을 쉽게 하기 위해서 각 알맞은 (x1,y1,x2,y2) offset좌표를 저장해 놓습니다.
  • 마지막으로 백트래킹 구현부분입니다.
    • x,y좌표와 채워진 도미노 개수(cnt)를 매개변수로 하여, 아직 숫자가 채워지지 않은 좌표를 찾습니다.
    • 가능한 각 도미노 배치 경우의 수에 대해서, 나머지 도미노 중 하나씩 추가해보도록 구현합니다.
    • used변수로 해당 도미노의 사용여부를 체크합니다.
    • 만약 가로모양의 도미노를 채울 경우 y+2를 하고, 세로모양의 경우 y+1을 합니다.
    • 주어진 문제에서는 항상 답이 유일한 경우만 입력으로 주어지기 때문에, 남은 도미노를 다 채웠으면 출력하고 flag변수를 이용하여 바로 백트래킹 함수를 빠져나오도록 합니다.

구현(60)

코드
#include <iostream>
#include <vector>
#include <cstring>
#define MAXN 10
using namespace std;

bool row[MAXN][MAXN];
bool col[MAXN][MAXN];
bool cross[MAXN][MAXN];
vector<pair<int,int> > v;
bool visited[MAXN][MAXN];
int state[MAXN][MAXN];
int N;
int total_cnt;
struct node{
    int x1;
    int y1;
    int x2;
    int y2;
};
node dirs[4] = { {0,0,0,1},{0,1,0,0},{0,0,1,0},{1,0,0,0} };
vector<bool> used;
bool flag;
void check_state(int x, int y, int num,bool mode){
    row[x][num] = mode;
    col[y][num] = mode;
    cross[3*(x/3)+y/3][num] = mode;
}
bool check(int x, int y, int num){
    if (row[x][num] || col[y][num] || cross[3*(x/3)+y/3][num]) return false;
    return true;
}
bool is_range(int x, int y){
    return !(x<0||y<0||x>=MAXN-1 || y >= MAXN-1 || state[x][y]!=0);
}
void backtrack(int x, int y, int cnt){
    if (flag) return;
    if (total_cnt == cnt){
        for (int i = 0; i < MAXN-1; i++){
            for (int j =0 ;j < MAXN-1; j++){
                cout << state[i][j];
            }
            cout <<"\n";
        }
        flag = true;
        return;
    }
    if (y == MAXN -1) {
        backtrack(x+1,0,cnt);
        return;
    }
    if (state[x][y]!=0) {
        backtrack(x,y+1,cnt);
        return;
    }
    for (int i = 0 ;i < 4; i++){
        int x1 = x + dirs[i].x1;
        int y1 = y + dirs[i].y1;
        int x2 = x + dirs[i].x2;
        int y2 = y + dirs[i].y2;
        if (!is_range(x1,y1)|| !is_range(x2,y2)) continue;
        for (int j = 0; j < v.size(); j++){
            if (used[j]) continue;
            int num1 = v[j].first;
            int num2 = v[j].second;
            if (!check(x1,y1,num1) || !check(x2,y2,num2)) continue;
            used[j] = true;
            check_state(x1,y1,num1,true);
            check_state(x2,y2,num2,true);
            state[x1][y1] = num1;
            state[x2][y2] = num2;
            // 가로로 채울경우 y+2
            if (i < 2) backtrack(x,y+2,cnt+1);
            // 세로로 채울 경우 y+1
            else backtrack(x,y+1,cnt+1);
            check_state(x1,y1,num1,false);
            check_state(x2,y2,num2,false);
            used[j] = false;
            state[x1][y1] = 0;
            state[x2][y2] = 0;
        }
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    int t= 1;
    while (1){
        cin >> N;
        if (N ==0) break;
        flag = false;
        v.clear();
        memset(row,false,sizeof(row));
        memset(col,false,sizeof(col));
        memset(cross,false,sizeof(cross));
        memset(visited,false,sizeof(visited));
        memset(state,0,sizeof(state));
        for (int i = 0; i < N; i++){
            int num1,num2;
            string s1,s2;
            cin >> num1 >> s1 >> num2 >> s2;
            visited[num1][num2] = true;
            visited[num2][num1] = true;
            state[s1[0]-'A'][s1[1]-'0'-1] = num1;
            state[s2[0]-'A'][s2[1]-'0'-1] = num2;
            check_state(s1[0]-'A',s1[1]-'0'-1,num1,true);
            check_state(s2[0]-'A',s2[1]-'0'-1,num2,true);
        }
        for (int i = 1; i <= 9; i++){
            string s;
            cin >> s;
            state[s[0]-'A'][s[1]-'0'-1] = i;
            check_state(s[0]-'A',s[1]-'0'-1,i,true);
        }
        for (int i = 1; i < 9; i++){
            for (int j = i+1; j <= 9; j++){
                if (visited[i][j]) continue;
                v.push_back({i,j});
            }
        }
        total_cnt = v.size();
        used.resize(v.size(),false);

        cout << "Puzzle " << t << "\n";
        backtrack(0,0,0);
        t++;
    }
    return 0;
}
디버깅
  • if (!check(x1,y1,num1) || !check(x2,y2,num2)) continue;부분을 if (!check(x1,y1,num1) || check(x2,y2,num2)) continue;로 잘못 구현하여서 이를 디버깅하는데 시간이 소모되었습니다.
제출결과
  • 16ms

마무리

  • 스도쿠를 응용한 백트래킹 문제였습니다. 취약한 부분을 연습하기에 아주 적합한 문제로 상당한 구현력이 요구되어 시간이 오래걸렸습니다.