[入門向選講] 插頭DP:從零概念到入門 (例題:HDU1693 COGS1283 BZOJ2310 BZOJ2331)

轉載請註明原文地址:http://www.cnblogs.com/LadyLex/p/7326874.htmlhtml

最近搞了一下插頭DP的基礎知識……這真的是一種很鍛鍊人的題型……node

每一道題的狀態都不同,而且有很多的分類討論,讓插頭DP十分鍛鍊思惟的全面性和嚴謹性。數組

下面咱們一塊兒來學習插頭DP的內容吧!app

插頭DP主要用來處理一系列基於連通性狀態壓縮的動態規劃問題,處理的具體問題有不少種,而且通常數據規模較小。ide

因爲棋盤有很特殊的結構,使得它能夠與「連通性」有很強的聯繫,所以插頭DP最多見的應用要數在棋盤模型上的應用了。函數

下面咱們給出一道很簡單的例題,而且由這道簡單的例題構建出插頭DP的基本解題思路,在狀態確立,狀態轉移以及程序實現幾個方面進行一一介紹.post

例題一:HDU1693 Eat the Trees

Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)學習

Problem Description
Most of us know that in the game called DotA(Defense of the Ancient), Pudge is a strong hero in the first period of the game. When the game goes to end however, Pudge is not a strong hero any more.
So Pudge’s teammates give him a new assignment—Eat the Trees!
The trees are in a rectangle N * M cells in size and each of the cells either has exactly one tree or has nothing at all. And what Pudge needs to do is to eat all trees that are in the cells.
There are several rules Pudge must follow:
I. Pudge must eat the trees by choosing a circuit and he then will eat all trees that are in the chosen circuit.
II. The cell that does not contain a tree is unreachable, e.g. each of the cells that is through the circuit which Pudge chooses must contain a tree and when the circuit is chosen, the trees which are in the cells on the circuit will disappear.
III. Pudge may choose one or more circuits to eat the trees.
Now Pudge has a question, how many ways are there to eat the trees?
At the picture below three samples are given for N = 6 and M = 3(gray square means no trees in the cell, and the bold black line means the chosen circuit(s))

Input
The input consists of several test cases. The first line of the input is the number of the cases. There are no more than 10 cases.
For each case, the first line contains the integer numbers N and M, 1<=N, M<=11. Each of the next N lines contains M numbers (either 0 or 1) separated by a space. Number 0 means a cell which has no trees and number 1 means a cell that has exactly one tree.
Output
For each case, you should print the desired number of ways in one line. It is guaranteed, that it does not exceed 2 63 – 1. Use the format in the sample.
題目大意:給出一張n*m有障礙的棋盤,要求用 任意條迴路 遍歷整個棋盤,不能通過障礙格子,要求統計不一樣的行走方案數。
Sample Input
2
6 3
1 1 1
1 0 1
1 1 1
1 1 1
1 0 1
1 1 1
2 4
1 1 1 1
1 1 1 1
Sample Output
Case 1: There are 3 ways to eat the trees.
Case 2: There are 2 ways to eat the trees.

狀態確立

  首先,咱們要了解插頭DP中最重要的關鍵詞:「插頭」優化

 ---插頭ui

  在插頭DP中,插頭表示一種聯通的狀態,以棋盤爲例,一個格子有一個向某方向的插頭,就意味着這個格子在這個方向能夠與外面相連(與插頭那邊的格子聯通)。

  值得注意的一點是,插頭不是表示將要去某處的虛擬狀態,而是表示已經到達某處的現實狀態。

  也就是說,若是有一個插頭指向某個格子,那麼這個格子已經和插頭來源聯通了,咱們接下來要考慮的是從這個插頭往哪裏走。

  這是很重要的一點理解,下面討論的狀態轉移都是在此基礎上展開的,請務必注意。

  咱們已經有了插頭,天然要利用插頭來狀態轉移。通常來講,咱們從上往下,從左往右逐行逐格遞推。

 ---逐格遞推

  咱們考慮第i行的某一個格子:走向它的方案,可能由上一行的下插頭轉移而來,也多是本行的右插頭轉移而來。

  所以咱們須要記錄這些地方有沒有插頭,也就是利用狀壓的思想。咱們記錄的這個「有沒有插頭」的東西,就被咱們稱爲輪廓線。字面意思,輪廓線就是記錄了棋盤這一行與上一行交界的輪廓中插頭的狀況。輪廓線上方是已經決策完的格子,下方是未決策的。顯然,對於本題的輪廓線,與它直接相連的格子有m個,插頭有m+1個,我我的的習慣是給插頭編號0~m。

  

  ·上圖就是輪廓線的一種可能狀況。

  因爲數據範圍比較小,輪廓線的插頭狀態咱們通常能夠利用X進制壓位來表示。

  對於本題來講,題目的限制條件比較少,能夠走多個迴路,而不是像某些題同樣只能走一個迴路(走一個迴路的時候要維護插頭間的連通性,咱們下文再討論),所以咱們直接記錄,只要用二進制來表示某一位置有沒有插頭便可:設0表示沒有插頭,1表示有插頭。(大概這是最簡單的一種插頭類型了......)

狀態轉移

 ---行間轉移

  咱們先考慮兩行之間的轉移:顯然,第i行的下插頭決定了第i+1行的格子有沒有上插頭,所以咱們應該把這個信息傳遞到下一行。

  在轉移的時候,當前行插頭0到插頭m-1可能會給下一行帶來貢獻,而第m個插頭必定爲0(結合定義,想一下爲何)。

  容易發現,當前行的0~m-1號插頭會變成下一行初始的1~m號插頭,所以咱們能夠直接利用位運算進行轉移。

  對於本題,只須要將上一行的某個狀態左移一位(<<1,即*2)便可

  行間的轉移仍是比較簡單的,具體代碼實現的話,下面是一種能夠參考的方式

  (這是我剛學插頭DP時候用的一種比較蠢的打法,使用狀態數組f[i][j][k]表示決策到第i行第j列,插頭狀態爲k的方案數,後面使用Hash表的時候咱們還有其餘方式)

1 if(i<n)//bin[i]表示2的i次方
2     for(int j=0;j<bin[m];j++)
3         f[i+1][0][j<<1]=f[i][m][j];

  下面咱們考慮具體的逐格轉移,這也是插頭DP的核心模塊所在。

  對於本題來講,當咱們決策到某個格子(x,y)時,假如它不是障礙格子,可能會出現以下三種狀況:

  

  狀況1,這個格子沒有上插頭,也沒有左插頭,那麼因爲咱們要遍歷整張圖,因此咱們要新建插頭,把這個格子與其餘格子連起來,相應的,咱們要把原來輪廓線對應位置的插頭改成1.

  狀況2,這個位置有上插頭,也有左插頭。因爲咱們不要求只有一條迴路,所以迴路能夠在這裏結束。咱們直接更新答案便可。

  狀況3,只有一個插頭。那麼這個插頭能夠向其餘方向走:向下和向右都可以。因此咱們修改一下輪廓線並更新對應狀態的答案便可。

  值得注意的是,若是一個格子是障礙格,那麼當且僅當沒有插頭連向它時,這纔是一個合法狀態。由於根據咱們剛纔插頭的定義:

  

  「值得注意的一點是,插頭不是表示將要去某處的虛擬狀態,而是表示已經到達某處的現實狀態。

  也就是說,若是有一個插頭指向某個格子,那麼這個格子已經和插頭來源聯通了,咱們接下來要考慮的是從這個插頭往哪裏走。」

 

  因此,對應障礙格既不能連入插頭,也不能連出插頭。這一點須要特別注意。

代碼實現

  本題的分類討論仍是相對簡單的,在處理完上面的內容後咱們只須要按照上面的思路代碼實現便可。

  (代碼裏狀態轉移頗有意思……)

 1 #include <cstdio>
 2 #include <cstring>
 3 using namespace std;
 4 typedef long long LL;
 5 int n,m,bin[20],mp[13][13];
 6 LL f[13][13][(1<<12)+10];
 7 inline void Execution(int x,int y)
 8 {
 9     int plug1=bin[y-1],plug2=bin[y];
10     for(int j=0;j<bin[m+1];j++)
11         if(mp[x][y])
12         {
13             f[x][y][j]+=f[x][y-1][j^plug1^plug2];
14             if( (( j>>(y-1) )&1)== ((j>>(y) )&1) )continue;
15             f[x][y][j]+=f[x][y-1][j];
16         }
17         else
18             if(!(j&plug1)&&!(j&plug2))f[x][y][j]=f[x][y-1][j];
19             else f[x][y][j]=0;
20 }
21 int main()
22 {
23     int t;scanf("%d",&t);
24     bin[0]=1;for(int i=1;i<=15;i++)bin[i]=bin[i-1]<<1;
25     for(int u=1;u<=t;u++)
26     {
27         scanf("%d%d",&n,&m);
28         for(int i=1;i<=n;i++)
29             for(int j=1;j<=m;j++)
30                 scanf("%d",&mp[i][j]);
31         memset(f,0,sizeof(f));f[1][0][0]=1;
32         for(int i=1;i<=n;i++)
33         {
34             for(int j=1;j<=m;j++)Execution(i,j);
35             if(i!=n)for(int j=0;j<bin[m];j++)
36                     f[i+1][0][j<<1]=f[i][m][j];
37         }
38         printf("Case %d: There are %lld ways to eat the trees.\n",u,f[n][m][0]);
39     }
40 }

  經過剛纔這道題,你應該已經對插頭DP是什麼,以及插頭DP的基本概念與思想有了基本的瞭解。

  那麼下面,咱們經過下一道題來強化分類討論能力,以及學習對連通性的限制方法。

例題2:COGS1283. [HNOI2004] 郵遞員

時間限制:10 s   內存限制:162 MB

【題目描述】

Smith在P市的郵政局工做,他天天的工做是從郵局出發,到本身所管轄的全部郵筒取信件,而後帶回郵局。

他所管轄的郵筒很是巧地排成了一個m*n的點陣(點陣中的間距都是相等的)。左上角的郵筒剛好在郵局的門口。

Smith是一個很是標新立異的人,他但願天天都能走不一樣的路線,可是同時,他又不但願路線的長度增長,他想知道他有多少條不一樣的路線可走。

你的程序須要根據給定的輸入,給出符合題意的輸出:

l 輸入包括點陣的m和n的值;

l 你須要根據給出的輸入,計算出Smith可選的不一樣路線的總條數;

【輸入格式】

輸入文件postman.in只有一行。包括兩個整數m, n(1 <= m <= 10, 1 <= n <= 20),表示了Smith管轄內的郵筒排成的點陣。

【輸出格式】

輸出文件只有一行,只有一個整數,表示Smith可選的不一樣路線的條數。

【樣例輸入】

2 2 說明:該輸入表示,Smith管轄了2*2的一個郵筒點陣。

【樣例輸出】

2

【提示】

  有了上一題的經驗,不難看出,本題依然是一個在棋盤模型上解決的簡單迴路問題(簡單迴路是指起點和終點相同的簡單路徑)。

  而咱們要求的是能一遍遍歷整個棋盤的簡單迴路個數。

  但是,若是直接搬用上一題的作法,你會發現一些問題:

  

  好比對於上圖的狀況,在上一題中這是一個合法解,但在本題中不是。那麼咱們就應該思考上題插頭定義的片面性在哪裏,並想出新的插頭定義

  容易觀察到,若是每一個格子都在迴路中的話,最後全部的格子應該都經過插頭鏈接成了一個連通塊

  所以咱們還須要記錄每行格子的連通狀況.這時咱們就要引入一種新的方法:最小表示法。這是一種用來標記連通性的方法。

  具體的過程是:第一個非障礙格子以及與它連通的全部格子標記爲1,而後再找第一個未標記的非障礙格子以及與它連通的格子標記爲2,……重複這個過程,直到全部的格子都標記完畢.好比連通訊息((1,2,5),(3,6),(4)),就能夠表示爲{1,1,2,3,1,2}

  可是,在實際的代碼實現中,這樣的最小表示法有些冗餘:若是某個格子沒有下插頭,那麼它就不會對下一行的格子產生影響,這個狀態就是多餘的。

  所以,咱們轉換優化的角度,用最小表示法來表示插頭的聯通性:若是這個插頭存在,那麼就標記這個插頭對應的格子的連通標號,若是這個插頭不存在,那麼標記爲0..

  在這樣優化後,不只狀態表示更加簡單,並且狀態總數將會大大減小.

  接下來,咱們用改進過的最小表示法,繼續思考上面的問題:如何定義新的插頭狀態?

  若是每一個格子都在迴路中的話,咱們還能夠獲得,每一個格子應該剛好有且僅有2個插頭。

  咱們來看下面幾張圖片:

  

  相信細心的你可以發現,輪廓線上方的路徑是由若干條互不相交的路徑構成的(這是確定的,簡單反證:若是最終相交就構不成迴路了)。

  更有趣的是,每條路徑的兩個端點剛好對應了輪廓線上的兩個插頭

  咱們又知道,一條路徑應該對應着一個連通塊,所以這兩個插頭同屬一個連通塊,而且不與其餘的連通塊聯通

  而且,咱們在狀態轉移的時候也不會改變這種性質:

  上文的狀況1對應着新增一條路徑,插頭爲2;

  狀況2意味着把2條路徑合爲一條,聯通塊變爲1個,插頭仍是2個;

  狀況3只有一個插頭壓根不會改變插頭數量。

  那麼如今咱們知道了,簡單迴路問題必定知足任什麼時候候輪廓線上每個連通份量剛好有2個插頭。

  互不相交……兩個插頭……展開你的聯想,你能想到什麼?

  沒錯,這正是括號匹配!咱們能夠按照與括號匹配類似的方式,將輪廓線上每一條路徑上中左邊那個插頭標記爲左括號插頭,右邊那個插頭標記爲右括號插頭。

  因爲插頭之間不會交叉,那麼左括號插頭必定能夠與右括號插頭一一對應。

  這樣咱們就能夠解決上面的聯通性問題:咱們可使用一種新的定義方式:3進製表示——0表示無插頭,1表示左括號插頭,2表示右括號插頭,記錄下全部的輪廓線信息。

  可是,值得注意的是,X進制的解碼轉碼是較慢並且較麻煩的。

  在空間容許的狀況下,建議使用2k進制,而且加上Hash表去重。這樣不只能夠減小狀態,因爲仍然可使用二進制位運算,運算速度相比之下也增長了很多。

  下面,咱們利用剛纔新的插頭定義方式來考慮本題的狀態轉移問題。

  依然設當前轉移到格子(x,y),設y-1號插頭狀態爲p1,y號插頭狀態爲p2。

  狀況1:p1==0&&p2==0.

    這種狀態和上一題的狀況1是相似的,咱們只須要新建一個新路徑便可:下插頭設爲左括號插頭,右插頭設爲右括號插頭

  狀況2:p1==0&&p2!=0.

    這種狀態和上一題的狀態3相似,咱們依然能夠選擇「直走」和「轉彎」兩種策略

  狀況3:p1!=0&&p2==0.

    這種狀態和狀況2相似,再也不贅述。

  狀況4:p1==1&&p2==1.

    這種狀態把2個左括號插頭相連,那麼咱們須要將右邊那個左括號插頭(p2)對應的右括號插頭q2修改爲左括號插頭。

  狀況5:p1==1&&p2==2.

    因爲路徑兩兩不相交,因此這種狀況只能是本身和本身撞在了一塊兒,即造成了迴路。

    因爲只能有一條迴路,所以只有在x==n&&y==m時,這種狀態纔是合法的,咱們能夠用它更新答案。

  

  狀況6:p1==2&&p2==1.

    這種狀態至關於把2條路徑相連,並無更改其餘的插頭

  狀況7:p1==2&&p2==2.

    這種狀態與狀況4類似,這種狀態把2個右括號插頭相連,那麼咱們須要將左邊那個右括號插頭(p1)對應的左括號插頭q1修改爲右括號插頭。

  接下來咱們只要代碼實現上述過程便可。

  但咱們依然有一個很大的優化點:Hash表的使用。Hash表能夠經過去重以及排除無用狀態極大的加速插頭DP的速度。

  Hash表的打法不惟一,下面僅介紹我學習的打法(感謝stdafx學長)

  與Hash表相關的主要內容有:

  1.mod變量,爲Hash表的大小和模數

  2.size變量,存儲Hash表大小;

  3.hash數組,存儲某個餘數對應的編號

  4.key數組,存儲狀態

  5.val數組,存某個狀態對應的方案數

  在給出一個新狀態時,咱們在已有Hash表內搜索是否存在這一狀態,若是有,那就修改這個狀態對應的val值;若是沒有,那就給他新建一個編號

  具體的代碼實現大概長這樣:

 

 1 struct node{int state,next;};
 2 struct Hash_map
 3 {
 4     int val[MOD],adj[MOD],e;node s[MOD];
 5     inline void intn()
 6     {
 7         memset(val,0x7f,sizeof(val)),e=0,
 8         memset(s,0,sizeof(s)),memset(adj,0,sizeof(adj));
 9     }
10     inline int &operator [] (const int &State)
11     {
12         int pos=State%MOD,i;
13         for(i=adj[pos];i&&s[i].state!=State;i=s[i].next);
14         if(!i)s[++e].state=State,s[e].next=adj[pos],adj[pos]=i=e;
15         return val[i];
16     }
17 }f[2];

 

有了Hash表,咱們再來考慮狀態轉移時的幾個小細節:

咱們狀態轉移的主要工做通常有三個:

1.查詢某個插頭對應的類型(對應下文Find)

2.查找與某個插頭匹配的對應插頭(對應下文Link)

3.修改狀態中某個插頭的類型(對應下文Set)

因爲這三個操做很經常使用,因此我把他們寫成了函數,方便調用。這三個操做的代碼見下:

 1 inline int Find(int State,int id){return (State>>((id-1)<<1))&3;}
 2 inline void Set(int &State,int bit,int val){bit=(bit-1)<<1;State|=3<<bit,State^=3<<bit,State|=val<<bit;}
 3 inline int Link(int State,int pos)
 4 {
 5     int cnt=0,Delta=(Find(State,pos)==1)?1:-1;//這個變量決定向左尋找匹配仍是向右
 6     for(int i=pos;i&&i<=m+1;i+=Delta)
 7     {
 8         int plug=Find(State,i);
 9         if(plug==1)cnt++;
10         else if(plug==2)cnt--;
11         if(cnt==0)return i;
12     }
13     return -1;
14 }

有了上面這些操做,本題的所有代碼實現已經水到渠成了:咱們只須要把上面7種狀況一一對應實現便可。代碼見下:

(還有一個注意點,記得寫高精度!)

  1 #include <cstdio>
  2 #include <cstring>
  3 #include <algorithm>
  4 #include <cmath>
  5 using namespace std;
  6 typedef long long LL;
  7 const int cube=(int)1e9,mod=2601;
  8 int n,m;
  9 struct Data_Analysis
 10 {
 11     int bit[6];
 12     inline void Clear(){memset(bit,0,sizeof(bit));}
 13     Data_Analysis(){Clear();}
 14     inline void Set(int t){Clear();while(t)bit[++bit[0]]=t%cube,t/=cube;}
 15     inline int &operator [](int x){return bit[x];}
 16     inline void Print()
 17     {
 18         printf("%d",bit[bit[0]]);
 19         for(int i=bit[0]-1;i>0;i--)printf("%09d",bit[i]);
 20         printf("\n");
 21     }
 22     inline Data_Analysis operator + (Data_Analysis b)
 23     {
 24         Data_Analysis c;c.Clear();
 25         c[0]=max(bit[0],b[0])+1;
 26         for(int i=1;i<=c[0];i++)
 27             c[i]+=bit[i]+b[i],c[i+1]+=c[i]/cube,c[i]%=cube;
 28         while(!c[c[0]])c[0]--;
 29         return c;
 30     }
 31     inline void operator += (Data_Analysis b){*this=*this+b;}
 32     inline void operator = (int x){Set(x);}
 33 }Ans;
 34 struct Hash_Sheet
 35 {
 36     Data_Analysis val[mod];
 37     int key[mod],size,hash[mod];
 38     inline void Initialize()
 39     {
 40         memset(val,0,sizeof(val)),memset(key,-1,sizeof(key));
 41         size=0,memset(hash,0,sizeof(hash));
 42     }
 43     inline void Newhash(int id,int v){hash[id]=++size,key[size]=v;}
 44     Data_Analysis &operator [](const int State)
 45     {
 46         for(int i=State%mod;;i=(i+1==mod)?0:i+1)
 47         {
 48             if(!hash[i])Newhash(i,State);
 49             if(key[hash[i]]==State)return val[hash[i]];
 50         }
 51     }
 52 }f[2];
 53 inline int Find(int State,int id){return (State>>((id-1)<<1))&3;}
 54 inline void Set(int &State,int bit,int val){bit=(bit-1)<<1;State|=3<<bit,State^=3<<bit,State|=val<<bit;}
 55 inline int Link(int State,int pos)
 56 {
 57     int cnt=0,Delta=(Find(State,pos)==1)?1:-1;
 58     for(int i=pos;i&&i<=m+1;i+=Delta)
 59     {
 60         int plug=Find(State,i);
 61         if(plug==1)cnt++;
 62         else if(plug==2)cnt--;
 63         if(cnt==0)return i;
 64     }
 65     return -1;
 66 }
 67 inline void Execution(int x,int y)
 68 {
 69     int now=((x-1)*m+y)&1,last=now^1,tot=f[last].size;
 70     f[now].Initialize();
 71     for(int i=1;i<=tot;i++)
 72     {
 73         int State=f[last].key[i];
 74         Data_Analysis Val=f[last].val[i];
 75         int plug1=Find(State,y),plug2=Find(State,y+1);
 76         if(Link(State,y)==-1||Link(State,y+1)==-1)continue;
 77         if(!plug1&&!plug2){if(x!=n&&y!=m)Set(State,y,1),Set(State,y+1,2),f[now][State]+=Val;}
 78         else if(plug1&&!plug2)
 79         {
 80             if(x!=n)f[now][State]+=Val;
 81             if(y!=m)Set(State,y,0),Set(State,y+1,plug1),f[now][State]+=Val;
 82         }
 83         else if(!plug1&&plug2)
 84         {
 85             if(y!=m)f[now][State]+=Val;
 86             if(x!=n)Set(State,y,plug2),Set(State,y+1,0),f[now][State]+=Val;
 87         }
 88         else if(plug1==1&&plug2==1)
 89             Set(State,Link(State,y+1),1),Set(State,y,0),Set(State,y+1,0),f[now][State]+=Val;
 90         else if(plug1==1&&plug2==2){if(x==n&&y==m)Ans+=Val;}
 91         else if(plug1==2&&plug2==1)Set(State,y,0),Set(State,y+1,0),f[now][State]+=Val;
 92         else if(plug1==2&&plug2==2)
 93             Set(State,Link(State,y),2),Set(State,y,0),Set(State,y+1,0),f[now][State]+=Val;
 94     }
 95 }
 96 int main()
 97 {
 98     scanf("%d%d",&n,&m);
 99     if(n==1||m==1){printf("1\n");return 0;}
100     if(m>n)swap(n,m);
101     f[0].Initialize();f[0][0]=1;
102     for(int i=1;i<=n;i++)
103     {
104         for(int j=1;j<=m;j++)Execution(i,j);
105         if(i!=n)
106         {
107             int now=(i*m)&1,tot=f[now].size;
108             for(int j=1;j<=tot;j++)
109                 f[now].key[j]<<=2;
110         }
111     }
112     Ans+=Ans;Ans.Print();
113 }

經過這道題的歷練,相信你對插頭DP的插頭定義,最小表示法,以及狀態優化的方法有了必定的瞭解。

尤爲須要培養的是插頭定義的「手感」,插頭定義絕對是你解題的關鍵。

接下來,咱們把目光轉移到「簡單路徑」上來。經過下面這道例題,相信會對這類簡單路徑&迴路問題有更深的理解。

BZOJ 2310: ParkII

Time Limit: 20 Sec  Memory Limit: 128 MB

Description

Hnoi2007-Day1有一道題目 Park:給你一個 m * n 的矩陣,每一個矩陣內有個
權值V(i,j) (可能爲負數),要求找一條迴路,使得每一個點最多通過一次,而且通過
的點權值之和最大,想必你們印象深入吧. 
無聊的小 C 同窗把這個問題稍微改了一下:要求找一條路徑,使得每一個點
最多通過一次,而且點權值之和最大,若是你跟小 C 同樣無聊,就麻煩作一下
這個題目吧.

Input

第一行 m, n,接下來 m行每行 n 個數即V( i,j)

Output

一個整數表示路徑的最大權值之和.

Sample Input

2 3
1 -2 1
1 1 1

Sample Output

5
【數據範圍】
30%的數據,n≤6;100%的數據,m<=100,n ≤ ≤8.
注意:路徑上有可能只有一個點.
 
咱們再來分析一下,這道題與上一道題又有什麼區別?
首先,從統計方案問題變成了最優解問題;
其次,問題由迴路問題變成了路徑問題;
還有,從遍歷整張圖變成了無需遍歷整張圖。
咱們來一項一項解決新出現的問題。
對於第一個問題:
  最優解問題能夠經過更改一下統計答案的方式來解決:原來咱們是加和求方案數,如今咱們是取max來求最優解。
對於第二個問題:
  咱們須要考慮一下路徑與迴路之間的差異在哪:
    ---對於迴路問題,輪廓線上的每個插頭,都有惟一肯定的另一個插頭與其對應;
    ---而對於路徑來講,輪廓線上的某些插頭可能沒有匹配插頭與之對應:它的另外一端多是路徑的一段
  所以,咱們考慮新加一種插頭類型來表示這種插頭:咱們使用4進制狀壓,用3表示這種沒有匹配插頭的插頭——我稱它爲「獨立插頭」
  在想好了新插頭的定義以後,咱們來考慮它帶來了哪些轉移:
    ---首先,以前的幾種轉移依然適用。
    ---同時,咱們新增:了下面幾種新狀況:
      狀況8:p1==0&&p2==0
        這時咱們有一種新策略:即除了以前添加一對括號插頭以外,咱們還能夠選擇在某一個方向添加獨立插頭,
         即p1←3或p2←3
      狀況9:p1==3&&p2==3
        這時,咱們把兩個獨立插頭連在一塊兒就造成了一條完整的路徑,若是此時除了p1,p2沒有其餘插頭,咱們就能夠在這時更新答案了。
      狀況10:p1==3&&p2!=3&&p2!=0
        這時,若是咱們把p1,p2鏈接,那麼與p2對應的插頭q2就變成了獨立插頭。
      狀況11:p1!=3&&p1!=0&&p2==3
        這種狀況與狀況10相似,再也不贅述。
      狀況12:p1==3&&p2==0
        在這種狀況下,咱們有2種選擇:
          ①路徑在此中止。若是沒有其餘的插頭,咱們此時就能夠統計答案了。
          ②路徑延續。咱們用和以前類似的方法把獨立插頭傳遞下去便可。
      狀況13:p1==0&&p2==3
        這種狀況與狀況12相似,再也不贅述。
      狀況14:p1==0&&p2!=3&&p2!=0
        這時咱們有一種新策略:
          把p2做爲路徑的一端點不在擴展,後繼插頭置爲0;同時把與p2匹配的插頭q2修改成獨立插頭3.
      狀況15:p1!=0&&p1!=3&&p2==0
        這種狀況與狀況14相似,再也不贅述。
      狀況16:p1==1&&p2==2
        原來在迴路問題中這種狀況是合法的,可是如今咱們正在考慮路徑問題,這種狀況(本身的左括號插頭接本身的右括號插頭)造成了迴路,所以應該捨去。
對於第三個問題:
  這個問題其實也是對應着一個新的狀態:
  若是當前狀態爲p1==0&&p2==0,那麼咱們能夠不選這個格子,直接跳過,這樣咱們就解除了對遍歷整張棋盤的限制。
至此,新出現的問題所有獲得解決。咱們只須要將上面的新狀況轉化成代碼實現便可。
  可是,在實際操做中,咱們仍然有一個能夠優化的地方:因爲本題限制只用一條路徑,所以表示路徑的獨立插頭在同一個狀態內最多隻能出現2個。所以,咱們能夠在枚舉到每一個狀態時用O(m)時間進行一下判斷。這樣的效果是十分顯著的。
  
  如圖,上面一份代碼是不加檢驗函數的程序運行時間,下面一份是加上以後的運行時間。很明顯,效率獲得了極大的提高
  檢驗函數大概長這樣:
 1 inline bool check(int State)
 2 {
 3     int cnt=0,cnt1=0;
 4     for(int i=1;i<=m+1;i++)
 5     {
 6         int plug=Find(State,i);
 7         if(plug==3)cnt++;
 8         else if(plug==1)cnt1++;
 9         else if(plug==2)cnt1--;
10     }
11     return cnt>2||cnt1!=0;
12 }
 至此,這道題被咱們徹底解決。具體的AC代碼見下:
  1 #include <cstdio>
  2 #include <cstring>
  3 #include <algorithm>
  4 using namespace std;
  5 const int N=110,M=10,mod=16631;
  6 int n,m,ans,v[N][M];
  7 struct Hash_System
  8 {
  9     int key[mod],size,hash[mod],val[mod];
 10     inline void Initialize()
 11     {
 12         memset(key,-1,sizeof(key));memset(val,0xaf,sizeof(val));
 13         size=0;memset(hash,0,sizeof(hash));
 14     }
 15     inline void Newhash(int id,int State){hash[id]=++size;key[size]=State;}
 16     inline int &operator [] (const int State)
 17     {
 18         for(int i=State%mod;;i=(i+1==mod)?0:i+1)
 19         {
 20             if(!hash[i])Newhash(i,State);
 21             if(key[hash[i]]==State)return val[hash[i]];
 22         }
 23     }
 24 }f[2];
 25 inline int max(int a,int b){return a>b?a:b;}
 26 inline int Find(int State,int pos){return (State>>((pos-1)<<1))&3;}
 27 inline void Set(int &State,int pos,int val){pos=(pos-1)<<1,State|=(3<<pos),State^=(3<<pos),State^=(val<<pos);}
 28 inline int Link(int State,int pos)
 29 {
 30     int cnt=0,Delta=(Find(State,pos)==1)?1:-1;
 31     for(int i=pos;i&&i<=m+1;i+=Delta)
 32     {
 33         int plug=Find(State,i);
 34         if(plug==1)cnt++;
 35         else if(plug==2)cnt--;
 36         if(cnt==0)return i;
 37     }
 38     return -1;
 39 }
 40 inline bool check(int State)
 41 {
 42     int cnt=0,cnt1=0;
 43     for(int i=1;i<=m+1;i++)
 44     {
 45         int plug=Find(State,i);
 46         if(plug==3)cnt++;
 47         else if(plug==1)cnt1++;
 48         else if(plug==2)cnt1--;
 49     }
 50     return cnt>2||cnt1!=0;
 51 }
 52 inline void Execution(int x,int y)
 53 {
 54     int now=((x-1)*m+y)&1,last=now^1,tot=f[last].size;
 55     f[now].Initialize();
 56     for(int i=1;i<=tot;i++)
 57     {
 58         int State=f[last].key[i],Val=f[last].val[i];
 59         if (check(State)||State>=(1<<((m+1)<<1)))continue;
 60         int plug1=Find(State,y),plug2=Find(State,y+1);
 61         int ideal=State;Set(ideal,y,0),Set(ideal,y+1,0);//ideal表明去掉y-1,y兩個插頭以後的輪廓線狀態。
 62         int empty1=ideal,empty2=ideal;
 63         if(!plug1&&!plug2)
 64         {
 65             f[now][ideal]=max(f[now][ideal],Val);
 66             if(x<n&&y<m)Set(State,y,1),Set(State,y+1,2),f[now][State]=max(f[now][State],Val+v[x][y]);
 67             if(x<n)Set(empty1,y,3),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
 68             if(y<m)Set(empty2,y+1,3),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
 69         }
 70         else if(plug1&&!plug2)
 71         {
 72             if(x<n)f[now][State]=max(f[now][State],Val+v[x][y]);
 73             if(y<m)Set(empty1,y+1,plug1),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
 74             if(plug1==3){if(!ideal)ans=max(ans,Val+v[x][y]);}
 75             else Set(empty2,Link(State,y),3),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
 76         }
 77         else if(!plug1&&plug2)
 78         {
 79             if(y<m)f[now][State]=max(f[now][State],Val+v[x][y]);
 80             if(x<n)Set(empty2,y,plug2),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
 81             if(plug2==3){if(!ideal)ans=max(ans,Val+v[x][y]);}
 82             else Set(empty1,Link(State,y+1),3),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
 83         }
 84         else if(plug1==1&&plug2==1)Set(empty1,Link(State,y+1),1),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
 85         else if(plug1==1&&plug2==2)continue;
 86         else if(plug1==2&&plug2==1)f[now][ideal]=max(f[now][ideal],Val+v[x][y]);
 87         else if(plug1==2&&plug2==2)Set(empty2,Link(State,y),2),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
 88         else if(plug1==3&&plug2==3){if(!ideal)ans=max(ans,Val+v[x][y]);}
 89         else if(plug2==3)Set(empty1,Link(State,y),3),f[now][empty1]=max(f[now][empty1],Val+v[x][y]);
 90         else if(plug1==3)Set(empty2,Link(State,y+1),3),f[now][empty2]=max(f[now][empty2],Val+v[x][y]);
 91     }
 92 }
 93 int main()
 94 {
 95     scanf("%d%d",&n,&m);
 96     for(register int i=1;i<=n;i++)
 97         for(register int j=1;j<=m;j++)
 98             scanf("%d",&v[i][j]),ans=max(ans,v[i][j]);
 99     f[0].Initialize();f[0][0]=0;
100     for(register int i=1;i<=n;i++)
101     {
102         for(register int j=1;j<=m;j++)Execution(i,j);
103         if(i!=n)    
104             for(int j=1,last=(i*m)&1,tot=f[last].size;j<=tot;j++)
105                 f[last].key[j]<<=2;
106     }
107     printf("%d\n",ans);
108 }
 
 通過了這三道題的「洗禮」,相信你已經對插頭DP中棋盤簡單路徑&迴路問題略知一二了。
可是,插頭DP不只僅只有這一種類型題;有的時候,某些題的插頭定義只適用於那一道題。
這時候,就須要咱們培養一種靈活的思惟, 從多個角度去尋找新的插頭定義方式
下面咱們再來看一道題目。這道題的插頭定義……和上面幾道題都有不一樣。

BZOJ 2331: [SCOI2011]地板

Time Limit: 5 Sec  Memory Limit: 128 MB

Description

lxhgww的小名叫L」,這是由於他老是很喜歡L型的東西。小L家的客廳是一個矩形,如今他想用L型的地板來鋪滿整個客廳,客廳裏有些位置有柱子,不能鋪地板。如今小L想知道,用L型的地板鋪滿整個客廳有多少種不一樣的方案?

須要注意的是,以下圖所示,L型地板的兩端長度能夠任意變化,但不能長度爲0。鋪設完成後,客廳裏面全部沒有柱子的地方都必須鋪上地板,但同一個地方不能被鋪屢次。

Input

輸入的第一行包含兩個整數,RC,表示客廳的大小。

接着是R行,每行C個字符。’_’表示對應的位置是空的,必須鋪地板;’*’表示對應的位置有柱子,不能鋪地板。

Output

輸出一行,包含一個整數,表示鋪滿整個客廳的方案數。因爲這個數可能很大,只需輸出它除以20110520的餘數。

Sample Input

2 2

*_

__

Sample Output

1

HINT

R*C<=100

看到這道題,咱們很容易發現上面幾道題的全部插頭定義方式都不在適用了。

所以,咱們須要自行尋找到新的插頭定義方式。

容易注意到,和上題的「路徑」相比,本題的合法路徑「L型地板」有一些特殊的地方:拐彎且僅拐彎一次。

這因爲一條路徑只有兩種狀態:拐彎過和沒拐彎過,所以咱們能夠嘗試着這樣定義新的插頭:

咱們使用三進制,0表明沒有插頭,1表明沒拐彎過的路徑,2表明已經拐彎過的路徑。

依然設當前轉移到格子(x,y),設y-1號插頭狀態爲p1,y號插頭狀態爲p2。

那麼會有下面的幾種狀況:

  狀況1:p1==0&&p2==0

    這時咱們有三種可選的策略:

      ①以當前位置爲起點,從p1方向引出一條新的路徑(把p1修改成1號插頭)

      ②以當前位置爲起點,從p2方向引出一條新的路徑(把p2修改成1號插頭)

      ③以當前位置爲「L」型路徑的轉折點,向p1,p2兩個方向均引出一個2號插頭.

  狀況2:p1==0&&p2==1

    因爲p2節點尚未拐過彎,所以咱們有2種可選的策略:

      ①繼續直走,不拐彎,即上->下(把p1修改成1號插頭,p2置0)

      ②選擇拐彎,即上->右(把p2改成2號插頭)

  狀況3:p1==1&&p2==0

    這種狀況和狀況2相似,再也不贅述。

  狀況4:p1==0&&p2==2

    因爲p2節點已經拐過彎,因此咱們有以下的兩種策略:

    ①路徑在此中止。那麼咱們以本格做爲L型路徑的一個端點。若是當前處於最後一個非障礙格子,若是沒有其餘的插頭,咱們此時就能夠統計答案了。
    ②路徑延續。因爲p2已經轉彎過,所以咱們只能選擇繼續直走,即上->下(把p1修改成2號插頭,p2置0)和以前類似的方法把獨立插頭傳遞下去便可。

  狀況5:p1==2&&p2==0

    這種狀況與狀況4相似,再也不贅述。

  狀況6:p1==1&&p2==1

    這種狀況下,兩塊地板均沒有拐過彎,所以咱們能夠在本格將這兩塊地板合併,造成一個合法的「L」型路徑,並將本格看作他們的轉折點。(把p1和p2都置爲0)

至此,新插頭定義的狀態轉移已經討論完成。

不難發現,這種新的插頭定義能夠處理可能發生的全部可行狀況。

咱們只須要把他們轉化爲代碼實現便可,具體見下:

  1 #include <cstdio>
  2 #include <cstring>
  3 #include <algorithm>
  4 using namespace std;
  5 const int N=105,HASH=200097,mod=20110520;
  6 int ans,n,m,last_x,last_y;char c[N][N];bool room[N][N];
  7 struct Hash_System
  8 {
  9     int val[HASH],key[HASH],hash[HASH],size;
 10     inline void Initialize()
 11     {
 12         memset(val,0,sizeof(val)),memset(hash,0,sizeof(hash));
 13         memset(key,-1,sizeof(key)),size=0;
 14     }
 15     inline void Newhash(int id,int State){hash[id]=++size,key[size]=State;}
 16     inline int &operator [] (const int State)
 17     {
 18         for(register int i=State%HASH;;i=(i+1==HASH)?0:i+1)
 19         {                
 20             if(!hash[i])Newhash(i,State);
 21             if(key[hash[i]]==State)return val[hash[i]];
 22         }
 23     }
 24 }f[2];
 25 inline int Find(int State,int pos){return (State>>((pos-1)<<1))&3;}
 26 inline void Set(int &State,int pos,int val)
 27     {pos=(pos-1)<<1,State|=(3<<pos),State^=(3<<pos),State^=(val<<pos);}
 28 inline void Print(int State){for(int i=0;i<=m;i++)printf("%d",(State>>(i<<1))&3);}
 29 inline void Execution(int x,int y)
 30 {
 31     register int now=((x-1)*m+y)&1,last=now^1,tot=f[last].size;
 32     f[now].Initialize();
 33     for(register int i=1;i<=tot;i++)
 34     {
 35         int State=f[last].key[i],Val=f[last].val[i];
 36         int plug1=Find(State,y),plug2=Find(State,y+1);
 37         if(room[x][y])
 38         {
 39             if(!plug1&&!plug2)
 40             {
 41                 if(room[x+1][y])Set(State,y,1),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 42                 if(room[x][y+1])Set(State,y,0),Set(State,y+1,1),f[now][State]=(f[now][State]+Val)%mod;
 43                 if(room[x][y+1]&&room[x+1][y])Set(State,y,2),Set(State,y+1,2),f[now][State]=(f[now][State]+Val)%mod;
 44             }
 45             else if(!plug1&&plug2)
 46             {
 47                 if(plug2==1)
 48                 {
 49                     if(room[x+1][y])Set(State,y,1),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 50                     if(room[x][y+1])Set(State,y,0),Set(State,y+1,2),f[now][State]=(f[now][State]+Val)%mod;
 51                 }
 52                 else 
 53                 {
 54                     Set(State,y,0),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 55                     if(x==last_x&&y==last_y&&!State)ans=(ans+Val)%mod;
 56                     if(room[x+1][y])Set(State,y,2),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 57                 }
 58             }
 59             else if(plug1&&!plug2)
 60             {
 61                 if(plug1==1)
 62                 {
 63                     if(room[x][y+1])Set(State,y,0),Set(State,y+1,1),f[now][State]=(f[now][State]+Val)%mod;
 64                     if(room[x+1][y])Set(State,y,2),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 65                 }
 66                 else 
 67                 {
 68                     Set(State,y,0),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 69                     if(x==last_x&&y==last_y&&!State)ans=(ans+Val)%mod;
 70                     if(room[x][y+1])Set(State,y,0),Set(State,y+1,2),f[now][State]=(f[now][State]+Val)%mod;
 71                 }
 72             }
 73             else if(plug1==1&&plug2==1)
 74             {
 75                 Set(State,y,0),Set(State,y+1,0),f[now][State]=(f[now][State]+Val)%mod;
 76                 if(x==last_x&&y==last_y&&!State)ans=(ans+Val)%mod;
 77             }
 78         }
 79         else if(!plug1&&!plug2)f[now][State]=(f[now][State]+Val)%mod;
 80     }
 81 }
 82 int main()
 83 {
 84     scanf("%d%d",&n,&m);
 85     for(register int i=1;i<=n;i++)scanf("%s",c[i]+1);
 86     for(register int i=1;i<=n;i++)
 87         for(register int j=1;j<=m;j++)room[i][j]=(c[i][j]=='*')?0:1;
 88     if(m>n)
 89     {
 90         for(register int i=1;i<=n;i++)
 91             for(register int j=i+1;j<=m;j++)
 92                 swap(room[i][j],room[j][i]);
 93         swap(n,m);
 94     }
 95     for(register int i=1;i<=n;i++)
 96         for(register int j=1;j<=m;j++)
 97             if(room[i][j])last_x=i,last_y=j;
 98     f[0].Initialize();f[0][0]=1;
 99     for(register int i=1;i<=n;i++)
100     {
101         for(register int j=1;j<=m;j++)Execution(i,j);
102         if(i!=n)
103             for(register int j=1,last=(i*m)&1,tot=f[last].size;j<=tot;j++)
104                 f[last].key[j]<<=2;
105     }
106     printf("%d\n",ans);
107 }

 面對一道以前沒有見過的問題,咱們經過尋找插頭的新定義成功解決了該題。

相信你已經對插頭定義有了初步的瞭解,接下來,必定要在作題時繼續培養這種能力,這樣才能百戰不殆。

下面,再給出幾道不是那麼簡單的題目,有興趣的讀者能夠試着作一下:

BZOJ1187.[HNOI2007]神奇遊樂園
BZOJ2595[Wc2008]遊覽計劃
POJ3133Manhattan Wiring
POJ1739 Tony's Tour

上面講解的4道題都是插頭DP中比較基礎的問題,但各自都體現出了不一樣的側重點。

插頭DP是一類很美妙的問題,當你看到本身辛苦想出、碼出的分類討論AC的時候,心中必定會有很大的成就感吧!

但願讀完這篇博文的你能有所收穫,對插頭DP有更深入的瞭解!

相關文章
相關標籤/搜索