目录
前言
其实我在之前的blog中写过一道极其相似的题目。。。。。。
lowbit函数
这是一个利用二进制位运算取出二进制数最后一位’1‘的函数
数独
数独大家肯定都玩过,本题就是利用计算机尝试模拟人做数独游戏时的情形,每次从空白最少的行开始填写,每次填写都比对这个数字是否符合要求,如果没有符合要求的数字,则重新填写上一个数字,大家可以回忆下做数独时的情形,也可以为剪枝提供思路
靶形数组
问题描述
小城和小华都是热爱数学的好学生,最近,他们不约而同地迷上了数独游戏,好胜的他们想用数独来一比高低。但普通的数独对他们来说都过于简单了,于是他们向 Z 博士请教,Z 博士拿出了他最近发明的“靶形数独”,作为这两个孩子比试的题目。
靶形数独的方格同普通数独一样,在 9格宽且 9 格高的大九宫格中有 9 个3 格宽且 3 格高的小九宫格(用粗黑色线隔开的)。在这个大九宫格中,有一些数字是已知的,根据这些数字,利用逻辑推理,在其他的空格上填入 1 到 9 的数字。每个数字在每个小九宫格内不能重复出现,每个数字在每行、每列也不能重复出现。但靶形数独有一点和普通数独不同,即每一个方格都有一个分值,而且如同一个靶子一样,离中心越近则分值越高。(如图)
上图具体的分值分布是:最里面一格(黄色区域)为 10 分,黄色区域外面的一圈(红色区域)每个格子为 9 分,再外面一圈(蓝色区域)每个格子为 8 分,蓝色区域外面一圈(棕色区域)每个格子为 7 分,最外面一圈(白色区域)每个格子为 6分,如上图所示。比赛的要求是:每个人必须完成一个给定的数独(每个给定数独可能有不同的填法),而且要争取更高的总分数。而这个总分数即每个方格上的分值和完成这个数独时填在相应格上的数字的乘积的总和
总分数即每个方格上的分值和完成这个数独时填在相应格上的数字的乘积的总和。如图,在以下的这个已经填完数字的靶形数独游戏中,总分数为 2829。游戏规定,将以总分数的高低决出胜负。
由于求胜心切,小城找到了善于编程的你,让你帮他求出,对于给定的靶形数独,能够得到的最高分数。
输入格式
一共 9 行。每行 9 个整数(每个数都在 0~9 的范围内),表示一个尚未填满的数独方格,未填的空格用“0”表示。每两个数字之间用一个空格隔开。
输出格式
输出共 1行。输出可以得到的靶形数独的最高分数。如果这个数独无解,则输出整数 -1。
问题分析
子网格位置
首先要解决子网格位置问题,给定一个点(x,y)的坐标给如何知道该点位于哪个子网格中,我们先对各个子网格编号
1 | 2 | 3 |
4 | 5 | 6 |
7 | 8 | 9 |
然后我们再找规律:
最笨但是最实用的方法就是用9个if语句判断一下确定位置,别瞧不起它,这个是真的有用
不开玩笑了,哈哈,其实这个是有公式的:
所在位置分数
不管三七二十一,枚举就完事了
解决了这两个问题,我们再考虑剪枝
优化搜索顺序剪枝1
会玩数独的伙伴肯定清楚,填数字要从数字最多的一行开始填,也就是空白最少的一行,因为这样很可能提前确定一些数字,计算机也是这样,网格中的数字越多,dfs需要递归的次数就越少,所以每次我们都选择0(也就是空白)最少的一行开始填写
优化搜索顺序剪枝2
很简单的道理,如果1~9中有数字前面已经搜索过了,那么就没有必要再搜了,直接剪掉,这里多说两句,实现这个方法的办法有两种,一种是用一个9位二进制代表1~9,然后用lowbit()函数逐一取出二进制数中的最后一个1,这样可以实现上述剪枝,更简单的就是用循环实现,但是需要加一点小改动,详见代码
可行性剪枝
这个就是根据题意来的,每行,每列,每个子网格中不能存在相同的数字
代码
#include<iostream>
#include <cstring>
#include <algorithm>
using namespace std;
bool flag = false;
int Max = 0;
int map[15][15]; //九宫格
struct node {
int row; //行的编号
int num; //该行中0的数量
}cnt[15];
bool row[15][15]; //row[i][x] 标记在第i行中数字x是否出现
bool col[15][15]; //col[j][y] 标记在第j列中数字y是否出现
bool grid[15][15]; //grid[k][x] 标记在第k个3*3子格中数字x是否出现
bool cmp(node a, node b) {
return a.num < b.num; //升序排列
}
int query(int x, int y) {
if (y <= 3 && x <= 3) return 1;
if (y > 3 && y <= 6 && x <= 3) return 2;
if (y > 6 && x <= 3) return 3;
if (y <= 3 && x > 3 && x <= 6) return 4;
if (y > 3 && y <= 6 && x > 3 && x <= 6) return 5;
if (y > 6 && x > 3 && x <= 6) return 6;
if (y <= 3 && x > 6) return 7;
if (y > 3 && y <= 6 && x > 6) return 8;
else return 9;
}
int point(int i, int j)
{
if (i == 1 || j == 1 || i == 9 || j == 9) return 6;
if (i == 2 || j == 2 || i == 8 || j == 8) return 7;
if (i == 3 || j == 3 || i == 7 || j == 7) return 8;
if (i == 4 || j == 4 || i == 6 || j == 6) return 9;
return 10;
}
void DFS(int x, int y, int pos, int p,int sum) {
/*if (flag) {
//这里不能结束,我们要遍历所有的情况
return;
}*/
if (p == 10) {
//打印结果,在dfs内打印优势是可以借助dfs本身的回溯将row,col,grid初始化
Max = max(sum, Max);
//flag = true;
return;
}
//剪枝,如果map中该位置已经有数字那么直接下一个,或者下一层
if (map[x][y]) {
if (y == 9) DFS(cnt[p + 1].row, 1, 1, p + 1,sum);
else DFS(x, y + 1, pos, p,sum);
return;
}
//int k = query(x, y); //最直接方法
int k = 3 * ((x - 1) / 3) + (y - 1) / 3 + 1; //公式法
int mul = point(x, y);
//#define lowbit(x) ((x)&-(x))
for (int i = pos; i <= 9; i++) { //其实这里可以用一个九位二进制数代表1~9这几个数字是否可选,用lowbit()函数取出第一个1(这个是树状数组中的函数),这样就可以实现减少重复次数,但我觉得使用pos变量也可以实现同样的效果
if (!row[x][i] && !col[y][i] && !grid[k][i]) { //利用这仨进行可行性剪枝
/*同样的,这里的row,col,grid数组都可以是用九个九位二进制数来代替,但这样编码起来太麻烦了,我写不出来,所以干脆用数组,感兴趣的伙伴可以试一下,写出来了@我一下,第一时间给你点赞*/
map[x][y] = i;
sum += i * mul; //加上分数
row[x][i] = true;
col[y][i] = true;
grid[k][i] = true;
//优化搜索顺序剪枝,每次填写0最少的一行
if (y == 9) DFS(cnt[p + 1].row, 1, 1, p + 1,sum);
else DFS(x, y + 1, pos, p,sum); //最优化剪枝,如果前面已经搜过,就代表一定标记过了,就不需要继续了,下一次循环从pos开始
//回溯
map[x][y] = 0;
sum -= i * mul; //这里一定要记得分数也要回溯,分数也要回溯,分数也要回溯,重要的事情说三遍
row[x][i] = false;
col[y][i] = false;
grid[k][i] = false;
}
}
return;
}
int main() {
int sum = 0;
for (int i = 1; i <= 9; i++) {
for (int j = 1; j <= 9; j++) {
cin >> map[i][j];
//初始化
if (map[i][j]) {
int mul = point(i, j);
sum += mul * map[i][j];
//int k = query(i, j);
//这个公式很好推的,加油
int k = 3 * ((i - 1) / 3) + (j - 1) / 3 + 1;
row[i][map[i][j]] = true;
col[j][map[i][j]] = true;
grid[k][map[i][j]] = true;
}
else {
//对每行的索引用0的多少进行排列,首先要知道每行0的数量
cnt[i].num++;
cnt[i].row = i;
}
}
}
//每次填写0少的一行,我们玩数独也是这样玩的吧
sort(cnt + 1, cnt + 9 + 1, cmp);
//这是打印搜索行顺序代码
/*for (int k = 1; k <= 9; k++) {
cout << cnt[k].row<<" ";
}
cout<<endl;*/
//开始dfs
DFS(cnt[1].row, 1, 1, 1,sum);
if (Max==0) {
cout << -1 << endl; //注意如果没有答案要输出-1,一开始没注意
}
else {
cout << Max << endl;
}
return 0;
}