bzoj2728 [HNOI2012]与非(并查集+数位dp)

64 篇文章 0 订阅
11 篇文章 0 订阅

题目链接

分析:
看一下括号里的算法,就感到深深的绝望。。。

首先我们要找到NAND的一些性质:

!a=a NAND a
a&b=!(a NAND b)

有了&和!,其余的位运算就都可以表示了
也就是说:NAND可以代替所有的位运算

那么这样就能表示所有的数了吗?

不然

NAND还有一个重要的性质:

如果a[1]~a[n]所有数的第i位和第j位相同,那么nand出来的数第i位和第j位也相同

我们用并查集处理出有哪些位是一样的

现在问题变成了:
一个二进制数,有若干位必须相同,求[L,R]范围内能够组成的数的个数

可以用数位dp解决,计算小于等于x的可以组成的数的个数

从高位向低位dp,
如果x该位为1,我们有两种选择:

  • 不选这个1,那低位可以任意选,方案 +=2p + = 2 p ,不用往下做了(p为还未确定的集合数)
  • 选了这个1,低位还不能任意选,继续往下做

思路还是很好理解的,然而怎么用并查集???
好吧,也不是很严格的并查集
我们可以直接暴力的两两匹配

那么dp呢?
一开始我以为是记搜,然而递推即可:
边界是X

  • 如果X的第i位是1
    • 这一位填0,:if (vis[fa[i]]!=1) ans+=1LL<<p[i-1];
      注意一定要判断一下,因为我们填的是一个集合
      if (vis[fa[i]]==0) break;
      如果符合这条判断,说明一定是在之前已经填过这个集合了,而且填的是0,我们就没有必要继续计算了
    • 这一位填1:之前我们已经统计过这个集合填0的贡献了,所以直接在这一位标记上1
      if (fa[i]==i) vis[i]=1;
      之所以要判断一下,是因为一个集合我们只标记位数最大的那个就可以了
  • 如果X的第i位是0
    • 这一位直接填0就好了:if (fa[i]==i) vis[i]=0;
    • 如果这一位之前填的是1,那么直接break

tip

注意强制类型转换

WA了无数次,主要有一下几点:

  • 判断
if (++o>=(1LL<<K)) return (1LL<<p[K]); 
//一定要写成++o
  • L,R是ll类型的
#include<bits/stdc++.h>
#define ll long long

using namespace std;

const int N=1005;
int n,K,fa[N],vis[N],p[N];
ll L,R,a[N];

int find(int x) {
    if (fa[x]!=x) fa[x]=find(fa[x]);
    return fa[x];
}

int check(int i,int j) {
    for (int k=1;k<=n;k++) {
        bool f1=a[k]>>i&1;
        bool f2=a[k]>>j&1;
        if (!((f1&&f2)||(!f1&&!f2))) return 0;
    }
    return 1;
}

ll solve(ll o) 
{
    if (++o>=(1LL<<K)) return (1LL<<p[K]); 

    ll ans=0;
    memset(vis,-1,sizeof(vis));
    for (int i=K;i>0;i--) 
    {
        if ((o>>i-1)&1) 
        {                        //这一位是1,有两种选择 0/1 
            if (vis[fa[i]]!=1) ans+=1LL<<p[i-1];   //该集合选0,剩下的集合就可以随便选 
            if (fa[i]==i) vis[i]=1;                //该位选1,标记一下 
            if (vis[fa[i]]==0) break;              //该集合选择的是0,剩下的贡献已经统计过了 
        } 
        else 
        {
            if (fa[i]==i) vis[i]=0;
            if (vis[fa[i]]==1) break;
        }
    } 
    return ans;   
}

int main()
{
    scanf("%d%d%lld%lld",&n,&K,&L,&R); L--;
    for (int i=1;i<=n;i++) scanf("%lld",&a[i]);

    for (int i=1;i<=K;i++) {
        fa[i]=i;
        for (int j=i-1;j;j--) 
            if (check(i-1,j-1)&&find(i)!=find(j)) fa[fa[j]]=fa[i];
    }

    for (int i=1;i<=K;i++) {    //1~i位有多少个集合 
        p[i]=p[i-1];
        if (find(i)==i) p[i]++;
    }

    printf("%lld",solve(R)-solve(L));
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值