ps:做CF的時候碰到了一個線性基的概念,然后在網上學習了一下,發現相關的資料很少,所以打算來寫一個我個人的理解。
線性代數中 有極大線性無關組和空間的基的概念。 線性基的性質與此類似。
首先來看一個問題:
給出N個數,要從中選出一個最大的子集,使得子集中的任意個元素異或值不為0.
這個和極大線性無關組有些類似。異或可以看出是模2域下的加法運算,如果把一個數轉化為二進制,對應成一個由01構成的向量, 所有這些向量就構成了一個線性空間。
原問題就轉化為求這個向量組的極大線性無關組,把這樣一個極大線性無關組成為線性基。 可以用O(60*60*n)的高斯消元來解決。
但是還有更加快速的構造線性基的方法: 復雜度是O(60*n)
首先來證明一個性質:
取向量組中的兩個向量a,b,把a,b中的某一個替換成a xor b, 替換前后向量組中的向量的線性組合得到的空間相同。 通俗的說就是 替換前后 能異或出來的值一樣。
證明:
不妨把b替換成了c=a xor b.
對於一個值x,如果得到它 不需要用到b 那么替換沒有影響,照樣能得到x。
如果需要b b可以通過b=c xor a來得到, 因此替換后照樣能組合出x。
也就是說 舊的向量組能組合出來的數 新的向量組也能組合出來。
反過來,對於新的向量組,如果把c替換成 c xor a 就得到了舊的向量組,根據上面的證明
新的向量組能組合出來的數 舊的向量組也能組合出來。
因此替換前后能組合出來的數一樣。
所以如果把向量組里的向量互相異或, 極大線性無關向量組的大小是不變的。 利用這個性質 可以用下面的代碼來求:
1 void Guass() 2 { 3 for (int i=1;i<=sz;i++) 4 { 5 for (int j=62;j>=0;j--) 6 { 7 if ((A[i]>>j)&1) 8 { 9 if (!P[j]) {P[j]=A[i]; break;} 10 else A[i]^=P[j]; 11 } 12 } 13 } 14 for (int j=0;j<=62;j++) if (P[j]) r++; 15 }
上面提到O(60*60*n)的高斯消元,是行與行之間互相異或. 而這個代碼是列與列之間互相異或。r就是極大線性無關組的大小。
基本思想是:從左往右掃描每個向量,對於第i個向量的第j位,如果前面已經有第j位為1的向量,那么把第i個向量異或那個向量。
這樣最后得到的向量組,不考慮0向量, 最高位的1的位置 是互不相同的。 顯然這些向量線性無關。
於是這樣構造出的極大線性無關組,也就是線性基,具有以下性質:
性質一:最高位1的位置互不相同。 這是根據上面的構造方法得出的。
性質二:任意一個可以用這些向量組合出的向量x,組合方式唯一。
證明: 假設x的組合方法不唯一, 也就是說 $x= a_1\ xor\ a_2\ \cdots\ a_p\ =\ b_1\ xor\ b_2\ \cdots\ b_q$
那么$x\ xor\ x\ =\ 0\ =\ a_1\ xor\ a_2\ \cdots\ a_p\ xor\ b_1\ xor\ b_2\ \cdots\ b_q$
也就是說 可以用這個向量組里的向量組合出0向量, 與線性無關矛盾。 故組合方法唯一。
性質三:線性基的任意一個子集異或和不為0. 利用性質一 腦補一下就出來了。
然后來看幾個應用: 參考這篇博文:http://www.cnblogs.com/ljh2000-jump/p/5869991.html
應用一:給出一個數組,詢問一個數能否表示為數組中的某些數的異或和。
解法: 先求出線性基, 從二進制高位往低位判斷,假設當前x的這一位是1,如果線性基中存在某個向量最高位的1在這一位,那么這個向量肯定要取,把x異或這個向量,否則沒法組合出來。
應用二: 給出一個數組,詢問第k小異或和。 (hdu 3949)
解法: 首先用前面的方法得到一組線性基。 然后我們可以把這組線性基 再次利用相互異或空間不變的性質把它改造一下,使得它有更加優美的性質:
具體改造方法如下:
首先根據性質一得到的基最高位1的位置互不相同。 記$a_i$為線性基中最高位的1在第i位的 向量, 可以按照i從大到小的順序,用$a_i$去異或$a_j\ \ (j>i)$.
這樣最后得到的向量組又多了一個優美的性質: 只有$a_i$的第i位是1,其他的第i位都是0. 有了這個性質,就容易證明要求第k小的異1或和,只要把k二進制拆分,第j位是1就異或第j個線性基中的向量。 正確性是顯然的...但是表達能力有限,我寫不出來。。
另外有一個小坑: 就是0能不能被異或出來的問題。 如果線性基的大小和原數組一樣,0是不能被異或出來的,否則可以。
注意long long, 第一次寫判斷無解的時候 k>=(1<<m) 1后面沒加LL 怎么查都查不出來。。。
AC代碼:

1 #include <cstdio>
2 #include <iostream>
3 #include <queue>
4 #include <algorithm>
5 #include <cstring>
6 #include <set>
7 using namespace std; 8
9 #define N 10010
10 typedef long long ll; 11
12 int m; 13 ll A[N],P[65]; 14
15 void Guass(int n) 16 { 17 memset(P,0,sizeof(P)); 18 for (int i=1;i<=n;i++) 19 { 20 for (int j=63;j>=0;j--) 21 { 22 if ((A[i]>>j)&1) 23 { 24 if (P[j]) A[i]^=P[j]; 25 else {P[j]=A[i]; break;} 26 } 27 } 28 } 29
30 for (int i=63;i>=0;i--) 31 { 32 if (!P[i]) continue; 33 for (int j=i+1;j<=62;j++) 34 { 35 if ((P[j]>>i)&1) P[j]^=P[i]; 36 } 37 } 38
39 m=0; 40 for (int j=0;j<=63;j++) if (P[j]) P[m++]=P[j]; 41 } 42
43 int main() 44 { 45 //freopen("in.in","r",stdin); 46 //freopen("out.out","w",stdout);
47
48 int T,n,Q; ll k; scanf("%d",&T); 49 for (int cas=1;cas<=T;cas++) 50 { 51 printf("Case #%d:\n",cas); 52 scanf("%d",&n); 53 for (int i=1;i<=n;i++) scanf("%I64d",&A[i]); 54 Guass(n); scanf("%d",&Q); 55
56 while (Q--) 57 { 58 scanf("%I64d",&k); 59 if (n!=m) k--; 60 if (k>=(1ll<<m)) printf("-1\n"); 61 else
62 { 63 ll ans=0; 64 for (int j=0;j<=63;j++) if ((k>>j)&1) ans^=P[j]; 65 printf("%I64d\n",ans); 66 } 67 } 68 } 69 return 0; 70 }
應用三:BZOJ2460
題目大意: 有一些礦石,每個礦石有一個a和一個b值,要求選出一些礦石,b的和最大且不存在某個礦石子集它們的a的異或和為0.
解法:
向按b從大到小排序,依次加入礦石,判斷和前面選中的礦石是否沖突。 可以發現這個過程和前面求線性基的算法是一樣的。
貪心的正確性 證明可以用擬陣。 可以參考 劉雨辰的 《對擬陣的初步研究》的線性擬陣內容 。
還有一個證明:https://blog.csdn.net/lqybzx/article/details/79416710
AC代碼:

1 #include <cstdio>
2 #include <iostream>
3 #include <queue>
4 #include <algorithm>
5 #include <cstring>
6 #include <set>
7 using namespace std; 8
9 #define N 1010
10 typedef long long ll; 11
12 struct node 13 { 14 ll x,y; 15 bool operator < (const node &t)const{return y>t.y;} 16 }p[N]; 17 ll A[63]; 18
19
20 int main() 21 { 22 //freopen("in.in","r",stdin); 23 //freopen("out.out","w",stdout);
24
25 int n; scanf("%d",&n); ll ans=0; 26 for (int i=1;i<=n;i++) scanf("%lld%lld",&p[i].x,&p[i].y); 27 sort(p+1,p+n+1); 28
29 for (int i=1;i<=n;i++) 30 { 31 for (int j=61;j>=0;j--) 32 { 33 if ((p[i].x>>j)&1) 34 { 35 if (A[j]) p[i].x^=A[j]; 36 else {A[j]=p[i].x; break;} 37 } 38 } 39 if (p[i].x) ans+=p[i].y; 40 } 41 printf("%lld\n",ans); 42 return 0; 43 }
應用四: BZOJ2115
題目大意: 給出一個連通無向圖,求從1到n異或和最小的路徑。
解法: 注意圖是連通的,又是無向圖,因此假設已經找到了1到n的一條路徑,圖上的任意一個回路都可以加到這個路徑上。 具體做法是先從1走到環上的一點,繞着環走一圈回來,這樣點1到環的那段路 走了2次 抵消,相當於只把環加入了路徑。
先任意找一條1到n的路徑p0,可以證明任意一條1到n路徑的異或和都可以通過p0加上一些環得到。
證明: 記另外一條路徑為p1,那么沿p0從1走到n,在沿着p1從n走到1 構成一個環G, 因此p1的異或和可以通過p0加上環G得到。
所有環的異或值的組合構成一個線性空間,找到這個線性空間的一組線性基。
我們先要找出所有的環: 做一次DFS,得到DFS樹,每條返祖邊對應一個簡單環,即沒有重復點的環。
下面證明所有的環(包括有重復邊 重復點的)的異或值都可以由這些簡單環異或得到。
可以在DFS樹上來考慮。 沿着環走一圈,如果走的是返祖邊,那么異或上對應的簡單環,容易發現最終的結果就是這些簡單環的異或和。
因此我們只要找到這些簡單環的一個極大線性無關組就可以作為一組線性基了。
根據線性基的性質一,可以貪心的來找答案。一開始隨便找一條路異或和為s,然后從高位往低位考慮,如果當前第j位 s是0,線性基中存在$a_j\ \ (最高位1在第j位的向量)$,那么異或它答案變優。 具體實現的話 for(int i = 62; i >= 0; i--) ans = max(ans, ans ^ P[i]); 就好了。
代碼:

1 #include <cstdio>
2 #include <iostream>
3 #include <queue>
4 #include <algorithm>
5 #include <cstring>
6 #include <set>
7 using namespace std; 8
9 #define N 200010
10 typedef long long ll; 11
12 int n,m,tot,sz; 13 int head[N],to[N],nxt[N]; 14 ll w[N],dis[N],P[66],A[N]; 15 bool vis[N]; 16
17 void Add_Edge(int x,int y,ll z){to[tot]=y; w[tot]=z; nxt[tot]=head[x]; head[x]=tot++;} 18
19 void Dfs(int x,ll s) 20 { 21 vis[x]=true; dis[x]=s; 22 for (int i=head[x];i!=-1;i=nxt[i]) 23 { 24 int y=to[i]; 25 //cout<<x<<" "<<y<<" "<<vis[y]<<endl;
26 if (vis[y]) A[++sz]=dis[y]^s^w[i]; 27 else Dfs(y,s^w[i]); 28 } 29
30 } 31
32
33 void Guass() 34 { 35 for (int i=1;i<=sz;i++) 36 { 37 for (int j=62;j>=0;j--) 38 { 39 if ((A[i]>>j)&1) 40 { 41 if (!P[j]) {P[j]=A[i]; break;} 42 else A[i]^=P[j]; 43 } 44 } 45 } 46 for (int j=0;j<=62;j++) if (P[j]) r++; 47 } 48
49 int main() 50 { 51 //freopen("in.in","r",stdin); 52 //freopen("out.out","w",stdout);
53
54 int x,y; ll z; 55 scanf("%d%d",&n,&m); 56 memset(head,-1,sizeof(head)); 57 for (int i=1;i<=m;i++) 58 { 59 scanf("%d%d%lld",&x,&y,&z); 60 Add_Edge(x,y,z); 61 Add_Edge(y,x,z); 62 } 63 Dfs(1,0); 64 Guass(); 65 ll ans = dis[n]; 66 for(int i = 62; i >= 0; i--) ans = max(ans, ans ^ P[i]); 67 printf("%lld\n", ans); 68 return 0; 69 }
應用五: http://codeforces.com/contest/724/problem/G
給出一個無向圖,求所有不同的三元組(u,v,s)的s之和 表示u到v的路徑異或和為s。
解法:首先不同的連通塊分開處理即可。
我們可以計算每一位的貢獻. 對於第i位,我們只要算出有多少個(u,v,s) s的第i位是1. 每一個對答案的貢獻都是$2^i$。
根據上面那題的證明,可以知道對於確定的u,v,可以隨便找一條路,然后異或上一些環。 先找出簡單環,然后求出一組線性基(下面假設基的大小為r)。
假設我們隨便找的那條路異或和為s。分兩種情況:
1.s的第i位是0. 如果線性基中 不存在某個向量這一位是1 那么不管怎么取 都不能把這一位變成1,對答案沒貢獻。如果存在,那么線性基中其他r-1個向量先亂選,要想這一位變成1,那么剩下某一個這一位是1的向量取不取是定的。 所以一共有$2^{r-1}$種取法。
2.s的第i位是1. 如果線性基中 不存在某個向量這一位是1那么不管怎么取這一位都是1, 一共$2^{r}$種取法。 如果存在,那么線性基中其他r-1個向量先亂選,要想這一位變成1,那么剩下某一個這一位是1的向量取不取是定的。 所以一共有$2^{r-1}$種取法。
那么做法就顯然了:DFS一遍,dis[u]表示根到u的路徑異或和, 那么u,v之間路徑的異或和就是dis[u] xor dis[v].
所以考慮第i位的貢獻時,求出dis[x]這一位是1的有多少個,是0的有多少個,然后搞一搞就好了,具體看代碼。

1 #include <cstdio>
2 #include <iostream>
3 #include <queue>
4 #include <algorithm>
5 #include <cstring>
6 #include <set>
7 using namespace std; 8
9 #define N 100010
10 #define M 400010
11 typedef long long ll; 12
13 const int Mod=1000000007; 14 int n,m,tot,sz; 15 int head[N],to[M],nxt[M],t[N],p2[M]; 16 ll w[M],dis[N],A[M],P[63]; 17 bool vis[N]; 18
19 void Add_Edge(int x,int y,ll z){to[tot]=y,nxt[tot]=head[x],w[tot]=z,head[x]=tot++;} 20
21 void Dfs(int x,ll s) 22 { 23 vis[x]=true; dis[x]=s; t[++t[0]]=x; 24 for (int i=head[x];i!=-1;i=nxt[i]) 25 { 26 int y=to[i]; 27 if (vis[y]) A[++sz]=s^w[i]^dis[y]; 28 else Dfs(y,s^w[i]); 29 } 30 } 31
32 void Guass() 33 { 34 memset(P,0,sizeof(P)); 35 for (int i=1;i<=sz;i++) 36 { 37 for (int j=61;j>=0;j--) 38 { 39 if ((A[i]>>j)&1) 40 { 41 if (P[j]) A[i]^=P[j]; 42 else {P[j]=A[i]; break;} 43 } 44 } 45 } 46 } 47
48 int f(int x){return 1ll*x*(x-1)/2%Mod;} 49
50 int Solve() 51 { 52 int res=0,r=0; 53 for (int i=0;i<=61;i++) if (P[i]) r++; 54 for (int k=0;k<=61;k++) 55 { 56 bool flag=false; 57 for (int j=0;j<=61;j++) if ((P[j]>>k)&1) flag=true; 58
59 int cnt[2]={0}; int tmp; 60 for (int i=1;i<=t[0];i++) cnt[(dis[t[i]]>>k)&1]++; 61
62 tmp=(f(cnt[0])+f(cnt[1]))%Mod; 63 if (flag) tmp=1ll*tmp*p2[r-1]%Mod,tmp=1ll*p2[k]*tmp%Mod,res=(res+tmp)%Mod; 64
65 tmp=1ll*cnt[0]*cnt[1]%Mod; 66 if (flag) tmp=1ll*tmp*p2[r-1]%Mod; 67 else tmp=1ll*tmp*p2[r]%Mod; 68 tmp=1ll*p2[k]*tmp%Mod; res=(res+tmp)%Mod; 69
70 } 71
72 return res; 73 } 74
75 int main() 76 { 77 //freopen("in.in","r",stdin); 78 //freopen("out.out","w",stdout);
79
80 p2[0]=1; 81 for (int i=1;i<M;i++) p2[i]=1ll*p2[i-1]*2%Mod; 82
83 int x,y,ans=0; ll z; 84 scanf("%d%d",&n,&m); 85 memset(head,-1,sizeof(head)); 86 for (int i=1;i<=m;i++) 87 { 88 scanf("%d%d%I64d",&x,&y,&z); 89 Add_Edge(x,y,z); 90 Add_Edge(y,x,z); 91 } 92 for (int i=1;i<=n;i++) if (!vis[i]) 93 { 94 sz=t[0]=0; 95 Dfs(i,0); 96 Guass(); 97 ans=(ans+Solve())%Mod; 98 } 99 printf("%d\n",ans); 100 return 0; 101 }