异或线性基学习笔记
模拟赛考到了,还是板子,但不会,我有抑郁症。
1 基本定义及性质
对于一个集合
- 定义
为 的任意非空的子集异或和的所有可能值所形成的集合,则 。
- 原序列的任意一个数都可以由线性基内部的一些数异或得到。
- 线性基内部的任意数异或起来都不能得到
。 - 线性基内部的数个数唯一,且在保持性质一的前提下,数的个数是最少的。
证明就不讲了,可以去看洛谷日报。
下面的部分涉及后面的内容。
对于线性基还有一个神秘结论:对于任意两个个数
证明:考虑每一个线性基
都可以通过类似高斯消元的方法,使其变成一个等价的线性基 ,满足:
每一个数的最高位,在其它数中对应位是 。 这样对于初始的数,异或每一个数,都不会对其它数是否被异或没有影响,可以将每个数视作是独立的。
再考虑插入数的过程,对于某一位,若中没有一个数使得其最高位是它,则答案不受它影响,我们只考虑其它位。
设两个数的当前位为, 中最高位为这一位的数为 ,分类讨论:
,则 ,则在计算的时候 都会异或 ,对应 ; ,则 ,则在计算的时候 都会异或 ,对应 ; ,则 ,则在计算的时候三个函数都不会异或 ,对应 ; ,则 ,则在计算的时候 都会异或 ,对应 。 注意到对于任意的
, 会异或 , 会异或 , 会异或 。而: 在
时当且仅当: 上式显然成立,所以原命题成立。
2 基本操作
异或线性基的最高位互不相同,所以可以有以下的操作。
2.1 插入
考虑从高位向低位插入,如果这个位置上没有数,就插入,否则强制让这一位置零(就是异或
若无法插入,说明线性基内已有子集的异或和为当前插入的数,说明插入后线性基内有子集元素异或和为
void Add(ll x) {
ll i;
for(i = 62; i >= 0; i--) {
if((x >> i) & 1ll) {
if(!d[i]) {
d[i] = x;
return ;
}
x ^= d[i];
}
}
p0 = true;
}
2.2 查询异或最大值
从高位向低位贪心即可。
ll Query_Max() {
ll res = 0, i;
for(i = 62; i >= 0; i--) {
if((res ^ d[i]) > res) {
res ^= d[i];
}
}
return res;
}
对于初值为 res
的初值设置为
2.3 查询异或最小值
考虑线性基内最小的
所以只需判断能否产生
ll Query_Min() {
ll i;
if(p0) {
return 0;
}
for(i = 0; i <= 62; i++) {
if(d[i]) {
return d[i];
}
}
}
2.4 查询是否在线性基内
因为线性基的最高位互不相同,充分理解后就可以轻松得到以下代码。
bool Count(ll x) {
ll i;
for(i = 62; i >= 0; i--) {
if((x >> i) & 1) {
x ^= d[i];
}
}
return x == 0;
}
2.5 合并线性基
暴力合并即可。
friend Base operator +(Base x, Base y) {
int i;
Base res = x;
for(i = 62; i >= 0; i--) {
if(y.d[i]) {
res.Add(y.d[i]);
}
}
return res;
}
2.6 查询第 小
根据线性基的性质,含有线性基内最高位为
可以从高位到低位枚举,注意特判
ll Query_kth(ll x) {
if(p0) {
x--;
}
ll i, res = 0;
for(i = 62; i >= 0; i--) {
if((x >> i) & 1) {
res ^= d[i];
}
}
return res;
}
2.7 完整代码
综上所述,我们可以得到以下代码:
struct Base {
ll d[65];
bool p0;
void Add(ll x) {
ll i;
for(i = 62; i >= 0; i--) {
if((x >> i) & 1ll) {
if(!d[i]) {
d[i] = x;
return ;
}
x ^= d[i];
}
}
p0 = true;
}
ll Query_Max() {
ll res = 0, i;
for(i = 62; i >= 0; i--) {
if((res ^ d[i]) > res) {
res ^= d[i];
}
}
return res;
}
ll Query_Min() {
ll i;
if(p0) {
return 0;
}
for(i = 0; i <= 62; i++) {
if(d[i]) {
return d[i];
}
}
}
bool Count(ll x) {
ll i;
for(i = 62; i >= 0; i--) {
if((x >> i) & 1) {
x ^= d[i];
}
}
return x == 0;
}
friend Base operator +(Base x, Base y) {
int i;
Base res = x;
for(i = 62; i >= 0; i--) {
if(y.d[i]) {
res.Add(y.d[i]);
}
}
return res;
}
ll Query_kth(ll x) {
if(p0) {
x--;
}
ll i, res = 0;
for(i = 62; i >= 0; i--) {
if((x >> i) & 1) {
res ^= d[i];
}
}
return res;
}
};
3 进阶版——带删除线性基
不会在线的,只能讲讲离线的。
3.1 原理
其实很简单,对于每个线性基内的数,记录一个被删除时间
3.2 插入
增加参数
void Add(ll x, int tim) {
ll i;
for(i = 62; i >= 0; i--) {
if((x >> i) & 1ll) {
if(t[i] < tim) {
swap(t[i], tim), swap(x, d[i]);
}
if(!t[i]) {
return ;
}
x ^= d[i];
}
}
p0 = true;
}
3.3 查询最大值
增加参数
ll Query_Max(int tim) {
ll res = 0, i;
for(i = 62; i >= 0; i--) {
if(tim < t[i] && (res ^ d[i]) > res) {
res ^= d[i];
}
}
return res;
}
4 例题
4.1 就是模拟赛那道题
给定一个序列,长度为
可令每个元素的删除时间为其下标,查询时以满足条件的最左端点的下标为
可使用双指针维护左端点,复杂度
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll Read() {
int sig = 1;
ll num = 0;
char c = getchar();
while(!isdigit(c)) {
if(c == '-') {
sig = -1;
}
c = getchar();
}
while(isdigit(c)) {
num = (num << 3) + (num << 1) + (c ^ 48);
c = getchar();
}
return num * sig;
}
void Write(ll x) {
if(x < 0) {
putchar('-');
x = -x;
}
if(x >= 10) {
Write(x / 10);
}
putchar((x % 10) ^ 48);
}
struct Base {
int t[65];
ll d[65];
bool p0;
void Add(ll x, int tim) {
ll i;
for(i = 62; i >= 0; i--) {
if((x >> i) & 1ll) {
if(t[i] < tim) {
swap(t[i], tim), swap(x, d[i]);
}
if(!t[i]) {
return ;
}
x ^= d[i];
}
}
p0 = true;
}
ll Query_Max(int tim) {
ll res = 0, i;
for(i = 62; i >= 0; i--) {
if(tim <= t[i] && (res ^ d[i]) > res) {
res ^= d[i];
}
}
return res;
}
}b;
const int N = 100005;
struct tNode {
ll p, t;
}a[N];
int main() {
int i, j, n = Read();
ll k = Read();
for(i = 1; i <= n; i++) {
a[i].t = Read(), a[i].p = Read();
}
ll sum = 0, ans = 0;
for(i = 1, j = 1; j <= n; j++) {
sum += a[j].p;
b.Add(a[j].t, j);
while(sum > k) {
sum -= a[i++].p;
}
ans = max(ans, b.Query_Max(i));
}
Write(ans);
return 0;
}
4.2 [WC2011] 最大 XOR 和路径
题目链接
考虑结点
此时答案显然。
考虑添加一些环,与路径不交(如红边所示):
考虑这个环怎么贡献到答案内,只需经过两次绿边,按箭头所示走即可,此时绿边不会统计到答案内。
考虑当环与原路径相交时,即结点
遍历整个图,当遇到已遍历的结点时将这个环统计进答案,可以证明每个环的异或和都可以由若干个这样的环异或和再异或得到。
答案即为链的路径异或和与若干个环异或和异或的最大值,使用线性基即可。
原路径可以为任意路径,具体见代码:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll Read() {
int sig = 1;
ll num = 0;
char c = getchar();
while(!isdigit(c)) {
if(c == '-') {
sig = -1;
}
c = getchar();
}
while(isdigit(c)) {
num = (num << 3) + (num << 1) + (c ^ 48);
c = getchar();
}
return num * sig;
}
void Write(ll x) {
if(x < 0) {
putchar('-');
x = -x;
}
if(x >= 10) {
Write(x / 10);
}
putchar((x % 10) ^ 48);
}
const int N = 100005;
struct Edge {
int to, nxt;
ll w;
}e[N << 1];
int n, head[N], cntedge;
void Add_Edge(int u, int v, ll w) {
e[++cntedge] = {v, head[u], w};
head[u] = cntedge;
}
bool vis[N];
struct Base {
ll d[65];
bool p0;
void Add(ll x) {
ll i;
for(i = 62; i >= 0; i--) {
if((x >> i) & 1ll) {
if(!d[i]) {
d[i] = x;
return ;
}
x ^= d[i];
}
}
p0 = true;
}
ll Query_Max(ll x) {
ll res = x, i;
for(i = 62; i >= 0; i--) {
if((res ^ d[i]) > res) {
res ^= d[i];
}
}
return res;
}
ll Query_Min() {
ll i;
if(p0) {
return 0;
}
for(i = 0; i <= 62; i++) {
if(d[i]) {
return d[i];
}
}
}
bool Count(ll x) {
ll i;
for(i = 62; i >= 0; i--) {
if((x >> i) & 1) {
x ^= d[i];
}
}
return x == 0;
}
friend Base operator +(Base x, Base y) {
int i;
Base res = x;
for(i = 62; i >= 0; i--) {
if(y.d[i]) {
res.Add(y.d[i]);
}
}
return res;
}
}b;
ll xorsum[N];
void Dfs(int u, ll cur) {
xorsum[u] = cur, vis[u] = true;
int i;
for(i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
ll w = e[i].w;
if(vis[v]) {
b.Add(cur ^ w ^ xorsum[v]);
}
else {
Dfs(v, cur ^ w);
}
}
}
int main() {
int m;
n = Read(), m = Read();
while(m--) {
int u = Read(), v = Read();
ll w = Read();
Add_Edge(u, v, w), Add_Edge(v, u, w);
}
Dfs(1, 0);
Write(b.Query_Max(xorsum[n]));
return 0;
}
4.2.1 [WC2011] 最大 XOR 和路径加强版
令原问题起点为
考虑上面的结论,然后可以预处理
#include <bits/stdc++.h>
#define ll long long
using namespace std;
inline ll Read() {
int sig = 1;
ll num = 0;
char c = getchar();
while(!isdigit(c)) { if(c == '-') sig = -1; c = getchar(); }
while(isdigit(c)) num = (num << 3) + (num << 1) + (c ^ 48), c = getchar();
return num * sig;
}
void Write(__int128 x) {
if(x < 0) putchar('-'), x = -x;
if(x >= 10) Write(x / 10);
putchar((x % 10) ^ 48);
}
const int N = 100005;
int n, m;
bool vis[N];
vector<pair<int, ll> > e[N];
ll xs[N], as[62][N], bs[62][N], ans[62][N];
struct Base {
ll b[62];
void Insert(ll x) {
for(int i = 61; i >= 0; i--) if((x >> i) & 1) {
if(!b[i]) { b[i] = x; return ; }
else x ^= b[i];
}
}
ll Query_Max(ll x) { for(int i = 61; i >= 0; i--) if((x ^ b[i]) > x) x ^= b[i]; return x; }
ll Query_Min(ll x) { for(int i = 61; i >= 0; i--) if((x ^ b[i]) < x) x ^= b[i]; return x; }
}base;
void Dfs(int u, ll cxs) {
xs[u] = cxs, vis[u] = true;
for(auto p : e[u]) {
int v = p.first; ll w = p.second;
if(vis[v]) base.Insert(w ^ cxs ^ xs[v]);
else Dfs(v, cxs ^ w);
}
}
int main() {
int i, j, q; n = Read(), m = Read(), q = Read();
for(i = 1; i <= m; i++) { int u = Read(), v = Read(); ll w = Read(); e[u].emplace_back(v, w), e[v].emplace_back(u, w); }
Dfs(1, 0);
for(i = 1; i <= n; i++) {
ll a = base.Query_Max(xs[i]), b = base.Query_Min(xs[i]);
for(j = 60; j >= 0; j--) as[j][i] = (a >> j) & 1, bs[j][i] = (b >> j) & 1;
}
for(i = 1; i <= n; i++) for(j = 0; j <= 60; j++) {
ans[j][i] += ans[j][i - 1] + (as[j][i] ? (i - 1 - bs[j][i - 1]) : bs[j][i - 1]);
as[j][i] += as[j][i - 1], bs[j][i] += bs[j][i - 1];
}
while(q--) {
int l = Read(), r = Read();
__int128 res = 0;
for(i = 0; i <= 60; i++) res += ((__int128)(1ll << i)) * ((__int128)(ans[i][r] - ans[i][l - 1] - (as[i][r] - as[i][l - 1]) * (l - 1 - bs[i][l - 1]) - (r - l + 1 - as[i][r] + as[i][l - 1]) * bs[i][l - 1]));
Write(res), putchar('\n');
}
return 0;
}
4.3 DZY Loves Chinese II
题目链接
正常做很难,考虑人类智慧。
如图,如果想要把
若我们能给每个
考虑如下构造:
- 对原图
随机一个生成树 。 - 对所有的
, 为一随机权值。 - 遍历整棵树,对于
,设 ,且 是 的父亲, 为端点为 的其它边的边权的异或和。
对于
假设对于
这样问题就转化为对于每个
#include <bits/stdc++.h>
#define ll long long
#define ull unsigned long long
using namespace std;
ll Read() {
int sig = 1;
ll num = 0;
char c = getchar();
while(!isdigit(c)) {
if(c == '-') {
sig = -1;
}
c = getchar();
}
while(isdigit(c)) {
num = (num << 3) + (num << 1) + (c ^ 48);
c = getchar();
}
return num * sig;
}
void Write(ll x) {
if(x < 0) {
putchar('-');
x = -x;
}
if(x >= 10) {
Write(x / 10);
}
putchar((x % 10) ^ 48);
}
const int N = 100005, M = 500005;
struct Base {
ull d[65];
bool p0;
void Init() {
memset(d, 0, sizeof(d));
p0 = false;
}
void Add(ull x) {
ll i;
for(i = 63; i >= 0; i--) {
if((x >> i) & 1ll) {
if(!d[i]) {
d[i] = x;
return ;
}
x ^= d[i];
}
}
p0 = true;
}
}b;
struct Edge {
int to, nxt;
bool is_tree;
ull w;
}e[M << 1];
int n, head[N], cntedge = 1;
void Add_Edge(int u, int v) {
e[++cntedge] = {v, head[u], 0ll, 0ll};
head[u] = cntedge;
}
bool vis[N];
void Dfs(int u) {
vis[u] = true;
int i;
for(i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(vis[v]) {
continue;
}
e[i].is_tree = e[i ^ 1].is_tree = true;
Dfs(v);
}
}
ull xorw[N];
void Dfs2(int u) {
vis[u] = true;
int i;
for(i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(vis[v] || !e[i].is_tree) {
continue;
}
Dfs2(v);
e[i].w = e[i ^ 1].w = xorw[v];
xorw[u] ^= xorw[v], xorw[v] = 0;
}
}
mt19937_64 rnd;
int main() {
rnd.seed(time(0));
int i, j, m;
n = Read(), m = Read();
for(i = 1; i <= m; i++) {
int u = Read(), v = Read();
Add_Edge(u, v), Add_Edge(v, u);
}
Dfs(1);
for(i = 1; i <= n; i++) {
for(j = head[i]; j; j = e[j].nxt) {
if(!e[j].is_tree) {
e[j].w = e[j ^ 1].w = rnd();
}
}
}
for(i = 1; i <= n; i++) {
for(j = head[i]; j; j = e[j].nxt) {
xorw[i] ^= e[j].w;
}
}
memset(vis, 0, sizeof(vis));
Dfs2(1);
int q = Read(), cnt = 0;
while(q--) {
int k = Read();
b.Init();
while(k--) {
int x = Read() ^ cnt;
b.Add(e[x * 2].w);
}
if(b.p0) {
printf("Disconnected\n");
}
else {
printf("Connected\n"), cnt++;
}
}
return 0;
}
4.4 玛里苟斯
题目链接
考虑对不同
考虑每一位对答案的贡献,可以证明对于二进制下的每一位,若一个数在该位的值为
,其对答案没有影响。
所以设个数在该位的值为 ,因为可以证明异或得到的数该位为 的方案数为: 所以异或得到的数该位为
的概率为 ,因此若 ,该位是从低位往高位数第 位,则其对答案的贡献为 。
暴力统计即可。
令
, 。
考虑二进制拆位后再平方,考虑从低位往高位数第位与第 位的贡献,根据 的结论有(下文的贡献系数指,若对答案的实际贡献为 ,其贡献系数为 ):
- 若
,且有数 满足 ,则其对答案的贡献系数是 。 - 若
,则:
- 若有两个数,使得有数
满足 :
- 若有数
满足 , 与 异或后会有 的贡献系数; - 否则会有
的贡献系数。 - 否则,若有两个数
使得 ,两数异或后会有 的贡献系数。 - 否则贡献系数为
。 分类讨论即可。
注意到若有数
,则答案至少有 。
因此由答案小于,可得每个数小于 。
当时,可得每个数小于 。
将数插入线性基,暴搜即可。
#include <bits/stdc++.h>
#define ll __int128
using namespace std;
ll Read() {
int sig = 1;
ll num = 0;
char c = getchar();
while(!isdigit(c)) {
if(c == '-') {
sig = -1;
}
c = getchar();
}
while(isdigit(c)) {
num = (num << 3) + (num << 1) + (c ^ 48);
c = getchar();
}
return num * sig;
}
void Write(ll x) {
if(x < 0) {
putchar('-');
x = -x;
}
if(x >= 10) {
Write(x / 10);
}
putchar((x % 10) ^ 48);
}
struct Base {
ll d[65];
int cnt = 0;
void Add(ll x) {
ll i;
for(i = 62; i >= 0; i--) {
if((x >> i) & 1ll) {
if(!d[i]) {
d[i] = x;
cnt++;
return ;
}
x ^= d[i];
}
}
}
ll Dfs(int u, ll res, ll k) {
if(u < 0) {
return (k == 3) ? res * res * res : ((k == 4) ? res * res * res * res : res * res * res * res * res);
}
return d[u] ? (Dfs(u - 1, res, k) + Dfs(u - 1, res ^ d[u], k)) : Dfs(u - 1, res, k);
}
}b;
int n, k;
bool cnt[2][65][65], cnts[65];
int main() {
n = Read(), k = Read();
if(k == 1) {
ll ans = 0;
while(n--) {
ll a = Read();
ans |= a;
}
Write(ans / 2);
if(ans & 1) {
printf(".5");
}
return 0;
}
else if(k == 2) {
ll ans = 0, i, j;
while(n--) {
ll a = Read();
for(i = 0; i <= 63; i++) {
if((a >> i) & 1ll) {
for(j = 0; j <= 63; j++) {
cnt[(a >> j) & 1ll][i][j] = true;
}
cnts[i] = true;
}
}
}
for(i = 0; i <= 63; i++) {
for(j = i + 1; j <= 63; j++) {
ll w = 0;
if(cnt[1][i][j]) {
if(cnt[0][i][j] || cnt[0][j][i]) {
w = 1;
}
else {
w = 2;
}
}
if(cnt[0][i][j] && cnt[0][j][i]) {
w = 1;
}
ans += w * (1ll << (i + j));
}
if(cnts[i]) {
ans += (1ll << (2ll * i));
}
}
Write(ans / 2);
if(ans & 1) {
printf(".5");
}
return 0;
}
else {
while(n--) {
ll a = Read();
b.Add(a);
}
ll x = b.Dfs(22, 0, k);
Write(x / (1ll << b.cnt));
if((x / (1ll << (b.cnt - 1))) & 1) {
printf(".5");
}
return 0;
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】