使用 DFS 轻松求解数独难题(C++ 的一个简单实现)

起因

都说懒惰是第一生产力,最近在玩数独游戏的时候,总会遇到拆解数独比较复杂的情况,就想着自己写个代码解题,解放双手。所以很快就写了一个简单的代码求解经典数独。拿来跑了几个最高难度的数独发现确实很爽!虽说是比较暴力的 DFS,但是由于数独中约束较多的性质,实际上要找出唯一解并不复杂,即使是最高难度的数独也可以在 0.04s 内解完,可以说是非常的方便。

思路

经典数独游戏由 9*9 的方格组成,每个方格可填 1~9 的数字,一般都有三种约束:同行,同列,同宫不可出现相同的数字。只要暴力时利用这些约束,就可以快速剪枝。

考虑最简单的情况:我们对于任何一个空位,可以尝试去填 1~9 的数字,并且检查三种约束是否满足。若满足,就继续填下一个空位。

处理约束

实际上,并不需要每个格子都去把 1~9 全部尝试。因为填的数字越多,约束就越强,我们就越容易发现之前填数时的错误。所以我们可以预先处理三种约束影响的格子范围:

void initializeRelation() {memset(digitsUsed, 0, sizeof digitsUsed);// sub-gridsfor (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {int num = i * 3 + j;for (int k = 0; k < 3; k++) {for (int l = 0; l < 3; l++) {int idx = calcIdx(i * 3 + k, j * 3 + l);group[2][idx] = num;r[num].push_back(idx);}}}}// rowsfor (int i = 0; i < N; i++) {for (int j = 0; j < N; j++) {int idx = calcIdx(i, j);group[0][idx] = i + N;r[i + N].push_back(idx);}}// columnsfor (int i = 0; i < N; i++) {for (int j = 0; j < N; j++) {int idx = calcIdx(j, i);group[1][idx] = i + N * 2;r[i + N * 2].push_back(idx);}}
}

预先处理完约束后,下次要找一个格子到底应该对应哪些约束时,就可以直接找到对应的 idx 序号了。

状态压缩

一个格子可以填 1~9 共九种数字,那么到底哪些是可以填的呢?就如同我们实际解数独时一样,我们可以在格子上标记一下有哪些数字是符合约束的。一个简单的方法是把这个状态压缩成二进制数,每个可用数字代表一个二进制位的 1,若不可用,则该位为 0。那么一个格子上的可用数字可用一个 9 位二进制数表示,范围是0~2^9,也即一个格子至多只有 512 种状态。

接下来 gcc 有一些方便的内建函数可以帮到我们,它们都是以 __builtin 开头:

  • __builtin_popcount(unsigned int x) 返回无符号整型x的二进制中 1 的个数
  • __builtin_ctz(unsigned int x)返回无符号整型x的二进制的末尾有多少个 0

上述函数也可使用 std::bitset<N>::count 等实现,作用类似。

现在计算某个格子还有多少可用数字就可以这样:

inline int calcUsable(int idx) {return 9 - __builtin_popcount(digitsUsed[idx]);
}

DFS

当我们枚举数字时,其实就是从当前状态中找到下一个可用数字,并根据约束关系删除与其相关的格子中的可用数字。

那么搜索时如何快速判断当前填的数字否可行呢?一个简单的思路是每次找到可用数字最少的格子,这样的格子可以确定更多的约束,搜索空间也更少,一旦失败了,我们可以迅速回滚。

那么把所有的空格子按照他们的[可用数字个数,可用数字状态]作为一个数对,我们就可以利用std::set构造出一个暴力 DFS 方案:

bool dfs() {if (grid.empty()) {return true;}pair<int, int> p = *grid.begin();grid.erase(p);int idx = p.second;int digitBit = MASK & ~digitsUsed[idx];for (int nextDigitBit = digitBit; nextDigitBit; nextDigitBit ^= lowbit(nextDigitBit)) {int digit = lowbit0Count(nextDigitBit);int currentDigitBit = 1 << digit;g[idx] = digit + 1;vector<int> last;for (int j = 0; j < 3; j++) {for (auto & x: r[group[j][idx]]) {auto it = grid.find(make_pair(calcUsable(x), x));if (it != grid.end() && (digitsUsed[x] | currentDigitBit) != digitsUsed[x]) {grid.erase(it);digitsUsed[x] = digitsUsed[x] | currentDigitBit;grid.insert(make_pair(calcUsable(x), x));last.push_back(x);}}}if (dfs()) {return true;}for (auto &x: last) {grid.erase(make_pair(calcUsable(x), x));digitsUsed[x] = digitsUsed[x] & ~currentDigitBit;grid.insert(make_pair(calcUsable(x), x));}}grid.insert(p);return false;
}

结语

由于只考虑经典数独,代码还是非常简洁而且高效的。而对于各种各样的变形数独,也可以考虑根据这种简化约束的方式去暴力求解。如果想要模仿人类解法,对强弱链等逻辑进行推演而非简单暴力的话,还需要更多的工作。

当然,数独如果由机器暴力计算就会缺失很多乐趣,但去寻找现有问题的一种代码实现也同样是另一种乐趣。我觉得能在数学游戏中找到自己喜欢的部分,并发掘出其中的趣味,其本身也是一种快乐的事情。

附录

最终代码如下,输入重定向于sudoku.in,输入格式中星号*代表空位,可在代码最后注释中看到样例。
输出格式为先输出整体的解,再输出只包含原数独中空位的解。

#include <bits/stdc++.h>using namespace std;const int N = 9;
const int R_NUM = 27;
const int GRID_NUM = 81;
const int MASK = (1 << N) - 1;char str[10][100];
int s[9][9];
int g[GRID_NUM];
int group[3][GRID_NUM]; // groups
vector<int> r[R_NUM]; // relations
set<pair<int, int>> grid;
int digitsUsed[GRID_NUM];/**
group 0:
000000000
111111111
222222222
333333333
444444444
555555555
666666666
777777777
888888888group 1:
012345678
012345678
012345678
012345678
012345678
012345678
012345678
012345678
012345678group 2:
000111222
000111222
000111222
333444555
333444555
333444555
666777888
666777888
666777888
**/inline int calcX(int idx) {return group[0][idx];
}inline int calcIdx(int x, int y) {return x * N + y;
}inline int lowbit(int x) {return x & (-x);
}inline int lowbit0Count(int x) {return __builtin_ctz(x);
}inline int calcUsable(int idx) {return 9 - __builtin_popcount(digitsUsed[idx]);
}void initializeRelation() {memset(digitsUsed, 0, sizeof digitsUsed);// sub-gridsfor (int i = 0; i < 3; i++) {for (int j = 0; j < 3; j++) {int num = i * 3 + j;for (int k = 0; k < 3; k++) {for (int l = 0; l < 3; l++) {int idx = calcIdx(i * 3 + k, j * 3 + l);group[2][idx] = num;r[num].push_back(idx);}}}}// rowsfor (int i = 0; i < N; i++) {for (int j = 0; j < N; j++) {int idx = calcIdx(i, j);group[0][idx] = i + N;r[i + N].push_back(idx);}}// columnsfor (int i = 0; i < N; i++) {for (int j = 0; j < N; j++) {int idx = calcIdx(j, i);group[1][idx] = i + N * 2;r[i + N * 2].push_back(idx);}}
}void fail() {printf("IMPOSSIBLE\n");exit(0);
}void printResult() {printf("Result:\n");for (int i = 0; i < N; i++) {for (int j = 0; j < N; j++) {printf("%d", g[calcIdx(i, j)]);}printf("\n");}
}void printFillableResult() {printf("\nFillable Result:\n");for (int i = 0; i < N; i++) {for (int j = 0; j < N; j++) {printf("%c", (s[i][j] == 0) ? g[calcIdx(i, j)] + '0' : '*');}printf("\n");}
}bool dfs() {if (grid.empty()) {return true;}pair<int, int> p = *grid.begin();grid.erase(p);int idx = p.second;int digitBit = MASK & ~digitsUsed[idx];for (int nextDigitBit = digitBit; nextDigitBit; nextDigitBit ^= lowbit(nextDigitBit)) {int digit = lowbit0Count(nextDigitBit);int currentDigitBit = 1 << digit;g[idx] = digit + 1;vector<int> last;for (int j = 0; j < 3; j++) {for (auto & x: r[group[j][idx]]) {auto it = grid.find(make_pair(calcUsable(x), x));if (it != grid.end() && (digitsUsed[x] | currentDigitBit) != digitsUsed[x]) {grid.erase(it);digitsUsed[x] = digitsUsed[x] | currentDigitBit;grid.insert(make_pair(calcUsable(x), x));last.push_back(x);}}}if (dfs()) {return true;}for (auto &x: last) {grid.erase(make_pair(calcUsable(x), x));digitsUsed[x] = digitsUsed[x] & ~currentDigitBit;grid.insert(make_pair(calcUsable(x), x));}}grid.insert(p);return false;
}int main() {freopen("sudoku.in", "r", stdin);initializeRelation();// Enter a sudoku puzzle: (9 lines with 9 characters on each line, use * for blank)for (int i = 0; i < N; i++) {scanf("%s", str[i]);}for (int i = 0; i < N; i++) {if (strlen(str[i]) != N) {exit(0);}for (int j = 0; j < N; j++) {int idx = calcIdx(i, j);if (str[i][j] == '*') {g[idx] = s[i][j] = 0;digitsUsed[idx] = 0;} else if (str[i][j] >= '1' && str[i][j] <= '9') {g[idx] = s[i][j] = str[i][j] - '0';} else {exit(0);}}}for (int idx = 0; idx < GRID_NUM; idx++) {if (g[idx] == 0) {for (int j = 0; j < 3; j++) {for (auto & cur: r[group[j][idx]]) {if (g[cur] != 0) {digitsUsed[idx] |= 1 << (g[cur] - 1);}}}// pair is (digitsLeftCount, idx)grid.insert(make_pair(calcUsable(idx), idx));}}if (dfs()) {printResult();printFillableResult();} else {printResult();fail();}return 0;
}/*
<Sample Input>*23456789
456789123
789123456
312645978
645978312
978312645
231564897
564897231
897231564**95*8*7*
23769***4
5**32**1*
8*1935**7
49*8*2*51
**3**6*2*
*1*2*4**6
6*8*****2
*7*1*38*******2***
2*4****7*
****5**49
**6**85**
******83*
57**4****
*3*7****6
*65*3**9*
7***9*1***/

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

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

相关文章

Ubuntu 22.04安装Rust编译环境并且测试

我参考的博客是《Rust使用国内Crates 源、 rustup源 |字节跳动新的 Rust 镜像源以及安装rust》 lsb_release -r看到操作系统版本是22.04,uname -r看到内核版本是uname -r。 sudo apt install -y gcc先安装gcc&#xff0c;要是结果给我的一样的话&#xff0c;那么就是安装好了…

【SpringBoot篇】分页查询 | 扩展SpringMvc的消息转换器

文章目录 &#x1f6f8;什么是分页查询&#x1f339;代码实现⭐问题&#x1f384;解决方法 做了几个项目&#xff0c;发现在这几个项目里面&#xff0c;都实现了分页查询效果&#xff0c;所以就总结一下&#xff0c;方便学习 我们基于黑马程序员的苍穹外卖来讲解分页查询的要点…

go-zero对数据库的操作

一、go-zerro中结合gorm来操作mysql数据库 1、这里我这就直接结合gorm-gen的方式来实现数据库操作,关于gorm-gen可以参考官网 2、创建一个数据库&#xff0c;并且创建一个表 -- ------------------------ -- 用户表 -- ------------------------ DROP TABLE IF EXISTS user; C…

Java中如何通过路径表达式找值:XPath和JsonPath以及SpEL详解及对比

大家好&#xff0c;我是G探险者。 我们编程时&#xff0c;在前后端数据交互和传输过程中&#xff0c;往往需要对报文中的某个字段或者某个标签的值进行解析读取&#xff0c;报文通常是以json或者xml作为数据交换格式&#xff0c;而json和xml这两种格式的报文结构都是具备一定的…

docker容器自启动

场景 当服务器关机重启后&#xff0c;docker容器每次都要去docker start 容器id 怎么可以下次让它自启动呢&#xff1f; 解决 先 # docker ps -a 查到之前启动过的容器id # docker update --restartalways 容器id重启后&#xff0c;reboot&#xff0c;就不用再单独去启动容…

string类的总结

目录 1.为什么要学习string类 2.string的标准库 3.string类的常用接口说明 1.string类对象的常见构造 2.string类对象的容量操作 3.string类对象的3种遍历方法 3.1 [ ] 下标 3.2 基于范围的for循环 3.3 迭代器 4 string类对象的元素访问 4.1 operator[]&#xff1a; 4.…

【洛谷 P3853】[TJOI2007] 路标设置 题解(二分答案+循环)

[TJOI2007] 路标设置 题目背景 B 市和 T 市之间有一条长长的高速公路&#xff0c;这条公路的某些地方设有路标&#xff0c;但是大家都感觉路标设得太少了&#xff0c;相邻两个路标之间往往隔着相当长的一段距离。为了便于研究这个问题&#xff0c;我们把公路上相邻路标的最大…

目标检测—YOLO系列(二 ) 全面解读复现YOLOv1 PyTorch

精读论文 前言 从这篇开始&#xff0c;我们将进入YOLO的学习。YOLO是目前比较流行的目标检测算法&#xff0c;速度快且结构简单&#xff0c;其他的目标检测算法如RCNN系列&#xff0c;以后有时间的话再介绍。 本文主要介绍的是YOLOV1&#xff0c;这是由以Joseph Redmon为首的…

交通 | 神奇动物在哪里?Operations Research经典文章

论文作者&#xff1a;Robert G. Haight, Charles S. Revelle, Stephanie A. Snyder​ 论文原文&#xff1a;Robert G. Haight, Charles S. Revelle, Stephanie A. Snyder, (2000) An Integer Optimization Approach to a Probabilistic Reserve Site Selection Problem. Operat…

C语言比较运算符

常用比较运算符 常用的比较运算符有&#xff1a; ! < > < > 运算符术语示例表示结果相等于2 10!不等于2 ! 11<小于2 < 10>大于2 > 11<小等于2 < 10>大于等于2 > 11 比较运算符注意事项 C语言的比较运算中&#xff0c;真用数字 1 表示…

VR智慧景区:VR赋能文旅产业,激活消费潜能

随着国家数字化战略的不断深入实施&#xff0c;文旅产业数字化转型的步伐也在逐渐加快&#xff0c;以VR技术赋能文旅产业&#xff0c;让文旅景区线上线下双渠道融合&#xff0c;进一步呈现文化底蕴、激活消费潜能。 VR智慧景区以沉浸式、互动式、科技感的方式&#xff0c;将景区…

C语言编程陷阱(八)

陷阱36:不要使用指针作为函数的返回值 有时候,我们可能想要用一个函数来返回一个指针,比如返回一个动态分配的内存,或者返回一个数组的某个元素的地址。但是,如果我们不小心,我们可能会犯一个很常见的错误,就是返回一个局部变量的地址。例如,看看下面的代码: #inclu…

Thrift协议详解

前言特点高效性的体现可拓展性的体现 应用场景示例拓展其他常用协议接口描述语言&#xff08;IDL&#xff09;TBinaryProtocolTCompactProtocolTDebugProtocolTDenseProtocolTJSONProtocol 前言 Thrift协议是一种接口描述语言和二进制通讯协议&#xff0c;它被用来定义和创建跨…

基于STM32的多组外部中断(EXTI)的优化策略与应用

在某些嵌入式应用中&#xff0c;可能需要同时处理多个外部中断事件。STM32系列微控制器提供了多组外部中断线&#xff08;EXTI Line&#xff09;&#xff0c;可以同时配置和使用多个GPIO引脚作为外部中断触发器。为了有效管理和处理多组外部中断&#xff0c;我们可以采取一些优…

腾讯云轻量应用服务器使用场景列举说明

腾讯云轻量应用服务器&#xff08;TencentCloud Lighthouse&#xff09;是新一代开箱即用、面向轻量应用场景的云服务器产品&#xff0c;轻量应用服务器可用于搭建中小型网站、Web应用、博客、论坛、小程序/小游戏、电商、云盘/图床、云端开发测试和学习环境等轻量级、中低负载…

uart控制led与beep

仲裁模块代码&#xff1a; // 外设控制模块&#xff0c;根据uart接收到的数据&#xff0c;控制led与beep的标志信号。 module arbit(input wire sys_clk ,input wire sys_rst_n ,input wire pi_flag …

Nginx负载均衡机制及常见问题

介绍 Nginx是目前主流的WEB服务器发布软件&#xff0c;不仅可以作为强大的web服务器&#xff0c;也可以作为一个反向代理服务器&#xff0c;而且nginx还可以按照调度规则实现动静分离&#xff0c;可以按照轮询、ip_hash、URL哈希、权重等多种方式对后端服务器做负载均衡&#…

Qt实现将字节数组以hex形式显示到文本框的方法

0. 最简单的方法&#xff1a; QString hexString buf.toHex( ); ui->rx_textEdit->append(hexString); 1. 使用 toHex 和循环插入空格&#xff1a; - 优点&#xff1a; 这种方法使用了Qt提供的现成功能&#xff0c;更符合Qt的风格&#xff0c;而且代码相对简洁易懂…

短视频ai剪辑分发账号矩阵系统(招商oem)----源头技术开发

短视频ai剪辑分发账号矩阵系统 1. 视频剪辑工具——原创短视频一键生成&#xff0c;视频剪辑亮点分析 &#xff08;1&#xff09;多模式智能剪辑 包含智能混剪逻辑、智能组合、场景顺序、图片生成视频等多种模式。在视频创作上也做了简化&#xff0c;即使是没有剪辑能力的创…

Android resource/drawable转换成Uri,Kotlin

Android resource/drawable转换成Uri&#xff0c;Kotlin private fun convertResource2Uri(resId: Int): Uri {return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE "://" resources.getResourcePackageName(resId) / resources.getResourceTypeName(resI…