线性基 学习笔记
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 }