一、異或線性基
現在對於n個數字(下面稱這n個數爲原數集):,找到一組數 ,可以用 中數的異或組合來表示中的數(即集合和集合通過異或出來的值域是一樣的),那麼稱 就是 的一個線性基。
例如:
對於集合,它的線性基就是。
因爲, ^ = 。
所以通過 中的元素異或,就可以直接得到 中的所有元素。
先說一說線性基有什麼性質:
1.原數集中的任意一個數字都能夠通過線性基中的元素異或出來(這與線性基的構建方式有關),同時也說明線性基不唯一。
2.原數集中的數字異或出來的值域與線性基中的元素以後出來的值域相等(通過上一條性質可知)。
3.線性基中沒有異或和爲零的非空子集:現在假設存在這樣一個子集使得 的異或和爲零,那麼根據異或的性質能得出:,既然 已經能用除他之外的線性基元素表示出來,我們便沒有必要再將 放在線性基中。
4.線性基中的選取元素的每一種方案,都對應一個異或值,不存在多種選取方案對應同一個異或值的情況:現在假設存在這種情況,那麼我們就會存在一個非空子集的異或值爲零,這與上一條性質矛盾。
5.線性基是滿足以上性質的最小集合,即線性基中不存在任何一個多餘的元素。
構造線性基
首先定義兩個數組:
是原數集 , 是在二進制中二進制位爲 最高位爲第 位的數。
對於可能不好理解,舉個例子: ,在二進制下 11,其二進制位爲 1 最高位爲第 1 位,所以
;
我們主要通過插入操作來實現線性基的構造如下:
令插入的數爲,考慮 的二進制最高位 ,
- 若線性基的第 位爲 (),則直接在該位插入,退出;
- 若線性基的第 位已經有值,則 ,重複以上操作直到
插入順序的不同會導致線性基的不同。
舉個例子,原數集
既然要異或,先將原數集變爲二進制表示:
直接觀察,我們可以發現, ^ , ^
所以我們可以用來表示,也可以用來表示。
故都是原數集的線性基。
下面使用插入操作來模擬:
-
先插入,其第三位是最高位,且故,本次插入結束。
-
再插入,其第三位是最高位,且,故令 ^ ;
此時本次插入還未結束,這個是上一個 ^ 來的,其第一位是最高位,且,故 。 -
最後插入最後一個元素 ,其第一位是最高位,且,故令 ^ ,插入結束;
到此求出一個線性基,如果將的插入順序交換就是另一個線性基。
//插入線性基操作 p[i]是最高位爲i的數 Maxbit是二進制位的最大值,正常設爲63
void insert_lb(ll x) {
for(int j = Maxbit - 1; j >= 0; j--) {
if(x & (1ll << j)) {
if(!p[j]) {
p[j] = x;
break;
} else {
x ^= p[j];
}
}
}
}
線性基應用
1.詢問數字x是否在當前線性基異或集合中
bool check(ll x){
for(int i=Maxbit - 1;i>=0;--i)
if(x & (1ll << i)){
if(!p[i]) return 0;
x^=p[i];
if(!x) return 1;
}
}
2.求異或最大值
ll max_xor() {
ll ans=0;
for(int i=Maxbit-1; i >= 0; i--)
if(ans^p[i]>ans)
ans=ans^p[i];
return ans;
}
因爲我們求最大值,所以,我們從二進制高位開始枚舉,因爲先枚舉高位,異或後 只可能 後面的低位會變大,所以比較一下異或後低位可能變大的值,維護一下最大值即可。
例題:luogu P3812 【模板】線性基
3.求異或最小值
int min_xor()
{
int cnt=0;//cnt代表線性基的個數,n代表原序列個數
for(int i=0; i < Maxbit; i++)
if(p[i])
cnt++;
if(cnt<n)
return 0;
else
{
for(int i=0; i<=61; i++)
if(p[i])
return p[i];
}
}
線性基最小的非零數就是異或的最小值,因爲異或其他數都會使結果變大。
還要注意的一點就是
- 如果線性基的個數和原數集的個數不相同的話,那麼原數集中元素異或結果必定有 ,
- 如果線性基的個數和原數集的個數相等,則異或後沒有
4.求異或第 k 小的值
方法:先將原線性基中,除了 ,其他所有數的第 位全部變成 。
舉個例子:
線性基爲
將除 之外,其他數的第 位全變爲
將除 之外,其他數的第 位全變爲
然後從小到大把所有線性基中的元素,存在一個新的數組 中。
我們這樣處理排序後,任意兩個數進行異或後 就只會變大,不會變小,因爲只有上的第 位是 其他都是 ,異或後必然變大,這樣就了一定的單調性。
對於的二進制中的數位 ,若 的二進制在 位爲1,我們就把數組中不爲零的且排名爲 的數異或到ans上。
爲什麼可以這樣? 其實這和二進制的原理十分相似,我們來分析一下。(首先拋開異或和爲 情況不說,一會單獨考慮)。
以 爲例:
的二進制爲 。
二進制位的第零位和第二位是 ,那麼第五小的異或和就是 ^
的二進制爲 。
二進制位的第一位和第二位是 ,那麼第六小的異或和就是 ^
通過 我們上邊紅字部分的單調性,導致了這樣一條規律。
下面主要是解決異或和 有沒有 的問題,這個問題在求異或最小值時已經提過了。
結論:
- 如果線性基的個數和原數集的個數不相等的話,那麼原數集中元素異或結果必定有 ,
- 如果線性基的個數和原數集的個數相等,則異或後沒有
可以簡單地舉例證明一下:
原數集,取一個線性基,因爲 ^ ,所以 ^ ^ 。
而就不會產生這種情況。
HDU 3949 XOR
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define ll long long
using namespace std;
const int Max = 57;
const int Maxbit = 63;
ll p[Maxbit]; //保存線性基
ll s[Maxbit]; //重構後的線性基
ll k,x;
int cas,icas = 0;
int n,cnt = 0,Q;
//將 x 插入線性基
void insert_lb(ll x) {
for(int j = Maxbit - 1; j >= 0; j--) {
if(x & (1ll << j)) {//如果 x的第 j 位爲 1
if(!p[j]) {//如果之前沒有數插入過
p[j] = x;
break;
} else {//如果已經有數插入過
x ^= p[j];
}
}
}
}
//重構線性基
void rebuild(){
for(int i = 0; i < Maxbit; i++) {
for(int j = Maxbit - 1; j > i; j--) {
if(p[j] & (1ll << i)) {
p[j] ^= p[i];
}
}
}
//從小到大排序
for(int i = 0 ; i < Maxbit; i++) {
if(p[i]) s[cnt++] = p[i];
}
}
int main() {
scanf("%d",&cas);
while(cas--) {
//初始化
printf("Case #%d:\n",++icas);
memset(p,0,sizeof(p));
cnt = 0;
scanf("%d",&n);
for(int i = 1; i <= n; i++) {
scanf("%lld",&x);
insert_lb(x);
}
rebuild();
//開始查詢
scanf("%d",&Q);
while(Q--) {
ll ans = 0;
scanf("%lld",&k);
if(n != cnt) k--;// 如果線性基 和 原數集個數不同 含有 0
if(k >= (1ll << cnt)){
printf("-1\n");
continue;
}
for(int i = Maxbit-1; i >= 0; i--)
if(k & (1ll << i)) ans ^= s[i];
printf("%lld\n",ans);
}
}
return 0;
}
/*
2
2
1 2
4
1 2 3 4
3
1 2 3
5
1 2 3 4 5
2
5
21 11 6 3 16
16
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
*/
5.有向圖找最大異或路徑
主要通過 luogu P4151 [WC2011]最大XOR和路徑 這道題來理解
二、整數線性基
線性空間是一個關於以下兩個運算封閉的向量集合:
- 向量加法,其中均爲向量。
- 標量乘法,也稱爲數乘運算,其中是向量,是常數(標量)。
給定若干向量,若向量 能由 經過向量加法和標量乘法運算得出,則稱向量 能被向量表出。顯然,能表出的所有向量構成一個線性空間,被稱爲這個線性空間的生成子集。
任意選出線性空間中的若干個向量,如果其中存在一個向量能被其他向量表出,則稱這些向量線性相關,否則稱這些向量線性無關。
線性無關的生成子集被稱爲線性空間的基底,簡稱基。基的另一種定義是線性空間的極大線性無關子集。一個線性空間的所有基包含的向量個數都相等,這個數被稱爲線性空間的維數。
例如,平面直角座標系中的所有向量構成一個二維線性空間,它的一個基就是單位向量集合。平面直角座標系的軸上的所有向量構成了一個一維線性空間,它的一個基就是。
對於一個行列的矩陣,我們可以把它的每一行看作一個長度爲的向量,稱爲“行向量”。矩陣的個行向量能夠表出所有向量構成的一個一維線性空間,這個線性空間的維數被成爲矩陣的"行秩"。類似地,我們可以定義列向量和列秩。實際上,矩陣的行秩一定等於列秩,它們都被稱爲矩陣的秩。
把這個的矩陣看作"係數矩陣"進行高斯消元(增廣矩陣的最後一列全看作零),得到一個簡化階梯矩陣。顯然,簡化階梯矩陣的所有非零行向量線性無關。因爲初等行變換就是行向量之間進行的向量加法與標量乘法運算,所以高斯消元不改變行向量表出的線性空間。於是,簡化階梯形矩陣的所有非零行向量就是該線性空間的一個基,非零行向量的個數就是矩陣的秩。
【例題1】luogu P3265 [JLOI2015]裝備購買
題意:
個裝備,每個裝備 個屬性,每個裝備還有個價格 。如果手裏有的裝備的每一項屬性爲它們分配係數(實數)後可以相加得到某件裝備,則不必要買這件裝備。求最多裝備下的最小花費。
思路
每個裝備就是一個向量,要求最多裝備的數量,就是求出個向量的秩。
還要求花最少的錢,我們只需要在選出在相同作用下,價格更小的那個向量即可。比如和都可以表出 ,但是 的價格更低,所以我們優先選擇。
這樣我們先給向量提前排個序,將價格小的放在前面即可。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define ld long double
#define eps 1e-6
using namespace std;
const int N = 507;
int n,m,cnt = 0,sum = 0,p[N] = {0};
struct Node{
int cost;
ld a[N];
}z[N];
bool cmp(const Node &a,const Node &b){
return a.cost < b.cost;
}
void debug(){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
printf("%Lf ",z[i].a[j]);
}
puts("");
}
}
void Gauss(){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
if(fabs(z[i].a[j]) > eps){
if(!p[j]){
p[j] = i;
cnt++;
sum += z[i].cost;
break;
}else {
ld tmp = z[i].a[j] / z[p[j]].a[j];
for(int k = j; k <= m ; k++)
z[i].a[k] -= z[p[j]].a[k] * tmp;
}
debug();
}
}
}
}
int main(){
scanf("%d %d",&n,&m);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
scanf("%Lf",&z[i].a[j]);
}
}
for(int i = 1; i <= m; i++) scanf("%d",&z[i].cost);
sort(z,z + n,cmp);
Gauss();
printf("%d %d\n",cnt,sum);
return 0;
}