前些時間在手機上下了個數獨游戲(Sudoku),用以在火車上消遣時間,游戲設置了easy,medium, hard和very hard4個難度等級。一開始玩easy的,大概6-7分鍾,后來試着來個hard,竟然花了30分鍾,太被打擊了,后來就想着來段code來節省點腦細胞。
數據游戲規則
數獨游戲是一個9x9的網格,每個格子是1-9中的任意一個數,游戲開始時,部分格子是填好數字的,游戲內容就是將空白格子填上數字,使得
- 每行都沒有重復數字
- 每列都沒有重復數字
- 每個3x3的網格內也沒有重復數字
如下圖所示,這是一個Very Hard級別的數獨。

游戲求解
如何來求解這個問題呢?一開始想着根據自己玩游戲的方法來處理。最直觀的一條規則:
先分析出每個位置的可能值,將那些可能取值僅有1個的格子確定下來
這樣就會減少該格子所在行列和子塊的空格的可能取值,采用遞歸的方式就可以依次得到其他格子的數字。如下面圖中填寫的紅色數字。

想法是美好的,可現實卻是殘酷的,搞好一運行,部分easy的可以解決,可大部分的游戲都解決不了,更不用說最前面那個Very Hard圖中的那個例子了。
顯然,上述規則還不夠完備,導致某些問題解決不了,再仔細想想自己玩游戲的時候的思路,由於網格是9x9的,那么每行,每列和每個子塊都必然包含1-9的所有數字,下面考慮同一行中的情況(同列和同子塊類似):
若某個格子可取值123,但該行其他所有空行都不可能去1,那么該格子必須取1
這條規則寫起來比第一個稍微復雜點,但還不算難,寫好后一測,easy的基本都ok,可hard的還是搞不定,只能繼續挖了。最后想到還有另外一條規則:
若同一行中,A格子可取12,B可取12,C可取123,那么可以確定AB必然1個是1另一個是2,那么C中的12就可以消掉了
規則是想到了,可不好用code來表達,結果也就這么不了了之了。
后來的某天,想到可以換個思路,電腦運算能力那么NB的,讓它去遍歷應該可以問題不大(當然要是寫個81重的循環估計是要崩潰的,不信可以試試),簡單的方式是采用深度優先的遞歸調用來實現,具體思路如下:
- 從上到下從左到右遍歷網格,到第一個空格子出,計算出空格子可能的取值
- 給空格子依次賦一個可能值后遞歸調用,若該值錯誤,則遞歸到某一步會導致一個空格子沒有可取的值
直接上代碼:
#define SUDOKU_DIM 9
#define BLOCK_SIZE 3
#define SUDOKU_VALUE 9
bool Solve(byte *pSudoku)
{
byte *pSudokuTmp = pSudoku;
//======find first empty======
int idx, r, c;
pSudokuTmp = pSudoku;
for(idx = 0; idx < SUDOKU_DIM * SUDOKU_DIM; idx++, pSudokuTmp++)
{
if(pSudoku[idx] == 0) break;
}
if(idx == SUDOKU_DIM * SUDOKU_DIM) return true;
r = idx / SUDOKU_DIM;
c = idx % SUDOKU_DIM;
//======find prossiable value======
bool bValuePossiable[SUDOKU_VALUE + 1];
memset(bValuePossiable, 1, sizeof(bool) * (SUDOKU_VALUE + 1));
//row
pSudokuTmp = pSudoku + r * SUDOKU_DIM;
for(int i = 0; i < SUDOKU_DIM; i++)
{
bValuePossiable[pSudokuTmp[i]] = false;
}
//col
pSudokuTmp = pSudoku + c;
for(int i = 0; i < SUDOKU_DIM; i++, pSudokuTmp += SUDOKU_DIM)
{
bValuePossiable[*pSudokuTmp] = false;
}
//block
int startRow = (r / BLOCK_SIZE) * BLOCK_SIZE;
int startCol = (c / BLOCK_SIZE) * BLOCK_SIZE;
pSudokuTmp = pSudoku + startRow * SUDOKU_DIM + startCol;
for(int i = 0; i < BLOCK_SIZE; i++, pSudokuTmp += SUDOKU_DIM)
{
for(int j = 0; j < BLOCK_SIZE; j++)
{
bValuePossiable[pSudokuTmp[j]] = false;
}
}
//======try to decide======
for(int i = 1; i <= SUDOKU_VALUE; i++)
{
if(!bValuePossiable[i]) continue;
pSudoku[idx] = i;
bool bret = Solve(pSudoku);
if(bret) return true;
}
pSudoku[idx] = 0;
return false;
}
下面是測試代碼,由於這里主要關注求解方法,測試代碼寫得有點水,輸入一個數獨游戲比較麻煩:
void print_Sudoku(byte *pu8Sudoku)
{
printf("==============begin===============\n\n");
for(int i = 0; i < SUDOKU_DIM; i++)
{
for(int j = 0; j < SUDOKU_DIM; j++, pu8Sudoku++)
{
printf("%d ", *pu8Sudoku);
if((j + 1) % BLOCK_SIZE == 0)
{
printf(" ");
}
}
printf("\n");
if((i+1)%BLOCK_SIZE == 0)
{
printf("\n");
}
}
printf("============== end ===============\n");
}
void test_sudoku()
{
byte au8Sudoku[SUDOKU_DIM * SUDOKU_DIM]=
{
//5, 0, 0, 4, 1, 0, 0, 8, 0,
//9, 0, 0, 0, 0, 0, 1, 0, 0,
//8, 0, 0, 6, 7, 9, 0, 0, 5,
//0, 4, 0, 7, 0, 0, 5, 9, 1,
//0, 8, 0, 0, 6, 0, 0, 7, 0,
//7, 9, 1, 0, 0, 3, 0, 2, 0,
//1, 0, 0, 2, 5, 6, 0, 0, 9,
//0, 0, 7, 0, 0, 0, 0, 0, 3,
//0, 5, 0, 0, 3, 7, 0, 0, 6
//
0, 0, 0, 9, 0, 0, 1, 0, 2,
4, 0, 0, 1, 0, 6, 0, 0, 0,
0, 8, 0, 0, 2, 0, 0, 0, 5,
6, 3, 0, 0, 0, 0, 5, 0, 0,
0, 0, 4, 5, 0, 7, 9, 0, 0,
0, 0, 5, 0, 0, 0, 0, 3, 1,
9, 0, 0, 0, 6, 0, 0, 1, 0,
0, 0, 0, 8, 0, 9, 0, 0, 7,
7, 0, 6, 0, 0, 2, 0, 0, 0
// 0, 0, 0, 4, 0, 0, 3, 0, 6,
// 0, 0, 0, 7, 1, 3, 0, 0, 4,
// 5, 4, 3, 9, 0, 0, 1, 0, 0,
// 3, 9, 0, 0, 0, 0, 6, 0, 0,
// 0, 7, 8, 6, 0, 5, 9, 2, 0,
// 0, 0, 6, 0, 0, 0, 0, 3, 7,
// 0, 0, 1, 0, 0, 9, 7, 8, 2,
// 7, 0, 0, 3, 6, 2, 0, 0, 0,
// 9, 0, 5, 0, 0, 1, 0, 0, 0
};
bool bret = Solve(au8Sudoku);
printf("%s\n", bret? "Ok":"NO");
print_Sudoku(au8Sudoku);
}
運行結果如下:

速度也比我想象的快多了,畢竟是電腦哪。當然,假如數獨有多個解,上面的算法也就只能得到一個解。
