好學易懂 從零開始的插頭DP(二)
好學易懂 從零開始的插頭DP(二)
前情提要
上篇文章裡,我們解決了一個例題,瞭解了,從迴路模型轉換到插頭模型的一些性質和優點。知道了只要按規則放置插頭,就可以保證都是閉合迴路。現在,讓我們對例題做一些改變,之前是可以多個閉合迴路,如果現在要求只能有一條閉合迴路呢?
備註:本文引用了一些大佬論文的圖片和文字。
例題的變化
洛谷P5056 【模板】插頭DP
給出 n×m 的方格,有些格子不能鋪線,其它格子必須鋪,形成一個閉合迴路。問有多少種鋪法?( 1 < = n , m < = 12 ) (1<=n,m<=12)(1<=n,m<=12)
那麼,在上一題的基礎上,我們怎麼保證只有一條迴路呢?或者說我們怎麼知道這是哪一個迴路呢?
從下意識地反應開始——最小表示法
每一個迴路都是一個連通塊,很容易聯想到並查集。我們給每個迴路編號,應該就可以知道是屬於哪個迴路的了。
讓我們回到上一篇文章的這個圖,當時,我們記錄了每個插頭的有無,現在我們還要額外記錄這些插頭屬於哪個迴路,記錄每個插頭的一個編號。
稍加思考可以發現,對於一個輪廓線(圖中紅色的線),它上面一定有偶數個插頭。因為是迴路,要一去一回。那m+1個插頭,至多屬於(m+1)/2個迴路,那麼用一個(m+3)/2進位制數可以狀態壓縮表示。
我們用dp[i][j][S][mask]表示一個狀態,其中(i,j)為格子座標,S為插頭的有無。新增加的mask記錄每個插頭屬於哪個迴路。很容易發現,可以把S合併到mask裡面去,只要這一位是0就是不存在插頭,否則就存在。
當然我們知道,編號需要制定一個規則,使得編號唯一。我們不妨設從上到下,從左到右,第一個必須走的格點所在的迴路為1號迴路,第二個必須走且沒被標記的為2號迴路,以此類推。當兩個迴路合併的時候,下一個新的迴路選擇最小的未被使用的數字標記。
這就是最小表示法。
進一步的優化——括號匹配法
當然,我們發現,上面的下意識地反應,雖然也有可操作性,但是太麻煩了。畢竟這個mask確實有點大,而且我們最後明明只有一個迴路,很多標記最後是浪費的。讓我們重新審視輪廓線上的插頭,希望能發現些有用的性質。
1:之前,我們發現,對於一個輪廓線,它上面一定有偶數個插頭。因為是迴路,要一去一回。更準確的說,對於一個迴路,有且僅有兩個插頭,否則他們目前一定還是兩個不同的迴路。
“仔細觀察上面的圖,可以發現輪廓線上方是由若干條互不相交的路徑構成的,而每條路徑的兩個埠恰好對應了輪廓線上的兩個插頭! 一條路徑上的所有格子對應的是一個連通塊,而每條路徑的兩個埠對應的兩個插頭是連通的而且不與其他任何一個插頭連通.”
2:輪廓線上從左到右4個插頭a, b, c, d,如果a, c連通,並且與b不連通,那麼b, d一定不連通.
“證明:反證法,如果a, c連通,b, d連通,那麼輪廓線上方一定至少存在一條a到c的路徑和一條b到d的路徑.如圖,兩條路徑一定會有交點,不妨設兩條路徑相交於格子P,那麼P既與a, c連通,又與b, d連通,可以推出a, c與b, d連通,矛盾,得證.”
觀察上面兩個性質,成對匹配,不會交叉,很自然就會聯想到括號匹配。我們將一個迴路的左側的插頭標記為左括號,右側的插頭標記為右括號,一種合法的插頭情況,不就是一種合法的括號匹配嘛?
學過狀態壓縮的你一定可以輕鬆的表示出狀態,這裡我們用0表示沒插頭,1表示左括號,2表示右括號。從而用一個三進位制數表示出了mask。當然,實際應用的時候,四進位制會比較好,因為可以位運算。提取出每一位也會更快。
這就是括號匹配法。
如何實現——雜湊
很明顯,這題裡,括號匹配法比最小表示法優秀一些。但是,對於這個資料範圍來說,列舉所有狀態也需要12124^13,達到了1e10的複雜度,顯然超標了,怎麼辦呢?
我們知道mask是描述括號匹配的狀態的,但是括號匹配合法數是一個卡特蘭數,這裡顯然沒有4^13這麼多的有用狀態,我們做一個雜湊對映,僅保留有用的狀態。(程式碼裡的insert函式)模數掛錶就行了,模數這裡取得是299987(學大佬的)
狀態轉移
做過上一道題目,此時我們對於狀態轉移,應該駕輕就熟了吧。和上一個的區別是不再是有無插頭,而是左括號右括號,變成了三進位制的狀態,建議先自己思考下。圖片擋內容大法。
0:如果當前格點不能走,一個括號都不能有。
1:如果左側和上方都沒有括號。那麼伸出去的兩個插頭分別標記為左括號和右括號,相互匹配。
2:如果是左括號+沒括號,類似之前那個題目。把當前格子伸出去的那個插頭標記為左括號。右括號+沒括號類似。
3:如果是沒括號+左括號,類似之前那個題目。把當前格子伸出去的那個插頭標記為左括號。沒括號+右括號類似。
4:如果左側和上方都有括號那麼,自然要把這兩個括號去掉。但是與前一題不同:括號分左括號和右括號,這裡還需要額外的分類討論。
(a):都是左括號,那麼後方最近的兩個右括號,他們與這兩個左括號匹配。為了保證刪掉這兩個左括號後依舊括號匹配,要把右方第一個右括號改成左括號。
(b):都是右括號,與(a)類似,前方最近的兩個左括號,與這兩個右括號匹配。為了保證刪掉這兩個右括號後依舊括號匹配,要把前方第一個左括號改成右括號。
(c):右括號加左括號,直接刪去即可。因為前方第一個左括號,匹配這個右括號,後方第一個右括號,匹配這個左括號。現在這兩對括號標記的迴路合併了,直接刪去能表示連通性且括號匹配。
(d):左括號加右括號,表示形成迴路,因為只能有一條迴路,所有隻有在最後一個可走的點處可以合攏。
程式碼
大題思路已經出來了,剩下的就是實踐了,這裡提供一份AC程式碼。
程式碼如下:
1 #include<stdio.h> 2 #include<iostream> 3 #include<cstring> 4 using namespace std; 5 const long long hs=299987; 6 long long n,m,ex,ey,now,last,ans; 7 long long a[13][13],head[300000],next[2<<24],que[2][2<<24],val[2][2<<24],cnt[2],inc[13]; 8 void init() 9 { 10 scanf("%lld%lld",&n,&m); 11 for (int i=1;i<=n;i++) 12 { 13 for (int j=1;j<=m;j++) 14 { 15 char ch=getchar(); 16 while (ch!='*'&&ch!='.') ch=getchar(); 17 if (ch!='.') a[i][j]=0; 18 else 19 { 20 a[i][j]=1; 21 ex=i; 22 ey=j; 23 } 24 } 25 } 26 inc[0]=1; 27 for(int i=1;i<=13;i++) 28 { 29 inc[i]=inc[i-1]<<2; 30 } 31 } 32 inline void insert(long long bit,long long num) 33 { 34 long long u=bit%hs+1; 35 for(int i=head[u];i;i=next[i]) 36 { 37 if(que[now][i]==bit) 38 { 39 val[now][i]+=num; 40 return; 41 } 42 } 43 next[++cnt[now]]=head[u]; 44 head[u]=cnt[now]; 45 que[now][cnt[now]]=bit; 46 val[now][cnt[now]]=num; 47 } 48 void solve() 49 { 50 cnt[now]=1; 51 val[now][1]=1; 52 que[now][1]=0; 53 for(int i=1;i<=n;i++) 54 { 55 for(int j=1;j<=cnt[now];j++) 56 { 57 que[now][j]<<=2; 58 } 59 for(int j=1;j<=m;++j) 60 { 61 memset(head,0,sizeof(head)); 62 last=now; now^=1; 63 cnt[now]=0; 64 for(int k=1;k<=cnt[last];++k) 65 { 66 long long bit=que[last][k],num=val[last][k]; 67 long long b1=(bit>>((j-1)*2))%4,b2=(bit>>(j*2))%4; 68 if(!a[i][j]) 69 { 70 if(!b1&&!b2) insert(bit,num); 71 } 72 else if(!b1&&!b2) 73 { 74 if(a[i+1][j]&&a[i][j+1]) insert(bit+inc[j-1]+inc[j]*2,num); 75 } 76 else if(!b1&&b2) 77 { 78 if(a[i][j+1]) insert(bit,num); 79 if(a[i+1][j]) insert(bit-inc[j]*b2+inc[j-1]*b2,num); 80 } 81 else if(b1&&!b2) 82 { 83 if(a[i+1][j]) insert(bit,num); 84 if(a[i][j+1]) insert(bit-inc[j-1]*b1+inc[j]*b1,num); 85 } 86 else if(b1==1&&b2==1) 87 { 88 int flag=1; 89 for(int l=j+1;l<=m;++l) 90 { 91 if((bit>>(l*2))%4==1) flag++; 92 if((bit>>(l*2))%4==2) flag--; 93 if(!flag) 94 { 95 insert(bit-inc[j]-inc[j-1]-inc[l],num); 96 break; 97 } 98 } 99 } 100 else if(b1==2&&b2==2) 101 { 102 int flag=1; 103 for(int l=j-2;l>=0;--l) 104 { 105 if((bit>>(l*2))%4==1) flag--; 106 if((bit>>(l*2))%4==2) flag++; 107 if(!flag) 108 { 109 insert(bit-inc[j]*2-inc[j-1]*2+inc[l],num); 110 break; 111 } 112 } 113 } 114 else if(b1==2&&b2==1) insert(bit-inc[j-1]*2-inc[j],num); 115 else if(i==ex&&j==ey) ans+=num; 116 } 117 } 118 } 119 } 120 int main() 121 { 122 init(); 123 solve(); 124 printf("%lld\n",ans); 125 return 0; 126 }View Code of P5056