2019牛客暑期多校訓練營(第一場)----H-XOR

首先發出題目鏈接:
鏈接:https://ac.nowcoder.com/acm/contest/881/H
來源:牛客網
涉及:線性基

點擊這裏回到2019牛客暑期多校訓練營解題—目錄貼


題目如下:
在這裏插入圖片描述
在這裏插入圖片描述
開頭放出異或的性質:

在二進制中:
11=0            10=11\oplus1=0 \;\;\;\;\;\;1\oplus0=1
00=0            01=10\oplus0=0 \;\;\;\;\;\;0\oplus1=1

運算法則:
aa=0a\oplus a=0
ab=baa\oplus b=b\oplus a
abc=a(bc)=(ab)ca\oplus b\oplus c=a\oplus(b\oplus c)=(a\oplus b)\oplus c
d=abcd=a\oplus b\oplus c 可以推出 a=dbca=d\oplus b\oplus c
aba=ba\oplus b\oplus a=b

aa二進制爲10011001bb二進制爲10101010,則aba\oplus b二進制爲00110011


首先可以將問題轉化一下,題目中要求的不是滿足條件子集的數量之和,而是大小之和.

所以我們可以探求序列中每一個數aia_i序列中其他數組成滿足條件子集(條件是異或和爲0)的數量x,我們稱作這個值x爲這個數aia_i對於答案的貢獻

算出序列中每一個數對答案的貢獻,然後求和,即是答案。

啥意思捏,舉個例子
n=3,a={1,2,3}
序列中1的貢獻爲1,因爲1只能和2,3的異或和爲0。
序列中2的貢獻爲1,因爲2只能和1,3的異或和爲0。
序列中3的貢獻爲1,因爲3只能和2,1的異或和爲0。
所以答案是1+1+1=3


我們知道,一個序列的任意一個數都可以由這個序列線性基中的幾個數異或得到。不知道線性基的建議看看其他博客

所以開始我們可以求出原序列的線性基base[]在非線性基內選取任意個元素的異或和值,在線性基內也能選取一些數異或和,使得兩個異或和的值相同。

而兩個相同數異或後爲0。

假如線性基內有原序列r個數,所以非線性基元素有n-r個。非線性基內任意一個元素能和其他n-r-1個元素排列組合產生的子集組成新集合(這個集合的異或和的值,也能在線性基內找到一些數異或值,使得兩個值相等,兩個值再異或就等於0),所以非線性基內的n-r的元素,每個元素對答案的貢獻都是(nr)2nr1(n-r)*2^{n-r-1}

		for(i=1;i<=n;i++){
			scanf("%lld",&num[i]);//首先輸入一個數
			if(!insert(num[i],base))	rem[++flag]=num[i];//if裏面判斷這個數能不能插入線性基
			//如果不能插入,說明這個數是線性基外的數,就把這個數放到rem數組裏,rem數存非線性基數
			//flag是非線性基數的數量
			else index.push_back(i);//如果能插入,就用一個vector存這個數的下標,後面有用。
		}
		if(flag==0){//如果所有數都能插入線性基,線性基內的元素不可能存在異或和爲0的子集,所以答案爲0
			printf("0\n");
			continue;
		}	
		ans=(ans+(ll)flag*qpow(2,flag-1)%mod)%mod;//每一個非線性基元素對答案的貢獻


現在再來看線性基內的元素,每一個對答案的貢獻。

也就是說,對於線性基內每一個元素,能從序列除這個元素以外的序列中找到多少個子集,使得這個子集內的元素與這個元素的異或和是0。

於是我們可以這樣:
遍歷線性基內的元素base[i](由於範圍是101810^{18},所以線性基內大概有60個元素),然後對序列另外n-1個元素做一個線性基basedr[]。再來查看這一個元素base[i]能不能插入這個線性基basedr[]內。
 
 1.如果base[i]不能插入這個線性基basedr[]說明這個線性基也是整個序列線性基
(因爲這個線性基不僅能異或出序列另外n-1個元素,還能異或出base[i]
由於一個序列的所有線性基內元素個數相同(參考線性代數中的極大線性無關組),所以這個線性基內也是r個元素,那麼除這r個元素以外的n-r元素(這n-r個元素包括base[i]),每一個元素的貢獻也是2nr12^{n-r-1},所以base[i]的貢獻是2nr12^{n-r-1}
 
 2.如果base[i]能插入這個線性基basedr[],那說明除base[i]以外的n-1個元素不管怎麼異或,都不能異或出base[i],所以不存在一個包含base[i]的序列,使得序列異或和爲0,於是base[i]的貢獻爲0。

注意,線性基內的元素可能不是原序列的元素,所以遍歷線性基內的元素不能直接遍歷線性基數組,而用剛剛那個存下標的vector數組。

這樣,線性基內每一個元素的貢獻也算出來了。


優化:
在遍歷線性基內的元素,需要每次都要把其他n-1個元素的線性基求出來。時間花費比較大(但可能不會超時),於是可以這樣做:

先把非線性基序列求一遍線性基,假設求得線性基數組爲baser[],每次遍歷base[](原序列線性基)內的元素,就在循環內重新創建一個數組basedr[],把baser[]內的元素全部賦值給basedr[],然後再把base[]內除base[i]的元素全部嘗試插入basedr[]內。basedr[]數組就是除base[i]外n-1的元素的線性基了。

這樣每次遍歷線性基內的元素,求另外n-1個元素的線性基就只要循環120次左右。

for(i=1;i<=flag;i++)	insert(rem[i],baser);	//先把非線性基內的元素的線性基求出來,baser數組
for(i=0;i<index.size();i++){//遍歷線性基內的元素不能直接遍歷線性基數組(base數組),而要遍歷存下標的vector
	ll basedr[62],x=num[index[i]];//創建新的basedr數組,x是遍歷線性基(base數組)內的當前遍歷到的元素
	for(j=0;j<=60;j++)	basedr[j]=baser[j];//先把baser拷貝一份給basedr
	for(j=0;j<index.size();j++)//嘗試把base數組內(線性基)除x以外的值嘗試插入basedr數組(仍然要遍歷存下標的vector,而不能直接遍歷base數組)
		if(j!=i)	insert(num[index[j]],basedr);
	if(check(x,basedr))	ans=(ans+qpow(2,flag-1))%mod;//再來看看x能不能插入另外n-1個值的線性基內
}

舉個例子
n=6,a={1,2,3,4,7,8}

求得原序列線性基base爲{1,2,4,8},下標數組index爲{1,2,4,6},非線性基的數有{3,7}
非線性基每一個數的貢獻爲21=22^1=2,所以非線性基數的貢獻爲4

對於線性基內元素x=a[index[1]]=1,其他n-1個數的線性基爲{1,2,4,8},x能不能插進去,所以x貢獻爲21=22^1=2
對於線性基內元素x=a[index[2]]=2,其他n-1個數的線性基爲{1,3,4,8},x能不能插進去,所以x貢獻爲21=22^1=2
對於線性基內元素x=a[index[3]]=4,其他n-1個數的線性基爲{1,2,7,8},x能不能插進去,所以x貢獻爲21=22^1=2
對於線性基內元素x=a[index[4]]=8,其他n-1個數的線性基爲{1,2,4,0},x能插進去,所以x貢獻爲00

所以答案是4+2+2+2=10


代碼如下:

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
typedef long long ll;
const int mod=1e9+7;
ll base[62],rem[100005],baser[62],num[100005];
//base是原序列的線性基,rem是原序列非線性基元素,baser是非線性基元素得線性基,num是原序列
int n;
ll qpow(ll x,ll pow){//快速冪
	ll sum=1;
	while(pow){
		if(pow%2==1)	sum=sum*x%mod;
		pow>>=1;
		x=x*x%mod;
	}
	return sum;
}
bool insert(ll t,ll *base){//講一個數插入線性基
	int i;
	for(i=60;i>=0;i--)
		if(t&(1ll<<i)){
			if(!base[i]){
				base[i]=t;
				return true;
			}
			else t^=base[i];	
		}
	return false;
}
bool check(ll t,ll *base){//判斷一個數能不能插入線性基
	int i;
	for(i=60;i>=0;i--)
		if(t&(1ll<<i))	t^=base[i];
	return t==0;
}
int main(){
	while(~scanf("%d",&n)){
		int i,j,flag=0;//flag是非線性基內元素的數量
		ll ans=0;//答案
		vector<ll> index;//存base內(線性基)元素的下標
		memset(base,0,sizeof(base));
		memset(rem,0,sizeof(rem));
		memset(baser,0,sizeof(baser));
		for(i=1;i<=n;i++){
			scanf("%lld",&num[i]);//首先輸入一個數
			if(!insert(num[i],base))	rem[++flag]=num[i];//if裏面判斷這個數能不能插入線性基
			//如果不能插入,說明這個數是線性基外的數,就把這個數放到rem數組裏,rem數存非線性基數
			//flag是非線性基數的數量
			else index.push_back(i);//如果能插入,就用一個vector存這個數的下標,後面有用。
		}
		if(flag==0){//如果所有數都能插入線性基,線性基內的元素不可能存在異或和爲0的子集,所以答案爲0
			printf("0\n");
			continue;
		}	
		ans=(ans+(ll)flag*qpow(2,flag-1)%mod)%mod;//每一個非線性基元素對答案的貢獻
		for(i=1;i<=flag;i++)	insert(rem[i],baser);//先把非線性基內的元素的線性基求出來,baser數組
		for(i=0;i<index.size();i++){//遍歷線性基內的元素不能直接遍歷線性基數組(base數組),而要遍歷存下標的vector
			ll basedr[62],x=num[index[i]];//創建新的basedr數組,x是遍歷線性基(base數組)內的當前遍歷到的元素
			for(j=0;j<=60;j++)	basedr[j]=baser[j];//先把baser拷貝一份給basedr
			for(j=0;j<index.size();j++)//嘗試把base數組內(線性基)除x以外的值嘗試插入basedr數組(仍然要遍歷存下標的vector,而不能直接遍歷base數組)
				if(j!=i)	insert(num[index[j]],basedr);
			if(check(x,basedr))	ans=(ans+qpow(2,flag-1))%mod;//再來看看x能不能插入另外n-1個值的線性基內,記住,不能插入纔有貢獻
		}
		printf("%lld\n",ans);	
	}
	return 0;
} 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章