解法原理1
首先我們需要明白 \(\operatorname{nand}\) 的運算:
這個很好理解,因為 \(\operatorname{nand}\) 就是這麼定義的(從中文名字可以看出來)。
這個是因為下述式子:
最後:
因為 \(\operatorname{and},\operatorname{or}\) 我們都表示出來了,因此我們還可以用 \(\operatorname{nand}\) 來表示 \(\operatorname{xor}\)。
所以可以發現 \(\operatorname{nand}\) 實際上包含了全部我們需要的位運算。
解法原理2
擁有了全部的位運算,那麼現在我們是不是就能得到所有數字了呢?顯然不是,考慮一下的例子:
10101
00000
可以發現我們無論如何操作,都不能使第 \(2\) 位和第 \(4\) 位變成不同的數字,這就是我們這道題唯一的限制:假如在每個數 \(n\) 中都有數位 \(i,j\) 是相同的,那麼無論如何選擇,在結果中一定有 \(i=j\)。
回到剛才的例子,可以發現第 \(1,3,5\) 位也無法在結果中變成不同的數字,發現其實不一定要全部相等,而是每個數中的各位相等即可,因此歸納出本題的限制條件:
若在全部的 \(n\) 個數中都有第 \(i\) 位與第 \(j\) 位相等,那麼在結果中也會如此。
因此,本題首先需要我們求解出具有限制條件的數位,我們可以使用並查集來維護。
程式碼步驟1
因為本題 \(n,k\) 較小,可以考慮暴力列舉進行合併。
我們每次列舉兩個位置 \(i,j\) 判斷這兩個位置是否對全部的 \(n\) 個數都滿足上述條件,如果是,那麼我們就合併這兩個數。最後合併到一起的幾個數就是符合條件,並且範圍最大的結果。
請注意此處並查集合並操作的合併順序,具體用處請見解法原理3的流程第一條。
//主函式部分
for(int i=1;i<=k;++i){
fa[i]=i;//初始化
}
for(int i=1;i<=k;++i){
for(int j=i-1;j>0;--j){
//列舉全部可能的 i,j
if(check(i,j)) connect(i,j);
}
}
//並查集板子與 check()
// ---DSU Part---
int fa[61];
int find(int id){
if(fa[id]==id) return id;
fa[id]=find(fa[id]);
return fa[id];
}
void connect(int x,int y){
if(find(x)!=find(y)){
fa[fa[y]]=fa[x];
}
}
// --------------
// Check
bool check(int x,int y){
//檢查 x,y 是否符合條件
for(int i=1;i<=n;++i){
if(((a[i]>>x-1)^(a[i]>>y-1))&1)
//上面這句的意思是:判斷 a[i] 的第 x,y 位是否相等
return false;
}
return true;
}
解法原理3
求出全部“相互獨立的數”之後,我們現在需要求解答案了。
首先考慮統計出當前並查集中的集合個數 \(a\),這 \(a\) 個集合相互獨立,互不影響,並且每個集合都可以獨立地拼湊出 \(1\) 或 \(0\)(剛才的解法原理1證明了,這樣的拼湊總是可能的)。因此方案數應該為 \(2^{a}\)。
現在我們來考慮邊界問題。我們可以很容易地將邊界 \([l,r]\) 轉化成求 \([1,r]-[1,l-1]\),這樣我們就只需要處理右邊界了。下面我們可以把問題轉化成一個數位 DP。
假如右邊界 \(x\) 的第 \(i\) 位數為 \(0\),說明這一位可能受到了限制(相當於數位 DP 裡的 \(limit\)),例如 x=010010
,其右數第 \(3\) 位為 \(0\),這說明在前幾位填 010
的時候,這一位實際上只能填 \(0\) 了,因為如果填 \(1\) 就超出右邊界範圍了。
同時,因為同一個集合內的元素一定會相等,因此假如集合內任何一個元素受到了限制,那麼整個集合都會因此受到限制。基於這個想法,我們不妨來維護一個陣列 \(limit_{i}\),來判斷 \(i\) 是否受到了這樣的限制。
我們需要尋找出 \(x\) 從右往左第一個不受到限制的集合,並從它的末尾開始統計答案(因為實際上去除最右方的首個受限集合後,剩餘的集合就不會再受到限制了,讀者不妨自己試舉幾例)。將限制全部都轉移到代表元素上,可以基本總結出下面的流程:
- 假設每個集合的代表元素都在集合的最左邊(這很好實現,你只需要在實現並查集的時候將靠右的元素合併到靠左的位置即可)。
- 從右至左依次遍歷右邊界 \(x\) 的每一位 \(x_{i}\)。
- 假如 \(x_{i}=1\),並且此時代表元素未受到限制,且該集合尚未訪問過,則統計答案。統計後將該集合標記為已訪問。
- 假如 \(x_{i}=0\),將該集合標記為受限,若該集合已經訪問過,則退出迴圈,表示找到了此位置。
- 假如遇到代表元素受限的元素,說明這個受限的集合已經結束,也退出迴圈,表示找到了此位置。
現在我們透過此流程找到了第一個不受限制的數位 \(p\),並且 \(p\) 以前的集合有 \(s_{p}\) 個,那麼總方案數就為 \(2^{s_{p}}\)。
根據上式可以看出,為了計算這個答案,實際上我們還需維護一個集合數量的字首陣列。
程式碼步驟2
首先我們考慮到,假如右邊界 \(x\ge 2^{k}-1\),那麼它一定能夠包含全部的 \(2^{a}\) 種情況,這是最簡單的。
剩下的步驟即為解法原理3所述。
int ask(int x){
if(++x>=(1ll<<k)){//特殊情況
return (1ll<<s[k]);
}
int ans=0;
memset(limit,-1,sizeof(limit));
//注意清空陣列
for(int i=k;i>0;i--){
if(x&(1ll<<i-1)){
if(limit[fa[i]]!=1){
ans+=1ll<<s[i-1];
//統計答案
}
if(fa[i]==i){
limit[i]=1;
//標記訪問
}
if(limit[fa[i]]==0){
//步驟5
break;
}
}
else{
//步驟4
if(fa[i]==i){
limit[i]=0;
}
if(limit[fa[i]]==1){
break;
}
}
}
return ans;
}
//主函式的預處理與統計答案部分
for(int i=1;i<=k;i++){
s[i]=s[i-1];
if(find(i)==i){
//預處理集合數量字首和
s[i]++;
}
}
cout<<ask(r)-ask(l-1);
程式碼實現
AC Record
#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,k,l,r;
int a[1001],s[61],limit[61];
int fa[61];
int find(int id){
if(fa[id]==id) return id;
fa[id]=find(fa[id]);
return fa[id];
}
void connect(int x,int y){
if(find(x)!=find(y)){
fa[fa[y]]=fa[x];
}
}
bool check(int x,int y){
for(int i=1;i<=n;++i){
if(((a[i]>>x-1)^(a[i]>>y-1))&1) return false;
}
return true;
}
int ask(int x){
if(++x>=(1ll<<k)){
return (1ll<<s[k]);
}
int ans=0;
memset(limit,-1,sizeof(limit));
for(int i=k;i>0;i--){
if(x&(1ll<<i-1)){
if(limit[fa[i]]!=1){
ans+=1ll<<s[i-1];
}
if(fa[i]==i){
limit[i]=1;
}
if(limit[fa[i]]==0){
break;
}
}
else{
if(fa[i]==i){
limit[i]=0;
}
if(limit[fa[i]]==1){
break;
}
}
}
return ans;
}
signed main(){
cin>>n>>k>>l>>r;
for(int i=1;i<=n;++i){
cin>>a[i];
}
for(int i=1;i<=k;++i){
fa[i]=i;
}
for(int i=1;i<=k;++i){
for(int j=i-1;j>0;--j){
if(check(i,j)) connect(i,j);
}
}
for(int i=1;i<=k;i++){
s[i]=s[i-1];
if(find(i)==i){
s[i]++;
}
}
cout<<ask(r)-ask(l-1);
}