0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何用回溯算法来解决数独问题

算法与数据结构 来源:labuladong 作者:labuladong 2022-04-26 14:47 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

经常拿回溯算法来说事儿的,无非就是八皇后问题和数独问题了。那我们今天就通过实际且有趣的例子来讲一下如何用回溯算法来解决数独问题。

一、直观感受

说实话我小的时候也尝试过玩数独游戏,但从来都没有完成过一次。做数独是有技巧的,我记得一些比较专业的数独游戏软件,他们会教你玩数独的技巧,不过在我看来这些技巧都太复杂,我根本就没有兴趣看下去。

不过自从我学习了算法,多困难的数独问题都拦不住我了。下面是我用程序完成数独的一个例子:

372cdc3c-c3bd-11ec-bce3-dac502259ad0.gif

PS:GIF 可能出现 bug,若卡住点开查看即可,下同。

这是一个安卓手机中的数独游戏,我使用一个叫做 Auto.js 的脚本引擎,配合回溯算法来实现自动完成填写,并且算法记录了执行次数。

在后文,我会给出该脚本的实现思路代码以及软件工具的下载,你也可以拿来装13用

可以观察到前两次都执行了 1 万多次,而最后一次只执行了 100 多次就算出了答案,这说明对于不同的局面,回溯算法得到答案的时间是不相同的。

那么计算机如何解决数独问题呢?其实非常的简单,就是穷举嘛,下面我可视化了求解过程:

37639858-c3bd-11ec-bce3-dac502259ad0.gif

算法的核心思路非常非常的简单,就是对每一个空着的格子穷举 1 到 9,如果遇到不合法的数字(在同一行或同一列或同一个 3×3 的区域中存在相同的数字)则跳过,如果找到一个合法的数字,则继续穷举下一个空格子

对于数独游戏,也许我们还会有另一个误区:就是下意识地认为如果给定的数字越少那么这个局面的难度就越大。

这个结论对人来说应该没毛病,但对于计算机而言,给的数字越少,反而穷举的步数就越少,得到答案的速度越快,至于为什么,我们后面探讨代码实现的时候会讲。

上一个 GIF 是最后一关 70 关,下图是第 52 关,数字比较多,看起来似乎不难,但是我们看一下算法执行的过程:

378d7132-c3bd-11ec-bce3-dac502259ad0.gif

可以看到算法在前两行穷举了半天都没有走出去,由于时间原因我就没有继续录制了,事实上,这个局面穷举的次数大概是上一个局面的 10 倍。

言归正传,下面我们就来具体探讨一下如何用算法来求解数独问题,顺便说说我是如何可视化这个求解过程的

二、代码实现

首先,我们不用管游戏的 UI,先单纯地解决回溯算法,LeetCode 第 37 题就是解数独的问题,算法函数签名如下:

voidsolveSudoku(char[][]board);

输入是一个9x9的棋盘,空白格子用点号字符.表示,算法需要在原地修改棋盘,将空白格子填上数字,得到一个可行解。

至于数独的要求,大家想必都很熟悉了,每行,每列以及每一个 3×3 的小方格都不能有相同的数字出现。那么,现在我们直接套回溯框架即可求解。

前文回溯算法详解,已经写过了回溯算法的套路框架,如果还没看过那篇文章的,建议先看看

回忆刚才的 GIF 图片,我们求解数独的思路很简单粗暴,就是对每一个格子所有可能的数字进行穷举。对于每个位置,应该如何穷举,有几个选择呢?

很简单啊,从 1 到 9 就是选择,全部试一遍不就行了

//对board[i][j]进行穷举尝试
voidbacktrack(char[][]board,inti,intj){
intm=9,n=9;
for(charch='1';ch<= '9';ch++){
//做选择
board[i][j]=ch;
//继续穷举下一个
backtrack(board,i,j+1);
//撤销选择
board[i][j]='.';
}
}

emmm,再继续细化,并不是 1 到 9 都可以取到的,有的数字不是不满足数独的合法条件吗?而且现在只是给j加一,那如果j加到最后一列了,怎么办?

很简单,当j到达超过每一行的最后一个索引时,转为增加i开始穷举下一行,并且在穷举之前添加一个判断,跳过不满足条件的数字

voidbacktrack(char[][]board,inti,intj){
intm=9,n=9;
if(j==n){
//穷举到最后一列的话就换到下一行重新开始。
backtrack(board,i+1,0);
return;
}

//如果该位置是预设的数字,不用我们操心
if(board[i][j]!='.'){
backtrack(board,i,j+1);
return;
}

for(charch='1';ch<= '9';ch++){
//如果遇到不合法的数字,就跳过
if(!isValid(board,i,j,ch))
continue;

board[i][j]=ch;
backtrack(board,i,j+1);
board[i][j]='.';
}
}

//判断board[r][c]是否可以填入n
booleanisValid(char[][]board,intr,intc,charn){
for(inti=0;i< 9;i++){
//判断行是否存在重复
if(board[r][i]==n)returnfalse;
//判断列是否存在重复
if(board[i][c]==n)returnfalse;
//判断3x3方框是否存在重复
if(board[(r/3)*3+i/3][(c/3)*3+i%3]==n)
returnfalse;
}
returntrue;
}

emmm,现在基本上差不多了,还剩最后一个问题:这个算法没有 base case,永远不会停止递归。这个好办,什么时候结束递归?显然r == m的时候就说明穷举完了最后一行,完成了所有的穷举,就是 base case

另外,前文也提到过,为了减少复杂度,我们可以让backtrack函数返回值为boolean,如果找到一个可行解就返回 true,这样就可以阻止后续的递归。只找一个可行解,也是题目的本意。

最终代码修改如下:

booleanbacktrack(char[][]board,inti,intj){
intm=9,n=9;
if(j==n){
//穷举到最后一列的话就换到下一行重新开始。
returnbacktrack(board,i+1,0);
}
if(i==m){
//找到一个可行解,触发basecase
returntrue;
}

if(board[i][j]!='.'){
//如果有预设数字,不用我们穷举
returnbacktrack(board,i,j+1);
}

for(charch='1';ch<= '9';ch++){
//如果遇到不合法的数字,就跳过
if(!isValid(board,i,j,ch))
continue;

board[i][j]=ch;
//如果找到一个可行解,立即结束
if(backtrack(board,i,j+1)){
returntrue;
}
board[i][j]='.';
}
//穷举完1~9,依然没有找到可行解,此路不通
returnfalse;
}

booleanisValid(char[][]board,intr,intc,charn){
//见上文
}

现在可以回答一下之前的问题,为什么有时候算法执行的次数多,有时候少?为什么对于计算机而言,确定的数字越少,反而算出答案的速度越快

我们已经实现了一遍算法,掌握了其原理,回溯就是从 1 开始对每个格子穷举,最后只要试出一个可行解,就会立即停止后续的递归穷举。所以暴力试出答案的次数和随机生成的棋盘关系很大,这个是说不准的。

那么你可能问,既然运行次数说不准,那么这个算法的时间复杂度是多少呢

对于这种时间复杂度的计算,我们只能给出一个最坏情况,也就是 O(9^M),其中M是棋盘中空着的格子数量。你想嘛,对每个空格子穷举 9 个数,结果就是指数级的。

这个复杂度非常高,但稍作思考就能发现,实际上我们并没有真的对每个空格都穷举 9 次,有的数字会跳过,有的数字根本就没有穷举,因为当我们找到一个可行解的时候就立即结束了,后续的递归都没有展开。

这个 O(9^M) 的复杂度实际上是完全穷举,或者说是找到所有可行解的时间复杂度。

如果给定的数字越少,相当于给出的约束条件越少,对于计算机这种穷举策略来说,是更容易进行下去,而不容易走回头路进行回溯的,所以说如果仅仅找出一个可行解,这种情况下穷举的速度反而比较快。

至此,回溯算法就完成了,你可以用以上代码通过 LeetCode 的判题系统,下面我们来简单说下我是如何把这个回溯过程可视化出来的。

三、算法可视化

让算法帮我玩游戏的核心是算法,如果你理解了这个算法,剩下就是借助安卓脚本引擎 Auto.js 调 API 操作手机了,工具我都放在后台了,你等会儿就可以下载。

用伪码简单说下思路,我可以写两个函数:

voidsetNum(Buttonb,charn){
//输入一个方格,将该方格设置为数字n
}

voidcancelNum(Buttonb){
//输入一个方格,将该方格上的数字撤销
}

回溯算法的核心框架如下,只要在框架对应的位置加上对应的操作,即可将算法做选择、撤销选择的过程完全展示出来,也许这就是套路框架的魅力所在:

for(charch='1';ch<= '9';ch++){
Buttonb=newButton(r,c);
//做选择
setNum(b,ch);
board[i][j]=ch;
//继续穷举下一个
backtrack(board,i,j+1);
//撤销选择
cancelNum(b);
board[i][j]='.';
}

以上思路就可以模拟出算法穷举的过程:

37639858-c3bd-11ec-bce3-dac502259ad0.gif

--- EOF ---

审核编辑 :李倩

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 代码
    +关注

    关注

    30

    文章

    4976

    浏览量

    74382
  • 回溯算法
    +关注

    关注

    0

    文章

    10

    浏览量

    6758

原文标题:搞懂回溯算法,我终于能做数独了

文章出处:【微信号:TheAlgorithm,微信公众号:算法与数据结构】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    【OFDR】实时感知、动态重构与历史状态回溯!昊衡科技-三维场重构软件

    路径映射三维螺旋路径映射支持TCP实时数据传输,支持导入本地TXT数据,对试验过程进行回溯分析,方便后期数据复盘与优化。数据回放功能界面从实时数据采集到三维场可视化,再
    的头像 发表于 01-29 17:40 1484次阅读
    【OFDR】实时感知、动态重构与历史状态<b class='flag-5'>回溯</b>!昊衡科技-三维场重构软件

    回溯示波器的四次认知跃迁

    最近示波器圈热闹得有些“魔幻”,我抱着科普心态凑了几次热点,关于某款国产示波器的内容却接连被申退。倒也理解,行业风口下的争议本就难免。但比起纠结单一产品,或许我们更该静下心聊聊:这个被称为电子
    的头像 发表于 12-19 15:39 6823次阅读
    <b class='flag-5'>回溯</b>示波器的四次认知跃迁

    何用FPGA控制ADV7513实现HDMI画面显示和音频播放

    HDMI接口显示使用DMT时序+TMDS编码实现。当用FPGA控制HDMI的数据传输时,通常可以采用纯RTL实现TMDS算法或者使用专门的HDMI芯片(如ADV7513)这两种方案完成。本文主要是介绍如
    的头像 发表于 12-02 11:05 7051次阅读
    如<b class='flag-5'>何用</b>FPGA控制ADV7513实现HDMI画面显示和音频播放

    8种常用的CRC算法分享

    CRC 计算单元可按所选择的算法和参数配置来生成数据流的 CRC 码。有些应用中,可利用 CRC 技术验证数据的传输和存储的完整性。 8 种常用的 CRC 算法,包括: CRC16_IBM
    发表于 11-13 07:25

    智化光伏电站管理系统的“智化”价值体现

    物联网、大数据、人工智能、云计算、数字孪生等多种技术,通过技术融合与数据驱动,实现光伏电站效率跃升、成本优化以及资产增值的核心价值。 智化实现路径主要是从技术架构、功能模块等层面进行,依赖于技术架构与功
    的头像 发表于 11-07 15:21 697次阅读
    <b class='flag-5'>数</b>智化光伏电站管理系统的“<b class='flag-5'>数</b>智化”价值体现

    如何使用恢复算法实现开平方运算

    本文主要描述如何使用恢复算法实现开平方运算。 简介 开平方的恢复算法其实与除法的恢复算法十分相似。首先我们假设X为输入的操作数(它应该为正数),而他的平方根可以表示为Qn=0.q1
    发表于 10-24 13:33

    e203乘法运算结构及算法原理

    Booth算法 对于普通的乘法运算,以两个8比特二进制为例,可以写为图一所示的8个部分积之和: 同理,两个32位二进制相乘,在扩展符号位后,可以分为33个部分和之和。如果直接将33个部分和
    发表于 10-22 06:43

    基于FPGA的CLAHE图像增强算法设计

    CLAHE图像增强算法又称为对比度有限的自适应直方图均衡算法,其算法原理是通过有限的调整图像局部对比度增强有效信号和抑制噪声信号。
    的头像 发表于 10-15 10:14 799次阅读
    基于FPGA的CLAHE图像增强<b class='flag-5'>算法</b>设计

    显和集显有什么区别?一篇搞懂!​

    选电脑时,“显卡”是决定使用体验的关键硬件。有人纠结“集显够不够用”,有人担心“显太贵且笨重”。其实显(独立显卡)和集显(集成显卡)没有绝对的 “好坏”,只有“是否适配需求”:集显适合日常办公
    的头像 发表于 09-19 16:09 8517次阅读
    <b class='flag-5'>独</b>显和集显有什么区别?一篇搞懂!​

    真随机和伪随机的区别

    随机在当前程序运行环境中是一种常用参数,目前主要分为两种,伪随机和真随机,本期我们就来讲一下二者的区别。
    的头像 发表于 08-27 17:46 2890次阅读

    生产线回溯追溯系统选型:中设智控方案如何破解行业痛点?

    中设智控产线回溯追溯方案,从硬件到功能,精准破解行业痛点,为电子制造、新能源等行业提供高效、可靠的生产管理工具,助力企业实现智能化生产升级,值得选型参考。
    的头像 发表于 07-18 11:19 1184次阅读
    生产线<b class='flag-5'>回溯</b>追溯系统选型:中设智控方案如何破解行业痛点?

    在友晶DE1-SOC开发板实现谜题求解器

    游戏是一种广受欢迎的数学游戏。在其基本且被广泛认可的形式中,包含一个 9 × 9 的网格,其中某些方格已填入数字。该游戏的目的是通过填入剩余的方格
    的头像 发表于 07-16 16:14 850次阅读
    在友晶DE1-SOC开发板实现<b class='flag-5'>数</b><b class='flag-5'>独</b>谜题求解器

    何用电容式的片式 CHIP LAN 网络变压器(电感)替代消费级传统网络变压器,电气原理图是怎样的?

    Hqst石门盈盛(华强盛)电子导读:如何用电容式的片式 CHIP LAN 网络变压器(电感)替代消费级传统网络变压器,电气原理图是怎样的?这节将和大家一起做探讨.....
    的头像 发表于 07-13 11:01 2178次阅读
    如<b class='flag-5'>何用</b>电容式的片式 CHIP LAN 网络变压器(电感)<b class='flag-5'>来</b>替代消费级传统网络变压器,电气原理图是怎样的?

    斩获阿拉丁神灯奖两大权威奖项,Yeelight易引领行业智化升级

    近日,阿拉丁神灯奖获奖名单公布,智能照明领导品牌Yeelight易,凭借其卓越的品牌创新力与产品技术实力,一举夺得“2025阿拉丁神灯奖智品牌奖”,旗下明星产品Yeelight智能筒射灯T4更
    的头像 发表于 05-30 12:48 939次阅读
    斩获阿拉丁神灯奖两大权威奖项,Yeelight易<b class='flag-5'>来</b>引领行业<b class='flag-5'>数</b>智化升级

    山东LP-SCADA故障回溯功能的好处

    关键字:LP-SCADA, LP-SCADA平台 , LP-SCADA系统, 软件回溯功能,蓝鹏测控 得益于本平台毫秒级的采集延迟,本平台除了具有普通监控采集平台的所有监控功能外,还可用于产线、设备
    发表于 05-29 14:42