【筆記】DLX演算法及常見應用
參考資料
問題引入
精確覆蓋問題:
有r個由1~n組成的集合S1,S2,S3....Sr,要求選擇若干集合,使得1~n恰好只在一個集合裡出現。
數獨問題:
在9×9的矩陣裡填數,使得每一行每一列每一個九宮格里1~9都恰好出現一次
解法分析
先考慮精確覆蓋問題,我們將其建成一個r×n的01矩陣,第i行第j列為1表示第i個集合裡有元素j,那選擇若干集合就轉化為選擇若干行
一種很直觀的想法是進行回溯深搜——每當選中一行,則將該行以及該行為1的列都刪掉,這樣就得到了一個更小的矩陣,回溯的時候將刪掉的行加回來
這個演算法我們稱之為X演算法,在求解的過程中有大量的快取矩陣和回溯矩陣的過程,如何快取矩陣以及相關的資料(保證後面的回溯能正確恢復資料)比較複雜低效。
於是有神犇想了一種資料結構來維護這種刪除和插入的操作——提到高效插入和刪除,可以想到連結串列吧。
DLX就是一種用了雙向連結串列思想來維護矩陣的資料結構
模型
DLX用的資料結構是交叉十字迴圈雙向鏈,每個元素不僅是橫向迴圈雙向鏈中的一份子,又是縱向迴圈雙向鏈的一份子因為精確覆蓋問題的矩陣往往是稀疏矩陣(矩陣中,0的個數多於1),DLX僅僅記錄矩陣中值是1的元素。
每個元素有6個分量:
Left指向左邊的元素、Right指向右邊的元素、Up指向上邊的元素、Down指向下邊的元素、Col指向列標元素、Row指示當前元素所在的行
一些輔助元素:
Ans():Ans陣列,在求解的過程中保留當前的答案,以供最後輸出答案用。
Head元素:求解的輔助元素,在求解的過程中,當判斷出Head.Right=Head(也可以是Head.Left=Head)時,求解結束,輸出答案。Head元素只有兩個分量有用。其餘的分量對求解沒啥用
C元素:列標元素,每列有一個列標元素。本文開始的題目的列標元素分別是C1、C2、C3、C4、C5、C6、C7。每一列的元素的Col分量都指向所在列的列標元素。列標元素的Col分量指向自己(也可以是沒有)。在初始化的狀態下,Head.Right=C1、C1.Right=C2、……、C7.Right=Head、Head.Left=C7等等。列標元素的分量Row=0,表示是處在第0行。
結構體框架
#define FOR(i,A,s) for(int i = A[s]; i != s; i = A[i])
struct DLX { //成員變數 int n, sz; // 列數,結點總數 int S[MAXC]; // 各列結點數 int row[MAXN], col[MAXN]; // 各結點行列編號 int L[MAXN], R[MAXN], U[MAXN], D[MAXN]; // 十字連結串列 vector<int>vec; int ansd, ans[MAXR]; // 解 //成員函式 void init(int n);//n為列數 void remove(int c); void restore(int c); bool dfs(int d);//d為遞迴深度 bool solve(); //數獨所需函式 void build(); void decode(int code,int &a,int &b,int &c); inline int encode(int a,int b,int c); void output(); void addRow(int r); inline int trans(int x,int y); };
各部分實現
void init
建立頭結點,列標元素,橫向成環,縱向成環(指向自己),初始化各變數
void DLX::init(int n) { // n是列數 this->n = n; for(int i = 0 ; i <= n; i++) { U[i] = i; D[i] = i; L[i] = i-1, R[i] = i+1; } R[n] = 0; L[0] = n; sz = n + 1; memset(S, 0, sizeof(S)); }
bool solve
解決精確覆蓋問題的介面,如果有解返回true,無解返回false
bool DLX::solve() { f(!dfs(0)) return false; return true; }
bool dfs
如果找到解(R[0] == 0 ) 則記錄解的長度並返回
否則繼續深搜下去,搜不到答案則返回false
bool DLX::dfs(int d) { if (R[0] == 0) { // 找到解 ansd = d; // 記錄解的長度 return true; } // 找S最小的列c int c = R[0]; // 第一個未刪除的列 FOR(i,R,0) if(S[i] < S[c]) c = i; remove(c); // 刪除第c列 FOR(i,D,c) { // 用結點i所在行覆蓋第c列 ans[d] = row[i]; FOR(j,R,i) remove(col[j]); // 刪除結點i所在行能覆蓋的所有其他列 if(dfs(d+1)) return true; FOR(j,L,i) restore(col[j]); // 恢復結點i所在行能覆蓋的所有其他列 } restore(c); // 恢復第c列 return false; }
void remove
刪除第c列
void DLX::remove(int c) { L[R[c]] = L[c]; R[L[c]] = R[c]; FOR(i,D,c) FOR(j,R,i) { U[D[j]] = U[j]; D[U[j]] = D[j]; --S[col[j]]; } }
void restore
恢復第c列
void DLX::restore(int c) { FOR(i,U,c) FOR(j,L,i) { ++S[col[j]]; U[D[j]] = j; D[U[j]] = j; } L[R[c]] = c;R[L[c]] = c; }
void addRow
增加一行
void DLX::addRow(int r) { int first = sz; for(int i = 0; i < vec.size(); i++) { int c = vec[i]; // cout<<c<<" "; L[sz] = sz - 1; R[sz] = sz + 1; D[sz] = c; U[sz] = U[c]; D[U[c]] = sz; U[c] = sz; row[sz] = r; col[sz] = c; S[c]++; sz++; } // cout<<endl; R[sz - 1] = first; L[first] = sz - 1; }
數獨問題
1、把數獨問題轉換為精確覆蓋問題
2、設計出資料矩陣
3、用舞蹈鏈(Dancing Links)演算法求解該精確覆蓋問題
4、把該精確覆蓋問題的解轉換為數獨的解
首先看看數獨問題(9*9的方格)的規則
1、每個格子只能填一個數字
2、每行每個數字只能填一遍
3、每列每個數字只能填一遍
4、每宮每個數字只能填一遍
把上面的表述換個說法
1、每個格子只能填一個數字
2、每行1-9的這9個數字都得填一遍(也就意味著每個數字只能填一遍)
3、每列1-9的這9個數字都得填一遍
4、每宮1-9的這9個數字都得填一遍
那麼我們現在將9×9數獨問題轉換成一個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列就代表數獨中的一宮!
建好模型後跑DLX演算法即可求解
void DLX::build() { for(int i=0;i<9;i++) { for(int j=0;j<9;j++) { for(int k=0;k<9;k++){ if(sudoku[i][j]==-1||sudoku[i][j]==k){ //cout<<i<<" "<<j<<" "<<k<<endl; vec.clear(); vec.push_back(encode(0,i,j)); vec.push_back(encode(1,i,k)); vec.push_back(encode(2,j,k)); vec.push_back(encode(3,trans(i,j),k)); addRow(encode(i,j,k)); } } } } return; } inline int DLX::encode(int a,int b,int c){ return 81*a + b*9+c+1; } void DLX::decode(int code,int &a,int &b,int &c){ code--; c = code%9;code/=9; b = code%9;code/=9; a = code; } inline int DLX::trans(int x,int y){ x = x/3; y = y/3; return x*3+y; } void DLX::output() { for(int i=0;i<ansd;i++){ int r,c,v; decode(ans[i],r,c,v); sudoku[r][c]=v; } for(int i=0;i<9;i++){ for(int j=0;j<9;j++){ printf("%d",sudoku[i][j]+1); } putchar('\n'); } }
貼一下模板題poj2676的程式碼
#include<cstdio> #include<vector> #include<cstring> #include<iostream> using namespace std; int _; int sudoku[10][10]; const int MAXN = 81*4*81+10,MAXR = 9*9*9+10,MAXC = 81*4+10; // 行編號從1開始,列編號為1~n,結點0是表頭結點; 結點1~n是各列頂部的虛擬結點 struct DLX { //成員變數 int n, sz; // 列數,結點總數 int S[MAXC]; // 各列結點數 int row[MAXN], col[MAXN]; // 各結點行列編號 int L[MAXN], R[MAXN], U[MAXN], D[MAXN]; // 十字連結串列 vector<int>vec; int ansd, ans[MAXR]; // 解 //成員函式 void init(int n);//n為列數 void remove(int c); void restore(int c); bool dfs(int d);//d為遞迴深度 bool solve(); void addRow(int r); //數獨所需函式 void build(); void decode(int code,int &a,int &b,int &c); inline int encode(int a,int b,int c); void output(); inline int trans(int x,int y); }; #define FOR(i,A,s) for(int i = A[s]; i != s; i = A[i]) void DLX::output() { for(int i=0;i<ansd;i++){ int r,c,v; decode(ans[i],r,c,v); sudoku[r][c]=v; } for(int i=0;i<9;i++){ for(int j=0;j<9;j++){ printf("%d",sudoku[i][j]+1); } putchar('\n'); } } void DLX::addRow(int r) { int first = sz; for(int i = 0; i < vec.size(); i++) { int c = vec[i]; // cout<<c<<" "; L[sz] = sz - 1; R[sz] = sz + 1; D[sz] = c; U[sz] = U[c]; D[U[c]] = sz; U[c] = sz; row[sz] = r; col[sz] = c; S[c]++; sz++; } // cout<<endl; R[sz - 1] = first; L[first] = sz - 1; } inline int DLX::encode(int a,int b,int c){ return 81*a + b*9+c+1; } inline int DLX::trans(int x,int y){ x = x/3; y = y/3; return x*3+y; } void DLX::build() { for(int i=0;i<9;i++) { for(int j=0;j<9;j++) { for(int k=0;k<9;k++){ if(sudoku[i][j]==-1||sudoku[i][j]==k){ //cout<<i<<" "<<j<<" "<<k<<endl; vec.clear(); vec.push_back(encode(0,i,j)); vec.push_back(encode(1,i,k)); vec.push_back(encode(2,j,k)); vec.push_back(encode(3,trans(i,j),k)); addRow(encode(i,j,k)); } } } } return; } void DLX::init(int n) { // n是列數 this->n = n; // 虛擬結點 for(int i = 0 ; i <= n; i++) { U[i] = i; D[i] = i; L[i] = i-1, R[i] = i+1; } R[n] = 0; L[0] = n; sz = n + 1; memset(S, 0, sizeof(S)); } void DLX::remove(int c) { L[R[c]] = L[c]; R[L[c]] = R[c]; FOR(i,D,c) FOR(j,R,i) { U[D[j]] = U[j]; D[U[j]] = D[j]; --S[col[j]]; } } void DLX::restore(int c) { FOR(i,U,c) FOR(j,L,i) { ++S[col[j]]; U[D[j]] = j; D[U[j]] = j; } L[R[c]] = c; R[L[c]] = c; } bool DLX::dfs(int d) { if (R[0] == 0) { // 找到解 ansd = d; // 記錄解的長度 return true; } // 找S最小的列c int c = R[0]; // 第一個未刪除的列 FOR(i,R,0) if(S[i] < S[c]) c = i; remove(c); // 刪除第c列 FOR(i,D,c) { // 用結點i所在行覆蓋第c列 ans[d] = row[i]; FOR(j,R,i) remove(col[j]); // 刪除結點i所在行能覆蓋的所有其他列 if(dfs(d+1)) return true; FOR(j,L,i) restore(col[j]); // 恢復結點i所在行能覆蓋的所有其他列 } restore(c); // 恢復第c列 return false; } void DLX::decode(int code,int &a,int &b,int &c){ code--; c = code%9;code/=9; b = code%9;code/=9; a = code; } bool DLX::solve() { if(!dfs(0)) return false; return true; } void input(){ for(int i=0;i<9;i++) for(int j=0;j<9;j++) scanf("%1d",&sudoku[i][j]),sudoku[i][j]--; } DLX dlx; int main(){ scanf("%d",&_); while(_--) { input(); dlx.init(81*4); dlx.build(); bool ok = dlx.solve(); if(ok) { dlx.output(); } } }poj2676