DLX大意:通過雙向鏈表高效維護爆搜的搜索與回溯解決完全覆蓋問題
感覺就是很暴力的一種思想,是個人都想得到。
前置芝士:不重複與重複完全覆蓋問題
-
不重複完全覆蓋問題
我們有一些工具人,他們的工作時間各不相同,我們要選擇幾個人使得每天都有且僅有一個人在工作。
顯然,我們選擇人B,E,F可以滿足條件。
這種選擇幾種方案並使得區域完全覆蓋且不重複的就是不重複完全覆蓋問題。 -
重複完全覆蓋問題
對於上面的圖顯而易見就是ABCDEF都選。
這種問題一般要求方案最少的數目。
這種選擇幾種方案並使得區域完全覆蓋可以重複的就是重複完全覆蓋問題。
算法:
其實挺簡單的。
還是這個圖。
我們隨便選一列(天),不妨選星期一,然後選中第一個該列爲1的行A作爲第一個工具。
然後把行A和行A中所有爲1的列在整個表中刪除,效果如下:
明顯A已經失去作用,所以放置play扔掉(工具人本性
並且A所可以工作的日子不需要再討論。
以上對於重複和不重複完全覆蓋問題都是相同的,以下有一點不同。
分支1-1:對於重複完全覆蓋問題,我們什麼都不幹,只是刪掉列即可,效果如上圖。
分支1-2:對於不重複完全覆蓋問題,我們需要將A所可以工作的列 該列狀態爲1的行 在整個表中刪掉(不可重複),聽起來有點拗口,其實就是去重,會長這樣:
人全沒了,代表你失敗了,此時需要回溯。
因爲1-1狀態太多,不再贅述,繼續深入1-2。
我們回溯到原圖:
行A已經驗證過不可行,我們就是同樣星期一有空的行B。
一波操作後如下圖:
再選一列,不妨爲星期二(都是第一個,只能選E,再一波操作:
一個F解決所有問題,找到了一組可行解,返回。
這玩意就是X算法,毫無難度。
Dancing Links用於維護這個刪行列和恢復行列的操作,其實就是雙向鏈表,一個上下左右的聯繫。
就可以便捷地修改原表了!
技巧:
很容易想到在選擇列時選擇點最少的那一列,能夠減少枚舉次數。
某些題目可以利用IDA* 來優化DLX。
IDA*可以看看另一篇blog:https://blog.csdn.net/tylon2006/article/details/103967989
代碼:
加個表頭方便刪除和查詢。
若dfs中不使用表可以只刪表頭,使用則要整列刪除,但要保留該行連續。
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
struct data{
int l,r,u,d,col,row;//左,右,上,下,所在列,所在行。
void make(int L,int R,int U,int D,int Col,int Row){
l=L,r=R,u=U,d=D,col=Col,row=Row;
}
};
int a[10];//存儲新建行的點
struct DLX{
data p[200010];//點
int siz[2010],last[2010],cnt;//每列點數,每列最後的點編號(便於加點),總點數
void delc(int x){//刪點的上下關係
p[p[x].u].d=p[x].d;
p[p[x].d].u=p[x].u;
}
void delr(int x){//刪點的左右關係
p[p[x].l].r=p[x].r;
p[p[x].r].l=p[x].l;
}
void linkc(int x){//恢復點的上下關係
p[p[x].u].d=p[p[x].d].u=x;
}
void linkr(int x){//恢復點的左右關係
p[p[x].l].r=p[p[x].r].l=x;
}
void clear(int x){//清空,建出所有列的列頭
cnt=0;
p[0].l=p[0].r=0;
p[0].u=p[0].d=0;//head=0用於遍歷
for(int i=1;i<=x;i++){
cnt++,p[cnt].make(i-1,p[i-1].r,i,i,i,0);
linkr(i),siz[i]=0,last[i]=i;
}
}
void add(int x){//添加行x
if(a[0]==0) return;
cnt++,p[cnt].make(cnt,cnt,last[a[1]],p[last[a[1]]].d,a[1],x);
linkc(cnt),siz[a[1]]++,last[a[1]]=cnt;
for(int i=2;i<=a[0];i++){
cnt++,p[cnt].make(cnt-1,p[cnt-1].r,last[a[i]],p[last[a[i]]].d,a[i],x);
linkc(cnt),linkr(cnt),siz[a[i]]++,last[a[i]]=cnt;
}
}
void del(int x){//刪列x
//可重複覆蓋
delr(x);
}
/*
void del(int x){
/*不重複覆蓋問題
delr(x);
for(int i=p[x].u;i!=x;i=p[i].u)
for(int j=p[i].r;j!=i;j=p[j].r)
delc(j),siz[p[j].col]--;
}
void del(int x){
//需要使用表
for(int i=p[x].d;i!=x;i=p[i].d) delr(i);
}
*/
void resume(int x){//恢復列x 就是反過來
//可重複覆蓋
linkr(x);
}
/*
void resume(int x){
//不重複覆蓋問題
linkr(x);
for(int i=p[x].d;i!=x;i=p[i].d)
for(int j=p[i].l;j!=i;j=p[j].l)
linkc(j),siz[p[j].col]++;
}
void resume(int x){
//需要使用表
for(int i=p[x].d;i!=x;i=p[i].d) linkr(i);
}
*/
bool dance(int x){//跳舞,即找答案
if(!p[0].r){
//記錄和輸出答案
return true;
}
int pos=0,minn=2e9;
for(int i=p[0].r;i;i=p[i].r)
if(siz[p[i].col]<minn){//找點數最小列
minn=siz[p[i].col];
pos=p[i].col;
}
if(minn==0) return false;//如果有列沒有行可以覆蓋,則無解,返回0
//不需要使用表
del(pos);//先刪掉該列
for(int i=p[pos].u;i!=pos;i=p[i].u){
for(int j=p[i].l;j!=i;j=p[j].l) del(p[j].col);
ans[x+1].work(p[i].row);
if(dance(x+1)) return true;//找到解才返回1
for(int j=p[i].r;j!=i;j=p[j].r) resume(p[j].col);
}
resume(pos);//恢復該列
/*
//需要使用表
for(int i=p[pos].u;i!=pos;i=p[i].u){
del(i);
for(int j=p[i].l;j!=i;j=p[j].l) del(j);//,cout<<j<<endl;
dance(x+1);
for(int j=p[i].r;j!=i;j=p[j].r) resume(j);
resume(i);
}
*/
return false;
}
}dlx;
int main(){
dlx.clear(/*列數*/);
}
例題:
poj3076
poj1084(IDA*優化DLX