插頭DP_最小表示法 模板詳解
宣告
演算法說明
如果還不知道插頭dp中插頭以及輪廓線等概念是什麼東西的話,請移步:插頭與輪廓線與基於連通性狀態壓縮的動態規劃
那麼這裡就簡單說一下狀態的轉移:
對於m列的格子,用一個m+1的陣列code來表示輪廓線上的資訊(包括連通性)。
對於當前的格子(i,j),code[j-1]中是它左側格子的插頭資訊,code[j]中時它上方格子的插頭資訊。
我們根據這兩個值處理(i,j)的所有可能的狀態,然後將 code[j-1]設為(i,j)下方格子的插頭資訊,code[j]設為(i,j)右側插頭資訊。
當j已經是最後一列時,我們將code陣列的所有元素向右平移,將第一個元素code[0]設為0。
狀態數量:
我們根絕左上角的插頭狀態來求解右下的插頭狀態!(這裡的左上或者右下指的都是輪廓線轉角那個地方上面或者下面)
首先是左上角的插頭狀態:
1)向右插頭、向下插頭
2)只有向右插頭
3)最優向下插頭
4)沒有插頭
他們分別對應的右下插頭狀態:
1)必須包含上插頭和左插頭
2)①向右的插頭 ② 向下的插頭
3)①向下的插頭 ② 向右的插頭
4)必須包含下插頭和右插頭
最後是最小表示法中聯通分量的重新表示,對應如下:
1)因為沒有下一個和右邊的插頭了
① :當這個點是最終的點的時候,我們只需要將他們下側和右側的資訊狀態更新為0
② :不是最終的點時,我麼需要將兩個聯通分量連線為同一個,將後面的合併為前一個聯通分量中,然後下側以及右側資訊狀態更新為0
2 3)這兩個狀態的更新有些許的類似:
因為他們都會向右側或者下側更新,那麼我們就只需要將右側或者下側的連通分量資訊更新為上一個狀態的就好,另一個更新為0(沒有插頭,也就沒有連通性)
4)我們要重新開一個連通分量來進行計算,這裡就用一個其他連通分量到達不了的數進行操作,在Encode函式中,他會被ch陣列重新編碼的
#include<stdio.h> #include<iostream> #include<string.h> #include<algorithm> #define LL long long using namespace std; /** 對於m列的格子,用一個m+1的陣列code來表示輪廓線上的資訊(包括連通性)。 對於當前的格子(i,j),code[j-1]中是它左側格子的插頭資訊,code[j]中時它上方格子的插頭資訊。 我們根據這兩個值處理(i,j)的所有可能的狀態,然後將 code[j-1]設為(i,j)下方格子的插頭資訊,code[j]設為(i,j)右側插頭資訊。 當j已經是最後一列時,我們將code陣列的所有元素向右平移,將第一個元素code[0]設為0。 **/ const int MAXD=15; const int HASH=30007;//一個比實際容量稍大的素數 const int STATE=1000010;//雜湊表的最大元素個數 using namespace std; int N,M; int maze[MAXD][MAXD]; int code[MAXD];//表示這一行輪廓線上插頭的資訊 int ch[MAXD];//最小表示法使用 int ex,ey;//最後一個非障礙格子的座標 struct HASHMAP { int head[HASH],next[STATE],size; LL state[STATE];//和size相關聯的狀態 LL f[STATE];//某種狀態出現的次數 void init() { size=0; memset(head,-1,sizeof(head));//用單獨連結串列法處理碰撞 } void push(LL st,LL ans) { int i; int h=st%HASH; for(i=head[h];i!=-1;i=next[i]) if(state[i]==st)//找到了此鍵值 { f[i]+=ans;//鍵值已存在,在這種狀態下只是把次數加進去就好,否則新增狀態 return; } state[size]=st;//然後更新狀態,將st狀態加到相應的每一個數組 f[size]=ans; next[size]=head[h]; head[h]=size++; } }hm[2]; void decode(int *code,int m,LL st)//把某行上的輪廓資訊解成一個code陣列 { for(int i=m;i>=0;i--) { code[i]=st&7;//只去最後三位,也就是每一個佔有三位,總共36位置,最大2^36 st>>=3; } } LL encode(int *code,int m)//最小表示法 m<=12顯然只有6個不同的連通分量 { //將陣列code狀態壓縮為st,轉換為八進位制的壓縮,因為最多隻有6個不同的狀態 //ch陣列就是將code陣列減小,變成有序的,這裡就可以很好的將下面的13解釋好 int cnt=1; memset(ch,-1,sizeof(ch)); ch[0]=0; LL st=0; for(int i=0;i<=m;i++) { if(ch[code[i]]==-1)ch[code[i]]=cnt++;//這裡是給每一個狀態編號,如果存在一個新的話,那麼就新給一個號碼,也就是連通的判斷! code[i]=ch[code[i]]; st<<=3;//0~7 很重要因為最多有6個狀態,那麼用二進位制表示的話,也就是佔用三個位置 st|=code[i]; } return st;//返回最終輪廓上的連通分量資訊 } void shift(int *code,int m)//當到最後一列的時候,相當於需要把code中所有元素向右移一位 { for(int i=m;i>0;i--)code[i]=code[i-1]; code[0]=0; } void dpblank(int i,int j,int cur)//i,j表示當前位置、cur是當前狀態,操作之後就是cur^1啦 總共就三大種情況 { int k,left,up; for(k=0;k<hm[cur].size;k++) { decode(code,M,hm[cur].state[k]); left=code[j-1];//左邊的狀態 up=code[j];//上面的狀態 if(left&&up)//狀態都存在 { if(left==up)//在同一連通分量的情況下,只有最後一個節點才可以 { if(i==ex&&j==ey) { code[j-1]=code[j]=0;//最終合併成一個迴路 if(j==M)shift(code,M); hm[cur^1].push(encode(code,M),hm[cur].f[k]); } } else//不在同一個連通分量則合併成同一個 {//只有一種情況,就是必選左上角的插頭 code[j-1]=code[j]=0;//那麼下一個格子中的left以及up都會為0,因為在這裡要連成同一個連通分量 for(int t=0;t<=M;t++) if(code[t]==up) //將和上在同一個連通分量中的連線到向左的連用分量中 code[t]=left; if(j==M)shift(code,M); hm[cur^1].push(encode(code,M),hm[cur].f[k]); } } else if((left&&(!up))||((!left)&&up))//只有一個有插頭資訊右下沒有插頭則連出來一個 {//對於當前格子(i,j)code[j-1]是它左側的格子插頭資訊,code[j]是它右邊的格子插頭資訊 //處理後:code[j-1]是(i,j)下方格子插頭資訊,code[j]是~右邊格子插頭資訊 int t; if(left)t=left; else t=up; if(maze[i][j+1])//右邊沒有障礙 {//包含兩種情況:有向右的插頭 -> 指向右邊 、 有向下的插頭 -> 指向右邊 code[j-1]=0; code[j]=t; hm[cur^1].push(encode(code,M),hm[cur].f[k]); } if(maze[i+1][j])//下邊沒有障礙 {//有向右的插頭 -> 組向下邊 、 有向下的插頭 -> 指向下邊 code[j-1]=t; code[j]=0; if(j==M)shift(code,M); hm[cur^1].push(encode(code,M),hm[cur].f[k]); } } else//無插頭 , 則構造新的連通塊,因為所有都要被包含 { if(maze[i][j+1]&&maze[i+1][j]) { code[j-1]=code[j]=13; //那麼這個點有必須有連通,那麼我們就必須在這裡新增一個右下的插頭! //但是這裡我們必須找一個別的連通分量到達不了的陣列,方式和其他連通分量形成同一個連通分量, //那麼我們的Encode函式不在乎這裡的數字是多少,其中的ch數字會將code變為有序的陣列! hm[cur^1].push(encode(code,M),hm[cur].f[k]); } } } } void dpblock(int i,int j,int cur)//一個障礙是不可能有向下和向右的插頭的,那就設其為0 { int k; for(k=0;k<hm[cur].size;k++) { decode(code,M,hm[cur].state[k]);//解碼 code[j-1]=code[j]=0; if(j==M)shift(code,M);//換行 hm[cur^1].push(encode(code,M),hm[cur].f[k]);//畢竟是向後走了一格 //把當前的資料cur=0壓到另一個位置cur=1==>把當前的資料cur=1壓到另一個位置cur=0 } } char str[MAXD]; void init() { memset(maze,0,sizeof(maze)); //首先將所有的標記為有障礙,然後下面更新沒有障礙的情況 ex=0; for(int i=1;i<=N;i++) { scanf("%s",str); for(int j=0;j<M;j++) { if(str[j]=='.') { ex=i; ey=j+1;//記錄最後一個位置 maze[i][j+1]=1;//沒有障礙的標記為1 } } } } void solve() { int i,j,cur=0; LL ans=0; hm[cur].init();//cur=0 hm[cur].push(0,1);//加入沒插頭的狀態cur=0 for(i=1;i<=N;i++) for(j=1;j<=M;j++) { hm[cur^1].init();//每到一個位置,把另一組清零 清空cur=1==>清空cur=0 if(maze[i][j])dpblank(i,j,cur);//當前這個進行設定 else dpblock(i,j,cur); cur^=1; } for(i=0;i<hm[cur].size;i++)//現在的cur要是放在迴圈裡就是待計算的位置 ans+=hm[cur].f[i];//各種狀態的和就是總的可能的方案數 printf("%I64d\n",ans); } int main() { while(scanf("%d%d",&N,&M)!=EOF) { init(); if(ex==0)//沒有空的格子 { printf("0\n"); continue; } solve(); } return 0; }
後記
這個東西連連續續弄了三天才把這個模板看明白,當然這三天中兩天在五一放假,昨天整整玩了一天,罪過啊、罪過,理解這個模板一定要把每種插頭下狀態的轉移以及連通分量的轉移畫一遍,這樣就很容易理解,希望可以幫助到你,上面的解釋部分可能說的不是很明白,我沒有再作圖解釋,希望大家諒解,如果有錯誤,歡迎大家在下方評論,謝謝!