基於連通性狀態壓縮的動態規劃--【插頭DP】模板超級詳細解釋
阿新 • • 發佈:2019-02-14
斷斷續續卡了本公舉三天的插頭dp終於搞完了,貌似好多網友也都是學了好多天才搞懂的,特別用成就感,作為一個模板160+行的dp也是醉了
首先一定要看陳丹琪的論文!一個高中女孩能讓許多大老爺們為難好多天真是厲害,然後我結合孫大神的課件充分理解了模板上的每一句話,哈哈哈,勞資看懂了~~其實還應該說一嘴的是雜湊,本以為上學期期末學過一點,能看懂,結果還是又看了雜湊的課件才徹底領會。剛開始自己還在輪廓線的連通是什麼意思上卡了好久。具體的實現看程式碼,良心之作,每行都註釋啊用的是ural1519的題 這個題是一條迴路的方案數
/* 最小表示法 鄺斌模板 2016.1.10 有註釋版 */ #include<stdio.h> #include<iostream> #include<string.h> #include<algorithm> using namespace std; 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; long long state[STATE]; long long f[STATE]; void init() { size=0; memset(head,-1,sizeof(head));//用單獨連結串列法處理碰撞 } void push(long long st,long long ans)//key->value { int i; int h=st%HASH; for(i=head[h];i!=-1;i=next[i])//這裡要注意是next if(state[i]==st)//找到了此鍵值 { f[i]+=ans;//鍵值已存在,在這種狀態下只是把次數加進去就好啦 return; } state[size]=st; f[size]=ans; next[size]=head[h]; head[h]=size++; } }hm[2]; void decode(int *code,int m,long long st)//把某行上的輪廓資訊解成一個code陣列 { for(int i=m;i>=0;i--) { code[i]=st&7;//要是隻有2中狀態就&1唄 st>>=3; } } long long encode(int *code,int m)//最小表示法 m<=12顯然只有6個不同的連通分量 { int cnt=1; memset(ch,-1,sizeof(ch)); ch[0]=0; long long 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 8進製表示 st|=code[i];//<==>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)//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; for(int t=0;t<=M;t++)//所謂的O(n)複雜度 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))//寫的真墨跡 直接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;//只要是一個沒出現過的就好,因為代入函式不涉及它到底是幾 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; } } } } void solve() { int i,j,cur=0; long long 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);//當前這個進行設定。計算cur=0==>計算cur=1 else dpblock(i,j,cur); cur^=1;//cur變成了另一個數cur=1==>變成了cur=0 } for(i=0;i<hm[cur].size;i++)//現在的cur要是放在迴圈裡就是待計算的位置 ans+=hm[cur].f[i];//各種狀態的和就是總的可能的方案數 printf("%I64d\n",ans); } int main() { freopen("in.txt","r",stdin); freopen("out.txt","w",stdout); while(scanf("%d%d",&N,&M)!=EOF) { init(); if(ex==0)//沒有空的格子 { printf("0\n"); continue; } solve(); } return 0; }
在今天晚上之前興致勃勃的以為自己領會了全部意思,7點多的時候看程式碼,發現居然每遍歷一個位置就要init()一下,size也沒了,整個連結串列也沒了,這還的了?其實在這之前自己想到了這是利用滾動陣列的思想,但是突然糾結於每讀入一個就清空,那之前的資訊怎麼存?後來把這兩個數變化的次序寫了下來,發現是把某個數的值賦給另一個之後,它才被清零@。@
多麼有成就感的事!