插头DP & 轮廓线DP

柒葉灬

2019-08-11 15:08:38

Personal

# 插头DP 和 轮廓线DP ------ [专题 A - Mondriaan's Dream ](https://cn.vjudge.net/contest/317037#problem)(轮廓线DP) **题意:** 给你一个$n\times m $ 的棋盘,用$1 \times 2$ 的块覆盖整个棋盘,问方案数?($n,m \in [1,11] $) **思路:** 数据范围很小,考虑用状压DP,单纯的状压DP的话转移起来有点困难,但若考虑单个位置的时候,情况变得简单起来: 1. 若该位置被覆盖,则不用管它 2. 若该位置没有被覆盖:(1)摆一个朝下的 (2)摆一个朝右的 普通的状压DP相当于还要枚举每个位置的状态,而轮廓线只要枚举一个就可以了。 具体细节看代码回忆。 ```cpp #include<cstdio> #include<cstring> #include<iostream> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int maxn=15,maxm=1.8e6; int n,m; bool mark[maxn][maxn]; long long dp[2][1<<11]; int num(int x){return 1<<x-1;} int getnum(int S,int x){return (S>>x-1)&1;} int main(){ while(scanf("%d%d",&n,&m),n&&m){ memset(mark,0,sizeof(mark)); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) mark[i][j]=1; memset(dp,0,sizeof(dp)); int cur=0; dp[0][0]=1; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cur^=1;memset(dp[cur],0,sizeof(dp[cur])); for(int S=0;S<1<<m;S++){ int now=getnum(S,j),right=getnum(S,j+1); long long val=dp[cur^1][S]; if(now){ dp[cur][S^num(j)]+=val; }else { if(mark[i+1][j]) dp[cur][S^num(j)]+=val; if(mark[i][j+1]&&!right) dp[cur][S^num(j+1)]+=val; } } } } cout<<dp[cur][0]<<endl; } return 0; } ``` ------- [专题 B - 方格取数(1) ](https://cn.vjudge.net/contest/317037#problem/B) **题意:** ~~中文题面,自己看题~~ **思路:** 取一个数的时候只需要关心两个事: (1)该位置能不能取 (2)这个位置取了会导致哪些位置的数不能取。 用 $1$ 表示这个位置可以取 $0$ 表示不能取,轮廓线DP即可。 ------ [专题 C - Pebbles ](https://cn.vjudge.net/contest/317037#problem/C) **题意:** 给一个 $n \times m$ 的矩阵,从中取出不相邻的若干个数,使得和最大。($n,m \in [3,15]$) (注意,这里相邻包括8个方向) **思路:** 跟上一题的方格取数差不多,多加一进制表示这个位置以及下方的位置也不能取即可。 ----- ## ------------- 分 ------------- 割 ------------- 线 ------------- ### 很简单介绍 解决有关**连通性状压问题**一般考虑到插头 DP, 有关插头DP的文章:[插头DP](https://www.cnblogs.com/LadyLex/p/7326874.html) 注意:一定要区分**最小表示法** 和 **括号配对法** 不然会很乱,还有对于配对指的是两个插头**当前在一个连通块内**。 写多了题目形成自己的板子就行了。 ------------------ [专题 D - Eat the Trees ](https://cn.vjudge.net/contest/317037#problem/D) **题意:** 给 $n\times m$ 的矩阵,矩阵上有一些障碍,找**任意个**回路覆盖全部非障碍格子,求方案数。($ n ,m \in [1,11]$) **思路:** 由于不需要形成唯一回路,所以可以只记录有没有插头,至于左插头还是右插头可以不用管。 ```cpp #include<bits/stdc++.h> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int maxn=15,maxm=1.8e6; int n,m; long long dp[maxn][1<<13]; bool mark[maxn][maxn]; int getnum(int S,int x){ return (S>>(x-1))&1; } int num(int x){ return 1<<x-1; } int main(){ for(int _,cas=(cin>>_,1);cas<=_;cas++){ scanf("%d%d",&n,&m); memset(mark,0,sizeof(mark)); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) scanf("%d",&mark[i][j]); memset(dp,0,sizeof(dp)); dp[0][0]=1; int cur=0; for(int i=1;i<=n;i++){ cur^=1;memset(dp[cur],0,sizeof(dp[cur])); for(int S=0;S<1<<m;S++) dp[cur][S<<1]=dp[cur^1][S]; for(int j=1;j<=m;j++){ cur^=1;memset(dp[cur],0,sizeof(dp[cur])); for(int S=0;S<1<<m+1;S++){ int left=getnum(S,j),up=getnum(S,j+1); long long val=dp[cur^1][S]; if(!mark[i][j]){ if(!left&&!up)dp[cur][S]+=val; }else if(!left&&!up){ if(mark[i+1][j]&&mark[i][j+1]) dp[cur][S^num(j)^num(j+1)]+=val; }else if(!left&&up){ if(mark[i+1][j]) dp[cur][S^num(j)^num(j+1)]+=val; if(mark[i][j+1]) dp[cur][S]+=val; }else if(left&&!up){ if(mark[i+1][j]); dp[cur][S]+=val; if(mark[i][j+1]) dp[cur][S^num(j)^num(j+1)]+=val; }else if(left&&up){ dp[cur][S^num(j)^num(j+1)]+=val; } } } } printf("Case %d: There are %lld ways to eat the trees.\n",cas,dp[cur][0]); } return 0; } ``` --------- [ 专题E - Formula 1 ](https://cn.vjudge.net/contest/317037#problem/E)(插头DP) **真·入门题** **题意:** 给 $n\times m$ 的矩阵,矩阵上有一些障碍,找**一个**回路覆盖全部非障碍格子,求方案数。($ n ,m \in [2,12]$) **思路:** 这题不能像上一题一样只记录有没有插头了,因为如果形成多个回路就完了,需要记录该插头是左插头还是右插头。 (左插头的意思其实就是一个联通路径的左边插头) 在处理每一个格子的时候,相邻的只有左边和上面, 所以记左边插头状态为 $left$ ,上边的插头状态为 $up$ $0$ 表示没有插头,$1$ 表示是个左插头,$2$ 表示是个右插头 形成回路的点必须是最后一个,否则就可能出现多个回路。 每一对左插头右插头配对了,就说明多了一个回路, 而这题只能有一个,所以在最后的时候配对就行了, **大力分类讨论:** 1. 如果这个点是障碍,那么必须$left==0\ and \ up==0$ 才能转移 2. 如果$ !left \ and \ !up$ 那么必须创造一个向下且向右的新插头(每个空格必须覆盖) 3. 如果$left \ and \ !up$,那么左插头要么延伸向下,要么向右,分两种情况讨论 4. 如果$!left \ and \ up$,同第 $3$ 种情况 5. 如果$left==1 \ and \ up==1$ 两个左插头碰在一起,那么连起来之后,对应的**两个右插头**则相应变为一对,其中**左边的变为左插头**。(模拟一下就想明白了) 6. 如果$left==2 \ and \ up==2 $ 两个右插头碰在一起,那么连起来之后,对应的**两个左插头**则相应变为一对,其中**右边的变为右插头**。(模拟一下就想明白了) 7. 如果$left==2 \ and \ up==1 $ 左边是右插头,右边是左插头,说明左右两个连通块连在了一起,模拟一下发现对应的两个插头都不需要改变。 8. 如果$left==1 \ and \ up==2 $ 左边左插头,右边右插头,说明要产生新的回路了,此时判断这个点是不是最后一个点,如果是则加入答案,否则就是不合法状态了。 ------- 看似转移情况很多,~~实际上很多时候在复制粘贴~~ 自己写的时候只要自己模拟一下就行了,想通了话理解就能写出来了。 ```cpp #include<bits/stdc++.h> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int maxn=15,maxm=1.8e6; bool cur1; int n,m,ex,ey; long long ans; char mp[maxn][maxn]; bool mark[maxn][maxn]; const int P=2333; struct DP{ int tot,head[P],st[maxm],nxt[maxm]; long long sum[maxm]; void clear(){ memset(head,0,sizeof(head));tot=0; } int size(){return tot;} void insert(int S,long long val){ int x=S%P; for(int i=head[x];i;i=nxt[i]){ if(st[i]==S){ sum[i]+=val; return; } } st[++tot]=S;sum[tot]=val; nxt[tot]=head[x];head[x]=tot; } }dp[2]; int getnum(int S,int x){ return (S>>((x-1)*2))&3; } int num(int x,int p){ return p<<(x-1<<1); } int main(){ while(cin>>n>>m){ memset(mark,0,sizeof(mark)); for(int i=1;i<=n;i++){ scanf("%s",mp[i]+1); for(int j=1;j<=m;j++){ if(mp[i][j]=='.'){ ex=i;ey=j; mark[i][j]=1; } } } int cur=0; ans=0; dp[0].clear(); dp[0].insert(0,1); for(int i=1;i<=n;i++){ for(int j=1;j<=dp[cur].size();j++) dp[cur].st[j]<<=2; for(int j=1;j<=m;j++){ cur^=1;dp[cur].clear(); for(int k=1;k<=dp[cur^1].size();k++){ int S=dp[cur^1].st[k]; long long val=dp[cur^1].sum[k]; int left=getnum(S,j),up=getnum(S,j+1); if(!mark[i][j]){ if(!left&&!up) dp[cur].insert(S,val); }else if(!left&&!up){ if(mark[i+1][j]&&mark[i][j+1]) dp[cur].insert(S+num(j,1)+num(j+1,2),val); }else if(left&&!up){ if(mark[i+1][j]) dp[cur].insert(S,val); if(mark[i][j+1]) dp[cur].insert(S^num(j,left)^num(j+1,left),val); }else if(!left&&up){ if(mark[i][j+1]) dp[cur].insert(S,val); if(mark[i+1][j]) dp[cur].insert(S^num(j+1,up)^num(j,up),val); }else if(left==1&&up==1){ int cnt=1; for(int l=j+2;l<=m+1;l++){ if(getnum(S,l)==1)cnt++; else if(getnum(S,l)==2){ cnt--; if(cnt==0){ S-=num(l,1); break; } } } dp[cur].insert(S^num(j,left)^num(j+1,up),val); }else if(left==2&&up==1){ dp[cur].insert(S^num(j,left)^num(j+1,up),val); }else if(left==2&&up==2){ int cnt=1; for(int l=j-1;l>=1;l--) if(getnum(S,l)==1){ cnt--; if(cnt==0){ S+=num(l,1); break; } }else if(getnum(S,l)==2)cnt++; dp[cur].insert(S^num(j,left)^num(j+1,up),val); }else if(left==1&&up==2){ if(i==ex&&j==ey) ans+=val; } } } } cout<<ans<<endl; } return 0; } ``` -------- [专题F](https://cn.vjudge.net/contest/317037#problem/F) F题题意:一个N*M的图,其中X表示障碍,*表示空地,O表示必经点,求走一条回路经过所有必经点右多少种方案。 思路:对于空的格子可以有两种选择,加入插头或者不加入,终止状态则变成经过必经点后面的点。 ------ [专题G](https://cn.vjudge.net/contest/317037#problem/G) G题题意:一个n*m的房子,输入如样例,每个空格表示一个房间,数字表示把两个相邻的房间连起来所需的费用,#表示墙,求把所有房间连成一条回路的最小代价。 思路:在延伸出插头的时候加入权值防止重复或者漏加 ------ [专题H](https://cn.vjudge.net/contest/317037#problem/H) H题题意:一个n*m矩阵,求从左上角走到右下角(路径可以不经过所有点,但不能重复走一个点)的最小代价。 思路:(1)从外面加一条不存在的路线,使得形成回路 (2)引入独立插头进行DP 解法(1)更为方便 ------- [专题I](https://cn.vjudge.net/contest/317037#problem/I) I题题意:一个n*m的图,#表示障碍,求从左下角走到右下角,经过所有非障碍点的路径方案数。 思路:与上题相似 ------ [专题J](https://cn.vjudge.net/contest/317037#problem/J) J题题意:一个n*m的图,0表示空地,1表示障碍,2/3表示2/3这条线的一个端点(2/3只会出现两次),求把2个2和2个3连起来且不相交的最短长度-2的值(可以看看题面的图)。如果无解输出0 。 思路:这题看似需要简单路径,其实并不,因为求得是路径长度和最短的,所以就算出现了无用的回路也无所谓,肯定会被最优解覆盖,所以插头就变成了 属于2还是属于3 两种插头。 ------- [专题K](https://cn.vjudge.net/contest/317037#problem/K) K题题意:一个蜂巢,它有n行8列,编号见图,输入m个坐标表示障碍,求用多条回路铺满整个蜂巢有多少种方案。 思路:这题就是轮廓线多了几条边而已,分奇偶讨论不同轮廓线状态,代码有bug会极其难调 ------- [专题L](https://cn.vjudge.net/contest/317037#problem/L) L题题意:一个n*m的图求从左上角走到左下角,经过所有点,有多少种方案,如果没有输出Impossible (2 <= N <= 7, 1 <= M <=10^9) 思路:m有点大,考虑矩阵快速幂,矩阵快速幂的时候不能形成回路,要特殊处理最后一排,因为 n<=7 爆搜出来的状态数不超过130,所以复杂度有保证。 -------- [专题M](https://cn.vjudge.net/contest/317037#problem/M)(独立插头的应用) M题题意:一个n*m的矩阵,0表示障碍,其他数表示这个点的权值,求从任意起点出发,在任意终点结束(路径上的点不能重复走)的路径中,权值最大是多少。 思路:因为是一条路径,而且起点终点任意,所以需要引入独立插头,在每一个状态都不能同时存在大于2的独立插头,有的话不转移就行了。 独立插头的转移可以看插头DP开头放出来的博客, 或者说看代码(5K)理解。 ```cpp #include<bits/stdc++.h> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int maxn=20; int n,m; int ans,mark[maxn][maxn]; const int P=23333,maxm=1e6; struct DP{ int tot,head[P],st[maxm],nxt[maxm]; int mx[maxm]; void insert(int S,int val){ int x=S%P; for(int i=head[x];i;i=nxt[i]){ if(st[i]==S){ mx[i]=max(mx[i],val); return; } } st[++tot]=S;mx[tot]=val; nxt[tot]=head[x];head[x]=tot; } void clear(){ memset(head,0,sizeof(head)); tot=0; } int size(){ return tot; } }dp[2]; int getnum(int S,int x){ return (S>>(x-1<<1))&3; } int num(int x,int p){ return p<<(x-1<<1); } bool check(int S){ int cnt=0; while(S){ if((S&3)==3)cnt++; S>>=2; } return cnt<3; } int main(){ for(int _=(cin>>_,_);_--;){ scanf("%d%d",&n,&m); ans=0; memset(mark,0,sizeof(mark)); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ scanf("%d",&mark[i][j]); ans=max(ans,mark[i][j]); } int cur=0; dp[0].clear(); dp[0].insert(0,0); for(int i=1;i<=n;i++){ for(int k=1;k<=dp[cur].size();k++) dp[cur].st[k]<<=2; for(int j=1;j<=m;j++){ cur^=1;dp[cur].clear(); for(int k=1;k<=dp[cur^1].size();k++){ int S=dp[cur^1].st[k]; if(!check(S))continue; int val=dp[cur^1].mx[k]; int left=getnum(S,j); int up=getnum(S,j+1); if(!mark[i][j]){ if(!left&&!up)dp[cur].insert(S,val); continue; } val+=mark[i][j]; if(!left&&!up){ dp[cur].insert(S,val-mark[i][j]); if(mark[i+1][j]&&mark[i][j+1]) dp[cur].insert(S^num(j,1)^num(j+1,2),val); if(mark[i+1][j]) dp[cur].insert(S^num(j,3),val); if(mark[i][j+1]) dp[cur].insert(S^num(j+1,3),val); }else if(!left&&up){ if(mark[i+1][j]){ dp[cur].insert(S^num(j,up)^num(j+1,up),val); } if(mark[i][j+1]) dp[cur].insert(S,val); if(up==1){ int cnt=1; for(int l=j+2;;l++){ if(getnum(S,l)==1)cnt++; else if(getnum(S,l)==2){ cnt--; if(cnt==0){ S+=num(l,1); break; } } } dp[cur].insert(S^num(j+1,up),val); }else if(up==2){ int cnt=1; for(int l=j-1;;l--){ if(getnum(S,l)==2)cnt++; else if(getnum(S,l)==1){ cnt--; if(cnt==0){ S+=num(l,2); break; } } } dp[cur].insert(S^num(j+1,up),val); }else { S^=num(j+1,3); if(S==0)ans=max(ans,val); } }else if(left&&!up){ if(mark[i+1][j]) dp[cur].insert(S,val); if(mark[i][j+1]) dp[cur].insert(S^num(j,left)^num(j+1,left),val); if(left==1){ int cnt=1; for(int l=j+2;;l++){ if(getnum(S,l)==1)cnt++; else if(getnum(S,l)==2){ cnt--; if(cnt==0){ S+=num(l,1); break; } } } dp[cur].insert(S^num(j,left),val); }else if(left==2){ int cnt=1; for(int l=j-1;;l--){ if(getnum(S,l)==2)cnt++; else if(getnum(S,l)==1){ cnt--; if(cnt==0){ S+=num(l,2); break; } } } dp[cur].insert(S^num(j,left),val); }else { S^=num(j,3); if(S==0)ans=max(ans,val); } }else if(left==1&&up==1){ int cnt=1; for(int l=j+2;;l++){ if(getnum(S,l)==1)cnt++; else if(getnum(S,l)==2){ cnt--; if(cnt==0){ S-=num(l,1); break; } } } dp[cur].insert(S^num(j,left)^num(j+1,up),val); }else if(left==2&&up==2){ int cnt=1; for(int l=j-1;;l--){ if(getnum(S,l)==2)cnt++; else if(getnum(S,l)==1){ cnt--; if(cnt==0){ S+=num(l,1); break; } } } dp[cur].insert(S^num(j,left)^num(j+1,up),val); }else if(left==2&&up==1){ dp[cur].insert(S^num(j,left)^num(j+1,up),val); }else if(left==1&&up==3||left==3&&up==1){ int cnt=1; for(int l=j+2;;l++){ if(getnum(S,l)==1)cnt++; else if(getnum(S,l)==2){ cnt--; if(cnt==0){ S+=num(l,1); break; } } } dp[cur].insert(S^num(j,left)^num(j+1,up),val); }else if(left==2&&up==3||left==3&&up==2){ int cnt=1; for(int l=j-1;;l--){ if(getnum(S,l)==2)cnt++; else if(getnum(S,l)==1){ cnt--; if(cnt==0){ S+=num(l,2); break; } } } dp[cur].insert(S^num(j,left)^num(j+1,up),val); }else if(left==3&&up==3){ S^=num(j,left)^num(j+1,up); if(S==0)ans=max(ans,val); } } } } cout<<ans<<endl; } return 0; } ``` -------- [专题N](https://cn.vjudge.net/contest/317037#problem/N) N题题意:一个n*m的矩阵,*表示障碍,每个点都必须在一个环上,且环之间不能嵌套(大环套小环),求恰好形成K个环的方案数。 思路:大环不能套小环是个很麻烦的事情,如何处理这个是本题的关键,有一个神奇的做法就是形成回路的时候左右配对的括号必须是偶数,一方面可以保证可以有方案使得没有环嵌套自己,也能让这个环没有嵌套别的环。 --------- 当初为什么要学插头DP呢,就是为了解决这个题目↓ **「CTSC2010」产品销售** $\ \ \ \ \ \ \ \ \ \ $ ← 写个专题就是为了能A了这题! ↑ ↑ 对就是他,这题就算知道是插头DP也不一定搞得出来 ↑ 是一个好题 ~~(毒瘤)~~ 题目在203上的#262 ----------------- 上面说过,一般的插头DP解决的是**连通性状压问题** 基础的就是回路了,拓展一下就是路径。 而这题不仅是路径还带有方向,所以需要改变一下插头的定义, 不能再是简单的左插头右插头了, 对于带有方向的路径问题: 1. 设插头0为没有插头 2. 设插头1为**向起点方向走的** 3. 设插头2为**向终点方向走的** 这样子可以保证既不产生回路(显然), 又能保证一个状态的表示 _(那之前有一个用独立插头的那题?能不能这么写呢?待补)_ 这题需要知道上一次填的数是什么,下一步是变大还是变小, 前者压8位,后者压2位,用10位二进制表示一个状态。 --------------- 这一压之后还有一个问题就是, **怎么获知哪些数被取过了?** 先给出结论,对于一个状态,唯一对应一种取法, 具体证明,详见枚举结果:) _↓ 模拟成果_ 这里设3个插头分别为ABC, 默认若在同种插头状态下 权值A<权值B<权值C 一、有3个插头的情况 1. ABC都向小:不存在(只有一个起点) 2. AB向小,C向大:权值较大B肯定是从终点来的,A与C构成联通。——被取的区间为 $ [B,n*m] $ 和 $ [A,C] $。为什么C不与较大的B联通不用说了吧? 3. A向小,BC向大:权值较小B肯定是从起点来的,A与C构成联通。——被取的区间为 $[1,B]$和$[A,C]$。 3. ABC都向大:不存在(只有一个终点) 二、有2个插头的情况 1. AB都向小:不存在(只有一个起点) 2. A向小,B向大:被取的区间为$[A,B]$。 3. AB都向大:不存在(只有一个终点) 三、有1个插头的情况 1. A向小:被取的区间为$[A,n*m]$ 2. A向大:被取的区间为$[1,A]$ 四、有0个插头的情况 1. 初始状态和最终状态 ---------------- **如果觉得上述方法过于麻烦,可以用bitset** 接着就是常规插头DP的分类讨论, 这里省略了01值相等是否的判断 设左边的权值为$leftnum$,状态为$leftst$ 设上边的权值为$upnum$,状态为$upst$ 一、$leftst==0\ \ \&\&\ \ upst==0 $ 1. 插入$[2,n*m-1]$的值, 2. 插入$n*m$ 3. 插入$1$,一定要注意起点在边界上 二、$leftst≠0\ \ \&\&\ \ upst==0 $ 1. 插头延伸向下。 2. 插头延伸向右。 3. 注意延伸到头的情况,尤其是到了起点的情况。 三、$leftst==0\ \ \&\&\ \ upst≠0 $ 1. 插头延伸向下。 2. 插头延伸向右。 3. 注意延伸到头的情况,尤其是到了起点的情况。 四、$leftst≠0\ \ \&\&\ \ upst≠0 $ 1. 如果$leftst==upst$ :不合法 2. 如果插头对应的后一个权值不一样:不合法 3. 合法就将两个插头接在一起。 (对于是否取过的判断在check的结构体内,对于插头的连接在main函数里面) #### 最后贴上代码 ```cpp #include<bits/stdc++.h> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int maxn=55,P=11192869; int n,m; int A[4][maxn],B[maxn*3]; char str[maxn*3]; const int P2=233341,maxm=2e5; struct DP{ int tot,head[P2],nxt[maxm],sum[maxm]; long long st[maxm]; int size(){ return tot; } void clear(){ memset(head,0,sizeof(head)); tot=0; } void insert(long long S,int val){ int x=S%P2; for(int i=head[x];i;i=nxt[i]){ if(st[i]==S){ sum[i]=(sum[i]+val)%P; return; } } st[++tot]=S;sum[tot]=val; nxt[tot]=head[x];head[x]=tot; } }dp[2]; int get(long long S,int x){ return (S>>(x-1)*10)&1023; } int getst(long long S,int x){//1表示向小 2表示向大 return get(S,x)&3; } int getnum(long long S,int x){ return get(S,x)>>2; } long long num(int val,int st,int p){ return 1LL*(val<<2|st)<<(p-1)*10; } struct Check{ int id; struct node{ int op,x; bool operator <(const node &_)const{ return op!=_.op?op<_.op:x<_.x; } }A[4]; void add(int S){ if(!S)return; A[++id]=(node){S&3,S>>2}; } void init(long long S){ id=0; while(S){ add(S&1023); S>>=10; } sort(A+1,A+1+id); } bool check(int x){ if(id==3){ if(A[2].op==1){ if(x>=A[2].x)return false; if(x>=A[1].x&&x<=A[3].x)return false; }else { if(x<=A[2].x)return false; if(x>=A[1].x&&x<=A[3].x)return false; } }if(id==2){ if(x>=A[1].x&&x<=A[2].x)return false; }else if(id==1){ if(A[1].op==1){ if(x>=A[1].x)return false; }else { if(x<=A[1].x)return false; } } return true; } }T; int main(){ // freopen("trip.in","r",stdin); // freopen("trip.out","w",stdout); cin>>n>>m; for(int i=1;i<=n;i++){ scanf("%s",str+1); for(int j=1;j<=m;j++) A[i][j]=(str[j]=='1'); } scanf("%s",str+1); for(int i=1;i<=n*m;i++) B[i]=(str[i]=='1'); int cur=0; dp[0].insert(0,1); for(int j=1;j<=m;j++){ for(int k=1;k<=dp[cur].size();k++) dp[cur].st[k]<<=10; for(int i=1;i<=n;i++){ cur^=1;dp[cur].clear(); for(int k=1;k<=dp[cur^1].size();k++){ long long S=dp[cur^1].st[k]; int val=dp[cur^1].sum[k]; T.init(S); int leftst=getst(S,i),leftnum=getnum(S,i); int upst=getst(S,i+1),upnum=getnum(S,i+1); if(!leftst&&!upst){ if(B[n*m]==A[i][j]&&T.check(n*m)){ if(i<n)dp[cur].insert(S^num(n*m,1,i+1),val); if(j<m)dp[cur].insert(S^num(n*m,1,i),val); } if(B[1]==A[i][j]&&T.check(1)&&(i==1||i==n||j==1||j==m)){ if(i<n)dp[cur].insert(S^num(1,2,i+1),val); if(j<m)dp[cur].insert(S^num(1,2,i),val); } if(i<n&&j<m){ for(int l=2;l<n*m;l++){ if(B[l]==A[i][j]&&T.check(l)){ dp[cur].insert(S^num(l,1,i)^num(l,2,i+1),val); dp[cur].insert(S^num(l,2,i)^num(l,1,i+1),val); } } } }else if(!leftst&&upst){ int now=upnum+upst*2-3; if(A[i][j]!=B[now]||!T.check(now))continue; S^=num(upnum,upst,i+1); if(now==n*m){ dp[cur].insert(S,val); }else if(now==1){ if(i==1||j==1||i==n||j==m)dp[cur].insert(S,val); }else { if(i<n)dp[cur].insert(S^num(now,upst,i+1),val); if(j<m)dp[cur].insert(S^num(now,upst,i),val); } }else if(leftst&&!upst){ int now=leftnum+leftst*2-3; if(A[i][j]!=B[now]||!T.check(now))continue; S^=num(leftnum,leftst,i); if(now==n*m){ dp[cur].insert(S,val); }else if(now==1){ if(i==1||j==1||i==n||j==m)dp[cur].insert(S,val); }else { if(i<n)dp[cur].insert(S^num(now,leftst,i+1),val); if(j<m)dp[cur].insert(S^num(now,leftst,i),val); } }else if(leftst&&upst){ int now=leftnum+leftst*2-3; if(upnum+upst*2-3!=now||upst==leftst||!T.check(now))continue; if(A[i][j]!=B[now])continue; dp[cur].insert(S^num(upnum,upst,i+1)^num(leftnum,leftst,i),val); } } } } printf("%d\n",dp[cur].sum[1]); return 0; } ```