數獨求解,第一個想到的方法就是DFS回溯。但是簡單回溯法在求解單個數獨時效率還能接受,放在五重數獨(武士數獨)上可以就有點差強人意了。我要是用它來解武士數獨的話也沒必要為這道題寫篇博客了 🐱。
開搞
在此之前又仔細學習了一遍DancingLinks。DLX算法解數獨的關鍵在於將數獨轉化為精確覆蓋問題,這一步在單個矩陣的情況下還是比較容易的,但在武士數獨上就比較繁瑣了。
定義數據結構
const static int SAMURAI_EDGE = 21;
const static int SAMURAI_MATRIX = 441;
const static int SAMURAI_ROWS = 405;
const static int SUDOKU_EDGE = 9;
const static int SUDOKU_MATRIX = 81;
const static int COLUMN_SIZE = 1692;
class DLNode
{
public:
DLNode * Left; // 左結點
DLNode *Right; // 右結點
DLNode *Up; // 上結點
DLNode *Down; // 下結點
DLNode *Col; // 所屬列結點
int row; // 行號
int nums; // 該列存在的結點個數(當結點為列結點時有效,否則為-1)
DLNode(DLNode *Col, int n, int s = -1):
Left(this), Right(this), Up(this), Down(this),
Col(Col), row(n), nums(s){ if (Col) Col->Add2Colume(this); };
~DLNode() {};
void Add2Row(DLNode *node); // 添加結點到該行末尾
void Add2Colume(DLNode *node); // 添加結點到該列尾
void RemoveCol(); // 移除該結點所在的列
void RecoverCol(); // 還原列
void Remove(); // 移除該結點關聯的行和列
};
class DancingLinks
{
public:
DancingLinks(int s[SAMURAI_EDGE][SAMURAI_EDGE]);
~DancingLinks();
DLNode *Head;
std::vector<DLNode *> Cols; // 列向量
std::vector<DLNode *> Ans; // 保存結果
bool DLX(); // DLX算法求解
void ShowResult(int result[SAMURAI_MATRIX]); // 輸出結果
};
數獨規則:
- 每個格子只能填一個數字
- 每行每個數字只能填一遍(1-9)
- 每列每個數字只能填一遍(1-9)
- 每宮每個數字只能填一遍(1-9)
武士數獨精確覆蓋問題
武士數獨有五個數獨組成,需要 \(21 \times 21\) 大小的矩陣存儲數據,即 \(441\) 個元素。給五個數獨編號
約束定義(索引從0開始)
- 定義441列
第0列:表示位置(0, 0)填了一個數字
第1列:表示位置(0, 1)填了一個數字
. . . . . .
第20列:表示位置(0, 20)填了一個數字
第21列:表示位置(1, 0)填了一個數字
. . . . . .
第440列:表示位置(20, 20)填了一個數字
位置\((X,Y)\), \(Col = X \times 21 + Y\)
- 定義405列(5個數獨,總共45行)
第441列:0號數獨的第0行填了數字1
第442列:0號數獨的第0行填了數字2
. . . . . .
第449列:0號數獨的第0行填了數字9
第450列:0號數獨的第1行填了數字1
. . . . . .
第845列:4號數獨的第8行填了數字9
第N列定義為 第\(S\)號數獨\(X\)行填了數字\(Y\),它們之間的關系為
\(N = 441 + S \times 81 + X \times 9 + (Y-1)\)
- 定義405列(5個數獨,總共45列)
第846列:0號數獨的第0列填了數字1
第847列:0號數獨的第0列填了數字2
. . . . . .
第857列:0號數獨的第0列填了數字9
第858列:0號數獨的第1列填了數字1
. . . . . .
第1250列:4號數獨的第8列填了數字9
第N列定義為 第\(S\)號數獨\(X\)列填了數字\(Y\),它們之間的關系為
\(N = 441 + 405 + S \times 81 + X \times 9 + (Y-1)\)
- 定義441列(\(21\times21\)矩陣,總共49個宮,為方便計算沒有刪去空白的宮)
第1251列:第0宮填了數字1
第1252列:第0宮填了數字2
. . . . . .
第1259列:第0宮填了數字9
第1260列:第1宮填了數字1
. . . . . .
第1691列:第48宮填了數字9
第N列定義為 第\(S\)宮填了數字\(D\),它們之間的關系為
\(N = 441 + 405 + 405 + S \times 9 + (D-1)\)
由上1692列完成了對武士數獨的精確覆蓋問題約束定義
初始化Dancing Links
用上圖數獨為例,(0, 0) 位置為 9,轉換為DancingLinksz中的一行,則第0,449, 857, 1259列為 1 (即存在結點),其余列為 0。
Dancing Links初始化
DancingLinks::DancingLinks(int sam[SAMURAI_EDGE][SAMURAI_EDGE])
{
Head = new DLNode(nullptr, 0);
// 創建列結點 1692個
for (int i = 0; i < COLUMN_SIZE; i++)
{
auto t = new DLNode(nullptr, 0, 0);
Head->Add2Row(t);
Cols.push_back(t);
}
std::vector<DLNode *> Rows; // 保存初始已存在數字的結點
for (int r = 0; r < SAMURAI_EDGE; r++)
{
for (int c = 0; c < SAMURAI_EDGE; c++)
{
for (int d = 0; d < SUDOKU_EDGE; d++)
{
// 計算行數
int row = (r * SAMURAI_EDGE * SUDOKU_EDGE) + (c * SUDOKU_EDGE) + d;
int sq = (c / 3) + ((r / 3) * 7);
int t = VALID_SQUARE[sq];
if (t > 0)
{
auto node = new DLNode(Cols[r * SAMURAI_EDGE + c], row);
for (int i = 0; i < 2; i++)
{
// 判斷sq號宮屬於第幾號數獨
int sd = (t > 5 && !i) ? 4 : t-1;
// 當前r,c屬於sd號數獨的幾行幾列
//(感覺用數組索引更方便, 一開始我是直接硬算行列,后來在網上看到有人用數組的方式實現)
int sdr = SUDOKU_ROW[sd][r];
int sdc = SUDOKU_COLUMN[sd][c];
// 五個數獨 總共45行 1-9數字情況 405列
node->Add2Row(new DLNode(Cols[SAMURAI_MATRIX +
(sd * SUDOKU_MATRIX) +
(sdr * SUDOKU_EDGE) + d], row));
node->Add2Row(new DLNode(Cols[SAMURAI_MATRIX + SAMURAI_ROWS +
(sd * SUDOKU_MATRIX) +
(sdc * SUDOKU_EDGE) + d], row));
if (t < 6) i++;
t -= 5;
}
node->Add2Row(new DLNode(Cols[SAMURAI_MATRIX + SAMURAI_ROWS + SAMURAI_ROWS +
(sq * SUDOKU_EDGE) + d], row));
if (sam[r][c] == (d + 1))
{
Rows.push_back(node);
}
}
}
}
}
for (auto col = Head->Right; col != Head; col = col->Right)
{
if (!col->nums) col->RemoveCol();
}
for (auto iter = Rows.begin(); iter != Rows.end(); iter++)
{
(*iter)->Remove();
Ans.push_back(*iter);
}
}
算法執行過程
bool DancingLinks::DLX()
{
if (Head->Right == Head)
{
auto result = new int[Ans.size()];
for (int i = 0; i < Ans.size(); i++)
{
result[i] = Ans[i]->row;
}
ShowResult(result);
return true;
}
DLNode *col = nullptr;
int min = INT_MAX;
// 找到列元素最少的列
for (auto c = Head->Right; c != Head; c = c->Right)
{
if (min > c->nums)
{
col = c;
min = c->nums;
}
}
col->RemoveCol();
for (auto node = col->Down; node != col; node = node->Down)
{
Ans.push_back(node);
for (auto rnode = node->Right; rnode != node; rnode = rnode->Right)
{
rnode->Col->RemoveCol();
}
if (DLX()) return true;
for (auto lnode = node->Left; lnode != node; lnode = lnode->Left)
{
lnode->Col->RecoverCol();
}
Ans.pop_back();
}
col->RecoverCol();
return false;
}
結果輸出
數獨一:
數獨二:
完整代碼
https://github.com/xxy-im/SudokuNinja (代碼持續優化中)
小結
其本質雖然還是DFS回溯,但是在Dancing Links這一數據結構的加持下,回溯效率大大提升,求解時間小於0.1s,在內存暴增的年代,用些許內存的占用去換取運行時間的加速還是划算的。
后續計划在此基礎上增加OCR功能,並優化代碼使其適配任意形狀數獨 ~(沒時間就算了)~。
參考文章
https://en.wikipedia.org/wiki/Dancing_Links
https://www.cnblogs.com/grenet/p/3163550.html
https://www.acwing.com/solution/acwing/content/3843/