[BZOJ 2728][HNOI 2012]与非(并查集+计数问题)

142 篇文章 0 订阅
98 篇文章 0 订阅

题目链接

http://www.lydsy.com/JudgeOnline/problem.php?id=2728

思路

其实这个与非真的很神奇啊,它可以覆盖与、或、非、亦或四种运算,这个可以手玩出来(本渣太懒没试过。。。)。那么这样的话,不管 Ai 中某一位是0还是1,每个 Ai 选了多少次,最终的答案中的这一位既有可能是1,也有可能是0,但是某个数是否能被与非出来还是有限制的,观察到如果对于任意的 Ai,1<=Ai<=n 而言, Ai(2)[a]=Ai(2)[b](Aiab) ,那么最终无论如何与非,得到的答案的第 ab 位一定是相同的,这个也可以手玩出来(本渣太懒没试过=_=b)。
那么我们可以暴力枚举1到k上的两位 a,b,a<b (反正k很小, k2 复杂度几乎可以无视掉),通过并查集合并掉所有相同的列(为了便于叙述,以下均称相同的列组成的玩意是集合), belong[i]=i 所在的集合。同时可以求出 num= 集合的个数。
然后我们就需要求区间 [L,R] 上能被与非出来的数的个数了。很显然可以想到只需能求出区间 [1,x1] 的合法数个数就行了,写个函数 solve(x) 来求。

n[L,R]([L,R])=solve(R+1)solve(L)

那我们现在只需要关心 solve(x) 的过程了,比较显然的是由于最终可以与非出来的数的个数最多只能是 2num (每个集合可以选1或者0,有2种选法,一共 num 个集合)。而由于限制了二进制数的长度最大为 k ,因此x>=2k时,答案就永远是 2num 了,这个是很显然的,这里只需要在最开始加个特判就行。
那么现在的 x 就是合法的了,x1的位数一定是不超过 k 的,那么我们就要找所有的能与非出来的数a,1<=a<x。这是个非常基础的数位DP,我们维护一个数字 tmp= 在还没有被枚举到的位(区间 [1,i] )中尚未确定下来的集合个数。从 x 的高位往低位扫,对于每一位,进行下列操作:

  1. 如果x的当前位所属集合是0还是1尚未确定
    (1). x 是1

    (2).x是0
    标记 x 的当前位所属集合确定是0

  2. 否则x的当前位所属集合是0还是1已经确定了
    (1). x 的当前位是1,但是之前确定了它所在的集合都是0,直接退出
    (2).x的当前位是0,但是之前确定了它所在的集合都是1,直接退出

    代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <algorithm>
    
    #define MAXN 1010
    #define MAXK 70
    
    using namespace std;
    
    typedef long long int LL;
    
    LL a[MAXN]; //保存A1...An
    LL L,R;
    int n,k,num=0; //num=集合总数
    
    bool isSame(int x,int y) //判断n个Ai中,对于每个Ai,它的第x位和第y位是否都相同
    {
        for(int i=1;i<=n;i++)
            if(((a[i]>>x)^(a[i]>>y))&1)
                return false;
        return true;
    }
    
    int belong[MAXK]; //belong[i]=与列i相同的列j,j<i,j=i表示列i是独立不受影响的
    int mark[MAXK]; //mark[i]=-1表示尚未确定集合i是什么数字,0表示确定集合i中的每一位都是0,1表示确定每一位都是1
    
    LL solve(LL x) //求区间[1,x-1]的能与非出来的数字个数
    {
        if(x>=1LL<<k) return 1LL<<num;//x的长度超出了k
        int tmp=num; //tmp=有多少组集合在1~i位中未确定的
        LL ans=0;
        memset(mark,-1,sizeof(mark));
        for(int i=k-1;i>=0;i--) //枚举x的第i位
        {
            if(mark[belong[i]]==-1) //第i位所属集合没定,第i位是几,第i位所属的集合也定为这个数字
            {
                tmp--;
                if((x>>i)&1) //x的第i位是1
                {
                    mark[belong[i]]=1;
                    ans+=1LL<<tmp;
                }
                else
                    mark[belong[i]]=0;
            }
            else
            {
                if((x>>i)&1)
                {
                    if(mark[belong[i]]==0)
                    {
                        ans+=1LL<<tmp;
                        break;//x的第i位是1,但是x所属集合已经被确定为了0,直接退出
                    }
                }
                else if(mark[belong[i]]==1) break; //x的第i位是0,但是x所属集合已经被确定为了1,直接退出
            }
        }
        return ans;
    }
    
    int main()
    {
        scanf("%d%d%lld%lld",&n,&k,&L,&R);
        for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
        for(int i=0;i<k;i++)
        {
            bool flag=false;
            for(int j=0;j<i;j++)
                if(isSame(i,j)) //n个A数字中i位和j位都是一样的
                {
                    belong[i]=j;
                    flag=true;
                    break;
                }
            if(!flag)
            {
                num++;
                belong[i]=i;
            }
        }
        printf("%lld\n",solve(R+1)-solve(L));
        return 0;
    }
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值