DLX演算法及應用(一)DLX模板+解數獨
阿新 • • 發佈:2019-02-10
DLX演算法
原理:網上太多了,我就不寫了。。
用途:解決精確覆蓋問題
下面的程式碼是嚴格按照演算法寫的,其實對於這種沒有資料域的連結串列,是可以用陣列進行模擬的(見DLX演算法及應用(二)Matlab解數獨)。
程式碼中全部都用的是vector,更通用一些~
後半部分給出了一個解數獨的例項,如何將數獨轉換為精確覆蓋問題的文章也是網上一搜一大把。。。。我就不寫了。。
2014-2-15 修改:還是寫下如何轉換吧
精確覆蓋問題的矩陣表示:
給出這樣一個矩陣,
1 0 0 1 0 0 1
1 0 0 1 0 0 0
0 0 0 1 1 0 1
0 0 1 0 1 1 0
0 1 1 0 0 1 1
0 1 0 0 0 0 1
找出該矩陣一組行的集合,使得該組集合中每列有且只有一個1
例如第2,4,6行
那麼我們現在將數獨問題轉換成一個324列的精確覆蓋問題:
數獨有4個限制條件,將其分別轉換為81列
每個格子都要填入數字
---1到81列,表示數獨中9*9=81個格子是否填入了數字。如果是,則選取的01行在該01列上為1
每一行都要有1~9填入
---81+1到81*2列,每9列就代表數獨中的一行,如果該行有某個數字,則其對應的列上為1
每一列都要有1~9填入
---81*2+1到81*3列,每9列就代表數獨中的一列
每一宮都要有1~9填入
---81*3+1到81*4列,每9列就代表數獨中的一宮
那讓我們從第一個格子開始,到第81個格子,逐步構造01矩陣
對於已給出數字的格子,例如第 3 行第 5 列為7
那麼就插入一行,其中,第23,106,205,259列為1,其他為0,分別代表:
23 = (3-1)*9 + 5 第 3 行第 5 列填入了數字
106= 81 + (3-1)*9 + 7 第 3 行填入了7
205= 81*2 + (5-1)*9 + 7 第 5 列填入了7
259= 81*3 + (2-1)*9 + 7 第 2 宮填入了7
如果沒有給出數字,那這個格子就有9種可能性,插入9行,其中如下列為1:
第一行:23,100,199,253
第二行:23,101,200,254
........
第九行:23,108,207,261
這樣,構造的01矩陣,每行都有4個1。
在最多81*9行的01矩陣中,尋找一組精確覆蓋(81行),就可以求解一個數獨
程式碼如下
#include <time.h>
#include <iostream>
#include <limits.h>
#include <vector>
#include <fstream>
using namespace std;
struct Node{
Node *up, *down, *left, *right, *colRoot, *rowRoot;//上下左右四個指標以及指向行列物件的指標
int Num;//行物件特有,記錄行數
int Size;//列物件特有,記錄該列元素數
Node(int i = -1 ): Num(i),Size(0) {};//建構函式
};
class DLX{
public:
DLX(vector<vector<int> > &matrix, int m, int n);
~DLX() { delete Head;};//析構有點難寫
void init();
void link(vector<vector<int> > &matrix);
void cover(Node *cRoot);
void recover(Node *cRoot);
bool Search(int k = 0);
vector<int> getResult() const { return result;}
int getUpdates() const { return _updates;}
private:
Node *Head;
vector<int> result;//結果存放在這裡
int _row, _col, _updates;//記錄行列數,更新次數
};
DLX::DLX(vector<vector<int> > &matrix, int m, int n)
:_row(m),_col(n),_updates(0)
{
Head = new Node;
Head->up = Head;
Head->down = Head;
Head->right = Head;
Head->left = Head;
init();
link(matrix);
}
void DLX::init()
{
Node *newNode;
for (int ix = 0; ix < _col; ++ix)//表頭位置向後插入,構造列物件
{
newNode = new Node;
newNode->up = newNode;
newNode->down = newNode;
newNode->right = Head->right;
newNode->left = Head;
newNode->right->left = newNode;
Head->right = newNode;
}
for (int ix = 0; ix < _row; ++ix)//表頭位置向下插入,構造行物件
{
newNode = new Node(_row-ix);//注意序號是_row-ix
newNode->down = Head->down;
newNode->up = Head;
newNode->down->up = newNode;
Head->down = newNode;
}
}
void DLX::link(vector<vector<int> > &matrix)
{
Node *current_row, *current_col, *newNode, *current;//當前行物件,當前列物件,新節點,當前節點
current_row = Head;
for (int row = 0; row < _row; ++row)
{
current_row = current_row->down;
current_col = Head;
for (int col = 0; col < _col; ++col)
{
current_col = current_col->right;
if (matrix[row][col] == 0)//矩陣上為0的位置不設定節點
continue;
newNode = new Node;
newNode->colRoot = current_col;
newNode->rowRoot = current_row;//設定當前節點對應的行列物件
newNode->down = current_col;
newNode->up = current_col->up;
newNode->up->down = newNode;
current_col->up = newNode;//連結當前節點到列雙向鏈尾端
if (current_row->Size == 0)//行雙向鏈不應該把行物件包含進來
{
current_row->right = newNode;
newNode->left = newNode;
newNode->right = newNode;
current_row->Size++;
}
current = current_row->right;//設定當前節點(即行物件右的節點)
newNode->left = current->left;
newNode->right = current;
newNode->left->right = newNode;
current->left = newNode;//連結當前節點到行雙向鏈尾端
current_col->Size++;
}
}
}
void DLX::cover(Node *cRoot)//覆蓋列
{
++_updates;
cRoot->left->right = cRoot->right;
cRoot->right->left = cRoot->left;//刪除該列物件
Node *i, *j;
i = cRoot->down;
while (i != cRoot)
{
j = i->right;
while (j != i)
{
j->down->up = j->up;
j->up->down = j->down;
j->colRoot->Size--;
j = j->right;
}
i = i->down;
}
}
void DLX::recover(Node *cRoot)//整個演算法的精髓!!
{
Node *i, *j;
i = cRoot->up;
while (i != cRoot)
{
j = i->left;
while (j != i)
{
j->colRoot->Size++;
j->down->up = j;
j->up->down = j;
j = j->left;
}
i = i->up;
}
cRoot->right->left = cRoot;
cRoot->left->right = cRoot;
}
bool DLX::Search(int k)
{
if (Head->right == Head)//表空,則成功找到一組行的集合
return true;
Node *cRoot, *c;
int minSize = INT_MAX;
for(c = Head->right; c != Head; c = c->right)//根據啟發條件選擇列物件
{
if (c->Size < minSize)
{
minSize = c->Size;
cRoot = c;
if (minSize == 1)
break;
if (minSize == 0)//有一列為空,失敗
return false;
}
}
cover(cRoot);
Node *current_row,*current;
for (current_row = cRoot->down; current_row != cRoot; current_row = current_row->down)
{
result.push_back(current_row->rowRoot->Num);//將該行加入result中
for (current = current_row->right; current != current_row; current = current->right)
{
cover(current->colRoot);
}
if (Search(k+1))
return true;
for (current = current_row->left; current != current_row; current = current->left)
recover(current->colRoot);
result.pop_back();//發現該行不符合要求,還原result
}
recover(cRoot);
return false;
}
vector<vector<int> > sudoku2matrix(string &problem)//將數獨轉換為01矩陣
{
vector<vector<int> > matrix;
for (int ix = 0; ix < 81; ++ix)
{
int val = problem[ix] - '0';
vector<int> current_row(324,0);
if (val != 0)
{
current_row[ix] = 1;
current_row[81 + ix/9*9 + val -1] = 1;
current_row[162 + ix%9*9 +val -1] = 1;
current_row[243 + (ix/9/3*3+ix%9/3)*9 +val -1] = 1;
matrix.push_back(current_row);
continue;
}
for (int jx = 0; jx < 9; ++jx)
{
vector<int> current_row2(324,0);
current_row2[ix] = 1;
current_row2[81 + ix/9*9 + jx] = 1;
current_row2[162 + ix%9*9 +jx] = 1;
current_row2[243 + (ix/9/3*3+ix%9/3)*9 +jx] = 1;
matrix.push_back(current_row2);
}
}
return matrix;
}
vector<int> matrix2sudoku(vector<vector<int> > &matrix, vector<int> result)//將01矩陣翻譯為數獨
{
vector<int> solution(81);
for (int ix = 0; ix < 81; ++ix)
{
vector<int> current = matrix[result[ix]-1];
int pos = 0, val = 0;
for (int jx = 0; jx < 81; ++jx)
{
if (current[jx] == 1)
break;
++pos;
}
for (int kx = 81; kx < 162; ++kx)
{
if (current[kx] == 1)
break;
++val;
}
solution[pos] = val%9 + 1;
}
return solution;
}
void solve_sudoku(string &problem, ostream &os = cout)
{
clock_t start_1 = clock();
vector<vector<int> > matrix = sudoku2matrix(problem);
clock_t end_1 = clock();
float time_1=(float)(end_1-start_1)/CLOCKS_PER_SEC;
clock_t start_2 = clock();
DLX sudoku(matrix,matrix.size(),324);
clock_t end_2 = clock();
float time_2=(float)(end_2-start_2)/CLOCKS_PER_SEC;
clock_t start_3 = clock();
if (!sudoku.Search())
{
os << "該數獨無解!\n\n";
return;
}
clock_t end_3 = clock();
float time_3=(float)(end_3-start_3)/CLOCKS_PER_SEC;
clock_t start_4 = clock();
vector<int> solution = matrix2sudoku(matrix, sudoku.getResult());
clock_t end_4 = clock();
float time_4=(float)(end_4-start_4)/CLOCKS_PER_SEC;
for (int ix = 0; ix < 81; ++ix)
os << solution[ix] << ((ix+1)%9 ? '\0' : '\n');
os << "構造01矩陣用時: " << time_1 << "s\n"
<< "構造連結串列用時: " << time_2 << "s\n"
<< "Dancing用時: " << time_3 << "s\n"
<< "Dancing更新次數: " << sudoku.getUpdates() << "次\n"
<< "翻譯結果用時: " << time_4 << "s\n" << endl;
}
int main()
{
string problem;
ofstream outfile("solution.txt");
ifstream infile("problem.txt");
while (infile >> problem)
{
outfile << problem << endl;
if (problem.size() != 81)
{
outfile << "數獨不合法\n\n";
continue;
}
solve_sudoku(problem, outfile);
}
}
示例:
problem.txt檔案內容:
027380010010006735000000029305692080000000000060174503640000000951800070080065340
000000520080400000030009000501000600200700000000300000600010000000000704000000030
800000000003600000070090200050007000000045700000100030001000068008500010090000400
執行後生成的solution.txt內容
027380010010006735000000029305692080000000000060174503640000000951800070080065340
5 2 7 3 8 9 4 1 6
8 1 9 4 2 6 7 3 5
4 3 6 7 5 1 8 2 9
3 7 5 6 9 2 1 8 4
1 9 4 5 3 8 2 6 7
2 6 8 1 7 4 5 9 3
6 4 3 2 1 7 9 5 8
9 5 1 8 4 3 6 7 2
7 8 2 9 6 5 3 4 1
構造01矩陣用時: 0.002s
構造連結串列用時: 0.002s
Dancing用時: 0s
Dancing更新次數: 324次
翻譯結果用時: 0s
000000520080400000030009000501000600200700000000300000600010000000000704000000030
4 1 6 8 3 7 5 2 9
9 8 2 4 6 5 3 7 1
7 3 5 1 2 9 4 6 8
5 7 1 2 9 8 6 4 3
2 9 3 7 4 6 1 8 5
8 6 4 3 5 1 2 9 7
6 4 7 9 1 3 8 5 2
3 5 9 6 8 2 7 1 4
1 2 8 5 7 4 9 3 6
構造01矩陣用時: 0.002s
構造連結串列用時: 0.002s
Dancing用時: 0s
Dancing更新次數: 419次
翻譯結果用時: 0s
800000000003600000070090200050007000000045700000100030001000068008500010090000400
8 1 2 7 5 3 6 4 9
9 4 3 6 8 2 1 7 5
6 7 5 4 9 1 2 8 3
1 5 4 2 3 7 8 9 6
3 6 9 8 4 5 7 2 1
2 8 7 1 6 9 5 3 4
5 2 1 9 7 4 3 6 8
4 3 8 5 2 6 9 1 7
7 9 6 3 1 8 4 5 2
構造01矩陣用時: 0.002s
構造連結串列用時: 0.002s
Dancing用時: 0.001s
Dancing更新次數: 8321次
翻譯結果用時: 0s
最後一個數獨就是傳說中芬蘭數學家設計的最難數獨,可以從更新次數看出,確實挺難。。
整個演算法時間都浪費在了將數獨轉換成01矩陣和構造連結串列上,真正的DLX用時還是很短的!
(DLX演算法中一次覆蓋列的操作稱為一次更新)