超强数独解法o.O?带你使用DFS秒杀多解数独√

目录

什么是数独? 

数独的解法?

数独DFS算法详解

1. 初始化条件

2. 填入已初始化的数独表

3. 填数独

 4. 拓展问题

请问删掉数独中的哪两个数可以使得数独的解最大?

 删除的是哪两个数?

 最终代码

 main函数(如何执行这些代码) 


什么是数独? 

数独的要求是每一行,每一列,每一宫都包括1~9,但是不能有重复数字。


数独的解法?

主流为深度优先搜索算法,如果使用数据结构,有舞蹈链算法,本篇介绍深度优先搜索算法。


数独DFS算法详解

1. 初始化条件

我们的初始条件准备了5个,分别是row[N], col[N], cell[3][3],ones[M], map[M]

N = 9;

M = 111111111(二进制),511(十进制);

//设置9*9数独表
const int N = 9;
//设置mask长度 M的二进制:111111111,从右到左分别表示1 2 3 4 5 6 7 8 9 
const int M = 1 << N;//row、col、cell分别表示行、列、宫可填写数的编码
//ones、map是一个映射关系,ones表示有多少个1,map表示9位二进制的1代表的数字
int row[N], col[N], cell[3][3];
int ones[M], map[M];//数独表
int arr[9][9] = {4,0,0,9,0,0,0,0,3,0,8,0,0,0,1,0,9,0,0,0,0,0,2,0,7,0,0,0,3,0,0,0,0,0,0,4,0,0,6,7,0,0,5,0,0,2,0,0,0,0,0,0,6,0,0,0,7,0,3,0,6,0,0,0,5,0,6,0,0,0,0,0,1,0,0,0,0,9,0,0,2
};

那么M是用来干嘛的?

我使用了二进制来优化DFS算法,在下图中只有7不能填,因为mask为0。

    map和ones是一个映射关系,下标(二进制)->值(十进制)
    
    map[10] = 2,意思是二进制为10的数十进制为2 

    ones[11] = 2,意思是二进制为11的数十进制为2

下面初始化的意思是把所有位置都设置成所有数都可填的状态。

//只需一次初始化的数组map、ones
void _init()
{//once设置成false后不再执行这个函数once = false;//map和ones是一个映射关系,下标(二进制)->值(十进制)// //map[10] = 2,意思是二进制为10的数十进制为2for (int i = 0; i < N; i++){map[1 << i] = i + 1;}//ones[11] = 2,意思是二进制为11的数十进制为2for (int i = 0; i < M; i++){for (int j = 0; j < N; j++){ones[i] += i >> j & 1;}}
}//初始化条件数组
int init(int _arr[N][N])
{//设置row,col为111111111,代表1`9都在可填写状态for (int i = 0; i < N; i++){row[i] = col[i] = M - 1;}//在9个宫中设置值为111111111,代表1`9都在可填写状态for (int i = 0; i < 3; i++){for (int j = 0; j < 3; j++){cell[i][j] = M - 1;}}//只初始化一次if (once){_init();}//填入数独表的已知数字,完成初始化工作。return fill(_arr);
}

 fill函数是干嘛的?请往下看


2. 填入已初始化的数独表

fill函数的作用是填入数独表中已知的数字,返回一个整形代表待填入数独表的空位。

我们利用空位作为DFS的制约条件。

//将数组上已知数的位置、值信息做初始化记录,并记录需要填写的格子数
int fill(int _arr[N][N])
{//cnt为待填格子数int cnt = 0;//设置cnt,row、col、cell条件for (int i = 0; i < N; i++){for (int j = 0; j < N; j++){if (!_arr[i][j]){cnt++;}else{col[j] -= 1 << (_arr[i][j] - 1);row[i] -= 1 << (_arr[i][j] - 1);cell[i / 3][j / 3] -= 1 << (_arr[i][j] - 1);}}}return cnt;
}

 在行,列,宫对应的位置减去对应数的二进制码,这样可以把该数字在行、列、宫对应的二进制码设置为0,代表该数字在该行,该列,该宫已经不可以填写。

可举例填写4和6,三个条件的变化,等式右边为二进制码。 

3. 填数独

前置有3个功能函数。

这里说一下getmask

因为&的特点,col & row & cell运算,如果这三个其中一个的二进制码的某个位置上为0,那么返回的计算结果的那个位置的二进制码也为0。

draw是我们递归的灵魂,他的功能是在数组上填数,然后根据填的数修改row、col、cell。

//获得可填数的编码位(截断到最靠右的1) 例如10110 lowbit后得到10 
inline int lowbit(int x)
{return x & -x;
}//获取可填数据 col,row,cell经过位运算可得到一串二进制数字,二进制的1代表可以填进数独的数字
int getmask(int x, int y)
{//printBinary(row[x]);//std::cout << " ";//printBinary(col[y]);//std::cout << " ";//printBinary(cell[x/3][y/3] );//std::cout << " ";//printBinary(col[y] & row[x] & cell[x / 3][y / 3]);return col[y] & row[x] & cell[x / 3][y / 3];
}//填数字
void draw(int _arr[9][9], int x, int y, int num, bool is_set)
{//如果这个位置已经被填过,那么消除这个位置上的数字//如果没有,就设置成numif (is_set){_arr[x][y] = 0;}else{_arr[x][y] = num;}//将数字num转化成二进制码int v = 1 << (num - 1);//根据这个位置是否有数字,修改 + - 的逻辑if (is_set){v = -v;}// -v 代表此位置行,列,宫的可填数num已经填入,该行,列,宫不可再填numrow[x] -= v;col[y] -= v;cell[x / 3][y / 3] -= v;
}

我们 按照标题2. 的逻辑对数独表和三个条件进行增、改,然后搜索。

t_ret表示解的数量。max_ret表示最大解。

位置优化:通过两层循环,找出可填数最少的位置。

//填数独
bool dfs(int _arr[9][9], int cnt, int& t_ret)
{//如果可填数为0,则代表已经完成数独if (!cnt){return true;}//找出最小可选位置,x、y表示坐标,minv代表可填数int minv = 10;int x, y;//每一个为0的位置都可以通过getmask(x,y)找到一个9位的二进制数,每一个位置上的1都代表对应数字可填for (int i = 0; i < N; i++){for (int j = 0; j < N; j++){//如果状态码state中的1比minv小,则记录下该位置的xy坐标,并记录下最小可填值minvif (!_arr[i][j]){int state = getmask(i, j);if (ones[state] < minv){minv = ones[state];x = i, y = j;//std::cout << std::endl;//printBinary(state);}}}}//拿到状态码int state = getmask(x, y);//lowbit取到可填数(从小到大),填了就从状态码中消除对应位置上的1for (int i = state; i; i -= lowbit(i)){//拿到二进制对应的十进制数字numint num = map[lowbit(i)];//填入numdraw(_arr, x, y, num, false);//开始填数,如果已经填完数独,则打印,并记录解的数量t_ret,最大解max_retif (dfs(_arr, cnt - 1, t_ret)){//print_arr(_arr);t_ret++;max_ret = t_ret > max_ret ? t_ret : max_ret;}//撤销填入的numdraw(_arr, x, y, num, true);}//如果 i = state 的值是0,那么就代表没有数字可以填的,返回失败,并消除上一位的数字return false;
}

 4. 拓展问题

请问删掉数独中的哪两个数可以使得数独的解最大?

 删除的是哪两个数?

函数的逻辑是删除两个数,然后进行DFS,再然后把删除的数填回去,继续删除。

DFS进行之前,我们都初始化row,col,cell三个条件,这样能保证正常递归。

 这里我们使用vector和pair(C++),也就是数组和键值对的数据结构。

first代表x坐标,second代表y坐标。

//得到所有的数组,并记录下数独的最大解
int _getallarr(int tmp[9][9], int& time)
{//将每一个已知数字的x,y坐标记录到viistd::vector<std::pair<int, int>> vii;for (int i = 0; i < 9; i++){for (int j = 0; j < 9; j++){if (arr[i][j]){vii.push_back({ i,j });}}}//tmp1.tmp2存要删掉的两个数int tmp1, tmp2;//记录删除的数的坐标int max_ret_tmp = max_ret;//vpii的每一个元素都是一对坐标,我们只保留2对坐标std::vector<std::pair<int, int>> vpii;//依次删除两个数,为了保护源数独,把数据传入到tmp中for (int i = 0; i < vii.size(); i++){for (int j = i + 1; j < vii.size() - 1; j++){//存下要删掉的数,搜索完还原。tmp1 = tmp[vii[i].first][vii[i].second];tmp[vii[i].first][vii[i].second] = 0;tmp2 = tmp[vii[j].first][vii[j].second];tmp[vii[j].first][vii[j].second] = 0;//计算最大解int t_ret = 0;int cnt = init(tmp);dfs(tmp, cnt, t_ret);//如果最大解的数值发生变化,那么记录下该点的坐标。if (max_ret > max_ret_tmp){//此处还可做优化,比如说把2改成time,删time个数的最大解是哪三个?max_ret_tmp = max_ret;if (vpii.size() == 2){vpii.erase(vpii.begin(), vpii.end());}vpii.push_back(vii[i]);vpii.push_back(vii[j]);}//还原删除的数tmp[vii[i].first][vii[i].second] = tmp1;tmp[vii[j].first][vii[j].second] = tmp2;}}std::cout << "删除的坐标是:(" << vpii[0].first << vpii[0].second << ") && (" << vpii[1].first << vpii[1].second << ")" << std::endl;return max_ret;
}//计算最大解
int getMaxRet()
{//time为要删的数的个数int time = 2;//tmp为临时数组int tmp[9][9] = { 0 };copy_arr(tmp);//return _getallarr(tmp, time);
}

 最终代码

//设置9*9数独表
const int N = 9;
//设置mask长度 M的二进制:111111111,从右到左分别表示1 2 3 4 5 6 7 8 9 
const int M = 1 << N;//row、col、cell分别表示行、列、宫可填写数的编码
//ones、map是一个映射关系,ones表示有多少个1,map表示9位二进制的1代表的数字
//max_ret表示数独的最大解
//once 卡关,ones和map数组只需初始化一次 
int row[N], col[N], cell[3][3];
int ones[M], map[M];
int max_ret;
bool once = true;//数独表
int arr[9][9] = {4,0,0,9,0,0,0,0,3,0,8,0,0,0,1,0,9,0,0,0,0,0,2,0,7,0,0,0,3,0,0,0,0,0,0,4,0,0,6,7,0,0,5,0,0,2,0,0,0,0,0,0,6,0,0,0,7,0,3,0,6,0,0,0,5,0,6,0,0,0,0,0,1,0,0,0,0,9,0,0,2
};//打印二进制格式(调试用)
void printBinary(int num) 
{if (num == 0) {std::cout << "0";return;}int binary[32];int i = 0;while (num > 0) {binary[i] = num % 2;num /= 2;i++;}for (int j = i - 1; j >= 0; j--) {std::cout << binary[j];}
}//获得可填数的编码位(截断到最靠右的1) 例如10110 lowbit后得到10 
inline int lowbit(int x)
{return x & -x;
}//打印数独表
void print_arr(int _arr[9][9])
{for (int i = 0; i < N; i++){for (int j = 0; j < N; j++){std::cout << _arr[i][j];}std::cout << std::endl;}std::cout << std::endl;
}//复制数独表到tmp
void copy_arr(int tmp[][9])
{for (int i = 0; i < N; i++){for (int j = 0; j < N; j++){tmp[i][j] = arr[i][j];}}std::cout << std::endl;
}//获取可填数据 col,row,cell经过位运算可得到一串二进制数字,二进制的1代表可以填进数独的数字
int getmask(int x, int y)
{//printBinary(row[x]);//std::cout << " ";//printBinary(col[y]);//std::cout << " ";//printBinary(cell[x/3][y/3] );//std::cout << " ";//printBinary(col[y] & row[x] & cell[x / 3][y / 3]);return col[y] & row[x] & cell[x / 3][y / 3];
}//填数字
void draw(int _arr[9][9], int x, int y, int num, bool is_set)
{//如果这个位置已经被填过,那么消除这个位置上的数字//如果没有,就设置成numif (is_set){_arr[x][y] = 0;}else{_arr[x][y] = num;}//将数字num转化成二进制码int v = 1 << (num - 1);//根据这个位置是否有数字,修改 + - 的逻辑if (is_set){v = -v;}// -v 代表此位置行,列,宫的可填数num已经填入,该行,列,宫不可再填numrow[x] -= v;col[y] -= v;cell[x / 3][y / 3] -= v;
}//将数组上已知数的位置、值信息做初始化记录,并记录需要填写的格子数
int fill(int _arr[N][N])
{//cnt为待填格子数int cnt = 0;//设置cnt,row、col、cell条件for (int i = 0; i < N; i++){for (int j = 0; j < N; j++){if (!_arr[i][j]){cnt++;}else{col[j] -= 1 << (_arr[i][j] - 1);row[i] -= 1 << (_arr[i][j] - 1);cell[i / 3][j / 3] -= 1 << (_arr[i][j] - 1);}}}return cnt;
}//只需一次初始化的数组map、ones
void _init()
{//once设置成false后不再执行这个函数once = false;//map和ones是一个映射关系,下标(二进制)->值(十进制)// //map[10] = 2,意思是二进制为10的数十进制为2for (int i = 0; i < N; i++){map[1 << i] = i + 1;}//ones[11] = 2,意思是二进制为11的数十进制为2for (int i = 0; i < M; i++){for (int j = 0; j < N; j++){ones[i] += i >> j & 1;}}
}//初始化条件数组
int init(int _arr[N][N])
{//设置row,col为111111111,代表1`9都在可填写状态for (int i = 0; i < N; i++){row[i] = col[i] = M - 1;}//在9个宫中设置值为111111111,代表1`9都在可填写状态for (int i = 0; i < 3; i++){for (int j = 0; j < 3; j++){cell[i][j] = M - 1;}}//只初始化一次if (once){_init();}//填入数独表的已知数字,完成初始化工作。return fill(_arr);
}//填数独
bool dfs(int _arr[9][9], int cnt, int& t_ret)
{//如果可填数为0,则代表已经完成数独if (!cnt){return true;}//找出最小可选位置,x、y表示坐标,minv代表可填数int minv = 10;int x, y;//每一个为0的位置都可以通过getmask(x,y)找到一个9位的二进制数,每一个位置上的1都代表对应数字可填for (int i = 0; i < N; i++){for (int j = 0; j < N; j++){//如果状态码state中的1比minv小,则记录下该位置的xy坐标,并记录下最小可填值minvif (!_arr[i][j]){int state = getmask(i, j);if (ones[state] < minv){minv = ones[state];x = i, y = j;//std::cout << std::endl;//printBinary(state);}}}}//拿到状态码int state = getmask(x, y);//lowbit取到可填数(从小到大),填了就从状态码中消除对应位置上的1for (int i = state; i; i -= lowbit(i)){//拿到二进制对应的十进制数字numint num = map[lowbit(i)];//填入numdraw(_arr, x, y, num, false);//开始填数,如果已经填完数独,则打印,并记录解的数量t_ret,最大解max_retif (dfs(_arr, cnt - 1, t_ret)){//print_arr(_arr);t_ret++;max_ret = t_ret > max_ret ? t_ret : max_ret;}//撤销填入的numdraw(_arr, x, y, num, true);}//如果 i = state 的值是0,那么就代表没有数字可以填的,返回失败,并消除上一位的数字return false;
}//得到所有的数组,并记录下数独的最大解
int _getallarr(int tmp[9][9], int& time)
{//将每一个已知数字的x,y坐标记录到viistd::vector<std::pair<int, int>> vii;for (int i = 0; i < 9; i++){for (int j = 0; j < 9; j++){if (arr[i][j]){vii.push_back({ i,j });}}}//tmp1.tmp2存要删掉的两个数int tmp1, tmp2;//记录删除的数的坐标int max_ret_tmp = max_ret;std::vector<std::pair<int, int>> vpii;//依次删除两个数,为了保护源数独,把数据传入到tmp中for (int i = 0; i < vii.size(); i++){for (int j = i + 1; j < vii.size() - 1; j++){tmp1 = tmp[vii[i].first][vii[i].second];tmp[vii[i].first][vii[i].second] = 0;tmp2 = tmp[vii[j].first][vii[j].second];tmp[vii[j].first][vii[j].second] = 0;//计算最大解int t_ret = 0;int cnt = init(tmp);dfs(tmp, cnt, t_ret);if (max_ret > max_ret_tmp){max_ret_tmp = max_ret;if (vpii.size() == 2){vpii.erase(vpii.begin(), vpii.end());}vpii.push_back(vii[i]);vpii.push_back(vii[j]);}//还原删除的数tmp[vii[i].first][vii[i].second] = tmp1;tmp[vii[j].first][vii[j].second] = tmp2;}}std::cout << "删除的坐标是:(" << vpii[0].first << vpii[0].second << ") && (" << vpii[1].first << vpii[1].second << ")" << std::endl;return max_ret;
}//计算最大解
int getMaxRet()
{//time为要删的数的个数int time = 2;//tmp为临时数组int tmp[9][9] = { 0 };copy_arr(tmp);//return _getallarr(tmp, time);
}

 main函数(如何执行这些代码) 

int main()
{int t_ret = 0;int cnt = init(arr);dfs(arr,cnt, t_ret);std::cout << max_ret;std::cout << getMaxRet();return 0;
}

觉得写的不错的话给个三连加关注吧~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/130954.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Django开发之进阶篇

Django进阶篇 一、Django学习之模板二、Django学习之中间件默认中间件自定义中间件 三、Django学习之ORM定义模型类生成数据库表操作数据库添加查询修改删除 一、Django学习之模板 在 Django 中&#xff0c;模板&#xff08;Template&#xff09;是用于生成动态 HTML&#xff…

【Linux系统KVM虚拟机实战】LVM逻辑卷之磁盘扩容

【Linux系统KVM虚拟机实战】LVM逻辑卷之磁盘扩容 一、LVM与KVM介绍1.1 LVM介绍1.2 KVM介绍1.2.1 KVM简介1.2.2 KVM优点二、本次实践介绍2.1 本次实践简介2.2 环境规划三、虚拟机环境检查3.1 检查KVM虚拟机磁盘空间3.2 KVM虚拟机检查系统情况3.3 检查物理磁盘分区3.4 查看PV状态…

帝国cms改目录后打不开,帝国cms改目录生成后还是404

帝国CMS更改了网站域名或者栏目目录地址信息打不开的解决方法&#xff0c;一起来看看吧&#xff1a; 很多的小伙伴们&#xff0c;改了后台的系统设置里面的网站地址或者栏目目录地址&#xff0c;信息页就打不开的解决方法如下&#xff1a; 后台>系统>数据更新>更新信…

colab切换目录的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

自动拟人对话机器人在客户服务方面起了什么作用?

在当今数字时代&#xff0c;企业不断寻求创新的方法来提升客户服务体验。随着科技的不断进步和消费者期望的提升&#xff0c;传统的客户服务方式逐渐无法满足现代消费者的需求。因此&#xff0c;许多企业正在积极探索利用新兴技术来改进客户服务&#xff0c;自动拟人对话机器人…

自定义spring-boot-starter

自定义加载spring-boot-starter 第一步 创建一个Maven空项目 luban-spring-boot-starter 引入基础依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version>2.5.0</ve…

乌班图22.04 kubeadm简单搭建k8s集群

1. 我遇到的问题 任何部署类问题实际上对于萌新来说都不算简单&#xff0c;因为没有经验&#xff0c;这里我简单将部署的步骤和想法给大家讲述一下 2. 简单安装步骤 准备 3台标准安装的乌班图server22.04&#xff08;采用vm虚拟机安装&#xff0c;ip为192.168.50.3&#xff0…

【已解决】socket.gaierror: [Errno -3] Temporary failure in name resolution

问题描述 今天在环境迁移的过程中遇到多个问题&#xff0c;包括ModuleNotFoundError: No module named flask&#xff0c;socket.gaierror: [Errno -3] Temporary failure in name resolution以及Downloading: "https://huggingface.co/gyrojeff/YuzuMarker.FontDetection…

聊聊身边的嵌入式:用了七八年的电动牙刷,突然罢工了!!!

家里用了七八年的电动牙刷&#xff0c;前两天突然罢工。先尝试一下野蛮的修复方法(摔摔打打)&#xff0c;这种独家绝技屡试不爽&#xff0c;曾经修好过收音机&#xff0c;电视机&#xff0c;电子手表… 等等。不过这次&#xff0c;没有成功&#xff01;这周末终于有点儿时间&am…

ASUS华硕ZenBook灵耀X逍遥UXF3000E_UX363EA原装出厂预装Win11系统工厂模式安装包

下载链接&#xff1a;https://pan.baidu.com/s/1WLPp0e5AZErtX3bJIhTZMg?pwd2j7i 带有ASUS Recovery恢复功能、自带所有驱动、出厂主题壁纸、Office办公软件、MyASUS华硕电脑管家等预装程序 所需要工具&#xff1a;16G或以上的U盘(非必需) 文件格式&#xff1a;HDI,SWP,OFS,E…

您这边是个人公众号还是企业公众号哈

公众号迁移有什么作用&#xff1f;只能变更主体吗&#xff1f;公众号迁移不只是可以实现公众号变更公司主体&#xff0c;还可以实现个人公众号变成企业公众号、服务号改成订阅号&#xff08;不过订阅号暂时不能改成服务号了&#xff0c;最新的规定&#xff09;&#xff0c;开通…

华为云云耀云服务器L实例评测使用 | 云耀云服务器L实例Docker可视化Portainer容器管理

一、使用背景 之前一直在用阿里云或者腾讯云的服务器&#xff0c;现在接触了一下华为云的服务器实例&#xff0c;点开产品列表发现有弹性云服务器ECS、云耀云服务器HECS等&#xff0c;本文主要使用云耀云服务器&#xff0c;看到官方简介&#xff1a; 华为云耀云服务器&#x…