分析:
看一下括号里的算法,就感到深深的绝望。。。
首先我们要找到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;
之所以要判断一下,是因为一个集合我们只标记位数最大的那个就可以了
- 这一位填0,:
- 如果X的第i位是0
- 这一位直接填0就好了:
if (fa[i]==i) vis[i]=0;
- 如果这一位之前填的是1,那么直接break
- 这一位直接填0就好了:
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;
}