引入
今天在刷題的時候看到這樣一個題:
在n個數中求出異或和的最大值
發現並不是很會
然后學了線性基
算法介紹
若干數的線性基是一組數\(a_1,a_2,...a_n\),其中\(a_x\)的最高位的\(1\)在第\(x\)位。
通過線性基中元素\(xor\)出的數的值域與原來的數\(xor\)出數的值域相同。
設線性基中的元素為\(p_1,p_2,···p_n\)(\(p_i的數在二進制表示下 第i位是該數字的最高位1\))則 \(a_1,a_2,...a_n\) 可用 \(p_1,p_2,···p_n\) 通過\(xor\)運算得出
求線性基 我們考慮把每一個數對線性基的影響分次考慮
有如下代碼
插入一個數到線性基中
void ins(ll num){
for(ll i = 63;i >= 0;i--)//枚舉位數
if(num & (1ll << i)){ //注意這里的1ll
if(!p[i]){p[i] = num;break;}
num ^= p[i];
}
}
解釋一下 我們根據我們的定義 我們從大到小來枚舉當前\(num_{(2)}\)的\(第i位\) 如果發現\(p_i\)已經有元素了 就讓\(num = num xor p_i\) 否則插入當前的\(num\)
我們考慮有這么一個數\(z\) 如果他無法通過我們的方式對線性基產生變化 那么必有\(p_x\ xor\ p_y\ xor\ ······ xor\ z = 0\) 此時\(p_x\ xor\ p_y\ xor\ ······ \ =\ z\)
所以此時\(z\)已經能被線性基中的元素表示了 自然對線性基不產生變化
其實關於線性基的求法 我們還有高斯消元的做法 但筆者認為這里的方法比較簡單(明明是你不會高斯消元法)
用法
線性基通常有如下三個用法
- 查詢一個數是否能被集合中其他數異或表示
- 求一個集合異或最大/最小值
- 求一個集合異或的第k大值
下列給定三種方法的代碼
查詢一個元素是否可以被異或出來
從高到低,如果這一位為1就異或上這一位的線性基,把1消去,如果最后得到了0,那這個數就可以表示出來。
inline int ask(LL x) {
for(R int i=62;i>=0;i--)
if(x&(1LL<<i)) x^=p[i];
return x==0;
}
查詢一個集合異或最大值
從高到低 貪心異或即可
void get(){//從高到低 貪心選擇
for(ll i = 63;i >= 0;i--)
ans = max(ans,ans ^ p[i]);
}
查詢一個集合異或最小值
在集合中有0的情況下 集合異或最小值答案為零
否則為線性基中最小的元素
inline LL askmn() {
if(zero) return 0;
for(R int i=0;i<=62;i++)
if(p[i]) return p[i];
}
求一個集合異或的第k大值
首先考慮,要是每一位的選擇都不會影響下一位的話,那就可以直接從高到低按位去選擇就行了,就類似於二叉樹求rank的玩法。但是我們之前建出來的線性基是沒有這個性質的所以我們考慮重構一個數組d來解決這個問題。先上代碼:
inline void rebuild() {
cnt=0;top=0;
for(R int i=MB;i>=0;i--)
for(R int j=i-1;j>=0;j--)
if(p[i]&(1LL<<j)) p[i]^=p[j];
for(R int i=0;i<=MB;i++) if(p[i]) d[cnt++]=p[i];
}
然后就是按從大到小了
inline LL kth(int k) {
if(k>=(1LL<<cnt)) return -1;
LL ans=0;
for(R int i=MB;i>=0;i--)
if(k&(1LL<<i)) ans^=d[i];
return ans;
}
//考慮一手出現0的情況
printf("%lld\n",k-zero?kth(k-zero):0LL);