线性基
1.算法分析
线性基的本质是用一个数x的尽可能高位的1来代表x
线性基是向量空间的一组基,通常可以解决有关异或的一些题目。
通俗一点的讲法就是由一个集合构造出来的另一个集合,它有以下几个性质:
- 线性基的元素能相互异或得到原集合的元素的所有相互异或得到的值。
- 线性基是满足性质 1 的最小的集合。
- 线性基没有异或和为 0 的子集。
- 线性基中每个元素的异或方案唯一,也就是说,线性基中不同的异或组合异或出的数都是不一样的。
- 线性基中每个元素的二进制最高位互不相同
2.模板
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
struct Linear_Basis{
LL d[61],p[61]; // d记录原先的,p记录重构后的
int cnt, flag; // cnt记录重构后有多少个,flag记录是否出现过0
Linear_Basis() {
memset(d, 0 ,sizeof(d));
memset(p, 0, sizeof(p));
cnt = 0; // 当前线性基内元素个数
flag = 0; // 不存在0
}
// 构造线性基:逐个元素插入
bool insert(LL val) {
for (int i = 60;i >= 0; i--) {
if (val & (1ll << i)) { // 判断当前i位能否插入
if (!d[i]) { // 如果当前i位没插入过元素
d[i] = val; // 插入
return true; // 插入成功
}
val ^= d[i];
}
}
flag = 1; // 存在0
return false; // 插入失败
}
// 查询第最大异或和
LL query_max() {
LL ret = 0;
for (int i = 60; i >= 0; i--)
if ((ret ^ d[i]) > ret)
ret ^= d[i];
return ret;
}
// 查询最小异或和
LL query_min() {
for (int i = 0;i <= 60; i++)
if (d[i])
return d[i];
return 0;
}
// 重构,使之形成对角矩阵
void rebuild() {
for (int i = 60;i >= 0; i--) // 当前i位时,把后面的j位情况都和i位情况做异或
for (int j = i - 1;j >= 0; j--)
if (d[i] & (1ll << j))
d[i] ^= d[j];
for (int i = 0; i <= 60; i++) // 记录重构结果
if (d[i])
p[cnt++] = d[i];
}
// 查询第k小
LL kthquery(LL k){
LL ret = 0;
if (flag) { // 存在0
k--;
if (!k) return (LL)0;
}
if (k >= (1ll << cnt)) // 只能由2^cnt - 1个
return -1;
for (int i = 60; i >= 0; i--)
if (k & (1LL << i)) // 每位的贡献为2^i
ret ^= p[i];
return ret;
}
};
// 合并线性基
Linear_Basis merge(const Linear_Basis &n1, const Linear_Basis &n2) {
Linear_Basis ret = n1;
for (int i = 60; i >= 0; i--)
if (n2.d[i])
ret.insert(n2.d[i]);
return ret;
}
int main(){
int n;
scanf("%d",&n);
Linear_Basis lb;
for(int i=0;i<n;i++){
LL a;
scanf("%lld",&a);
lb.insert(a);
}
printf("%lld\n",lb.query_max());
return 0;
}
3.例题
P4570 [BJWC2011]元素
题意: 给n个整数,每个整数带一个权值v,求一个权值和最大的线性基。
题解: 按权值v从大->小排序,依次插入线性基。要是能够插入,那么累加到答案
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 1e3 + 10;
struct MAGIC {
LL val, magic;
bool operator<(const MAGIC &t) const {
return magic > t.magic;
}
}magic[N];
struct Linear_Basis{
LL d[61],p[61];
int cnt, flag = 0;
Linear_Basis(){
memset(d, 0, sizeof(d));
memset(p, 0, sizeof(p));
cnt = 0;
flag = 0;
}
// 构造线性基:逐个元素插入
bool ins(LL val) {
for (int i = 60;i >= 0; i--) {
if (val & (1ll << i)) { // 判断当前i位能否插入
if (!d[i]) { // 如果当前i位没插入过元素
d[i] = val; // 插入
break;
}
val ^= d[i];
}
}
return val > 0; // 返回1说明插入成功;返回0插入失败,说明出现异或和为0的情况
}
};
int main(){
int n;
scanf("%d",&n);
Linear_Basis lb;
LL sum = 0;
for(int i=0;i<n;i++){
LL t1, ma;
cin >> t1 >> ma;
magic[i].val = t1, magic[i].magic = ma;
}
sort(magic, magic + n);
for (int i = 0; i < n; ++i) {
if (lb.ins(magic[i].val)) sum += magic[i].magic;
}
printf("%lld\n", sum);
return 0;
}
acwing229 新nim游戏
题意: 第1和2回合,可以拿走任意堆,但不能全拿,从第3回合后开始和普通nim游戏一样。问第1回合拿走多少堆才能必胜。如果能必胜,输出拿走的石子数目;如果不能,输出-1.
题解: 要使必胜,那么第1回合拿完后,其余剩余石子无论怎么异或都不会出现0,那么把石子从大到小排序,逐个往线性基内插入。如果插入不了,说明出现0,必须拿走。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 1e3 + 10;
LL a[N];
struct Linear_Basis{
LL d[61],p[61];
int cnt, flag;
Linear_Basis(){
memset(d, 0, sizeof(d));
memset(p, 0, sizeof(p));
cnt = 0;
flag = 0;
}
// 构造线性基:逐个元素插入
bool ins(LL val) {
for (int i = 60;i >= 0; i--) {
if (val & (1ll << i)) { // 判断当前i位能否插入
if (!d[i]) { // 如果当前i位没插入过元素
d[i] = val; // 插入
break;
}
val ^= d[i];
}
}
return val > 0; // 返回1说明插入成功;返回0插入失败,说明出现异或和为0的情况
}
};
int main(){
int n;
scanf("%d",&n);
Linear_Basis lb;
LL sum = 0;
for(int i=0;i<n;i++){
cin >> a[i];
}
sort(a, a + n);
reverse(a, a + n);
for (int i = 0; i < n; ++i) {
if (!lb.ins(a[i])) sum += a[i];
}
if (!sum) sum = -1;
printf("%lld\n", sum);
return 0;
}
acwing210异或运算
题意: 求第k小异或和
题解: 板题
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 1e4 + 10;
int t, n, m, kase;
LL a[N];
struct Linear_Basis{
LL d[61],p[61];
int cnt, flag;
Linear_Basis() {
memset(d, 0 ,sizeof(d));
memset(p, 0, sizeof(p));
cnt = 0; // 当前线性基内元素个数
flag = 0; // 不存在0
}
// 构造线性基:逐个元素插入
bool insert(LL val) {
for (int i = 60;i >= 0; i--) {
if (val & (1ll << i)) { // 判断当前i位能否插入
if (!d[i]) { // 如果当前i位没插入过元素
d[i] = val; // 插入
return true; // 插入成功
}
val ^= d[i];
}
}
flag = 1; // 存在0
return false; // 插入失败
}
// 重构,使之形成对角矩阵
void rebuild() {
for (int i = 60;i >= 0; i--) // 当前i位时,把后面的j位情况都和i位情况做异或
for (int j = i - 1;j >= 0; j--)
if (d[i] & (1ll << j))
d[i] ^= d[j];
for (int i = 0; i <= 60; i++) // 记录重构结果
if (d[i])
p[cnt++] = d[i];
}
// 查询第k小
LL kthquery(LL k){
LL ret = 0;
if (flag) { // 存在0
k--;
if (!k) return (LL)0;
}
if (k >= (1ll << cnt)) // 只能由2^cnt - 1个
return -1;
for (int i = 60; i >= 0; i--)
if (k & (1LL << i)) // 每位的贡献为2^i
ret ^= p[i];
return ret;
}
};
int main(){
cin >> t;
while (t--) {
printf("Case #%d:\n", ++kase);
cin >> n;
Linear_Basis lb;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
lb.insert(a[i]);
}
lb.rebuild();
cin >> m;
for (int i = 1; i <= m; ++i) {
LL t;
cin >> t;
cout << lb.kthquery(t) << endl;
}
}
return 0;
}
acwing228异或
题意: 给一个无向连通图,求一条从1到n的路径(可以不是简单路径),使经过的边权的异或和最大。
题解: 本题要求最大XOR路径,可以分析出来最大只要是个环,那么都可以经过。因此只需要找到一条1~n的路径,而后不断把环的权值异或,看是否能变大即可。分析一下找环,如果不是一个简单环,那么一定可以由简单环异或得到,因此只要找简单环即可。找简单环只要dfs判断环的思路即可
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
int const N = 5e4 + 10, M = 1e5 + 10;
int e[M * 2], ne[M * 2], h[N], idx, st[N];
LL w[M * 2], a[N];
int n, m;
struct Linear_Basis{
LL d[61],p[61];
int cnt, flag;
Linear_Basis() {
memset(d, 0 ,sizeof(d));
memset(p, 0, sizeof(p));
cnt = 0; // 当前线性基内元素个数
flag = 0; // 不存在0
}
// 构造线性基:逐个元素插入
bool insert(LL val) {
for (int i = 60;i >= 0; i--) {
if (val & (1ll << i)) { // 判断当前i位能否插入
if (!d[i]) { // 如果当前i位没插入过元素
d[i] = val; // 插入
return true; // 插入成功
}
val ^= d[i];
}
}
flag = 1; // 存在0
return false; // 插入失败
}
// 查询第最大异或和
LL query_max(LL x) {
LL ret = x;
for (int i = 60; i >= 0; i--)
if ((ret ^ d[i]) > ret)
ret ^= d[i];
return ret;
}
}lb;
void add(int a, int b, LL c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// 找简单环
void dfs(int u, int fa) {
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j == fa) continue;
if (st[j]) {
lb.insert(a[u] ^ a[j] ^ w[i]); // 找到环,插入线性基
}
else { // 否则,更新
a[j] = a[u] ^ w[i];
dfs (j, u);
}
}
}
int main() {
cin >> n >> m;
memset(h, -1, sizeof h);
for (int i = 1, a, b; i <= m; ++i) {
LL d;
scanf("%d %d %lld", &a, &b, &d);
add(a, b, d), add(b, a, d);
}
dfs (1, -1);
cout << lb.query_max(a[n]) << endl; // 找到1~n的路径,然后线性基找最大值
return 0;
}
luogu P3857 [TJOI2008]彩灯
题意: 一组彩灯是由一排 N 个独立的灯泡构成的,并且有 M 个开关控制它们。问有多少种样式可以展示?有m行,每行给定n个字符,如:OX。如果第 i 个字母是大写字母 O,则表示这个开关控制第 i 盏灯,如果第 i 个字母是大写字母 X,则表示这个开关不控制此灯。N,M~50
题解: 灯泡开关就是0和1,选和不选,就是判断对角矩阵线性基选和不选。因此重构之后变成对角矩阵后,里面有x个基底,那么就要2^x种情况。这就等价于给定n个数,求n个数的子集的异或值有多少种
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
int const MOD = 2008;
int n, m;
struct Linear_Basis{
LL d[61],p[61];
int cnt, flag;
Linear_Basis() {
memset(d, 0 ,sizeof(d));
memset(p, 0, sizeof(p));
cnt = 0; // 当前线性基内元素个数
flag = 0; // 不存在0
}
// 构造线性基:逐个元素插入
bool insert(LL val) {
for (int i = 60;i >= 0; i--) {
if (val & (1ll << i)) { // 判断当前i位能否插入
if (!d[i]) { // 如果当前i位没插入过元素
d[i] = val; // 插入
return true; // 插入成功
}
val ^= d[i];
}
}
flag = 1; // 存在0
return false; // 插入失败
}
// 重构,使之形成对角矩阵
void rebuild() {
for (int i = 60;i >= 0; i--) // 当前i位时,把后面的j位情况都和i位情况做异或
for (int j = i - 1;j >= 0; j--)
if (d[i] & (1ll << j))
d[i] ^= d[j];
for (int i = 0; i <= 60; i++) // 记录重构结果
if (d[i])
p[cnt++] = d[i];
}
}lb;
int main(){
cin >> n >> m;
for (int i = 1; i <= m; ++i) {
LL all = 0;
for (int j = n - 1; j >= 0; --j) {
char op;
cin >> op;
if (op == 'O') all += (1ll << j);
}
lb.insert(all);
}
lb.rebuild();
cout << min(((1ll << lb.cnt)), (1ll << n)) % MOD;
return 0;
}
acwing209装备购买
题意: 游戏有n件装备,每件装备有m个属性,如果第i件装备可以被其他任意件装备乘上对应系数表示出来,那么第i件装备不需要花钱购买,否则需要花钱。求最多可以购买多少件装备,同时求出在最多件装备的情况下的最小花费。
装备数N ~ 500,属性数M ~ 500
题解: 借鉴线性基和高斯消元的方法,每次插入一个数字,就把它拿之前的元素消元,直到把当前元素消成当前位不存在的情况。如果当前数组不断消元最后为0,那么这个数字可以被其他数组表示。如果当前数组消到第i位时,不存在这样的数组,那么这个数组可以插入线性基内,这个数组不可以被其他数组表示
需要花钱购买该装备。为了这个花费最小,需要贪心从小到大排序
代码:
#include <bits/stdc++.h>
using namespace std;
int const N = 510;
double const eps = 1e-5;
int n, m;
double a[N][N]; // a表示线性基,第i个线性基的第j个元素为a[i][j]
struct COST {
double z[N], cost;
bool operator< (struct COST &t) const {
return cost < t.cost;
}
}cost[N];
// 插入线性基
bool insert(double x[]) {
for (int i = 1; i <= m; ++i) {
if (fabs(x[i]) < eps) continue; // 当前x为0,那么不需要插入
if (fabs(a[i][i]) > eps) { // 如果第i个线性基存在,那么需要对x进行消除操作,把第x[i]变成0
double div = x[i] / a[i][i];
for (int j = i; j <= m; ++j) { // 修改x数组
x[j] -= div * a[i][j];
}
}
else { // 如果第i个线性基不存在
for (int j = i; j <= m; ++j) a[i][j] = x[j]; // 插入到线性基内
return true;
}
}
return false;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cin >> cost[i].z[j];
}
}
for (int i = 1; i <= n; ++i) cin >> cost[i].cost;
sort(cost + 1, cost + n + 1);
// 不断插入线性基,如果能够插入,说明需要花费;否则,不需要花费
double res1 = 0, res2 = 0;
for (int i = 1; i <= n; ++i) {
if (insert(cost[i].z)) {
res1 ++;
res2 += cost[i].cost;
}
}
cout << res1 << " " << res2;
return 0;
}