用STL實現先深搜尋及先寬搜尋
——數獨
(sudoku)
例子
(2)前面我們用一個遊戲——數獨sudoku,實作出了一個簡單(但效率較差)的解法,驗證了我們的DFS和BFS演算法。為簡單起見,我們採用了最簡單的解法而暫不考慮效率。這裡,我們嘗試改進一下數獨問題解法的效率,並將新的解法與原來的解法進行一個效率的對比。從DFS/BFS演算法中可以看出,搜尋演算法的執行效率主要取決於問題狀態空間樹的大小,如果狀態空間樹每層的分支較少,則樹的結點也會較少,這樣搜尋就可以更快結束。所以,我的基本想法就是,盡量減少狀態空間樹中每層的分支數目。具體到數獨遊戲,我們原來的解法是:在二維數組中掃描到第一個未填數位空格,就以這個空格為基礎來產生下一層狀態結點分支,而根本沒有考慮過這個空格是否最佳的產生點。這樣自然會導致狀態空間樹的最高几層分支過多,結點數有可能急速增加,從而增長了搜尋時間。我的想法是,不要找到一個空格就立即用於產生下一層狀態結點,而是對目前狀態中的所有空格都計算出其可能的分支數目,然後以分支數目最少的一個空格為產生點來產生下一層狀態結點。這樣,狀態空間樹的最高几層的分支就會相對較少,從而降低搜尋的時間。顯然,這隻需要對原來的SudokuState::nextStep()成員函數進行改寫即可,如下:void SudokuState::nextStep(vector<SudokuState>& vs) const{ SudokuState newState; bool pos[SUDOKU_DIMS], bestPos[SUDOKU_DIMS]; int best = SUDOKU_DIMS+1, bestRow, bestCol; for (int row = 0; row < SUDOKU_DIMS; ++row) { for (int col = 0; col < SUDOKU_DIMS; ++col) { if (data_[row][col] == 0) // Space { fill_n(pos, SUDOKU_DIMS, true); for (int k = 0; k < SUDOKU_DIMS; ++k) { checkValue(pos, data_[k][col]); checkValue(pos, data_[row][k]); } int rs = row - (row % SUDOKU_ROWS); int cs = col - (col % SUDOKU_COLS); for (int i = 0; i < SUDOKU_ROWS; ++i) { for (int j = 0; j < SUDOKU_COLS; ++j) { checkValue(pos, data_[rs+i][cs+j]); } } int s = count(pos, pos+SUDOKU_DIMS, true); if (s == 0) // 如果有一個空格沒有可以填的值 { return; } if (best > s) { best = s; copy(pos, pos+SUDOKU_DIMS, bestPos); bestRow = row; bestCol = col; } } } } for (int k = 1; k <= SUDOKU_DIMS; ++k) { if (bestPos[k-1]) // a possible value { newState = *this; newState.data_[bestRow][bestCol] = k; vs.push_back(newState); } }} 與原來的函數相比,多了十幾行,增加了幾個局部變數,包括三個int和一個bool數組。這些新增的變數用於儲存找到的分支最少的空格的一些相關資料,分別是空格所在行、列,分支數量以及可選的數字。其中有一點要注意的是,在迴圈中如果發現有一個空格沒有可填入的數字,則說明目前狀態是一個無解的分支狀態,可以立即返回。經測試,新的解法可以正確地用於DFS和BFS演算法,並找到了正確答案,速度也有了大幅提高。在我的機器上進行測試結果(剔除I/O時間)如下:
| |
DFS (one answer) |
BFS (one answer) |
DFS (all answers) |
BFS (all answers) |
| 原來的解法 |
3.5ms |
5.8ms |
8.3ms |
8.3ms |
| 最佳化的解法 |
1.6ms |
2.3ms |
2.2ms |
2.3ms |
由於我只使用了一個數獨題目進行測試,所以結果並不具有廣泛代表性,但也可以在一定程度上說明新的解法已經有了明顯的速度提高。當然,這個解法還可以進一步最佳化,你如果有興趣,可以自己試著寫一下,也歡迎你把結果反饋給我。 關於數獨問題的討論我想就到此為止了,我們回過頭來看看我們的DFS/BFS演算法,其實它還有很多局限。你可能已經注意到了,它只適用於一些特殊的問題,即這些問題的狀態空間樹中的結點不能有重複。我們還是以原來的圖來說明一下:我們想象一下,如果狀態空間樹中的結點E與結點A是相同的狀態,那麼會有什麼結果?我們的DFS/BFS演算法將會陷入一個無限迴圈!幸好,數獨問題恰好不會出現這樣的狀況,但是象下象棋、推箱子等問題,都會存在這個問題,一個問題狀態在幾步之後可能會回到以前出現過的狀態。如何解決這個問題?我們將在以後繼續討論。