日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

labuladong的算法小抄_学会了回溯算法,我终于会做数独了

發(fā)布時(shí)間:2024/3/13 编程问答 58 豆豆
生活随笔 收集整理的這篇文章主要介紹了 labuladong的算法小抄_学会了回溯算法,我终于会做数独了 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

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

一、直觀感受

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

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

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

這是一個(gè)安卓手機(jī)中的數(shù)獨(dú)游戲,我使用一個(gè)叫做 Auto.js 的腳本引擎,配合回溯算法來實(shí)現(xiàn)自動(dòng)完成填寫,并且算法記錄了執(zhí)行次數(shù)。在后文,我會(huì)給出該腳本的實(shí)現(xiàn)思路代碼以及軟件工具的下載,你也可以拿來裝逼用。

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

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

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

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

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

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

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

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

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

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

void solveSudoku(char[][] board);

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

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

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

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

// 對(duì) board[i][j] 進(jìn)行窮舉嘗試 void backtrack(char[][] board, int i, int j) {int m = 9, n = 9;for (char ch = '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 加到最后一列了,怎么辦?

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

void backtrack(char[][] board, int i, int j) {int m = 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 (char ch = '1'; ch <= '9'; ch++) {// 如果遇到不合法的數(shù)字,就跳過if (!isValid(board, i, j, ch))continue;board[i][j] = ch;backtrack(board, i, j + 1);board[i][j] = '.';} }// 判斷 board[i][j] 是否可以填入 n boolean isValid(char[][] board, int r, int c, char n) {for (int i = 0; i < 9; i++) {// 判斷行是否存在重復(fù)if (board[r][i] == n) return false;// 判斷列是否存在重復(fù)if (board[i][c] == n) return false;// 判斷 3 x 3 方框是否存在重復(fù)if (board[(r/3)*3 + i/3][(c/3)*3 + i%3] == n)return false;}return true; }

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

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

最終代碼修改如下:

boolean backtrack(char[][] board, int i, int j) {int m = 9, n = 9;if (j == n) {// 窮舉到最后一列的話就換到下一行重新開始。return backtrack(board, i + 1, 0);}if (i == m) {// 找到一個(gè)可行解,觸發(fā) base casereturn true;}if (board[i][j] != '.') {// 如果有預(yù)設(shè)數(shù)字,不用我們窮舉return backtrack(board, i, j + 1);} for (char ch = '1'; ch <= '9'; ch++) {// 如果遇到不合法的數(shù)字,就跳過if (!isValid(board, i, j, ch))continue;board[i][j] = ch;// 如果找到一個(gè)可行解,立即結(jié)束if (backtrack(board, i, j + 1)) {return true;}board[i][j] = '.';}// 窮舉完 1~9,依然沒有找到可行解,此路不通return false; }boolean isValid(char[][] board, int r, int c, char n) {// 見上文 }

現(xià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è)是說不準(zhǔn)的。

那么你可能問,既然運(yùn)行次數(shù)說不準(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í)際上我們并沒有真的對(duì)每個(gè)空格都窮舉 9 次,有的數(shù)字會(huì)跳過,有的數(shù)字根本就沒有窮舉,因?yàn)楫?dāng)我們找到一個(gè)可行解的時(shí)候就立即結(jié)束了,后續(xù)的遞歸都沒有展開。

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

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

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

三、算法可視化

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

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

void setNum(Button b, char n) {// 輸入一個(gè)方格,將該方格設(shè)置為數(shù)字 n }void cancelNum(Button b) {// 輸入一個(gè)方格,將該方格上的數(shù)字撤銷 }

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

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

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

總結(jié)

以上是生活随笔為你收集整理的labuladong的算法小抄_学会了回溯算法,我终于会做数独了的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。