0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

如何用回溯算法來(lái)解決數(shù)獨(dú)問(wèn)題

算法與數(shù)據(jù)結(jié)構(gòu) ? 來(lái)源:labuladong ? 作者:labuladong ? 2022-04-26 14:47 ? 次閱讀

經(jīng)常拿回溯算法來(lái)說(shuō)事兒的,無(wú)非就是八皇后問(wèn)題和數(shù)獨(dú)問(wèn)題了。那我們今天就通過(guò)實(shí)際且有趣的例子來(lái)講一下如何用回溯算法來(lái)解決數(shù)獨(dú)問(wèn)題。

一、直觀感受

說(shuō)實(shí)話我小的時(shí)候也嘗試過(guò)玩數(shù)獨(dú)游戲,但從來(lái)都沒(méi)有完成過(guò)一次。做數(shù)獨(dú)是有技巧的,我記得一些比較專業(yè)的數(shù)獨(dú)游戲軟件,他們會(huì)教你玩數(shù)獨(dú)的技巧,不過(guò)在我看來(lái)這些技巧都太復(fù)雜,我根本就沒(méi)有興趣看下去。

不過(guò)自從我學(xué)習(xí)了算法,多困難的數(shù)獨(dú)問(wèn)題都攔不住我了。下面是我用程序完成數(shù)獨(dú)的一個(gè)例子:

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

PS:GIF 可能出現(xiàn) bug,若卡住點(diǎn)開查看即可,下同。

這是一個(gè)安卓手機(jī)中的數(shù)獨(dú)游戲,我使用一個(gè)叫做 Auto.js 的腳本引擎,配合回溯算法來(lái)實(shí)現(xiàn)自動(dòng)完成填寫,并且算法記錄了執(zhí)行次數(shù)。

在后文,我會(huì)給出該腳本的實(shí)現(xiàn)思路代碼以及軟件工具的下載,你也可以拿來(lái)裝13用。

可以觀察到前兩次都執(zhí)行了 1 萬(wàn)多次,而最后一次只執(zhí)行了 100 多次就算出了答案,這說(shuō)明對(duì)于不同的局面,回溯算法得到答案的時(shí)間是不相同的。

那么計(jì)算機(jī)如何解決數(shù)獨(dú)問(wèn)題呢?其實(shí)非常的簡(jiǎn)單,就是窮舉嘛,下面我可視化了求解過(guò)程:

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

算法的核心思路非常非常的簡(jiǎn)單,就是對(duì)每一個(gè)空著的格子窮舉 1 到 9,如果遇到不合法的數(shù)字(在同一行或同一列或同一個(gè) 3×3 的區(qū)域中存在相同的數(shù)字)則跳過(guò),如果找到一個(gè)合法的數(shù)字,則繼續(xù)窮舉下一個(gè)空格子。

對(duì)于數(shù)獨(dú)游戲,也許我們還會(huì)有另一個(gè)誤區(qū):就是下意識(shí)地認(rèn)為如果給定的數(shù)字越少那么這個(gè)局面的難度就越大。

這個(gè)結(jié)論對(duì)人來(lái)說(shuō)應(yīng)該沒(méi)毛病,但對(duì)于計(jì)算機(jī)而言,給的數(shù)字越少,反而窮舉的步數(shù)就越少,得到答案的速度越快,至于為什么,我們后面探討代碼實(shí)現(xiàn)的時(shí)候會(huì)講。

上一個(gè) GIF 是最后一關(guān) 70 關(guān),下圖是第 52 關(guān),數(shù)字比較多,看起來(lái)似乎不難,但是我們看一下算法執(zhí)行的過(guò)程:

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

可以看到算法在前兩行窮舉了半天都沒(méi)有走出去,由于時(shí)間原因我就沒(méi)有繼續(xù)錄制了,事實(shí)上,這個(gè)局面窮舉的次數(shù)大概是上一個(gè)局面的 10 倍。

言歸正傳,下面我們就來(lái)具體探討一下如何用算法來(lái)求解數(shù)獨(dú)問(wèn)題,順便說(shuō)說(shuō)我是如何可視化這個(gè)求解過(guò)程的。

二、代碼實(shí)現(xiàn)

首先,我們不用管游戲的 UI,先單純地解決回溯算法,LeetCode 第 37 題就是解數(shù)獨(dú)的問(wèn)題,算法函數(shù)簽名如下:

voidsolveSudoku(char[][]board);

輸入是一個(gè)9x9的棋盤,空白格子用點(diǎn)號(hào)字符.表示,算法需要在原地修改棋盤,將空白格子填上數(shù)字,得到一個(gè)可行解。

至于數(shù)獨(dú)的要求,大家想必都很熟悉了,每行,每列以及每一個(gè) 3×3 的小方格都不能有相同的數(shù)字出現(xiàn)。那么,現(xiàn)在我們直接套回溯框架即可求解。

前文回溯算法詳解,已經(jīng)寫過(guò)了回溯算法的套路框架,如果還沒(méi)看過(guò)那篇文章的,建議先看看。

回憶剛才的 GIF 圖片,我們求解數(shù)獨(dú)的思路很簡(jiǎn)單粗暴,就是對(duì)每一個(gè)格子所有可能的數(shù)字進(jìn)行窮舉。對(duì)于每個(gè)位置,應(yīng)該如何窮舉,有幾個(gè)選擇呢?

很簡(jiǎn)單啊,從 1 到 9 就是選擇,全部試一遍不就行了

//對(duì)board[i][j]進(jìn)行窮舉嘗試
voidbacktrack(char[][]board,inti,intj){
intm=9,n=9;
for(charch='1';ch<=?'9';ch++){
//做選擇
board[i][j]=ch;
//繼續(xù)窮舉下一個(gè)
backtrack(board,i,j+1);
//撤銷選擇
board[i][j]='.';
}
}

emmm,再繼續(xù)細(xì)化,并不是 1 到 9 都可以取到的,有的數(shù)字不是不滿足數(shù)獨(dú)的合法條件嗎?而且現(xiàn)在只是給j加一,那如果j加到最后一列了,怎么辦?

很簡(jiǎn)單,當(dāng)j到達(dá)超過(guò)每一行的最后一個(gè)索引時(shí),轉(zhuǎn)為增加i開始窮舉下一行,并且在窮舉之前添加一個(gè)判斷,跳過(guò)不滿足條件的數(shù)字

voidbacktrack(char[][]board,inti,intj){
intm=9,n=9;
if(j==n){
//窮舉到最后一列的話就換到下一行重新開始。
backtrack(board,i+1,0);
return;
}

//如果該位置是預(yù)設(shè)的數(shù)字,不用我們操心
if(board[i][j]!='.'){
backtrack(board,i,j+1);
return;
}

for(charch='1';ch<=?'9';ch++){
//如果遇到不合法的數(shù)字,就跳過(guò)
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;i9;i++){
//判斷行是否存在重復(fù)
if(board[r][i]==n)returnfalse;
//判斷列是否存在重復(fù)
if(board[i][c]==n)returnfalse;
//判斷3x3方框是否存在重復(fù)
if(board[(r/3)*3+i/3][(c/3)*3+i%3]==n)
returnfalse;
}
returntrue;
}

emmm,現(xiàn)在基本上差不多了,還剩最后一個(gè)問(wèn)題:這個(gè)算法沒(méi)有 base case,永遠(yuǎn)不會(huì)停止遞歸。這個(gè)好辦,什么時(shí)候結(jié)束遞歸?顯然r == m的時(shí)候就說(shuō)明窮舉完了最后一行,完成了所有的窮舉,就是 base case

另外,前文也提到過(guò),為了減少?gòu)?fù)雜度,我們可以讓backtrack函數(shù)返回值為boolean,如果找到一個(gè)可行解就返回 true,這樣就可以阻止后續(xù)的遞歸。只找一個(gè)可行解,也是題目的本意。

最終代碼修改如下:

booleanbacktrack(char[][]board,inti,intj){
intm=9,n=9;
if(j==n){
//窮舉到最后一列的話就換到下一行重新開始。
returnbacktrack(board,i+1,0);
}
if(i==m){
//找到一個(gè)可行解,觸發(fā)basecase
returntrue;
}

if(board[i][j]!='.'){
//如果有預(yù)設(shè)數(shù)字,不用我們窮舉
returnbacktrack(board,i,j+1);
}

for(charch='1';ch<=?'9';ch++){
//如果遇到不合法的數(shù)字,就跳過(guò)
if(!isValid(board,i,j,ch))
continue;

board[i][j]=ch;
//如果找到一個(gè)可行解,立即結(jié)束
if(backtrack(board,i,j+1)){
returntrue;
}
board[i][j]='.';
}
//窮舉完1~9,依然沒(méi)有找到可行解,此路不通
returnfalse;
}

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

現(xiàn)在可以回答一下之前的問(wèn)題,為什么有時(shí)候算法執(zhí)行的次數(shù)多,有時(shí)候少?為什么對(duì)于計(jì)算機(jī)而言,確定的數(shù)字越少,反而算出答案的速度越快?

我們已經(jīng)實(shí)現(xiàn)了一遍算法,掌握了其原理,回溯就是從 1 開始對(duì)每個(gè)格子窮舉,最后只要試出一個(gè)可行解,就會(huì)立即停止后續(xù)的遞歸窮舉。所以暴力試出答案的次數(shù)和隨機(jī)生成的棋盤關(guān)系很大,這個(gè)是說(shuō)不準(zhǔn)的。

那么你可能問(wèn),既然運(yùn)行次數(shù)說(shuō)不準(zhǔn),那么這個(gè)算法的時(shí)間復(fù)雜度是多少呢?

對(duì)于這種時(shí)間復(fù)雜度的計(jì)算,我們只能給出一個(gè)最壞情況,也就是 O(9^M),其中M是棋盤中空著的格子數(shù)量。你想嘛,對(duì)每個(gè)空格子窮舉 9 個(gè)數(shù),結(jié)果就是指數(shù)級(jí)的。

這個(gè)復(fù)雜度非常高,但稍作思考就能發(fā)現(xiàn),實(shí)際上我們并沒(méi)有真的對(duì)每個(gè)空格都窮舉 9 次,有的數(shù)字會(huì)跳過(guò),有的數(shù)字根本就沒(méi)有窮舉,因?yàn)楫?dāng)我們找到一個(gè)可行解的時(shí)候就立即結(jié)束了,后續(xù)的遞歸都沒(méi)有展開。

這個(gè) O(9^M) 的復(fù)雜度實(shí)際上是完全窮舉,或者說(shuō)是找到所有可行解的時(shí)間復(fù)雜度。

如果給定的數(shù)字越少,相當(dāng)于給出的約束條件越少,對(duì)于計(jì)算機(jī)這種窮舉策略來(lái)說(shuō),是更容易進(jìn)行下去,而不容易走回頭路進(jìn)行回溯的,所以說(shuō)如果僅僅找出一個(gè)可行解,這種情況下窮舉的速度反而比較快。

至此,回溯算法就完成了,你可以用以上代碼通過(guò) LeetCode 的判題系統(tǒng),下面我們來(lái)簡(jiǎn)單說(shuō)下我是如何把這個(gè)回溯過(guò)程可視化出來(lái)的。

三、算法可視化

讓算法幫我玩游戲的核心是算法,如果你理解了這個(gè)算法,剩下就是借助安卓腳本引擎 Auto.js 調(diào) API 操作手機(jī)了,工具我都放在后臺(tái)了,你等會(huì)兒就可以下載。

用偽碼簡(jiǎn)單說(shuō)下思路,我可以寫兩個(gè)函數(shù):

voidsetNum(Buttonb,charn){
//輸入一個(gè)方格,將該方格設(shè)置為數(shù)字n
}

voidcancelNum(Buttonb){
//輸入一個(gè)方格,將該方格上的數(shù)字撤銷
}

回溯算法的核心框架如下,只要在框架對(duì)應(yīng)的位置加上對(duì)應(yīng)的操作,即可將算法做選擇、撤銷選擇的過(guò)程完全展示出來(lái),也許這就是套路框架的魅力所在:

for(charch='1';ch<=?'9';ch++){
Buttonb=newButton(r,c);
//做選擇
setNum(b,ch);
board[i][j]=ch;
//繼續(xù)窮舉下一個(gè)
backtrack(board,i,j+1);
//撤銷選擇
cancelNum(b);
board[i][j]='.';
}

以上思路就可以模擬出算法窮舉的過(guò)程:

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

--- EOF ---

審核編輯 :李倩

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 代碼
    +關(guān)注

    關(guān)注

    30

    文章

    4791

    瀏覽量

    68694
  • 回溯算法
    +關(guān)注

    關(guān)注

    0

    文章

    10

    瀏覽量

    6620

原文標(biāo)題:搞懂回溯算法,我終于能做數(shù)獨(dú)了

文章出處:【微信號(hào):TheAlgorithm,微信公眾號(hào):算法與數(shù)據(jù)結(jié)構(gòu)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    DELL 1464獨(dú)顯圖紙.pdf

    DELL 1464 獨(dú)顯圖紙.pdf
    發(fā)表于 12-20 14:44 ?0次下載

    Pure path studio內(nèi)能否自己創(chuàng)建一個(gè)component,來(lái)實(shí)現(xiàn)特定的算法,例如LMS算法?

    TLV320AIC3254EVM-K評(píng)估模塊, Pure path studio軟件開發(fā)環(huán)境。 問(wèn)題:1.Pure path studio 內(nèi)能否自己創(chuàng)建一個(gè)component,來(lái)實(shí)現(xiàn)特定的算法
    發(fā)表于 11-01 08:25

    AFE76xx作為使用回送模式的單芯片寬帶中繼器

    電子發(fā)燒友網(wǎng)站提供《AFE76xx作為使用回送模式的單芯片寬帶中繼器.pdf》資料免費(fèi)下載
    發(fā)表于 10-08 11:30 ?0次下載
    AFE76xx作為使<b class='flag-5'>用回</b>送模式的單芯片寬帶中繼器

    何用PMBus解碼UCD90xxx故障日志

    電子發(fā)燒友網(wǎng)站提供《如何用PMBus解碼UCD90xxx故障日志.pdf》資料免費(fèi)下載
    發(fā)表于 09-25 10:04 ?0次下載
    如<b class='flag-5'>何用</b>PMBus解碼UCD90xxx故障日志

    RVBacktrace RISC-V極簡(jiǎn)棧回溯組件

    RVBacktrace組件簡(jiǎn)介一個(gè)極簡(jiǎn)的RISC-V棧回溯組件。功能在需要的地方調(diào)用組件提供的唯一API,開始當(dāng)前環(huán)境的棧回溯支持輸出addr2line需要的命令,使用addr2line進(jìn)行棧回溯支持結(jié)合反匯編,棧
    的頭像 發(fā)表于 09-15 08:12 ?404次閱讀
    RVBacktrace RISC-V極簡(jiǎn)棧<b class='flag-5'>回溯</b>組件

    何用 S7-200 實(shí)現(xiàn) Modbus 通信?

    電子發(fā)燒友網(wǎng)站提供《如何用 S7-200 實(shí)現(xiàn) Modbus 通信?.pdf》資料免費(fèi)下載
    發(fā)表于 09-14 10:22 ?1次下載

    回溯英特爾在跨越半個(gè)世紀(jì)的發(fā)展歷程

    我們以英特爾三位風(fēng)云人物的三句名言為線索,回溯英特爾在跨越半個(gè)世紀(jì)的發(fā)展歷程中,如何利用芯片技術(shù)的力量,影響信息時(shí)代,開啟未來(lái)之門。
    的頭像 發(fā)表于 08-16 14:58 ?633次閱讀

    神經(jīng)網(wǎng)絡(luò)如何用無(wú)監(jiān)督算法訓(xùn)練

    標(biāo)記數(shù)據(jù)的處理尤為有效,能夠充分利用互聯(lián)網(wǎng)上的海量數(shù)據(jù)資源。以下將詳細(xì)探討神經(jīng)網(wǎng)絡(luò)如何用無(wú)監(jiān)督算法進(jìn)行訓(xùn)練,包括常見的無(wú)監(jiān)督學(xué)習(xí)算法、訓(xùn)練過(guò)程、應(yīng)用及挑戰(zhàn)。
    的頭像 發(fā)表于 07-09 18:06 ?826次閱讀

    bp神經(jīng)網(wǎng)絡(luò)算法的基本流程包括哪些

    BP神經(jīng)網(wǎng)絡(luò)算法,即反向傳播神經(jīng)網(wǎng)絡(luò)算法,是一種常用的多層前饋神經(jīng)網(wǎng)絡(luò)訓(xùn)練算法。它通過(guò)反向傳播誤差來(lái)調(diào)整網(wǎng)絡(luò)的權(quán)重和偏置,從而實(shí)現(xiàn)對(duì)輸入數(shù)據(jù)的分類或回歸。下面詳細(xì)介紹BP神經(jīng)網(wǎng)絡(luò)
    的頭像 發(fā)表于 07-04 09:47 ?659次閱讀

    工控主機(jī)的集顯和獨(dú)顯口怎么區(qū)分

    工控主機(jī),作為工業(yè)控制領(lǐng)域的核心設(shè)備,其圖形處理能力對(duì)于整個(gè)系統(tǒng)的運(yùn)行效率至關(guān)重要。在工控主機(jī)中,集成顯卡(集顯)和獨(dú)立顯卡(獨(dú)顯)是兩種常見的圖形處理方案。它們各自具有不同的特點(diǎn)和應(yīng)用場(chǎng)景,因此,正確區(qū)分工控主機(jī)的集顯和獨(dú)顯接口顯得尤為重要。
    的頭像 發(fā)表于 05-21 18:19 ?1490次閱讀
    工控主機(jī)的集顯和<b class='flag-5'>獨(dú)</b>顯口怎么區(qū)分

    求助,關(guān)于STM32上開發(fā)函數(shù)調(diào)用堆棧回溯的問(wèn)題求解

    1、stm32f1系列 2、上了FreeRTOS 3、想開發(fā)函數(shù)調(diào)用回溯功能 在編譯選項(xiàng)中增加了--use_frame_pointer,編程一個(gè)正常的程序(之前一直run的),測(cè)試發(fā)現(xiàn),程序啟動(dòng)即crash,請(qǐng)問(wèn)有沒(méi)有高手之前遇到過(guò)?
    發(fā)表于 05-10 07:32

    三星明年起擬在芯片制造中利用回收氖氣

    據(jù)韓國(guó)《朝鮮經(jīng)濟(jì)日?qǐng)?bào)》披露,預(yù)計(jì)明年起,三星將在芯片生產(chǎn)環(huán)節(jié)啟用回收氖氣,成為全球率先采用該方法的企業(yè)。據(jù)悉,三星已經(jīng)聯(lián)合當(dāng)?shù)匾患也牧瞎狙邪l(fā)設(shè)備,以便從激光廢料流中提取氖氣,然后進(jìn)行提純處理,再投入使用。
    的頭像 發(fā)表于 03-08 13:50 ?511次閱讀

    獨(dú)石電容的特點(diǎn)及作用 獨(dú)石電容有極性嗎?獨(dú)石電容有分正負(fù)極嗎?

    獨(dú)石電容的特點(diǎn)及作用 獨(dú)石電容有極性嗎?獨(dú)石電容有分正負(fù)極嗎? 獨(dú)石電容是一種常見的電子元件。它主要由金屬氧化物半導(dǎo)體場(chǎng)效應(yīng)晶體管(MOSFET)和電介質(zhì)組成。
    的頭像 發(fā)表于 03-07 13:53 ?3416次閱讀

    獨(dú)石電容參數(shù)是什么?獨(dú)石電容和鉭電容區(qū)別在哪

    獨(dú)石電容參數(shù)是什么?獨(dú)石電容和鉭電容區(qū)別在哪? 獨(dú)石電容參數(shù)是指獨(dú)石電容器的一些重要參數(shù),這些參數(shù)用于描述獨(dú)石電容器的性能和特性。
    的頭像 發(fā)表于 03-07 13:53 ?1869次閱讀

    接觸器不吸合,如何用萬(wàn)用表來(lái)判斷接觸器線圈的好壞?

    接觸器不吸合,如何用萬(wàn)用表來(lái)判斷接觸器線圈的好壞? 接觸器是電氣控制裝置中常見的一種,廣泛應(yīng)用于電動(dòng)機(jī)的起動(dòng)、制動(dòng)和轉(zhuǎn)向等電力系統(tǒng)中。然而,在長(zhǎng)時(shí)間使用過(guò)程中,接觸器的線圈可能會(huì)出現(xiàn)老化、磨損等
    的頭像 發(fā)表于 02-18 14:52 ?3130次閱讀