【AtCoder Grand Contest 017 F】Zigzag

状压 DP。

题目大意:

有 $n$ 层,第 $i$ 层有 $i$ 个点,第 $i$($1\leq i\lt n$)层第 $j$ 个点和第 $i+1$ 层第 $j$ 个和第 $j+1$ 个点之间有边(分别为向左和向右)。

现在要从第 $1$ 层的 $1$ 号点出发,走 $m$ 条路径到第 $n$ 层,且必须保证第 $i$ 条的每部分都不在第 $i+1$ 条右边(可以重合)。

有一些形如 $(A,B,0/1)$ 的限制条件,表示第 $A$ 条路径在第 $A$ 层必须往左/右走。

求方案数。

题解:

设 $f_{i,j,S}$ 表示当前考虑到第 $i$ 条路径的第 $j$ 个位置,当前最左可以达到状态 $S$,当前的方案数。

枚举下一位,分类讨论:

  • 下一位选 $0$,必须保证 $S$ 的这一位为 $0$。直接转移,状态仍为 $S$。
  • 下一位选 $1$,且 $S$ 的这一位为 $1$。直接转移,状态仍为 $S$。
  • 下一位选 $1$,且 $S$ 的这一位为 $0$。那么新的状态 $T$ 为 $S$ 去掉 $i$ 之后最近的一个 $1$(如果存在)并加上 $i$。

时间复杂度 $O(2^nnm)$。

Code:

#include<cstdio>
#include<cstring>
const int md=1e9+7,N=20;
inline void upd(int&a){a+=a>>31&md;}
int C,n,m,k,_0[N],_1[N],f[N+1][1<<N];
inline bool check(int id,int S){return(S&_0[id])==0&&(S&_1[id])==_1[id];}
inline int get(int S,int i){
    int T=S&((1<<i)-1);
    if(!T)return(1<<i)|S;
    return(S^(1<<(31-__builtin_clz(T))))|(1<<i);
}
int main(){
    scanf("%d%d%d",&n,&m,&k),--n;
    C=1<<n;
    while(k--){
        int A,B,C;
        scanf("%d%d%d",&A,&B,&C);
        B=n-B;
        (C?_1:_0)[A-1]|=1<<B;
    }
    for(int i=0;i<C;++i)f[n][i]=check(0,i);
    for(int t=1;t<m;++t){
        for(int i=n-1;~i;--i)
        for(int S=0;S<C;++S)if(int d=f[i+1][S]){
            //0
            if(!(_1[t]>>i&1)&&!(S>>i&1))upd(f[i][S]+=d-md);
            //1
            if(!(_0[t]>>i&1)){
                if(S>>i&1)upd(f[i][S]+=d-md);else{
                    int T=get(S,i);
                    upd(f[i][T]+=d-md);
                }
            }
        }
        memcpy(f[n],*f,sizeof*f);
        for(int i=0;i<n;++i)memset(f[i],0,sizeof*f);
    }
    int ans=0;
    for(int i=0;i<C;++i)upd(ans+=f[n][i]-md);
    printf("%d\n",ans);
    return 0;
}