当前位置: 首页 > news >正文

使用 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***/

相关文章:

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

起因 都说懒惰是第一生产力&#xff0c;最近在玩数独游戏的时候&#xff0c;总会遇到拆解数独比较复杂的情况&#xff0c;就想着自己写个代码解题&#xff0c;解放双手。所以很快就写了一个简单的代码求解经典数独。拿来跑了几个最高难度的数独发现确实很爽&#xff01;虽说是…...

【SQL server】 表结构的约束和维护

表结构的约束和维护 修改表结构 (1)添加列 (2)删除列 (3)修改列alter table 表名 add 新列名 数据类型给员工表添加一列邮箱 alter table People add PeopleMail varchar(200)删除列 alter table People drop column PeopleMain修改列 alter table 表名 alter column 列名 数据…...

竞赛 题目:基于大数据的用户画像分析系统 数据分析 开题

文章目录 1 前言2 用户画像分析概述2.1 用户画像构建的相关技术2.2 标签体系2.3 标签优先级 3 实站 - 百货商场用户画像描述与价值分析3.1 数据格式3.2 数据预处理3.3 会员年龄构成3.4 订单占比 消费画像3.5 季度偏好画像3.6 会员用户画像与特征3.6.1 构建会员用户业务特征标签…...

Vue3-ref、reactive函数的watch

Vue3-ref、reactive函数的watch ref函数的watch 原理&#xff1a;监视某个属性的变化。当被监视的属性一旦发生改变时&#xff0c;执行某段代码。watch 属性中的数据需要具有响应式watch 属性可以使用箭头函数watch 属性可以监视一个或者多个响应式数据&#xff0c;并且可以配…...

【智能家居项目】FreeRTOS版本——多任务系统中使用DHT11 | 获取SNTP服务器时间 | 重新设计功能框架

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《智能家居项目》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 目录 &#x1f353;多任务系统中使用DHT11&#x1f345;关闭调度器&#x1f345;使用中断 &am…...

Power BI Desktop数据可视化图表

...

鸿蒙APP外包开发需要注意的问题

在进行鸿蒙&#xff08;HarmonyOS&#xff09;应用开发时&#xff0c;开发者需要注意一些重要的问题&#xff0c;以确保应用的质量、性能和用户体验。以下是一些鸿蒙APP开发中需要特别关注的问题&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软…...

Redis 19 事务

Redis通过MULTI、EXEC、WATCH等命令来实现事务&#xff08;transaction&#xff09;功能。事务提供了一种将多个命令请求打包&#xff0c;然后一次性、按顺序地执行多个命令的机制&#xff0c;并且在事务执行期间&#xff0c;服务器不会中断事务而改去执行其他客户端的命令请求…...

Fabric多机部署启动节点与合约部署

这是我搭建的fabric的网络拓扑 3 个 orderer 节点&#xff1b;组织 org1 , org1 下有两个 peer 节点&#xff0c; peer0 和 peer1; 组织 org2 , org2 下有两个 peer 节点&#xff0c; peer0 和 peer1; 以上是我的多机环境的网络拓扑&#xff0c;使用的是docker搭建的。我的网络…...

WordPress主题WoodMart v7.3.2 WooCommerce主题和谐汉化版下载

WordPress主题WoodMart v7.3.2 WooCommerce主题和谐汉化版下载 WoodMart是一款出色的WooCommerce商店主题&#xff0c;它不仅提供强大的电子商务功能&#xff0c;还与流行的Elementor页面编辑器插件完美兼容。 主题文件在WoodMart Theme/woodmart.7.3.2.zip&#xff0c;核心在P…...

Java 高等院校分析与推荐系统

1&#xff09;项目简介 随着我国高等教育的大众化&#xff0c;高校毕业生就业碰到了前所未有的压力&#xff0c;高校学生就业问题开始进入相关研究者们的视野。在高校学生供给忽然急剧增加的同时&#xff0c;我国高校大学生的就业机制也在发生着深刻的变化&#xff0c;作为就业…...

【JVM】Java虚拟机

本文主要介绍了JVM的内存区域划分,类加载机制以及垃圾回收机制. 其实JVM的初心,就是让java程序员不需要去了解JVM的细节,它把很多工作内部封装好了.但是学习JVM的内部原理有利于我们深入理解学习Java. 1.JVM的内存区域划分 JVM其实是一个java进程 ; 每个java进程,就是一个jvm…...

业务架构、技术架构、项目管理的有机结合

新入职的创业公司一年不行了。 这一年来没有上班&#xff0c;也因为大龄的问题找不到合适的工作。然后考了几个项目管理证书&#xff0c;又思考了一个技术兑现的问题。 技术本身是架构的执行层面&#xff0c;如果上面的公司战略、业务架构变小&#xff0c;缩水&#xff0c;或者…...

拜耳阵列(Bayer Pattern)以及常见彩色滤波矩阵(CFA)

一、拜耳阵列的来源 图像传感器将光线转化成电流&#xff0c;光线越亮&#xff0c;电流的数值就越大&#xff1b;光线越暗&#xff0c;电流的数值就越小。图像传感器只能感受光的强弱&#xff0c;无法感受光的波长。由于光的颜色由波长决定&#xff0c;所以图像传播器无法记录…...

【信息安全】浅谈IDOR越权漏洞的原理、危害和防范:直接对象引用导致的越权行为

前言 ┌──────────────────────────────────┐ │ 正在播放《越权访问》 - Hanser │ ●━━━━━━─────── 00:00 / 03:05 │ ↻ ◁ ❚❚ ▷ ⇆ └───────────────────────────────…...

uni-app 蓝牙打印, CPCL指令集使用

先上代码: GitHub - byc233518/uniapp-bluetooth-printer-demo: 使用uniApp 连接蓝牙打印机 Demo, CPCL 指令简单实用示例 (内含 芝珂,佳博,精臣 多个厂家指令集使用文档) 文件结构: ├── App.vue ├── CPCL 指令手册.pdf // 指令集参考手册 ├── LICENSE ├── R…...

vue-组件通信(二)

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;Vue篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来vue篇专栏内容:vue-组件通信(二) 目录 组件通信(二) &#xff08;1&#xff09; props / $emit 1. 父组件向子组…...

2023年【危险化学品经营单位安全管理人员】考试题及危险化学品经营单位安全管理人员模拟试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 危险化学品经营单位安全管理人员考试题是安全生产模拟考试一点通总题库中生成的一套危险化学品经营单位安全管理人员模拟试题&#xff0c;安全生产模拟考试一点通上危险化学品经营单位安全管理人员作业手机同步练习。…...

Uni-App常用事件

Uni-App是一个跨平台的前端开发框架&#xff0c;支持多个平台的应用开发&#xff0c;包括H5、小程序、App等。在Uni-App中&#xff0c;有许多常用的事件可以用来处理用户交互、页面生命周期等方面的逻辑。以下是一些Uni-App中常用的事件&#xff1a; 点击事件&#xff08;click…...

【笔记 Pytorch】稀疏矩阵、scipy.sparse模块的使用

安装&#xff1a;pip install scipy 描述&#xff1a;就是专门为了解决稀疏矩阵而生。导入模块&#xff1a;from scipy import sparse 优缺点总结 七种矩阵类型描述coo_matrix ★【名称】coordinate format 【优点】    ① 不同稀疏格式间转换效率高(特别是CSR和CSC)  …...

业务系统对接大模型的基础方案:架构设计与关键步骤

业务系统对接大模型&#xff1a;架构设计与关键步骤 在当今数字化转型的浪潮中&#xff0c;大语言模型&#xff08;LLM&#xff09;已成为企业提升业务效率和创新能力的关键技术之一。将大模型集成到业务系统中&#xff0c;不仅可以优化用户体验&#xff0c;还能为业务决策提供…...

树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频

使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源&#xff1a; http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...

Opencv中的addweighted函数

一.addweighted函数作用 addweighted&#xff08;&#xff09;是OpenCV库中用于图像处理的函数&#xff0c;主要功能是将两个输入图像&#xff08;尺寸和类型相同&#xff09;按照指定的权重进行加权叠加&#xff08;图像融合&#xff09;&#xff0c;并添加一个标量值&#x…...

OkHttp 中实现断点续传 demo

在 OkHttp 中实现断点续传主要通过以下步骤完成&#xff0c;核心是利用 HTTP 协议的 Range 请求头指定下载范围&#xff1a; 实现原理 Range 请求头&#xff1a;向服务器请求文件的特定字节范围&#xff08;如 Range: bytes1024-&#xff09; 本地文件记录&#xff1a;保存已…...

鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个生活电费的缴纳和查询小程序

一、项目初始化与配置 1. 创建项目 ohpm init harmony/utility-payment-app 2. 配置权限 // module.json5 {"requestPermissions": [{"name": "ohos.permission.INTERNET"},{"name": "ohos.permission.GET_NETWORK_INFO"…...

【Redis】笔记|第8节|大厂高并发缓存架构实战与优化

缓存架构 代码结构 代码详情 功能点&#xff1a; 多级缓存&#xff0c;先查本地缓存&#xff0c;再查Redis&#xff0c;最后才查数据库热点数据重建逻辑使用分布式锁&#xff0c;二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...

虚拟电厂发展三大趋势:市场化、技术主导、车网互联

市场化&#xff1a;从政策驱动到多元盈利 政策全面赋能 2025年4月&#xff0c;国家发改委、能源局发布《关于加快推进虚拟电厂发展的指导意见》&#xff0c;首次明确虚拟电厂为“独立市场主体”&#xff0c;提出硬性目标&#xff1a;2027年全国调节能力≥2000万千瓦&#xff0…...

根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的----NTFS源代码分析--重要

根目录0xa0属性对应的Ntfs!_SCB中的FileObject是什么时候被建立的 第一部分&#xff1a; 0: kd> g Breakpoint 9 hit Ntfs!ReadIndexBuffer: f7173886 55 push ebp 0: kd> kc # 00 Ntfs!ReadIndexBuffer 01 Ntfs!FindFirstIndexEntry 02 Ntfs!NtfsUpda…...

Spring Security 认证流程——补充

一、认证流程概述 Spring Security 的认证流程基于 过滤器链&#xff08;Filter Chain&#xff09;&#xff0c;核心组件包括 UsernamePasswordAuthenticationFilter、AuthenticationManager、UserDetailsService 等。整个流程可分为以下步骤&#xff1a; 用户提交登录请求拦…...

macOS 终端智能代理检测

&#x1f9e0; 终端智能代理检测&#xff1a;自动判断是否需要设置代理访问 GitHub 在开发中&#xff0c;使用 GitHub 是非常常见的需求。但有时候我们会发现某些命令失败、插件无法更新&#xff0c;例如&#xff1a; fatal: unable to access https://github.com/ohmyzsh/oh…...