[Tkey] 與非

HaneDaniko發表於2024-06-23

解法原理1

首先我們需要明白 \(\operatorname{nand}\) 的運算:

\[\operatorname{not}(a\operatorname{nand}b)=a\operatorname{and}b\tag{1} \]

這個很好理解,因為 \(\operatorname{nand}\) 就是這麼定義的(從中文名字可以看出來)。

\[(\operatorname{not}a)\operatorname{nand}(\operatorname{not}b)=a\operatorname{or}b\tag{2} \]

這個是因為下述式子:

\[\operatorname{not}((\operatorname{not}a)\operatorname{and}(\operatorname{not}b))=a\operatorname{or}b \]

最後:

\[a\operatorname{xor}b=(a\operatorname{and}(\operatorname{not}b))\operatorname{or}((\operatorname{not}a)\operatorname{and}b)\tag{3} \]

因為 \(\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\) 從右往左第一個不受到限制的集合,並從它的末尾開始統計答案(因為實際上去除最右方的首個受限集合後,剩餘的集合就不會再受到限制了,讀者不妨自己試舉幾例)。將限制全部都轉移到代表元素上,可以基本總結出下面的流程:

  1. 假設每個集合的代表元素都在集合的最左邊(這很好實現,你只需要在實現並查集的時候將靠右的元素合併到靠左的位置即可)。
  2. 從右至左依次遍歷右邊界 \(x\) 的每一位 \(x_{i}\)
  3. 假如 \(x_{i}=1\),並且此時代表元素未受到限制,且該集合尚未訪問過,則統計答案。統計後將該集合標記為已訪問。
  4. 假如 \(x_{i}=0\),將該集合標記為受限,若該集合已經訪問過,則退出迴圈,表示找到了此位置。
  5. 假如遇到代表元素受限的元素,說明這個受限的集合已經結束,也退出迴圈,表示找到了此位置。

現在我們透過此流程找到了第一個不受限制的數位 \(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);
}

相關文章