推薦論文: momodi的《Dancing Links 在搜尋中的應用》、Knuth的DLX論文、陳丹琦的《Dancing Links的應用》
Dancing Links是用來最佳化一類精確覆蓋問題中的DFS過程。
精確覆蓋問題是指在一個01矩陣中,選出一些行使每一列有且僅有1個1.
解法是Knuth提出的X演算法:
1.矩陣被全部刪除,搜尋成功退出。
2.選擇包含元素最少的一列c(可以隨便選一列)刪除,枚舉這列含1的行r作為解的一部分,刪除r行所有含1的列。
3.遞迴調用,成功返回,失敗則回溯。
一般搜尋中用bool數組標記行和列是否被刪除,通過列找所有含1的行需要r次,通過行找所有的列需要c次,
然而在搜尋過程中,矩陣的行和列不斷被刪除,不斷減少,後面含1的行列很少,成為疏鬆陣列,
這時再使用r或c次的尋找就是浪費時間了。
Dancing Links就是使用雙向迴圈十字鏈表來儲存矩陣,搜尋過程中鏈表大小會不斷減小,遍曆一次就不需要r或c次了。
雙向鏈表的刪除操作:
L[R[x]] = L[x]; R[L[x]] = R[x];
雙向鏈表的恢複操作也很簡單:
L[R[x]] = x; R[L[x]] = x;
Knuth將支援這個操作的鏈表命名為Dancing Links 。
Sudoku 數獨向精確覆蓋問題的轉換
數專屬4個約束條件:
1.在i行只能放一個數字k
2.在j列只能放一個數字k
3.在block(i,j)塊只能放一個數字k
4.i行j列只能放一個數字
所以建一個01矩陣:
行數n*n*n,n*n個格子,每個格子有n中可能,每種可能對應一行。
列數4*n*n,代表n*n個格子的4個約束條件。
如果數獨i行j列已經有值k,則在(i*n+j)*n+k行插入4個1,列數分別是:
i*n+k-1
n*n+j*n+k-1
2*n*n+block(i,j)*n+k-1
3*n*n+i*n+j
否則插入n行,k=1 to n。
如果n=16,那麼有4096行,1024列,矩陣遍曆需要4096*1024=4194304次,
但是1的個數只有4096*4=16384個,Dancing Links 遍曆只需要16384次。
這樣對比下就可以看出Dancing Links 節約了很多時間。
調用DLX演算法就可以求出數獨的一個解或者判斷無解。
n皇后問題也可以轉換為精確覆蓋問題。
還有一類重複覆蓋問題:
在一個01矩陣中,選出一些行使每一列至少有1個1.
需要配合A*演算法解決。
數獨模板:
//POJ3076 736 KB 172 ms G++ 2689 B #include<cstdio>#define N 4099#define M 1025int m=4,n=m*m,H=4*n*n,cnt,size[M],ans[16][16];struct Node{ int r,c; Node *U,*D,*L,*R;}node[16385],row[N],col[M],head,*p;void init(int r,int c){ cnt=0; head.L=head.R=head.U=head.D=&head; for(int i=0;i<c;i++){ col[i].r=r; col[i].c=i; col[i].L=&head; col[i].R=head.R; col[i].U=col[i].D=col[i].L->R=col[i].R->L=&col[i]; size[i]=0; } for(int i=r-1;i>=0;i--){ row[i].r=i; row[i].c=c; row[i].U=&head; row[i].D=head.D; row[i].L=row[i].R=row[i].U->D=row[i].D->U=&row[i]; }}void insert(int r,int c){ p=&node[cnt++]; p->r=r; p->c=c; p->R=&row[r]; p->L=row[r].L; p->L->R=p->R->L=p; p->U=&col[c]; p->D=col[c].D; p->U->D=p->D->U=p; ++size[c];}void delLR(Node *p){ p->L->R=p->R; p->R->L=p->L;}void delUD(Node *p){ p->U->D=p->D; p->D->U=p->U;}void resumeLR(Node *p){p->L->R=p->R->L=p;}void resumeUD(Node *p){p->U->D=p->D->U=p;}void cover(int c){ if(c==H) return; delLR(&col[c]); Node *R,*C; for(C=col[c].D;C!=&col[c];C=C->D)for(R=C->L;R!=C;R=R->L){--size[R->c];delUD(R);}}void resume(int c){ if(c==H) return; Node *R,*C; for(C=col[c].U;C!=&col[c];C=C->U)for(R=C->R;R!=C;R=R->R){++size[R->c];resumeUD(R);} resumeLR(&col[c]);}int dfs(int k){ if(head.L==&head)return 1; int INF=-1u>>1,r,c=-1;Node *p,*rc; for(p=head.R;p!=&head;p=p->R) if(size[p->c]<INF) INF=size[c=p->c];if(!size[c])return 0; cover(c); for(p=col[c].D;p!=&col[c];p=p->D){ for(rc=p->L;rc!=p;rc=rc->L) cover(rc->c);r=p->r-1;ans[r/(n*n)][r/n%n]=r%n; if(dfs(k+1)) return 1; for(rc=p->R;rc!=p;rc=rc->R) resume(rc->c); } resume(c); return 0;}void insert(int i,int j,int k){int r=(i*n+j)*n+k;insert(r,i*n+k-1);insert(r,n*n+j*n+k-1);insert(r,2*n*n+(i/m*m+j/m)*n+k-1);insert(r,3*n*n+i*n+j);}char s[16][20];void Sudoku(){int i,j,k;init(n*n*n+1,H);for(i=0;i<n;i++)for(j=0;j<n;j++)if(s[i][j]!='-')insert(i,j,k=s[i][j]-'A'+1);else{for(k=1;k<=n;k++)insert(i,j,k);}dfs(0);for(i=0;i<n;i++){for(j=0;j<n;j++)putchar(ans[i][j]+'A');puts("");}puts("");}int main(){int i; while(~scanf("%s",s[0])){for(i=1;i<n;i++)scanf("%s",s[i]);Sudoku();}}
精確覆蓋:
//01矩陣的完美覆蓋 HUST1017#include <iostream>#include <cstdio>#include <vector>using namespace std;/***最大行***/#define MAXROW 1005/***最大列***/#define MAXCOL 1005int ans[MAXROW+5];struct DancingLinksNode { int r, c; /***結點所在的行列位置***/ DancingLinksNode *U, *D, *L, *R;/***結點的上下左右結點指標***/};DancingLinksNode node[MAXROW * MAXCOL];/****備用結點****/ DancingLinksNode row[MAXROW];/****行頭****/DancingLinksNode col[MAXCOL];/****列頭****/DancingLinksNode head;/****表頭****/int cnt;/****使用了多少結點****/int size[MAXCOL];/****列含有多少個域****/int m, n;/****表的行與列變數****/void init(int r, int c)/****初始化,r, c分別表示表的大小***/ { cnt = 0;/****將可以使用的結點設為第一個****/ head.r = r; /****head結點的r,c分別表示表的大小,以備查****/ head.c = c; head.L = head.R = head.U = head.D = &head;/****初始化head結點****/ for(int i = 0; i < c; ++i) /***初始化列頭***/ { col[i].r = r; col[i].c = i; col[i].L = &head; col[i].R = head.R; col[i].L->R = col[i].R->L = &col[i]; col[i].U = col[i].D = &col[i]; size[i] = 0; } for(int i = r - 1; i > -1; --i)/***初始化行頭,在刪除的時候,如果碰到row[i].c == c的情形應當被跳過***/{ row[i].r = i; row[i].c = c; row[i].U = &head; row[i].D = head.D; row[i].U->D = row[i].D->U = &row[i]; row[i].L = row[i].R = &row[i]; }}inline void addNode(int r, int c)/****增加一個結點,在原表中的位置為r行,c列***/ { DancingLinksNode *ptr = &node[cnt++];/****找一個未曾使用的結點****/ ptr->r = r;/****設定結點的行列號****/ ptr->c = c; ptr->R = &row[r];/****將結點加入雙向鏈表中****/ ptr->L = row[r].L; ptr->L->R = ptr->R->L = ptr; ptr->U = &col[c]; ptr->D = col[c].D; ptr->U->D = ptr->D->U = ptr; ++size[c];/****將size域加1****/}inline void delLR(DancingLinksNode * ptr)/****刪除ptr所指向的結點的左右方向****/ { ptr->L->R = ptr->R; ptr->R->L = ptr->L;}inline void delUD(DancingLinksNode * ptr)/****刪除ptr所指向的結點的上下方向****/ { ptr->U->D = ptr->D; ptr->D->U = ptr->U;}inline void resumeLR(DancingLinksNode * ptr)/****重設ptr所指向的結點的左右方向****/ { ptr->L->R = ptr->R->L = ptr;}inline void resumeUD(DancingLinksNode * ptr)/****重設ptr所指向的結點的上下方向****/ { ptr->U->D = ptr->D->U = ptr;}inline void cover(int c)/****覆蓋第c例***/ { if(c == n)/**** c == n 表示頭****/ return; delLR(&col[c]);/****刪除表頭****/ DancingLinksNode *R, *C; for(C = col[c].D; C != (&col[c]); C = C->D) { if(C->c == n) continue; for(R = C->L; R != C; R = R->L){ if(R->c == n) continue; --size[R->c]; delUD(R); } delLR(C); }}inline void resume(int c)/****重設第c列****/ { if(c == n) return; DancingLinksNode *R, *C; for(C = col[c].U; C != (&col[c]); C = C->U) { if(C->c == n) continue; resumeLR(C); for(R = C->R; R != C; R = R->R) { if(R->c == n) continue; ++size[R->c]; resumeUD(R); } } resumeLR(&col[c]);/****把列頭接進表頭中****/}bool search(int k)/****搜尋核心演算法,k表示搜尋層數****/ { if(head.L == (&head)) /***搜尋成功,返回true***/ {printf("%d\n",k);for(int i=0;i<k;i++)printf("%d\n",ans[i]);return true;} /***c表示下一個列對象位置,找一個分支數目最小的進行覆蓋***/ int INF = (1<<30), c = -1; for(DancingLinksNode *ptr=head.L;ptr!=(&head);ptr=ptr->L) if(size[ptr->c] < INF) { INF = size[ptr->c]; c = ptr->c; } cover(c); /***覆蓋第c列***/ DancingLinksNode * ptr; for(ptr = col[c].D; ptr != (&col[c]); ptr = ptr->D) { DancingLinksNode *rc; ptr->R->L = ptr; for(rc = ptr->L; rc != ptr; rc = rc->L) cover(rc->c); ptr->R->L = ptr->L;ans[k]=ptr->r+1; if(search(k + 1)) return true; ptr->L->R = ptr; for(rc = ptr->R; rc != ptr; rc = rc->R) resume(rc->c); ptr->L->R = ptr->R; } resume(c);/***取消覆蓋第c列***/ return false;}int main() { while(scanf("%d%d", &m, &n) != EOF) { init(m, n); for(int i = 0; i < m; ++i) {int x,j;scanf("%d",&x);while(x--){scanf("%d",&j);j--;addNode(i, j);//i行j列為1} } if(!search(0)) puts("NO"); }}/*模型的建立自然是要把衝突的條件都擺出來,這裡有4個。1.同行不能有相同的數2.同列不能有相同的數3.同塊不能有相同的數除了這3個很顯然的約束,還有一個比較重要的,就是4.每個位置只能有一個數所以,對於一個9*9的數獨,如此設定行:(行標號,列標號),(行標號,數),(列標號,數),(塊標號,數)每塊都是9*9=81,一共是324個位置。其中比據我要加入一個i行j列的數字k那麼要加入一個含4個1的行,即(i,j),(i,k),(j,k)(block(i,j),k)block(i,j)返回(i,j)的塊標號如果有初始值的話把所有初始值都放一起,放在第0行,然後在第1層迴圈中做下特判,只進行一次覆蓋。。如果能保證第一次必然迴圈到此行就這麼做。否則,在遞迴開始前就先把這些列刪除。提出前一種方案只是因為寫起來方便。*/