武士數獨(五重數獨) 舞蹈鏈解法


數獨求解,第一個想到的方法就是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. 每個格子只能填一個數字
  2. 每行每個數字只能填一遍(1-9)
  3. 每列每個數字只能填一遍(1-9)
  4. 每宮每個數字只能填一遍(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列完成了對武士數獨的精確覆蓋問題約束定義

用上圖數獨為例,(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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM