题解 P7740 [NOI2021] 机器人游戏

· · 题解

更新了一下,那天晚上因为太赶,DP 的那部分好像不是很能让人理解,然后出现了一些明显的错误,引起了一些不必要的疑惑,在这里说句抱歉

首先声明,这是一个来自场外选手的题解,然后其目的是为了能让大部分人看懂,因为这题考察的完全是 noip 范围内的东西,所以个人认为很有价值,值得好好说一下

当然这也不算自己的题解,其实是读懂了某个场内选手的代码之后写出来的东西

首先来看最基础的容斥该怎么做

我们先预处理出来每个机器人向右移动 i 步后会对那个位置造成什么改变,把这个改变记作 v_i

v_i=0,表示当前位置不用变化

v_i=1,表示当前位置需要从 0 变为 1 或者从 1 变为 0

v_i=2,表示当前位置为 0

v_i=3,表示当前位置为 1

然后这段预处理大概长这样

v[0]=0;
for(int i=0 ; i<len ; ++i)
    if(s[i]=='R') v[++p]=0;
    else if(s[i]=='0') v[p]=2;
    else if(s[i]=='1') v[p]=3;
    else v[p]^=1;

然后维护一个 mask_i,表示 v=i 的位置的集合,可以用一个 long long 类型的变量存下来

然后对于每个位置 i,维护一个 msk_j,表示从这个点出发时每种 v_j 形成的集合

然后由于有在这个点右侧的位置,也有这个点根本到达不了的位置,这些位置我们就给它赋成 0

然后我们就可以求出所有 2^n 种选法每种 v 所对应的位置集合

for(int i=0 ; i<4 ; ++i) mask[i]=0;
for(int i=0 ; i<p ; ++i) mask[v[i]]|=1ull<<i;
for(int S=0 ; S<(1<<n) ; ++S)
    for(int i=0 ; i<4 ; ++i)
        msk[S][i]=0;
for(int i=0 ; i<n ; ++i) {
    for(int j=0 ; j<4 ; ++j) msk[1<<i][j]=mask[j]<<(l-i-1);
    msk[1<<i][0]|=(1ull<<(n-1-i))-1,msk[1<<i][0]|=(1ull<<n)-(1ull<<(n-1-i+p));
}
for(int S=0 ; S<(1<<n) ; ++S)
    for(int j=0 ; j<4 ; ++j)
        msk[S][j]=msk[S^(S&-S)][j]|msk[S&-S][j];

然后我们就需要求出每种状态在当前这一个机器人时的方案数

注意:这里的 i 指的不是从左往右数的第 i 个,而是从右往左数第 i 个,至于为什么,待会儿会说

不难发现,如果 msk_{S,0}msk_{S,1} 有交集,那么它们交集这些位置必须为空,因为不可能存在一个数既是 0 又是 1msk_{S,2}msk_{S,3} 同理。记这个为 emp

然后如果某个位置既被 msk_{S,0} 或者 msk_{S,1} 包含,又被 msk_{S,2} 或者 msk_{S,3} 包含,那么这个位置只能选 0 或者只能选 1,记这个为 rct

然后 rctemp 取个并集得到被限制了的位置

这样在 emp 中的位置只能有 1 种选法,就是空格子,在 rct 中而不在 emp 中的位置则有 2 种选法,剩下的位置则有 3 种选法,那么用 2 的次幂乘上一个 3 的次幂就是这个状态在当前这行的方案数了,把每种状态在每一行的方案乘起来就是总方案数,然后按照题面上的东西做一个容斥就行了

for(int S=0 ; S<(1<<l) ; ++S) {
    emp[S]=(msk[S][0]&msk[S][1])|(msk[S][2]&msk[S][3]);
    rct[S]=(msk[S][0]|msk[S][1])&(msk[S][2]|msk[S][3]);
    rct[S]|=emp[S];
    int c1=get(emp[S]),c2=get(rct[S]);
    nw[S]=1ll*pw3[n-c2]*pw2[c2-c1]%P,f0[S]=1ll*f0[S]*nw[S]%P;
}

然后这就是一个 m2^n 的做法,n \le 16 的情况已经完全能过了。

然后我们先观察特殊性质 A,B,发现这两个性质是一个常见的状压DP,直接把后面 p 个位置是否被选择为可行的位置压成新的一维。

当然这不是重点

大型比赛的部分分大部分都会告诉你正解的方向,这道题也不例外,说明正解需要用到状压 DP

观察数据范围,n\le32,这个数是 32 而不是 70,100 之类常见 n^k 算法的标准范围,我们可以考虑折半,把左一半先弄出来再来想右半部分该怎么弄。

此时左半部分变成了 n\le16,可以直接使用刚才提到的容斥的方法计算,但计算出来的所有方案必定至少包含左半部分的一个位置,那么我们接下来需要计算不包含左半部分位置的方案数

此时先维护两个新的数组 f1_{i,S},f2_{i,S}

令左半部分最右侧的位置为 pos

$f2_{i,S}$ 左半部分从右向左第 $i$ 个位置在它后面 $i$(包含它)个位置被选择的状态为 $S$ 时的总方案数 然后这个 $S$ 会影响到 $pos$ 位置可选的方案数,所以要更新一下 现在你应该知道刚才提到的 $i$ 是从右往左而不是从左往右是为什么了吧 这一部分更新大概如下 ```cpp for(int i=p ; i<=h2 ; ++i) for(int S=0 ; S<(1<<i) ; ++S) f2[i][S]=1ll*f2[i][S]*nw[S]%P; for(int S=0 ; S<(1<<h1) ; ++S) emp[S]|=msk[S][1], rct[S]|=msk[S][1]|msk[S][2]|msk[S][3], nw[S]=3-(rct[S]>>(h1-1)&1)-(emp[S]>>(h1-1)&1); for(int i=p ; i<=h2 ; ++i) for(int S=0 ; S<(1<<i) ; ++S) f1[i][S]=1ll*f1[i][S]*nw[S]%P; ``` 自己可以理解一下再进入下一步 然后就进行我们的状压 DP 令 $p$ 为当前要求的那个 $i$,即整个序列从右往左第 $i$ 个位置,我们要求这个位置后面的位置都不被选而它被选的方案数;这个 $i$ 实际上可以用上方左半部分那个 $i$ 替换一下,所以看到下面的 $f1_{p,S}$ 不要吃惊 首先我们先算出初始的方案数,即 $pos$(注意是 $pos$ 不是 $p$)强制被选的方案数 这可以用一个容斥完成 ```cpp for(int S=0 ; S<(1<<p) ; ++S) if(S&1) if(cnt[S]&1) Explus(res,f2[p][S]); else Exminus(res,f2[p][S]); ``` 然后根据对于每个 $i\in[0,n-2p),[n-2p,n-p),[n-p,n)$ 分类讨论一下 (这个 $i$ 和之前的 $i$ 不是同一个含义) 大体上说 DP 方程应该长这样(用了一个滚动数组) ```cpp g[t^1][S]=1ll*exminus(g[t][S>>1],g[t][S>>1|(1<<(p-1))])*f1[p][S]%P; ``` 由于 $i\in[0,n-2p)$ 枚举时任何状态都不包括 $p$,可能会使 $p$ 这个位置没被选到,那么就需要减掉一些方案,用多个 $f1_{p,0}$ 的乘积实现,因为你不能选上 $pos$,你得把含有 $pos$ 的减掉 ```cpp for(int i=0 ; i<=n-2*p ; ret=1ll*ret*f1[p][0]%P,++i,t^=1) for(int S=0 ; S<(1<<p) ; ++S) g[t^1][S]=1ll*exminus(g[t][S>>1],g[t][S>>1|(1<<(p-1))])*f1[p][S]%P; Exminus(g[t][0],ret); ``` 对于 $i\in[n-p,n)$,由于 $S$ 表示的是后面 $p$ 个数的状态,所以除非 $i=n-p$,否则当前位置不能被选 所以又是这样 ```cpp for(int i=n-p ; i<n ; ++i,t^=1) for(int S=0 ; S<(1<<p) ; ++S) if((S&1)==(i==n-p)) g[t^1][S]=1ll*exminus(g[t][S>>1],g[t][S>>1|(1<<(p-1))])*f1[p][S]%P; else g[t^1][S]=0; ``` 结尾又是一个容斥得出最终方案数 ```cpp for(int S=0 ; S<(1<<p) ; ++S) if(cnt[S]&1) Explus(res,g[t][S]); else Exminus(res,g[t][S]); ``` 终于完了!!! 希望能帮助大家qwq,上一次写得太糊了 放下代码 ```cpp #include<stdio.h> #include<string.h> #include<algorithm> using namespace std; typedef unsigned long long ull; const int P=1e9+7,N=35,B=65536,M=1005,L=105; int h1,h2; int n,m,mr,nr[M],ln[M],cnt[B]; int v[L]; char s[L]; int pw2[N],pw3[N]; ull mask[4],msk[B][4];//0:不变;1:变;2:为 0;3:为 1 ull emp[B],rct[B];//emp:必须为空的位置;rct:有限制的位置 int nw[B],f0[B];//nw:当前行状态为 S 的方案数;f0:所有行状态为 S 的方案数 int f1[N][B],f2[N][B]; //f1[i][S]:左半部分从右向左第 i个位置在它后面 i(包含它)个位置被选择的状态为 S时左半部分最右侧的点能够在操作后保持不变的方案数 //f2[i][S]:左半部分从右向左第 i个位置在它后面 i(包含它)个位置被选择的状态为 S时的总方案数 int g[2][B];//开个滚动数组,以免爆炸 inline int exminus(const int &a,const int &b) {int x=a-b;return x<0?x+P:x;} inline int explus(const int &a,const int &b) {int x=a+b;return x<P?x:x-P;} inline void Exminus(int &a,const int &b) {if((a-=b)<0) a+=P;} inline void Explus(int &a,const int &b) {if((a+=b)>=P) a-=P;} inline int mul2(int a) {Explus(a,a);return a;} inline int mul3(int a) {Explus(a,mul2(a));return a;} inline int get(ull S) {return cnt[S>>h1]+cnt[S&((1<<h1)-1)];} inline int calc(int p) { int res=0; for(int S=0 ; S<(1<<p) ; ++S) if(S&1) if(cnt[S]&1) Explus(res,f2[p][S]); else Exminus(res,f2[p][S]); for(int S=0 ; S<(1<<p) ; ++S) g[0][S]=0; int t=0,ret=1;g[0][0]=1; for(int i=0 ; i<=n-2*p ; ret=1ll*ret*f1[p][0]%P,++i,t^=1) for(int S=0 ; S<(1<<p) ; ++S) g[t^1][S]=1ll*exminus(g[t][S>>1],g[t][S>>1|(1<<(p-1))])*f1[p][S]%P; Exminus(g[t][0],ret); for(int i=n-2*p+1 ; i<n-p ; ++i,t^=1) for(int S=0 ; S<(1<<p) ; ++S) g[t^1][S]=1ll*exminus(g[t][S>>1],g[t][S>>1|(1<<(p-1))])*f1[p][S]%P; for(int i=n-p ; i<n ; ++i,t^=1) for(int S=0 ; S<(1<<p) ; ++S) if((S&1)==(i==n-p)) g[t^1][S]=1ll*exminus(g[t][S>>1],g[t][S>>1|(1<<(p-1))])*f1[p][S]%P; else g[t^1][S]=0; for(int S=0 ; S<(1<<p) ; ++S) if(cnt[S]&1) Explus(res,g[t][S]); else Exminus(res,g[t][S]); return res; } int main() { freopen("robot.in","r",stdin); freopen("robot.out","w",stdout); scanf("%d%d",&n,&m); h1=(n+1)>>1,h2=n-h1; for(int S=0 ; S<(1<<h1) ; ++S) cnt[S]=cnt[S>>1]+(S&1); for(int S=0 ; S<(1<<h1) ; ++S) f0[S]=1; for(int i=1 ; i<=h2 ; ++i) for(int S=0 ; S<(1<<i) ; ++S) f1[i][S]=f2[i][S]=1; pw2[0]=pw3[0]=1; for(int i=1 ; i<=n ; ++i) pw2[i]=mul2(pw2[i-1]),pw3[i]=mul3(pw3[i-1]); for(int o=0 ; o<m ; ++o) { scanf("%s",s); int len=strlen(s),p=0; v[0]=0; for(int i=0 ; i<len ; ++i) if(s[i]=='R') v[++p]=0; else if(s[i]=='0') v[p]=2; else if(s[i]=='1') v[p]=3; else v[p]^=1; if((++p)>n) continue ; for(int i=0 ; i<4 ; ++i) mask[i]=0; for(int i=0 ; i<p ; ++i) mask[v[i]]|=1ull<<i; int l=min(h1,n-p+1); for(int S=0 ; S<(1<<l) ; ++S) for(int i=0 ; i<4 ; ++i) msk[S][i]=0; for(int i=0 ; i<l ; ++i) { for(int j=0 ; j<4 ; ++j) msk[1<<i][j]=mask[j]<<(l-1-i); msk[1<<i][0]|=(1ull<<(l-1-i))-1; msk[1<<i][0]|=(1ull<<n)-(1ull<<(l-1-i+p)); } for(int S=0 ; S<(1<<l) ; ++S) for(int j=0 ; j<4 ; ++j) msk[S][j]=msk[S^(S&-S)][j]|msk[S&-S][j]; for(int S=0 ; S<(1<<l) ; ++S) { emp[S]=(msk[S][0]&msk[S][1])|(msk[S][2]&msk[S][3]); rct[S]=(msk[S][0]|msk[S][1])&(msk[S][2]|msk[S][3]); rct[S]|=emp[S]; int c1=get(emp[S]),c2=get(rct[S]); nw[S]=1ll*pw3[n-c2]*pw2[c2-c1]%P,f0[S]=1ll*f0[S]*nw[S]%P; } if(p>h2) continue ; for(int i=p ; i<=h2 ; ++i) for(int S=0 ; S<(1<<i) ; ++S) f2[i][S]=1ll*f2[i][S]*nw[S]%P; for(int S=0 ; S<(1<<h2) ; ++S) { emp[S]|=msk[S][1]; rct[S]|=msk[S][1]|msk[S][2]|msk[S][3]; nw[S]=3-(rct[S]>>(h1-1)&1)-(emp[S]>>(h1-1)&1); } for(int i=p ; i<=h2 ; ++i) for(int S=0 ; S<(1<<i) ; ++S) f1[i][S]=1ll*f1[i][S]*nw[S]%P; } int res=0; for(int S=1 ; S<(1<<h1) ; ++S) if(cnt[S]&1) Explus(res,f0[S]); else Exminus(res,f0[S]); for(int i=1 ; i<=h2 ; ++i) Explus(res,calc(i)); printf("%d",res); return 0; } ```