https://algospot.com/judge/problem/read/KAKURO2
카쿠로와 같이 특정한 제약에 해당하는 답을 찾는 문제들을 제약 충족 문제라고 함
답의 일부를 생성하고 나면 문제의 조건에 의해 다른 조각의 답에 대해 알게 되는 것
이를 이용하면 실제 탐색해야 할 탐색 공간의 수를 크게 줄일 수 있다.
채울 순서를 잘 정하면 제약 전파를 더 용이하게 할 수 있을 뿐만 아니라 답을 더 빨리 찾을 확률을 높여준다. 채울 순서 정하기는 사실 상 변수 순서 정하기와 값 순서 정하기를 합쳐 부르는 말이다.
변수 순서 정하기를 위해 해당 칸에 들어갈 수 있는 후보의 수가 가장 작은 칸부터 채워나가자.
getCandidates(len, sum, known[])
: 연속된 len개의 흰 칸에 적힌 수 합이 sum이어야 하는데, 그중 이미 적힌 숫자의 목록이 known[]일 때, 남은 칸에 들어갈 수 있는 숫자의 집합
getCandidates 는 어떻게 구현할 수 있을까?
==[1,2,…,9]의 부분 집합 중 크기가 len인 집합을 모두 생성해 보고 이 중 known을 모두 포함하며, 그 합이 sum인 집합을 모두 생성해 본다. 그리고 이 집합들의 합집합에서 known에 속한 원소들을 제외하자.==
// mask에 속한 원소들의 개수를 반환한다.
int getSize(int mask);
// mask에 속한 원소들의 합을 반환한다.
int getSum(int mask);
// len 칸의 합이 sum이고, 이 칸들에 이미 쓰인 수의 집합이 known일 때
// 나머지 칸에 들어갈 수 있는 숫자의 집합을 반환
int getCandidates(int len, int sum, int known) {
// 조건에 부합하는 집합들의 교집합
int allSets=0;
// 1~9의 부분집합(즉 모두 짝수)을 모두 생성하고 그 중
for(int set=0; set<1024; set+=2) {
// known을 포함하고, 크기가 len이며 합이 sum인 집합을 모두 찾는다
if((set&known)==known && getSize(set)==len && getSum(set)==sum) {
allSets |= set;
}
}
return allSets & ~known;
}탐색 중간에 getCandidates 를 호출하기에는 너무 많은 연산이 필요하다. 이런 경우 getCandidates 의 같은 값을 두 번 이상 계산하지 않도록 메모이제이션하거나, 탐색을 수행하기 전에 미리 계산해 두면 아주 유리하다.
// candidates[len][sum][known]=getCandidates(len,sum,known)
int candidates[10][46][1024];
void generateCandidates() {
memset(candidates, 0, sizeof(candidates));
// 1~9의 부분집합을 모두 생성
for(int set=0; set<1024; set+=2) {
int l=getSize(set), s=getSum(set);
int subset=set;
while(true) {
// 숫자 하나의 합이 s이고, 이미 subset의 숫자가 쓰여 있을 때
// 전체 숫자의 집합이 set이 되도록 나머지 숫자를 채워넣을 수 있다.
candidates[l][s][subset]|=(set & ~subset);
if(subset==0) break;
subset = (subset-1) & set;
}
}
}이제 칸의 좌표 (y, x)가 주어졌을 때 후보의 수를 어떻게 계산할까?
(y, x)에서 힌트 칸이 나올 때까지 위로 쭉 올라가서 힌트 칸을 찾은 뒤, 위 아래로 움직이면서 흰 칸의 수와 이미 쓰인 숫자들의 집합을 구한다.
-> 너무 오래걸림
// 게임판의 정보
// color: 각 칸의 색깔 (0=검은 칸 혹은 힌트 칸, 1=흰칸)
// value: 각 흰 칸에 쓴 숫자 (아직 쓰지 않은 칸은 0)
// hint: 각 칸에 해당하는두 힌트의 번호
int n, color[MAXN][MAXN], value[MAXN][MAXN], hint[MAXN][MAXN][2];
// 각 힌트의 정보
// sum: 힌트 칸에 쓰인 숫자
// length: 힌트 칸에 해당하는 흰 칸의 수
// known: 힌트 칸에 해당하는 흰 칸에 쓰인 숫자들의 집합
int q, sum[MAXN*MAXN], length[MAXN*MAXN], known[MAXN*MAXN];
// (y, x)에 val을 쓴다.
void put(int y, int x, int val) {
for(int h=0; h<2; ++h){
known[hint[y][x][h]]+=(1<<val);
}
value[y][x]=val;
}
// (y, x)에 쓴 val을 지운다
void remove(int y, int x, int val){
for(int h=0; h<2; ++h){
known[hint[y][x][h]]-=(1<<val);
}
value[y][x]=0;
}
// 힌트 번호가 주어질 때 후보의 집합을 반환
int getCandHint(int hint){
return candidates[length[hint]][sum[hint]][known[hint]];
}
// 좌표가 주어질 때 해당 칸에 들어갈 수 있는 후보의 집합을 반환
int getCandCoord(int y, int x){
return getCandHint(hint[y][x][0]) & getCandHint(hint[y][x][1]);
}위와 같이 구현하면 실제 탐색 과정에서는 게임판에 대한 정보는 거의 필요가 없다.
void printSolution();
// 답을 찾았으면 true, 아니면 false
bool search() {
// 아직 숫자를 쓰지 않은 흰 칸 중 후보의 수가 최소인 칸을 찾는다.
int y=-1, x=-1, minCands=1023;
for(int i=0; i<n; ++i){
for(int j=0; j<n; ++j){
if(color[i][j]==WHITE && value[i][j]==0) {
int cands=getCandCoord(i, j);
if(getSize(minCands)>getSize(cands)){
minCands=cands;
y=i; x=j;
}
}
}
}
// 이 칸에 들어갈 숫자가 없으면 실패
if(minCands==0) return false;
if(y==-1) {
printSolution();
return true;
}
for(int val=1; val<=9; ++val){
if(minCands & (1<<val)) {
put(y, x, val);
if(search()) return true;
remove(y, x, val);
}
}
return false;
}