1. 程式人生 > 其它 >[整理]插頭 dp

[整理]插頭 dp

全名是基於連通性狀態壓縮的動態規劃問題,從這個名字可以看出它的特徵:是狀壓 dp(廢話)、維護的是連通性。
怎麼理解呢?接下來以洛谷模板題為例講解它的維護方法。
考慮如何維護一條輪廓線,我們需要引入插頭的概念:一個格子的插頭是指它可以連出去的某條邊(或者理解為讓輪廓線插♂進來的地方)。對於這題我們應從上到下從左到右遞推,就應該考慮上一行對下一行有什麼影響:需要記錄每一行的插頭情況和連通情況(不理解沒關係,下面有詳細圖解)。
具體來說,插頭情況就是每個格子有沒有插頭,連通情況是指每個插頭對應的連通輪廓線情況。例如:下圖中紅線處的插頭情況可以狀壓成 \(1111\),連通情況是 \((1,2)\)

連通、\((3,4)\) 連通。

我們很容易發現,插頭之間的連通情況就像括號匹配一樣不會出現交叉,由此產生了括號表示法:用左/右括號代表每個連通分量中從左邊/右邊插出來的插頭,空代表沒有插頭,這樣上圖的連通情況又可以表示為 \(\texttt{(())}\),然後我們發現其實插頭情況已經在連通情況中表示出來了,所以去掉這一維。
然後我們就可以大力分類討論當前格子的左/上的插頭情況了:

  1. 當前格子為障礙,當且僅當都沒有插頭時才能轉移;
  2. 當前格子無障礙且都沒有插頭時,當前格子的右/下插頭都應連線;
  3. 有一個方向有插頭時,可以選擇直走或拐彎;

  4. 都有插頭且括號型別相同時,遍歷找到對應括號轉移(以都為左括號為例);
  5. 都有插頭且左插頭為右括號、上插頭為左括號時,閉合插頭;
  6. 都有插頭且左插頭為左括號、上插頭為右括號時,當且僅當轉移到結尾才能閉合。

由於有三種標記,狀壓時應使用高於 \(2\) 的進位制,為了方便位運算這裡採用四進位制狀壓。
然而我們發現這裡的空間複雜度是 \(\mathcal O(nm4^m)\),於是滾動陣列。但是時空複雜度依然巨大多,我們發現有很多的狀態是沒用的,可以不用暴力列舉上一行轉移而是用 Hash 表之類的東西存下來可行狀態。
模板題核心常數巨大程式碼(第一次見優化比本體難寫)

const int N=100003;
int n,m,a[20][20],ex,ey,ans;
struct Edge {
  int to[2],nxt,wei[2];
}e[N];
int hd[N],cnt[2];
il void ade(int cur,int st,int val){
  int u=st%N;
  for(rg int i=hd[u];i;i=e[i].nxt){
    if(e[i].to[cur]==st)return (void)(e[i].wei[cur]+=val);
  }
  e[++cnt[cur]].to[cur]=st,e[cnt[cur]].wei[cur]=val;
  e[cnt[cur]].nxt=hd[u],hd[u]=cnt[cur];
  return;
}
#define D(st,k) (st>>(k<<1)&3)
#define G(val,k) (val<<(k<<1))
signed main(){
  Read(n),Read(m);
  for(rg int i=1;i<=n;i++){
    char ipt[20];scanf("%s",ipt+1);
    for(rg int j=1;j<=m;j++){
      if(ipt[j]=='.')a[i][j]=1,ex=i,ey=j;
    }
  }
  int cur=0,lst=1;ade(cur,0,1);
  for(rg int i=1;i<=n;i++){
    for(rg int j=1;j<=cnt[cur];j++)e[j].to[cur]<<=2;
    for(rg int j=1;j<=m;j++){
      lst=cur,cur^=1,cnt[cur]=0,memset(hd,0,sizeof(hd));
      for(rg int c=1;c<=cnt[lst];c++){
        int st=e[c].to[lst],v=e[c].wei[lst];
        int lt=D(st,j-1),up=D(st,j),now;
        if(!a[i][j]){//1
          if(!lt&&!up)ade(cur,st,v);
        }else if(!lt&&!up){//2
          now=st+G(1,j-1)+G(2,j);
          if(a[i+1][j]&&a[i][j+1])ade(cur,now,v);
        }else if(!(lt&&up)){//3
          if(lt){
            if(a[i][j+1])ade(cur,st-G(lt,j-1)+G(lt,j),v);
            if(a[i+1][j])ade(cur,st,v);
          }else {
            if(a[i+1][j])ade(cur,st+G(up,j-1)-G(up,j),v);
            if(a[i][j+1])ade(cur,st,v);
          }
        }else if(lt==up){//4
          now=st-G(lt,j-1)-G(up,j);
          if(lt==1){
            for(rg int k=j+1,tp=1;k<=m;k++){
              int u=D(st,k);
              if(u==1)tp++;
              else if(u==2)tp--;
              if(!tp){
                ade(cur,now-G(1,k),v);break;
              }
            }
          }else {
            for(rg int k=j-2,tp=1;k>0;k--){
              int u=D(st,k);
              if(u==2)tp++;
              else if(u==1)tp--;
              if(!tp){
                ade(cur,now+G(1,k),v);break;
              }
            }
          }
        }else if(lt==2&&up==1){//5
          ade(cur,st-G(lt,j-1)-G(up,j),v);
        }else if(i==ex&&j==ey)ans+=v;//6
      }
    }
  }
  cout<<ans<<endl;
  KafuuChino HotoKokoa
}