——以“靶形数独”为例
进阶的搜索往往会将搜索与其他的知识结合运用,以减少搜索的复杂度
就比如这一题
题目描述
小城和小华都是热爱数学的好学生,最近,他们不约而同地迷上了数独游戏,好胜的他们想用数独来一比高低。但普通的数独对他们来说都过于简单了,于是他们向 Z 博士请教,Z 博士拿出了他最近发明的“靶形数独”,作为这两个孩子比试的题目。
靶形数独的方格同普通数独一样,在 \(9\) 格宽×\(9\) 格高的大九宫格中有\(9\) 个 \(3\) 格宽×\(3\) 格高的小九宫格(用粗黑色线隔开的)。在这个大九宫格中,有一些数字是已知的,根据这些数字,利用逻辑推理,在其他的空格上填入 \(1\) 到 \(9\)的数字。每个数字在每个小九宫格内不能重复出现,每个数字在每行、每列也不能重复出现。但靶形数独有一点和普通数独不同,即每一个方格都有一个分值,而且如同一个靶子一样,离中心越近则分值越高。(如图)

上图具体的分值分布是:最里面一格(黄色区域)为 \(10\) 分,黄色区域外面的一圈(红色区域)每个格子为\(9\)分,再外面一圈(蓝色区域)每个格子为\(8\) 分,蓝色区域外面一圈(棕色区域)每个格子为\(7\)分,最外面一圈(白色区域)每个格子为\(6\)分,如上图所示。比赛的要求是:每个人必须完成一个给定的数独(每个给定数独可能有不同的填法),而且要争取更高的总分数。而这个总分数即每个方格上的分值和完成这个数独时填在相应格上的数字的乘积的总和
总分数即每个方格上的分值和完成这个数独时填在相应格上的数字的乘积的总和。如图,在以下的这个已经填完数字的靶形数独游戏中,总分数为 \(2829\)。游戏规定,将以总分数的高低决出胜负。

由于求胜心切,小城找到了善于编程的你,让你帮他求出,对于给定的靶形数独,能够得到的最高分数。
输入格式
一共 \(9\) 行。每行\(9\)个整数(每个数都在 \(0−9\) 的范围内),表示一个尚未填满的数独方格,未填的空格用“\(0\)”表示。每两个数字之间用一个空格隔开。
输出格式
输出共 \(1\) 行。输出可以得到的靶形数独的最高分数。如果这个数独无解,则输出整数\(-1\)。
题目分析
刚拿到手,其实我就已经能够感觉到,这种题目暴力搜索必然是容易TLE的,数独的情况千变万化,并且情况也是多种多样,对于一个数独而言也很容易出现多解的情况,更何况这一题要的不是单个数独解,而是要求出全部解之后进行一个比较,也就意味着求解的过程当中,我们必须要控制好分支的个数,也就是一个节点与更深节点的连通量。
怎么优化呢,本蒟蒻一开始也不会,在百般思索之后(其实还是看了巨佬的提示),我意识到一个简单的结论,我们在玩数独的时候一定是会从填的比较多的地方开始入手的
0 0 0 0 0 6 0 0 3 0 0 0 0 0 0 6 0 0 0 0 0 0 0 3 0 0 0 0 0 0 1 0 0 2 0 0 0 0 0 0 3 0 0 0 4 0 2 7 0 0 0 0 3 0 1 0 0 0 6 8 4 7 9 0 9 6 2 7 0 1 0 5 8 0 0 0 9 0 3 0 0
就比如这一组数据,我想正常思维下我们都应该是会从已经填的比较充足的下部尤其是右下角开始入手(除非你是个铁憨憨)而并不会从几乎没填的左上角开始填充。
而一块区域一旦被填充得比较多,也就意味着这个位置所能填充的值就很有限了,就比如我们看\((8,8)\)的位置,他就只能填充\(8\)一个值,作为搜索起点的话可以大幅度减小无谓的搜索次数。
也就是说,在本题中,我们赋予每一个点一个精确度值,每一次都优先填充最高精确度的位置。精确度值位其所在行,列,宫的已经被填充的个数之和,也就是说,一个未被填充数的精确度值最高为\(24\),如果一个数已经被填充,则精确度值为\(0\)。
每次填充一个数就更新一次精确度值表(当然这个过程还可以得到很多优化,只需要更新所在行列与宫即可,我只是偷个懒)
总结来说,本题就是一条典型的搜索与贪心的结合题,贪心的正确性由搜索的完备性托底,而贪心的便捷性又可以提升搜索的速度。
AC代码如下,代码注释已经说得很详细了
Accepted Code
/* * @Author: Gehrychiang * @Date: 2020-02-08 20:26:40 * @Website: www.yilantingfeng.site * @E-mail: gehrychiang@aliyun.com */ #include <bits/stdc++.h> using namespace std; int visx[10][10]; //记录每一行的数字存放 int cntx[10]; //记录每一行的数字存放个数 int visy[10][10]; //记录每一列的数字存放 int cnty[10]; //记录每一列的数字存放个数 int visrec[10][10]; //记录每一宫的数字存放 int cntrec[10]; //记录每一宫的数字存放个数 int table[10][10]; //记录数独 int acc[10][10]; //记录每一个数字的精确度(确定值=0) int ans = 0; //答案 int flag = 0; //是否找到至少一个数独的解 int pos_cal(int x, int y) //求该位置位于哪一宫 { if (x >= 1 && x <= 3) { if (y >= 1 && y <= 3) return 1; else if (y >= 4 && y <= 6) return 2; else return 3; } else if (x >= 4 && x <= 6) { if (y >= 1 && y <= 3) return 4; else if (y >= 4 && y <= 6) return 5; else return 6; } else { if (y >= 1 && y <= 3) return 7; else if (y >= 4 && y <= 6) return 8; else return 9; } } int mul_cal(int x, int y) //求该位置的倍数 { if (x == 1 || y == 1 || x == 9 || y == 9) return 6; if (x == 2 || y == 2 || x == 8 || y == 8) return 7; if (x == 3 || y == 3 || x == 7 || y == 7) return 8; if (x == 4 || y == 4 || x == 6 || y == 6) return 9; if (x == 5 && y == 5) return 10; } bool check(int x, int y, int k) //在(x,y)处填充k是否合法 { if (visx[x][k] == 1 || visy[y][k] == 1 || visrec[pos_cal(x, y)][k] == 1) return false; else return true; } void dfs(int x, int y, int cur_lef, int sum) { if (cur_lef == 0) { //test // for (int i = 1; i <= 9; i++) // { // for (int j = 1; j <= 9; j++) // { // cout << table[i][j] << " "; // } // cout << endl; // } flag = 1; ans = max(sum, ans); return; } else { for (int k = 1; k <= 9; k++) { if (check(x, y, k)) { table[x][y] = k; int tmp = acc[x][y]; //暂存当前精确度,以备回溯 acc[x][y] = 0; //填充之后置0 //占位 cntx[x]++; cnty[y]++; cntrec[pos_cal(x, y)]++; visx[x][k] = 1; visy[y][k] = 1; visrec[pos_cal(x, y)][k] = 1; int nex, ney, maxacc = 0; //得到下一次位置 for (int i = 1; i <= 9; i++) { for (int j = 1; j <= 9; j++) { if (table[i][j] == 0) { acc[i][j] = cntx[i] + cnty[j] + cntrec[pos_cal(i, j)]; if (maxacc < acc[i][j]) { maxacc = acc[i][j]; nex = i; ney = j; } } } } dfs(nex, ney, cur_lef - 1, sum + table[x][y] * mul_cal(x, y)); //释放 visx[x][k] = 0; visy[y][k] = 0; visrec[pos_cal(x, y)][k] = 0; cntx[x]--; cnty[y]--; cntrec[pos_cal(x, y)]--; table[x][y] = 0; acc[x][y] = tmp; } } } return; } int main() { //freopen("","r",stdin); //freopen("","w",stdout); for (int i = 1; i <= 9; i++) { for (int j = 1; j <= 9; j++) { cin >> table[i][j]; if (table[i][j] != 0) { int tmpos = pos_cal(i, j); //占位计数 visx[i][table[i][j]] = 1; visy[j][table[i][j]] = 1; visrec[tmpos][table[i][j]] = 1; cntx[i]++; cnty[j]++; cntrec[tmpos]++; } } } int stx, sty, maxacc = 0, daitianchong = 0, orisum = 0; //求起点位置以及总共需要填充的个数和初始和 for (int i = 1; i <= 9; i++) { for (int j = 1; j <= 9; j++) { if (table[i][j] == 0) { ++daitianchong; acc[i][j] = cntx[i] + cnty[j] + cntrec[pos_cal(i, j)]; if (maxacc < acc[i][j]) { maxacc = acc[i][j]; stx = i; sty = j; } } else { orisum += table[i][j] * mul_cal(i, j); } } } dfs(stx, sty, daitianchong, orisum); if (flag) cout << ans << endl; else cout << "-1" << endl; //fclose(stdin); //fclose(stdout); return 0; }