题目链接:http://wikioi.com/problem/1174/
算法与思路:状态压缩 + 启发式搜索
想要看懂这篇题解需要有一定位运算的基础,初学者可以参考以下链接
http://www.matrix67.com/blog/archives/263
所谓启发式搜索,就是在状态空间中的搜索对每一个搜索的位置进行评估,得到最好的位置,
再从这个位置进行搜索直到目标。这样可以省略大量无谓的搜索路径,提高了效率。
在我们玩数独的时候,很平常的想法就是从数字缺少最少的那行或列开始填数,
这样受到的限制最多,可选取的数范围最小,而启发式搜索正是遵循这样的思路;
在搜索的过程中,需要判断所填的数字是否在其所在的行、列和小九宫格中出现过,如果不进行标记,
那么每次搜索都需要遍历行、列、小九宫格来得到合法的数字范围,时间代价太大;
而将行,列,小九宫格的状态转换成一个九位的二进制数,则可以很好的解决这一问题。
详见注释。
代码实现:
#include<stdio.h> #include<math.h> int row_fill[10] = {}, row_status[10] = {}, col_status[10] = {}; int m_sudo[5][5] = {}, row_lack[10] = {}; int ans = -1, s_order[10], map[10][10]; void score() //计算分数,更新ans { int sum = 0, i, j; for(i = 1; i < 5; i++) { for(j = i; j < 11 - i; j++) sum += (map[i][j] + map[10 - i][j]) * (5 + i); for(j = i + 1; j < 10 - i; j++) sum += (map[j][i] + map[j][10 - i]) * (5 + i); } sum += map[5][5] * 10; if(sum > ans) ans = sum; } void dfs(int k) //搜索部分,k不表示搜索第k行,而是第s_order[k]行 { if(k==10) score(); // k=10表示九行都搞定了,开始算分 else { int x, y, j, pos, p, i = s_order[k]; //i表示第i行,j表示第j列 x = 511 - row_fill[i]; //511(2)=111,111,111(2),故此行的意思是将第i行缺位取出来 //此时x中1表示缺位,y与x同 y = x & -x; //y是取出本行第一个缺位,在这一次搜索里就搜索这个缺位 row_fill[i] |= y; //下一次搜索时,这一位已填,故把缺位补上 j = (int)log2(y) + 1; //j就是y用二进制表示1所在的位数,即j列 pos = 511 - (row_status[i] | col_status[j] | m_sudo[(i-1)/3][(j-1)/3]); //这一步是取出可以填哪些数 while(pos > 0) { p = pos & -pos; //取出可以填的一个数 pos -= p; //去掉已填的数 map[i][j] = (int)log2(p) + 1; //填入map中 row_status[i] |= p; //修改row_status,col_status,m_sudo,这个数已用过,'|'写成'+'也行 col_status[j] |= p; m_sudo[(i-1)/3][(j-1)/3] |= p; if(x == y) dfs(k+1); //若x = y,则这一行只有一个空,即现在已填的空,故搜索k+1 else dfs(k); //若x不等于y,则这一行还有空没填,继续搜索这一行 row_status[i] -= p; //搜索完,还原row_status,col_status,m_sudo col_status[j] -= p; m_sudo[(i - 1) / 3][(j - 1) / 3] -= p; } row_fill[i] -= y; //s搜索完,还原row_fill[i] } } int main() { int i, j, used_n; for(i = 1; i < 10; i++) for(j = 1; j < 10; j++) { scanf("%d", &map[i][j]); //读入数独,数组map记的是数独。 if(map[i][j] > 0) { row_fill[i] |= 1 << (j - 1); //数组row_fill记的是第i行填数情况 //row_fill[i]写成二进制,第j位为0,表示map[i][j]=0,未填数, //同理第j位为1,表示map[i][j]>0,已填数 used_n = 1 << (map[i][j] - 1); //used_n写成二进制,第k位为1,表示数字k已用过 if(((row_status[i] & used_n) != 0) || ((col_status[j] & used_n) != 0) || ((m_sudo[(i - 1) / 3][(j - 1)/3] & used_n) != 0)) { printf("-1\n"); return 0; } //判断某一行,列,九宫格是否有同一数字出现两次 row_status[i] |= used_n; //数组row_status记录第i行数字使用情况 //row_status[i]写成二进制,第j位为0,表示i行,j没用过 //同理第j位为1,表示i行,j用过 col_status[j] |= used_n; //数组col_status记录第j列数字使用情况,意义同row_status m_sudo[(i-1)/3][(j-1)/3] |= used_n; //数组m_sudo记录某一小九宫格数字用的情况,意义同row_status } //九个小九宫格分别是 //m_sudo[0][0], m_sudo[0][1], m_sudo[0][2] //m_sudo[1][0], m_sudo[1][1], m_sudo[1][2] //m_sudo[2][0], m_sudo[2][1], m_sudo[2][2] else row_lack[i]++; //数组row_lack记的是某一行缺数的个数 } for(i = 1; i < 10; i++) s_order[i] = i; //数组s_order记录搜索各行的顺序 for(i = 1; i < 9; i++) //按各行空缺数的个数将s_order从小到大排序 for(j = i + 1; j < 10; j++) //使得一会搜的时候,先搜缺数少的行,这就是启发式搜索 if(row_lack[s_order[i]] > row_lack[s_order[j]]) { s_order[i] ^= s_order[j]; //swap位运算版 s_order[j] ^= s_order[i]; s_order[i] ^= s_order[j]; } for(i = 1; row_lack[s_order[i]] == 0; i++); //考虑到某一行缺数可能为0,故先找到缺数的行 dfs(i); printf("%d\n",ans); return 0; }