洛谷P2575 高手过招

题目

题意:

n*20的棋盘中有一些棋子,两人每次移动一个棋子,若该棋子右边有空位,就向右跳到第一个空格,若没有,则不能移动。若所有棋子都不能移动,就算输。问先手能不能胜

题解:

我们将一行棋盘看成一个二进制数,然后就显然易见的能递推出所有状态的必胜必败态
然后对于一个大棋盘,我们考虑将每一行的棋局存入sg数组,最后将每一行的sg值异或一下,为0则先手必败,否则必胜
刚开始我想出一种方法,时间复杂度是2^20*20,觉得太慢,但又想不出别的方法,结果就这么写了一个提交,竟然过了。
我们先考虑一行的棋盘。
假设以x为棋盘的状态,二进制中0为该位无棋子,1为有棋子。
首先,如果棋子都聚集在棋盘最右边,则任何一个棋子都不能动,所以sg[x]=0。
设x=(00…00111..11)2
其中有a个1,则x=2^0+2^1+…+2^(a-1)=2^a-1
这些状态我们可以预处理出。
然后,若x不是这个状态,则要删去右边的不可以移动的棋子。
若x=(100101011…111..11)2,前面部分是随意的,后面部分都是1,那最后的连续的1都不是可以移动的棋子
把x加1得到(100101011…1000..00)2,只要去掉最后一个1就可以了
所以x等价于(x+1)-lb(x+1),其中lb是求x最后一个1的状态的函数
最后:每次找最右边的没有计算过的一颗棋子,求出它的后继状态,这很容易。设t=x,每计算一次最右边的棋子,t中就减去这个棋子。设k=lb(t),从左到右枚举k,找到第一个空位并把原来的棋子放在这个位置上就行。

标程

#include<bits/stdc++.h>
using namespace std;
int sg[1048576],i,T,n,m,ans,s,x,a[21],j;
int lb(int x){
    return x&(-x);
}
int dfs(int x){
    if (sg[x]!=-1) return sg[x];
    int t=(x+1)-lb(x+1),k=lb(t),cnt=0,tmp;
    while (k){//边界条件是棋盘上还有可以移动且没被计算过的棋子
        t^=k;tmp=k;
        for (;(x^k)<x;k>>=1);//找没有棋子的位置
        a[cnt++]=dfs(x^k^tmp);//异或tmp是拿掉原来位置上的棋子相当于减,异或k是放到空位上,相当于加
        k=lb(t);
    }
    sort(a,a+cnt);
    if (a[0]) return sg[x]=0;
    for (int i=1;i<cnt;i++)
        if (a[i]-a[i-1]>1) return sg[x]=a[i-1]+1;
    return sg[x]=a[cnt-1]+1;
}
int main(){
    memset(sg,-1,sizeof(sg));
    for (i=0;i<=20;i++) sg[(1<<i)-1]=0;
    for (i=0;i<(1<<20);i++)
        if (sg[i]==-1) sg[i]=dfs(i);
    cin>>T;
    while (T--){
        scanf("%d",&n);
        ans=0;
        for (i=0;i<n;i++){
            scanf("%d",&m);
            s=0;
            for (j=0;j<m;j++) scanf("%d",&x),s|=(1<<20-x);
            ans^=sg[s];
        }
        printf("%s\n",ans?"YES":"NO");
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值