# 舞蹈鍊 Dancing Link(DLX) 寫數獨的時候,我發現除了DFS遞迴剪枝還有另一中更快的酷做法,~~但當時沉浸在AC的喜悅中就沒有學~~,後來在洛谷河道剛好逛到這東東,才學會這酷酷演算法(但我太笨畫很久才學會) \*本篇文章大量參考[這裡](https://www.luogu.com.cn/article/53by9ud1),~~因為是我學的地方~~,非常感謝作者大佬 - 舞蹈鍊是幹嘛的 舞蹈鍊最主要解決的問題是***精確覆蓋問題***,``在各個子集合中選擇若干個使其包含整個全集,並且各個子集合不重疊``,~~[上維基百科](https://zh.wikipedia.org/wiki/%E7%B2%BE%E7%A1%AE%E8%A6%86%E7%9B%96%E9%97%AE%E9%A2%98)~~,畫個圖就是這樣 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002145824.png?raw=true) 要選擇其中幾行,使每一列都有一個$1$,並且選擇的行中,有$1$的列不能重疊 [模板題](https://www.luogu.com.cn/problem/P4929) X algorithm 舞蹈鍊的靈魂 --- 這個問題解決的方法呢,就是~~暴力遞迴枚舉~~,具體過程如下: 1. 如果只剩一個集合(行),並且所有元素都是$1$ ,代表找到解了,否則去$4.$ 2. 選擇一個集合(行),把其中$1$覆蓋到的列刪除,包括重疊到的集合(行) 3. 如果此時剩下一個集合(行),回到$1.$ ,否則回到$2.$ 4. 如果只剩一個集合(行),且其中有元素是$0$ 代表先前選擇的集合有錯誤,回溯後繼續$2.$ 畫個圖流程大概是這樣: - 先選擇集合(1.),把重疊到的 (藍色) 和其 (黃色) 所在集合 (紫色) 都刪掉 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002151919.png?raw=true) - 把有顏色的拿掉變成這樣,只剩(3.)集合,但裡面有$0$,回溯到上一步 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002152012.png?raw=true) - 選擇集合(2.),一樣把重複到的刪掉 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002152401.png?raw=true) - 剩下集合(3.)且沒有$0$,找到解是 : 集合(2.)和(3.) ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002152524.png?raw=true) 雖然我圖畫得很亂可能不太好懂qwq,但~~看懂後~~可以發現,演算法最大的問題在於,要怎麼實現刪除行,回溯後還要恢復,顯然這用二維陣列來處理不只難寫而且時空複雜度也會極高。 那有什麼資料結構可以支持快速刪除和恢復呢? ``linked-list`` ! Dacing Link 舞蹈鍊 --- 先來複習一下``linked-list``要怎麼刪除和恢復 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002153956.png?raw=true) 假設我想刪除$2.$節點,只要改變一下$1.$和$3.$的指標就好,相當於$2.$節點消失了 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002154119.png?raw=true) 不過仔細看看$2.$節點,雖然他已經不在``linked-list``上了,但他的節點本身還是存在,且指標也還指向``linked-list``,所以我們可以透過這個特性恢復 回到``Dacing Link``本身,他是一個十字鍊,長這樣 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002154817.png?raw=true) 其實他是一個循環鍊,也就是頭尾會相連,~~但太麻煩了我懶的畫~~。因為這些特性,他有一個~~非常強大的名字~~,***十字交叉雙向循環鍊表***,好但~~沒人會這樣叫他~~,我也叫他舞蹈鍊。 那怎麼建圖(鍊?)呢? 把上面的$01$矩陣中的$1$變成一個節點,$0$則讓他空著,像這樣 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002161845.png?raw=true) 好亂,而且怎麼跑出一堆奇怪的節點了? - head節點 : 用途是方便我們確認答案,只是一個輔助節點 - 列節點 : 也是用判斷答案的輔助節點 - 行編號 : 集合的編號,只是畫出來比較好理解,==不是一個真實的節點== 有了這個資料結構,再把上面X algorithm的執行過程搬過來,你就會DLX了! 1. 如果沒有任何集合(行)存在,判斷還有沒有列節點(``head.r==head``),沒有就是找到答案了 2. 如果還有集合(行),選擇其中一個,把覆蓋到的、重複的等等節點刪除 3. 如果刪完沒有集合了,回到$1.$ ,否則回到$2.$ 4. 如果沒有任何集合存在,且還有列節點存在(``head.r!=head``),代表之前的決策有錯誤或根本無解,回溯並繼續遞迴枚舉 選擇(1.)集合,把覆蓋到的和其所在的集合刪除 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002162725.png?raw=true) 所有集合都消失了,但還有列節點,要回溯繼續求解 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002163555.png?raw=true) 選擇集合(2.),一樣刪除該刪除的節點,(這裡$1.$列節點應該是藍色,我畫錯了qwq) ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002163644.png?raw=true) 還有集合,繼續選擇(3.)集合 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002163946.png?raw=true) 沒有集合了,且列節點全空,找到答案了 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002164122.png?raw=true) 忍受一下我畫的醜圖,消化一下,再來直接上程式 DLX實作 --- 題目以[上面的模板題](https://www.luogu.com.cn/problem/P4929)做舉例 - 先看變數 ```cpp= struct nd{ int up,dw,l,r; int row,col; }; nd dl[200500]; int ans[200500],row[200500],sz[200500]; int n,m,cnt=0; ``` ``nd``是存節點的結構體,``dl``是節點的陣列,``ans``紀錄最後答案,``row``紀錄每一行(集合)的第一個節點,``sz``是每一列下面有多少個節點(可以不用,用於優化),``n``跟``m``是題目的定義,``cnt``用於加點 - 初始化 ```cpp= void init(){ for(int i=0;i<=m;i++){ dl[i].l=i-1; dl[i].r=i+1; dl[i].up=i; dl[i].dw=i; sz[i]=0; } dl[0].l=m; dl[m].r=0; cnt=m; } ``` 先處理好所有列節點,上下因為都沒有節點,所以指向自己,左右與相鄰的列節點形成環,其中``dl[0]``是``head``節點,至於``row``也就是行可以先不處理,因為他不是真實的點。 處理完圖會長這樣子 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002205713.png?raw=true) - 增加節點 ```cpp= void addnd(int r,int c){ dl[++cnt].up=dl[c].up; dl[cnt].dw=c; dl[c].up=cnt; dl[dl[cnt].up].dw=cnt; dl[cnt].col=c; dl[cnt].row=r; sz[c]++; if(!row[r]){ dl[cnt].r=cnt; dl[cnt].l=cnt; row[r]=cnt; }else{ dl[cnt].r=row[r]; dl[cnt].l=dl[row[r]].l; dl[row[r]].l=cnt; dl[dl[cnt].l].r=cnt; } } ``` ``r``和``c``是在第幾行第幾列增加節點,先把上下連好,上個圖應該比較清楚 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002211132.png?raw=true) 再來就處理左右的指標,要判斷他是不是這行的第一個節點,方便更新``row``陣列,應該不難理解,這裡就不上圖了~~因為我懶~~,如果真的不行可以自己畫畫看 - 刪除一列 ```cpp= void rm(int c){ dl[dl[c].l].r=dl[c].r; dl[dl[c].r].l=dl[c].l; for(int i=dl[c].dw;i!=c;i=dl[i].dw){ for(int j=dl[i].r;j!=i;j=dl[j].r){ dl[dl[j].up].dw=dl[j].dw; dl[dl[j].dw].up=dl[j].up; sz[dl[j].col]--; } } } ``` 最前面把這列的列節點從列的``linked-list``裡刪除,等同於刪除了這一列,但我們還要去枚舉刪除同在這一列的集合(行),因為不能重複覆蓋,只刪除上下指標,代表從候選集合中刪除,但還是保留左右指標,確保同一集合連在一起,以便後續回溯復原 ![image alt](https://github.com/hcismonkey/CP/blob/main/dacing_link_image/Pasted%20image%2020241002212748.png?raw=true) 畫的有點~~超~~亂,希望看得懂 - 復原一列 ```cpp= void rc(int c){ for(int i=dl[c].dw;i!=c;i=dl[i].dw){ for(int j=dl[i].r;j!=i;j=dl[j].r){ dl[dl[j].up].dw=j; dl[dl[j].dw].up=j; sz[dl[j].col]++; } } dl[dl[c].l].r=c; dl[dl[c].r].l=c; } ``` 就完全是刪除的反動作,枚舉所有因為這一列被刪除的集合,上下關係連回來,最後復原這一列的列節點,具體原理上面``linked-list``的復原有講到(~~雖然帶過而已~~),思考一下會發現刪除跟復原的順序很巧妙。 - 最後是Dacing Link的靈魂 跳舞(X algorithm) ```cpp= int dancing(int deep){ if(dl[0].r==0){ for(int i=1;i<deep;i++) cout<<ans[i]<<' '; return 1; } int C=dl[0].r; for(int i=dl[0].r;i!=0;i=dl[i].r){ if(sz[i]<sz[C]) C=i; } rm(C); for(int i=dl[C].dw;i!=C;i=dl[i].dw){ ans[deep]=dl[i].row; for(int j=dl[i].r;j!=i;j=dl[j].r) rm(dl[j].col); if(dancing(deep+1)==1) return 1; for(int j=dl[i].r;j!=i;j=dl[j].r) rc(dl[j].col); } rc(C); return 0; } ``` 先判斷是不是找到解了``(head.r==head)``,是的話輸出答案,不然繼續遞迴。選擇其中一列(``sz``的優化就在這,在洛谷會``TLE``,吸沒吸氧都一樣)這裡選擇節點最少的列,然後刪除,再去枚舉要選擇這一列的哪個集合,集合中所有節點所在的列刪除後遞迴下去,回溯時復原,回傳``1``代表找到解了,``0``代表還沒。 那重要的部分都講完了,整段程式在這,``main()``應該都看得懂,就不多說了 - AC code (155ms 4.45MB) ```cpp= #include<bits/stdc++.h> using namespace std; #define int long long struct nd{ int up,dw,l,r; int row,col; }; nd dl[200500]; int ans[200500],row[200500],sz[200500]; int n,m,cnt=0; void init(){ for(int i=0;i<=m;i++){ dl[i].l=i-1; dl[i].r=i+1; dl[i].up=i; dl[i].dw=i; sz[i]=0; } dl[0].l=m; dl[m].r=0; cnt=m; } void rm(int c){ dl[dl[c].l].r=dl[c].r; dl[dl[c].r].l=dl[c].l; for(int i=dl[c].dw;i!=c;i=dl[i].dw){ for(int j=dl[i].r;j!=i;j=dl[j].r){ dl[dl[j].up].dw=dl[j].dw; dl[dl[j].dw].up=dl[j].up; sz[dl[j].col]--; } } } void rc(int c){ for(int i=dl[c].dw;i!=c;i=dl[i].dw){ for(int j=dl[i].r;j!=i;j=dl[j].r){ dl[dl[j].up].dw=j; dl[dl[j].dw].up=j; sz[dl[j].col]++; } } dl[dl[c].l].r=c; dl[dl[c].r].l=c; } void addnd(int r,int c){ dl[++cnt].up=dl[c].up; dl[cnt].dw=c; dl[c].up=cnt; dl[dl[cnt].up].dw=cnt; dl[cnt].col=c; dl[cnt].row=r; sz[c]++; if(!row[r]){ dl[cnt].r=cnt; dl[cnt].l=cnt; row[r]=cnt; }else{ dl[cnt].r=row[r]; dl[cnt].l=dl[row[r]].l; dl[row[r]].l=cnt; dl[dl[cnt].l].r=cnt; } } int dancing(int deep){ if(dl[0].r==0){ for(int i=1;i<deep;i++) cout<<ans[i]<<' '; return 1; } int C=dl[0].r; for(int i=dl[0].r;i!=0;i=dl[i].r){ if(sz[i]<sz[C]) C=i; } rm(C); for(int i=dl[C].dw;i!=C;i=dl[i].dw){ ans[deep]=dl[i].row; for(int j=dl[i].r;j!=i;j=dl[j].r) rm(dl[j].col); if(dancing(deep+1)==1) return 1; for(int j=dl[i].r;j!=i;j=dl[j].r) rc(dl[j].col); } rc(C); return 0; } signed main(){ cin>>n>>m; init(); int t; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cin>>t; if(t==1) addnd(i,j); } } if(!dancing(1)){ cout<<"No Solution!"; } cout<<'\n'; return 0; } ``` 雜 --- 看完後你可能會好奇時間複雜度? 我可以很~~不~~負責任的告訴你,~~我不知道~~,我有去找過,但每個地方說的都不一樣,我也不是證明大師,就不亂下定義了,畢竟他是遞迴枚舉,複雜度不好估算,只能等你各位大佬證明了。 至於DLX可以在甚麼場景應用? 我目前聽過數獨,八皇后等等遞迴枚舉的地方,其實只要想辦法把問題轉成精確覆蓋問題就能使用DLX,這點也是DLX最難的地方(畢竟他算是模板),更多應用也得你各位大佬證明。 我只是個甚麼都不會的競程蒟蒻,如果這篇沒辦法讓你看懂我很抱歉qwq,~~那推[我學的地方](https://www.luogu.com.cn/article/53by9ud1)~~,如果這篇文章有任何問題,歡迎來[我的DC](https://discord.gg/TTWbRQ9bTy)提出(~~沒有問題也可以來找我聊天~~)