线性基
线性基
定义:给定数集 \(S\) ,以异或运算张成的数集与 \(S\) 相同的极大线性无关集,称为原数集的一个线性基。
性质:
- 原数集的任意数均可通过线性基异或得到。
- 线性基里任意一个子集异或和非 \(0\) 。
- 一个数集可能有多个线性基,但是线性基大小唯一,且是满足性质一是大小最小的。
- 值域为 \(V\) 的数集的线性基大小上界为 \(\log{V}\) 。
基本操作
记数组 \(p\) 表示数集的线性基,钦定若 \(p_i\) 有值则 \(p_i\) 的第 \(i\) 位为 \(1\) 且 \(> i\) 的位为 \(0\) 。
插入、查询存在性
从高到低枚举 \(k\) 的每一位,判断 \(p_i\) 这个位置是否有数,没有就插入并退出,否则将 \(k\) 异或上 \(p_i\) 继续判断(把最高位不断消掉)。
如果没有成功插入这个数,则说明这个数可以被线性基表示,无需插入。
时间复杂度 \(O(\log V)\) ,不难发现插入操作同时可以用于查询一个数是否能被线性基表示。
inline bool insert(int k) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i];
else {
++siz, p[i] = k;
return true;
}
}
return zero = true, false;
}
inline bool test(int k) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i];
else
return false;
}
return true;
}
求子集与某数的异或最值
因为低位不会影响高位,直接从线性基的最高位开始贪心即可,时间复杂度 \(O(\log V)\) 。
inline int querymax(int res = 0) {
for (int i = B - 1; ~i; --i)
if ((res ^ p[i]) > res)
res ^= p[i];
return res;
}
inline int querymin(int res) {
for (int i = B - 1; ~i; --i)
if ((res ^ p[i]) < res)
res ^= p[i];
return res;
}
求数集异或最值
异或最大值直接调用 querymax(0)
即可。
异或最小值一般就是线性基里的最小元素,因为插入这个元素的时候我们总是尽量让它的高位全消掉才来插入这一位。
需要特判原数集存在子集异或和为 \(0\) 的情况,时间复杂度 \(O(\log V)\) 。
inline int querymin() {
if (zero)
return 0;
for (int i = 0; i < B; ++i)
if (p[i])
return p[i];
return -1;
}
查询异或k小值
首先考虑,如果每一位的选择互不影响,那么将 \(k\) 二进制分解后选择即可。
但线性基不一定满足这个性质,于是考虑重新构造一个线性基。尽量把原来的线性基只留下最高位的 \(1\) ,剩下的位都用小的基底消掉,从而使得线性基中的数的异或值不会相互影响。
时间复杂度 \(O(\log^2 V)\) 。
inline void rebuild() {
for (int i = B - 1; ~i; --i)
for (int j = i - 1; ~j; --j)
if (p[i] >> j & 1)
p[i] ^= p[j];
}
inline int kth(int k) {
rebuild();
vector<int> d;
for (int i = 0; i < B; ++i)
if (p[i])
d.emplace_back(p[i]);
if (zero)
--k;
if (k >= (1 << d.size()))
return -1;
int res = 0;
for (int i = d.size() - 1; ~i; --i)
if (k >> i & 1)
res ^= d[i];
return res;
}
查询排名
不难理解。
inline int getrank(int k) {
int rk = 0;
for (int i = 0, now = 1; i < B; ++i)
if (p[i]) {
if (k >> i & 1)
rk += now;
now <<= 1;
}
return rk;
}
求并
将 \(A\) 的每个元素插入 \(B\) 即可,时间复杂度 \(O(\log^2 V)\) 。
inline friend LinearBasis operator | (LinearBasis a, LinearBasis b) {
for (int i = 0; i < B; ++i)
if (b.p[i])
a.insert(b.p[i]);
return a;
}
求交
引理:若 \(V_1, V_2\) 是线性空间,\(B_1, B_2\) 是它们的一组基。令 \(W = B_2 \cap V_1\) ,若 \(B_1 \cup (B_2 \setminus W)\) 线性无关,则 \(W\) 是 \(V_1 \cap V_2\) 的一组基。
证明:考虑对于任意 \(x \in V_1 \cap V_2\) ,假设 \(x\) 不能被 \(W\) 表示,则 \(v\) 一定可以被 \(S \cup T\) 表示,其中 \(S \subseteq W, T \subseteq B_2 \setminus W, T \ne \emptyset\) ,那么此时 \(T \cup B_1\) 一定线性相关,矛盾。
因此对于求 \(A \cap B\) ,若 \(B_i\) 能被 \(A_{1 \sim x} \cup B_{1 \sim i - 1}\) 表示,则把 \(\bigoplus_{i = 1}^x A_i\) 或 \(\bigoplus_{j = 1}^{i - 1} B_j\) 加入交集的线性基中即可。
inline friend LinearBasis operator & (LinearBasis a, LinearBasis b) {
LinearBasis d = a, all = a; // all 表示目前所有可用的低位基
a = LinearBasis();
for (int i = B - 1; ~i; --i) {
if (!b.p[i])
continue;
ull k = 0, v = b.p[i]; // k 是把 b[i] 消至 0 所用到的 a 的异或和
for (int j = i; ~j; --j)
if (v >> j & 1) {
if (all.p[j])
v ^= all.p[j], k ^= d.p[j];
else {
all.p[j] = v, d.p[j] = k;
break;
}
}
if (!v)
a.insert(k);
}
return a;
}
应用
基本应用
P4301 [CQOI2013] 新Nim游戏
有 \(n\) 堆石子,第 \(i\) 堆有 \(a_i\) 个。第一回合双方可以任意拿走若干堆棋子,第二回合开始与传统 Nim 游戏一样,求先手必胜时第一回合拿走的最小石子数,或报告无解。
\(n \le 100\)
若在第一回合拿走石子后,剩下的石子存在异或和为 \(0\) 的子集,那么对手保留这个子集则会造成先手必败的局面。
考虑贪心从大到小把数加入线性基,若插入失败则必须取走。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 1e2 + 7, B = 31;
struct LinearBasis {
int p[B];
inline bool insert(int x) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i];
else
return p[i] = k, true;
}
return false;
}
} lb;
int a[N];
int n;
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
sort(a + 1, a + 1 + n, greater<int>());
ll ans = 0;
for (int i = 1; i <= n; ++i)
if (!lb.insert(a[i]))
ans += a[i];
printf("%lld", ans);
return 0;
}
P4839 P 哥的桶
维护 \(n\) 个初始为空的集合,\(m\) 次操作:
- 向某个集合插入一个数。
- 查询一段区间集合并的子集异或最大值。
\(n, m \le 5 \times 10^4\)
线段树维护线性基区间集合并即可,时间复杂度 \(O((n + m) \log n \log^2 V)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 7, B = 31;
struct LinearBasis {
int p[B];
inline void insert(int k) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i];
else {
p[i] = k;
break;
}
}
}
inline int querymax() {
int res = 0;
for (int i = B - 1; ~i; --i)
if ((res ^ p[i]) > res)
res ^= p[i];
return res;
}
inline friend LinearBasis getcup(LinearBasis a, LinearBasis b) {
for (int i = 0; i < B; ++i)
if (b.p[i])
a.insert(b.p[i]);
return a;
}
};
int n, m;
namespace SMT {
LinearBasis lb[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
void update(int x, int nl, int nr, int pos, int k) {
lb[x].insert(k);
if (nl == nr)
return;
int mid = (nl + nr) >> 1;
if (pos <= mid)
update(ls(x), nl, mid, pos, k);
else
update(rs(x), mid + 1, nr, pos, k);
}
LinearBasis query(int x, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r)
return lb[x];
int mid = (nl + nr) >> 1;
if (r <= mid)
return query(ls(x), nl, mid, l, r);
else if (l > mid)
return query(rs(x), mid + 1, nr, l, r);
else
return getcup(query(ls(x), nl, mid, l, r), query(rs(x), mid + 1, nr, l, r));
}
} // namespace SMT
signed main() {
scanf("%d%d", &m, &n);
while (m--) {
int op, x, y;
scanf("%d%d", &op, &x, &y);
if (op == 1)
SMT::update(1, 1, n, x, y);
else
printf("%d\n", SMT::query(1, 1, n, x, y).querymax());
}
return 0;
}
P11620 [Ynoi Easy Round 2025] TEST_34
维护数列 \(a_{1 \sim n}\) ,\(m\) 次操作:
- 将区间 \([l, r]\) 中所有数异或 \(k\) 。
- 求区间中所有子序列的异或和异或 \(v\) 的最大值。
\(n, m \le 5 \times 10^4\)
考虑引入差分数组 \(b_i = \oplus_{j = 1}^i a_i\) ,则区间修改可以转化为单点修改。
查询时注意到 \(a_{l \sim r}\) 的线性基与 \(a_l \cup b_{l + 1 \sim r}\) 的线性基等价,因此只要合并两个线性基查询即可。
线段树维护即可做到 \(O(m \log n \log^2 V)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e4 + 7, B = 31;
struct LinearBasis {
int p[B];
inline LinearBasis() {
memset(p, 0, sizeof(p));
}
inline void insert(int k) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i];
else {
p[i] = k;
break;
}
}
}
inline int querymax(int res) {
for (int i = B - 1; ~i; --i)
if ((res ^ p[i]) > res)
res ^= p[i];
return res;
}
inline friend LinearBasis operator | (LinearBasis a, LinearBasis b) {
for (int i = 0; i < B; ++i)
if (b.p[i])
a.insert(b.p[i]);
return a;
}
};
int a[N];
int n, m;
namespace BIT {
int c[N];
inline void update(int x, int k) {
for (; x <= n; x += x & -x)
c[x] ^= k;
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res ^= c[x];
return res;
}
} // namespace BIT
namespace SMT {
LinearBasis lb[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
void update(int x, int nl, int nr, int pos, int k) {
if (nl == nr) {
lb[x] = LinearBasis(), lb[x].insert(k);
return;
}
int mid = (nl + nr) >> 1;
if (pos <= mid)
update(ls(x), nl, mid, pos, k);
else
update(rs(x), mid + 1, nr, pos, k);
lb[x] = lb[ls(x)] | lb[rs(x)];
}
LinearBasis query(int x, int nl, int nr, int l, int r) {
if (l <= nl && nr <= r)
return lb[x];
int mid = (nl + nr) >> 1;
if (r <= mid)
return query(ls(x), nl, mid, l, r);
else if (l > mid)
return query(rs(x), mid + 1, nr, l, r);
else
return query(ls(x), nl, mid, l, r) | query(rs(x), mid + 1, nr, l, r);
}
} // namespace SMT
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) {
scanf("%d", a + i);
BIT::update(i, a[i]), BIT::update(i + 1, a[i]);
SMT::update(1, 1, n, i, a[i] ^ a[i - 1]);
}
while (m--) {
int op, l, r, k;
scanf("%d%d%d%d", &op, &l, &r, &k);
if (op == 1) {
BIT::update(l, k), SMT::update(1, 1, n, l, BIT::query(l) ^ BIT::query(l - 1));
if (r + 1 <= n)
BIT::update(r + 1, k), SMT::update(1, 1, n, r + 1, BIT::query(r + 1) ^ BIT::query(r));
} else {
if (l == r) {
printf("%d\n", max(k, k ^ BIT::query(l)));
continue;
}
LinearBasis res = SMT::query(1, 1, n, l + 1, r);
res.insert(BIT::query(l));
printf("%d\n", res.querymax(k));
}
}
return 0;
}
UOJ698. 【候选队互测2022】枪打出头鸟
给定集合 \(S_{1 \sim n}\) ,\(q\) 次询问,每次询问下标最小的不存在子集异或和为 \(k\) 的集合。
\(n \le 10^5\) ,\(q \le 10^6\)
设 \(A_i\) 表示 \(S_{1 \sim i}\) 线性基的交集,则可以二分找到最小的不能表示 \(k\) 的 \(A_i\) 即为答案,时间复杂度 \(O(n \log^2 V + q \log n \log^2 V)\) ,无法通过。
注意到后面的线性基是前面线性基的子线性基,考虑按照基底的消失时间建立一个新的线性基,每次查询这个新线性基中表示 \(k\) 的所有基底中第一个消失的即可。
时间复杂度 \(O(n \log^2 V + q \log V)\) 。
#include <bits/stdc++.h>
typedef unsigned long long ull;
using namespace std;
const int N = 1e5 + 7, B = 64;
int n, q;
struct LinearBasis {
ull p[B];
int d[B];
inline LinearBasis() {
memset(p, 0, sizeof(p));
}
inline void insert(ull k, int id = 0) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i];
else {
p[i] = k, d[i] = id;
break;
}
}
}
inline bool test(ull k) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i];
else
return false;
}
return !k;
}
inline int query(ull k) {
int res = n + 1;
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i], res = min(res, d[i] + 1);
else
return 1;
}
return res;
}
inline friend LinearBasis operator & (LinearBasis a, LinearBasis b) {
LinearBasis d = a, all = a; // all 表示目前所有可用的低位基
a = LinearBasis();
for (int i = B - 1; ~i; --i) {
if (!b.p[i])
continue;
ull k = 0, v = b.p[i]; // k 是把 b[i] 消至 0 所用到的 a 的异或和
for (int j = i; ~j; --j)
if (v >> j & 1) {
if (all.p[j])
v ^= all.p[j], k ^= d.p[j];
else {
all.p[j] = v, d.p[j] = k;
break;
}
}
if (!v)
a.insert(k);
}
return a;
}
} lb[N];
signed main() {
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; ++i) {
int k;
scanf("%d", &k);
while (k--) {
ull x;
scanf("%llu", &x);
lb[i].insert(x);
}
if (i > 1)
lb[i] = lb[i] & lb[i - 1];
}
LinearBasis all;
for (int i = n; i; --i)
for (int j = B - 1; ~j; --j)
if (lb[i].p[j])
all.insert(lb[i].p[j], i);
while (q--)
printf("%d\n", all.query(read<ull>()));
return 0;
}
QOJ7141. Power of three
有一个 \(n\) 行 \(m\) 列的矩阵,定义该矩阵的权值为:初始权值为 \(0\) ,考虑每一列,若第 \(i\) 列有奇数个 \(1\) ,则权值增加 \(a_i \times 3^{b_i}\) 。
可以删除一些行,最大化矩阵权值。
\(n \le 2 \times 10^5\) ,\(m \le 70\) ,\(a_i \in \{ -1, 1 \}\) ,\(1 \le b_i \le 35\) ,保证不存在 \((a_i, b_i) = (a_j, b_j)\)
观察权值的贡献形式,自然想到贪心最大化高位。
先考虑 \(b_i\) 互异的情况,问题转化为给出 \(n\) 个 \(m\) 位二进制数,求子集异或得到的数对应的最大权值,建立线性基后从高到低贪心确定即可。
考虑一般情况,不难发现有影响的就是存在 \(b_i = b_j, a_i = 1, a_j = -1\) 的情况,此时有可能 \(i, j\) 并不是独立的。
此时仍然可以贪心,若能同时操作二者使得权值变大则操作,否则怎么操作这一位的权值均不变,将其视为一个更小的向量加入线性基即可。
使用 bitset
维护线性基,时间复杂度 \(O(\frac{nm^2 + m^3}{\omega})\) 。
实现时可以将 \((1, x)\) 和 \((-1, x)\) 放在 \(2x - 1\) 和 \(2x - 2\) 位上,考虑到一个偶数位时则尝试消去,否则若满足:
- 下一位有值:可以调整这次操作的影响。
- 该基底下一位没有值:不影响下一位。
- 答案该位与下一位不同:可能可以使权值由 \(-3^x\) 加到 \(3^x\) 。
三者之一则尝试异或,否则将其视为更小的基底插入。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e5 + 7, M = 7e1 + 7, B = 37;
typedef bitset<B * 2> Bit;
struct LinearBasis {
Bit p[M];
inline void insert(Bit k) {
for (int i = B * 2 - 1; ~i; --i)
if (k.test(i)) {
if (p[i].none()) {
p[i] = k;
return;
}
k ^= p[i];
}
}
} lb;
ll pw[B];
int a[M], b[M];
char str[N][M];
int n, m;
signed main() {
pw[0] = 1;
for (int i = 1; i < B; ++i)
pw[i] = pw[i - 1] * 3;
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%s", str[i] + 1);
for (int i = 1; i <= m; ++i)
scanf("%d%d", a + i, b + i);
for (int i = 1; i <= n; ++i) {
Bit k;
for (int j = 1; j <= m; ++j)
if (str[i][j] == '1')
k.set(b[j] * 2 - (a[j] > 0 ? 1 : 2));
lb.insert(k);
}
Bit res;
for (int i = B * 2 - 1; ~i; --i) {
if (lb.p[i].none())
continue;
Bit now = lb.p[i];
if (~i & 1) {
if (res.test(i))
res ^= now;
} else if (lb.p[i - 1].any() || !now.test(i - 1) || res.test(i) != res.test(i - 1)) {
if (!res.test(i))
res ^= now;
} else
now.reset(i), now.reset(i - 1), lb.insert(now);
}
ll ans = 0;
for (int i = 1; i < B; ++i) {
if (res.test(i * 2 - 1))
ans += pw[i];
if (res.test(i * 2 - 2))
ans -= pw[i];
}
printf("%lld", ans);
return 0;
}
前缀线性基
CF1100F Ivan and Burgers
给定 \(a_{1 \sim n}\) ,\(q\) 次询问从给出区间中选若干位置,最大化异或和。
\(n, q \le 5 \times 10^5\)
若询问区间都是前缀,那么只要预处理所有前缀的线性基即可。
一个朴素的想法是用线段树查询区间的线性基,时间复杂度 \(O(n \log^2 V + q \log n \log^2 V)\) ,无法通过。
思考询问非前缀的难点,发现有的位置在 \([1, r]\) 时是有值的,而在 \([l, r]\) 时没有值。
考虑维护所有前缀的线性基,但是每个位置的值均保留出现下标最靠后的。这样查询最大值时只要在 \([1, r]\) 的线性基中查询该位置下标 \(\ge l\) 的值的异或最大值即可。
时间复杂度 \(O((n + q) \log V)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 7, B = 21;
struct LinearBasis {
int p[B], d[B];
inline void insert(int k, int id) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (!p[i]) {
p[i] = k, d[i] = id;
break;
} else {
if (id > d[i])
swap(p[i], k), swap(d[i], id);
k ^= p[i];
}
}
}
inline int querymax(int l) {
int res = 0;
for (int i = B - 1; ~i; --i)
if (d[i] >= l && (res ^ p[i]) > res)
res ^= p[i];
return res;
}
} lb[N];
int a[N];
int n, q;
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), lb[i] = lb[i - 1], lb[i].insert(a[i], i);
scanf("%d", &q);
while (q--) {
int l, r;
scanf("%d%d", &l, &r);
printf("%d\n", lb[r].querymax(l));
}
return 0;
}
P4570 [BJWC2011] 元素
给出 \(n\) 个二元组 \((x_i, y_i)\) ,需要选出若干元素满足任意一个子集的 \(x\) 异或和不为 \(0\) ,求 \(\sum y\) 的最大值。
\(n \le 10^3\)
线性基模板,插入时尽量留下大的 \(y\) 即可。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int B = 64;
struct LinearBasis {
ll p[B];
int w[B];
inline void insert(ll k, int id) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i]) {
if (id > w[i])
swap(p[i], k), swap(w[i], id);
k ^= p[i];
} else {
p[i] = k, w[i] = id;
break;
}
}
}
} lb;
int n;
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
ll x;
int y;
scanf("%lld%d", &x, &y);
lb.insert(x, y);
}
int ans = 0;
for (int i = 0; i < B; ++i)
if (lb.p[i])
ans += lb.w[i];
printf("%d", ans);
return 0;
}
P3292 [SCOI2016] 幸运数字
给出一棵包含 \(n\) 个点的树,\(q\) 次询问 \(\max_{V \subseteq Path(x, y)} \bigoplus_{x \in V} a_x\) 。
\(n \le 2 \times 10^4\),\(q \le 2 \times 10^5\)
先将路径拆为两条到 LCA 的链。
使用线性基维护答案,对于每个线性基,贪心的让高位的向量对应的点深度尽量大,查询时只要查询深度合法的点即可。
时间复杂度 \(O(n \log^2 V)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 2e4 + 7, LOGN = 15, Q = 2e5 + 7, B = 63;
struct LinearBasis {
ll p[B];
int d[B];
inline LinearBasis() {
memset(p, 0, sizeof(p));
memset(d, -1, sizeof(d));
}
inline void insert(ll k, int id) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i]) {
if (id > d[i])
swap(p[i], k), swap(d[i], id);
k ^= p[i];
} else {
p[i] = k, d[i] = id;
break;
}
}
}
inline ll querymax() {
ll res = 0;
for (int i = B - 1; ~i; --i)
if ((res ^ p[i]) > res)
res ^= p[i];
return res;
}
} ans[Q];
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<pair<int, int> > qry[N];
ll a[N];
int fa[N][LOGN], dep[N];
int n, q;
template <class T = int>
inline T read() {
char c = getchar();
bool sign = (c == '-');
while (c < '0' || c > '9')
c = getchar(), sign |= (c == '-');
T x = 0;
while ('0' <= c && c <= '9')
x = (x << 1) + (x << 3) + (c & 15), c = getchar();
return sign ? (~x + 1) : x;
}
void dfs1(int u, int f) {
fa[u][0] = f, dep[u] = dep[f] + 1;
for (int i = 1; i < LOGN; ++i)
fa[u][i] = fa[fa[u][i - 1]][i - 1];
for (int v : G.e[u])
if (v != f)
dfs1(v, u);
}
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int i = 0, h = dep[x] - dep[y]; h; ++i, h >>= 1)
if (h & 1)
x = fa[x][i];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
inline void dfs2(int u, LinearBasis lb) {
lb.insert(a[u], dep[u]);
for (auto it : qry[u])
for (int i = B; ~i; --i)
if (lb.d[i] >= it.first)
ans[it.second].insert(lb.p[i], dep[u]);
for (int v : G.e[u])
if (v != fa[u][0])
dfs2(v, lb);
}
signed main() {
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; ++i)
scanf("%lld", a + i);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
dfs1(1, 0);
for (int i = 1; i <= q; ++i) {
int x, y;
scanf("%d%d", &x, &y);
int lca = LCA(x, y);
qry[x].emplace_back(dep[lca], i), qry[y].emplace_back(dep[lca], i);
}
dfs2(1, LinearBasis());
for (int i = 1; i <= q; ++i)
printf("%lld\n", ans[i].querymax());
return 0;
}
P12827 「DLESS-2」XOR and Even
给定 \(a_{1 \sim n}\) ,\(q\) 次询问,询问有两种:
- 在 \([l, r]\) 中选偶数个数(可以不选),使得异或和 \(\le x\) ,求方案数。
- 在 \([l, r]\) 中选偶数个数(可以不选),最大化这些数和 \(x\) 的异或和。
\(n, q \le 5 \times 10^5\) ,\(a_i, x \in [0, 2^{30} - 1]\)
考虑令 \(b_i = a_i \oplus a_{i + 1}\) ,则其等价于在 \(b_{l \sim r - 1}\) 中选任意数异或,同理选奇数个只要强制钦定选 \(a_l\) 即可。
还有一种方法是在每个数的极高位赋上 \(1\) ,则第一问为了异或和 \(\le x\) 就一定会选偶数个,第二问只要在 \(x\) 极高位赋上 \(1\) 后同理也会选偶数个。
第二问是简单的,第一问可以单侧递归,因为每层只有一个有效值(否则恒 \(> x\) 或恒 \(< x\) ,该部分容易求)。
采用前缀线性基,时间复杂度 \(O((n + q) \log V)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Mod = 1e9 + 7;
const int N = 5e5 + 7, B = 30;
struct LinearBasis {
int p[B], d[B];
int siz;
inline LinearBasis() {
memset(p, 0, sizeof(p)), memset(d, 0, sizeof(d)), siz = 0;
}
inline void insert(int k, int id) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (!p[i]) {
p[i] = k, d[i] = id, ++siz;
break;
} else {
if (id > d[i])
swap(p[i], k), swap(d[i], id);
k ^= p[i];
}
}
}
inline int size(int lim, int high = B - 1) {
int siz = 0;
for (int i = high; ~i; --i)
siz += (p[i] && d[i] >= lim);
return siz;
}
ll dfs(int x, int lim, int now, int k) {
if (x == -1)
return (now <= k);
else if (!p[x] || d[x] < lim)
return dfs(x - 1, lim, now, k);
int y = now >> (x + 1), z = k >> (x + 1);
if (y > z)
return 0;
else if (y < z)
return 1 << size(lim, x);
else
return dfs(x - 1, lim, now, k) + dfs(x - 1, lim, now ^ p[x], k);
}
inline int querymax(int lim, int k) {
for (int i = B - 1; ~i; --i)
if (p[i] && d[i] >= lim)
k = max(k, k ^ p[i]);
return k;
}
} lb[N];
int a[N], pw[N];
int n, q;
inline int mi(int a, int b) {
int res = 1;
for (; b; b >>= 1, a = 1ll * a * a % Mod)
if (b & 1)
res = 1ll * res * a % Mod;
return res;
}
signed main() {
pw[0] = 1;
for (int i = 1; i < N; ++i)
pw[i] = 2ll * pw[i - 1] % Mod;
int T;
scanf("%d", &T);
while (T--) {
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i < n; ++i)
lb[i] = lb[i - 1], lb[i].insert(a[i] ^ a[i + 1], i);
while (q--) {
int op, l, r, x;
scanf("%d%d%d%d", &op, &l, &r, &x);
printf("%d\n", op ? lb[r - 1].querymax(l, x) :
lb[r - 1].dfs(B - 1, l, 0, x) * pw[r - l - lb[r - 1].size(l)] % Mod);
}
}
return 0;
}
图上异或边权环
P4151 [WC2011] 最大XOR和路径
给定一张带权无向图,求一条 \(1\) 到 \(n\) 的路径(不一定是简单路径),最大化路径上边权异或和。
\(n \le 5 \times 10^4\) ,\(m \le 10^5\)
注意到对于一个环,可以选整个环的边权异或和或不选,因为走到环和走回来边权会抵消。
先找出所有的环,由于这里的边权是异或的,因此只要将 dfs 树上一条返祖边与若干树边组成的环丢进线性基即可,可以证明任意一个环的边权异或和可以由这些环线性组合得到。
最后查询与任意一条合法路径的权值与所有环的子集的异或最大值即可,时间复杂度 \(O(n \log V)\) 。
类似的题(最大改最小):CF845G Shortest Path Problem?
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 5e4 + 7, B = 64;
struct Graph {
vector<pair<int, ll> > e[N];
inline void insert(int u, int v, ll w) {
e[u].emplace_back(v, w);
}
} G;
struct LinearBasis {
ll p[B];
inline void insert(ll x) {
for (int i = B - 1; ~i; --i)
if (x >> i & 1) {
if (p[i])
x ^= p[i];
else {
p[i] = x;
break;
}
}
}
inline ll querymax(ll res) {
for (int i = B - 1; ~i; --i)
if ((res ^ p[i]) > res)
res ^= p[i];
return res;
}
} lb;
ll dis[N];
bool vis[N];
int n, m;
void dfs(int u, int f) {
vis[u] = true;
for (auto it : G.e[u]) {
int v = it.first;
ll w = it.second;
if (v == f)
continue;
else if (vis[v])
lb.insert(dis[u] ^ w ^ dis[v]);
else
dis[v] = dis[u] ^ w, dfs(v, u);
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
ll w;
scanf("%d%d%lld", &u, &v, &w);
G.insert(u, v, w), G.insert(v, u, w);
}
dfs(1, 0);
printf("%lld", lb.querymax(dis[n]));
return 0;
}
CF724G Xor-matic Number of the Graph
给出一张无向带权图,三元组 \((u, v, s)\) 合法当且仅当存在一条 \(u\) 到 \(v\) 且权值异或和为 \(s\) 的路径(不一定要简单路径),求所有合法三元组 \(s\) 的和 \(\bmod 10^9 + 7\) 。
\(n \le 10^5\) ,\(m \le 2 \times 10^5\)
构建 dfs 树,则 \(u \to v\) 存在一条边权异或和为 \(dis_u \oplus dis_v\) 的路径。
先求出所有简单环的异或和丢到线性基里面,然后按位考虑贡献。
不妨设线性基大小为 \(siz\) 且当前位为 \(i\),那么求出所有第 \(i\) 位为 \(0\) 和 \(1\) 的 \(dis_u\) 的个数,分别记为 \(z\) 和 \(o\)。这样就可以算出当前位异或起来为 \(0\) 和 \(1\) 的数对个数,即 \(cnt_0 = \binom{z}{2} + \binom{o}{2}, cnt_1 = z \times o\) 。
若线性基第 \(i\) 位有值,则线性基共异或出 \(2^{siz-1}\) 个第 \(i\) 位为 \(0\) 或 \(1\) 的数,贡献为 \((cnt_0 + cnt_1) \times 2^{siz - 1} \times 2^i\) 。否则线性基共能异或出 \(2^{siz}\) 个第 \(i\) 位为 \(0\) 的数,贡献为 \(cnt1 \times 2^{siz} \times 2^i\) 。
时间复杂度 \(O(n \log V)\)。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int Mod = 1e9 + 7, inv2 = (Mod + 1) / 2;
const int N = 1e5 + 7, B = 64;
struct Graph {
vector<pair<int, ll> > e[N];
inline void insert(int u, int v, ll w) {
e[u].emplace_back(v, w);
}
} G;
struct LinearBasis {
ll p[B];
int siz;
inline void clear() {
memset(p, 0, sizeof(p)), siz = 0;
}
inline void insert(ll k) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i];
else {
p[i] = k, ++siz;
break;
}
}
}
} lb;
vector<int> vec;
ll dis[N];
int pw[N];
bool vis[N];
int n, m;
inline int add(int x, int y) {
x += y;
if (x >= Mod)
x -= Mod;
return x;
}
void dfs(int u, int f) {
vis[u] = true, vec.emplace_back(u);
for (auto it : G.e[u]) {
int v = it.first;
ll w = it.second;
if (v == f)
continue;
else if (vis[v])
lb.insert(dis[u] ^ w ^ dis[v]);
else
dis[v] = dis[u] ^ w, dfs(v, u);
}
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v;
ll w;
scanf("%d%d%lld", &u, &v, &w);
G.insert(u, v, w), G.insert(v, u, w);
}
pw[0] = 1;
for (int i = 1; i <= n; ++i)
pw[i] = 2ll * pw[i - 1] % Mod;
int ans = 0;
for (int i = 1; i <= n; ++i) {
if (vis[i])
continue;
lb.clear(), vec.clear(), dfs(i, 0);
for (int j = 0; j < B; ++j) {
bool flag = false;
for (int k = 0; k < B; ++k)
if (lb.p[k] >> j & 1) {
flag = true;
break;
}
int c[2] = {0, 0};
for (int it : vec)
++c[dis[it] >> j & 1];
int cnt0 = (1ll * c[0] * (c[0] - 1) / 2 + 1ll * c[1] * (c[1] - 1) / 2) % Mod,
cnt1 = 1ll * c[0] * c[1] % Mod;
if (flag)
ans = add(ans, 1ll * add(cnt0, cnt1) * pw[lb.siz - 1] % Mod * pw[j] % Mod);
else
ans = add(ans, 1ll * cnt1 % Mod * pw[lb.siz] % Mod * pw[j] % Mod);
}
}
printf("%d", ans);
return 0;
}
CF938G Shortest Path Queries
给出一张无向图,边带边权,\(q\) 次操作,操作有:加边、删边、查询两点间的异或最短路,保证任意时刻图均连通。
\(n, m, q \le 2 \times 10^5\)
线段树分治处理边存在性的变化,时间复杂度 \(O((m + q) \log q + q \log V)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 7, B = 31;
struct LinearBasis {
int p[B];
inline void insert(int x) {
for (int i = B - 1; ~i; --i)
if (x & (1 << i)) {
if (p[i])
x ^= p[i];
else {
p[i] = x;
return;
}
}
}
inline int querymin(int x) {
for (int i = B - 1; ~i; --i)
if (x >> i & 1)
x ^= p[i];
return x;
}
} lb;
struct Edge {
int u, v, w, beg, ed;
} e[N << 1];
map<pair<int, int>, int> mp;
pair<int, int> qry[N];
int n, m, q;
struct DSU {
int fa[N], siz[N], dis[N], sta[N];
int top;
inline void prework(int n) {
iota(fa + 1, fa + n + 1, 1), fill(siz + 1, siz + n + 1, 1);
memset(dis + 1, 0, sizeof(int) * n), top = 0;
}
inline int find(int x) {
while (x != fa[x])
x = fa[x];
return x;
}
inline int dist(int x) {
int res = 0;
while (x != fa[x])
res ^= dis[x], x = fa[x];
return res;
}
inline int insert(int x, int y, int w) {
w ^= dist(x) ^ dist(y), x = find(x), y = find(y);
if (x == y)
return w;
if (siz[x] < siz[y])
swap(x, y);
siz[fa[y] = x] += siz[y], dis[y] = w, sta[++top] = y;
return -1;
}
inline void remove(int k) {
while (top > k) {
int x = sta[top--];
siz[fa[x]] -= siz[x], fa[x] = x, dis[x] = 0;
}
}
} dsu;
namespace SMT {
vector<Edge> upd[N << 2];
inline int ls(int x) {
return x << 1;
}
inline int rs(int x) {
return x << 1 | 1;
}
void insert(int x, int nl, int nr, int l, int r, Edge k) {
if (l <= nl && nr <= r) {
upd[x].emplace_back(k);
return;
}
int mid = (nl + nr) >> 1;
if (l <= mid)
insert(ls(x), nl, mid, l, r, k);
if (r > mid)
insert(rs(x), mid + 1, nr, l, r, k);
}
void dfs(int x, int l, int r, LinearBasis Lb) {
int original = dsu.top;
for (Edge it : upd[x]) {
int res = dsu.insert(it.u, it.v, it.w);
if (~res)
Lb.insert(res);
}
if (l == r) {
if (qry[l] != make_pair(0, 0))
printf("%d\n", Lb.querymin(dsu.dist(qry[l].first) ^ dsu.dist(qry[l].second)));
} else {
int mid = (l + r) >> 1;
dfs(ls(x), l, mid, Lb), dfs(rs(x), mid + 1, r, Lb);
}
dsu.remove(original);
}
} // namespace SMT
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
if (u > v)
swap(u, v);
e[i] = (Edge){u, v, w, 1, -1}, mp[make_pair(u, v)] = i;
}
scanf("%d", &q);
for (int i = 1; i <= q; ++i) {
int op, u, v;
scanf("%d%d%d", &op, &u, &v);
if (u > v)
swap(u, v);
if (op == 1) {
int w;
scanf("%d", &w);
e[mp[make_pair(u, v)] = ++m] = (Edge){u, v, w, i, -1};
} else if (op == 2)
e[mp[make_pair(u, v)]].ed = i - 1;
else
qry[i] = make_pair(u, v);
}
for (int i = 1; i <= m; ++i)
if (e[i].ed == -1)
e[i].ed = q;
for (int i = 1; i <= m; ++i)
if (e[i].beg <= e[i].ed)
SMT::insert(1, 1, q, e[i].beg, e[i].ed, e[i]);
dsu.prework(n), SMT::dfs(1, 1, q, lb);
return 0;
}
挖掘线性基性质
P5556 圣剑护符
给定一棵树,点带点权,\(q\) 次操作:
- 将 \(x, y\) 间的简单路径上的所有点的点权异或上 \(z\) 。
- 询问是否存在 \(A, B \subseteq Path(x, y)\) 满足 \(A \ne B\) 且 \(\bigoplus_{x \in A} val_x = \bigoplus_{x \in B} val_x\) 。
\(n \le 10^5\)
修改直接用树剖处理,查询实际上就是判断 \(P(x, y)\) 是否有点无法插入线性基。
注意到线性基元素最多 \(O(\log V)\) 个,即若 \(|Path(x, y)| > \log V\) 则必定有点无法插入,否则暴力判断即可。
时间复杂度 \(O(n \log n \log^2 V)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7, B = 31;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
struct LinearBasis {
int p[N];
inline LinearBasis() {
memset(p, 0, sizeof(p));
}
inline bool insert(int k) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i];
else
return p[i] = k, true;
}
return false;
}
};
int a[N], fa[N], dep[N], siz[N], son[N], top[N], dfn[N];
int n, m, dfstime;
void dfs1(int u, int f) {
fa[u] = f, dep[u] = dep[f] + 1, siz[u] = 1;
for (int v : G.e[u]) {
if (v == f)
continue;
dfs1(v, u), siz[u] += siz[v];
if (siz[v] > siz[son[u]])
son[u] = v;
}
}
void dfs2(int u, int topf) {
top[u] = topf, dfn[u] = ++dfstime;
if (son[u])
dfs2(son[u], topf);
for (int v : G.e[u])
if (v != fa[u] && v != son[u])
dfs2(v, v);
}
inline int LCA(int x, int y) {
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]])
swap(x, y);
x = fa[top[x]];
}
return dep[x] < dep[y] ? x : y;
}
namespace BIT {
int c[N];
inline void modify(int x, int k) {
for (; x <= n; x += x & -x)
c[x] ^= k;
}
inline void update(int l, int r, int k) {
modify(l, k), modify(r + 1, k);
}
inline int query(int x) {
int res = 0;
for (; x; x -= x & -x)
res ^= c[x];
return res;
}
} // namespace BIT
inline void update(int x, int y, int z) {
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]])
swap(x, y);
BIT::update(dfn[top[x]], dfn[x], z), x = fa[top[x]];
}
if (dep[x] > dep[y])
swap(x, y);
BIT::update(dfn[x], dfn[y], z);
}
inline bool query(int x, int y) {
int lca = LCA(x, y);
if (dep[x] + dep[y] - dep[lca] * 2 >= B)
return true;
LinearBasis lb;
for (; x != lca; x = fa[x])
if (!lb.insert(BIT::query(dfn[x])))
return true;
for (; y != lca; y = fa[y])
if (!lb.insert(BIT::query(dfn[y])))
return true;
return !lb.insert(BIT::query(dfn[lca]));
}
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
for (int i = 1; i < n; ++i) {
int u, v;
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
dfs1(1, 0), dfs2(1, 1);
for (int i = 1; i <= n; ++i)
BIT::update(dfn[i], dfn[i], a[i]);
while (m--) {
char str[7];
int x, y;
scanf("%s%d%d", str, &x, &y);
if (str[0] == 'U') {
int z;
scanf("%d", &z);
update(x, y, z);
} else
puts(query(x, y) ? "YES" : "NO");
}
return 0;
}
CF959F Mahmoud and Ehab and yet another xor task
给定 \(a_{1 \sim n}\) ,\(q\) 次询问前 \(a_{1 \sim l}\) 中有多少子集异或和为 \(x\) ,答案对 \(10^9 + 7\) 取模。
\(n, q \le 10^5\)
考虑取出 \(a_{1 \sim l}\) 的线性基,若 \(x\) 不能被表示则答案为 \(0\) 。否则考虑不在线性基内的元素的数,这些数都可以被线性基异或为 \(0\) ,都可以选或不选,于是答案即为 \(2^{l - siz}\) ,其中 \(siz\) 表示线性基大小。
时间复杂度 \(O((n + m) \log V)\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 1e9 + 7;
const int N = 1e5 + 7, B = 20;
struct Linear {
int p[B];
int siz;
inline void insert(int k) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i];
else {
p[i] = k, ++siz;
break;
}
}
}
inline bool query(int k) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i];
else
return false;
}
return true;
}
} lb[N];
int a[N], pw[N];
int n, m;
signed main() {
scanf("%d%d", &n, &m);
pw[0] = 1;
for (int i = 1; i <= n; ++i)
pw[i] = 2ll * pw[i - 1] % Mod;
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), lb[i] = lb[i - 1], lb[i].insert(a[i]);
while (m--) {
int x, k;
scanf("%d%d", &x, &k);
printf("%d\n", lb[x].query(k) ? pw[x - lb[x].siz] : 0);
}
return 0;
}
P4869 albus就是要第一个出场
给定 \(a_{1 \sim n}\) ,求 \(m\) 在所有 \(2^n\) 个子集异或和的排名。
\(n \le 10^5\)
记 \(a_{1 \sim n}\) 的线性基大小为 \(siz\) ,则有 \(2^{siz}\) 个不同的数,每个数出现了 \(2^{n - siz}\) 次。
因此答案即为 \(m\) 在线性基中的排名乘上 \(2^{n - siz}\) 。
#include <bits/stdc++.h>
using namespace std;
const int Mod = 10086;
const int N = 2e5 + 7, B = 31;
struct LinearBasis {
int p[B];
int siz;
inline void insert(int k) {
for (int i = B - 1; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i];
else {
p[i] = k, ++siz;
break;
}
}
}
inline int getrank(int k) {
int rk = 0;
for (int i = 0, now = 1; i < B; ++i)
if (p[i]) {
if (k >> i & 1)
rk += now;
now <<= 1;
}
return rk;
}
} lb;
int a[N];
int n, m;
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i), lb.insert(a[i]);
scanf("%d", &m);
int ans = lb.getrank(m);
for (int i = 1; i <= n - lb.siz; ++i)
ans = 2ll * ans % Mod;
printf("%d", (ans + 1) % Mod);
return 0;
}
CF1163E Magical Permutation
给出集合 \(S\) ,定义排列 \(p_{0 \sim 2^x - 1}\) 合法当且仅当排列中任意两个相邻的元素的异或值 \(\in S\) 。求 \(x\) 最大的一个合法排列。
\(|S|, S_i \le 2 \times 10^5\)
首先有 \(x \le \log (\max S) + 1\) (否则一定有一对异或值有最高位,而 \(S\) 没有这个最高位),因此考虑枚举所有的 \(x\) 判断可行性。
猜测:一个 \(x\) 可行当且仅当 \(0 \sim 2^x - 1\) 都可以表示为 \(S\) 的一个子集异或和,即 \(S\) 的 \(< 2^x\) 的数组成的线性基大小为 \(x\) 。
必要性:找到排列中 \(0\) 的位置,向两边扩展即可证明。
充分性:只要构造一个排列即可。考虑求出 \(0 \sim 2^x - 1\) 对应线性基的哪个子集,把这些子集按照选取方案用二进制表示为 \(0 \sim 2^x - 1\) 。因为线性基的每个位置存的都是一个子集的异或和,问题转化为把 \(0 \sim 2^x - 1\)(所有子集)重新排列,使得任意相邻两个数,只有一位二进制为不同,构造格雷码即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 7, B = 19;
struct LinearBasis {
int p[B], mark[B];
int siz;
inline void insert(int k) {
for (int i = B - 1, state = 0; ~i; --i)
if (k >> i & 1) {
if (p[i])
k ^= p[i], state ^= mark[i];
else {
p[i] = k, mark[i] = state | (1 << i), ++siz;
break;
}
}
}
inline int query(int k) {
int res = 0;
for (int i = B - 1; ~i; --i)
if (k >> i & 1)
k ^= p[i], res ^= mark[i];
return res;
}
} lb;
int a[N], id[N];
int n;
signed main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", a + i);
sort(a + 1, a + 1 + n);
int ans = 0;
for (int i = 1, j = 0; i <= n; ++j) {
while (a[i] < (1 << j) && i <= n)
lb.insert(a[i++]);
if (lb.siz == j)
ans = j;
}
printf("%d\n", ans);
for (int i = 0; i < (1 << ans); ++i)
id[lb.query(i)] = i;
for (int i = 0, cur = 0; i < (1 << ans); ++i) {
printf("%d ", id[cur]);
if (i & 1)
cur ^= (cur & -cur) << 1;
else
cur ^= 1;
}
return 0;
}
带删线性基
P3733 [HAOI2017] 八纵八横
给出一张 \(n\) 个点 \(m\) 条边的无向连通图,边带边权 \(w_i\) 。\(q\) 次操作,操作有:
- 在点 \(x, y\) 之间加入一条边权为 \(w_i\) 的边。
- 将第 \(k\) 条新边权值改为 \(w_i\) 。
- 删掉第 \(k\) 条新边,保证这条边之后不会再出现。
对于初始状态和每次操作后,从图中找到一条从 \(1\) 出发,并回到 \(1\) 的一条权值最大路径,路径权值定义为经过边边权的异或和。点边均可多次经过,边经过多次边权也会被计算多次。
\(n, m \le 500\) ,\(q, \log W \le 10^3\) ,加边操作不不超过 \(550\) 次
将问题分为三个部分:
- 找到所有的环。
- 计算所有环的异或最大值。
- 对某个环的权值单点修改或删除某个环。
前两者是简单的,直接带权并查集配合线性基维护即可。
考虑将删除操作转化为将这个环的权值异或到 \(0\) ,这样走到这个环就没有贡献,问题转化为维护一个支持单点异或的集合的线性基。
考虑维护每一个数插入时被异或上的基底集合 \(cst_i\) ,假设现在单点修改 \(x\) :
-
找到一个不在线性基里面的数 \(i\) 并满足 \(x \in cst_i\) ,如果找不到就贪心找最低的包含 \(x\) 的基底。
-
然后用选出的这个数消去其它所有包含 \(x\) 的基底,这样就消去了线性基中 \(x\) 的影响。
-
最后修改这个选出的基底,再插入线性基即可。
单次修改复杂度 \(O(m + \log W)\) ,正确性就是如果第一步找不到显然没有影响,否则找最低位的基底去异或消除 \(x\) 不会影响高位。
记总共加入 \(x\) 条新边,则总时间复杂度为 \(O(n \log n + \frac{q(x + m + \log W) \log W}{\omega})\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7;
typedef bitset<N> bst;
bst val[N], cir[N];
int id[N];
int n, m, q, tot;
struct LinearBasis {
bst val[N], cst[N]; // val : 基底值 | cst : 组成
int pos[N]; // 第 i 位基底对应值的下标
inline LinearBasis() {
for (int i = 0; i < N; ++i)
cst[i].set(i);
}
inline void update(bst k, int id) {
int cur = 0;
for (int i = 1; i <= tot; ++i)
if (val[i].none() && cst[i].test(id)) {
cur = i;
break;
}
if (!cur) {
for (int i = 0; i < N; ++i)
if (pos[i] && cst[pos[i]].test(id)) {
cur = pos[i], pos[i] = 0;
break;
}
}
for (int i = 1; i <= tot; ++i)
if (i != cur && cst[i].test(id))
val[i] ^= val[cur], cst[i] ^= cst[cur];
val[cur] ^= k;
for (int i = N - 1; ~i; --i)
if (val[cur].test(i)) {
if (pos[i])
val[cur] ^= val[pos[i]], cst[cur] ^= cst[pos[i]];
else {
pos[i] = cur;
break;
}
}
}
inline bst query() {
bst res;
for (int i = N - 1; ~i; --i)
if (!res.test(i) && pos[i])
res ^= val[pos[i]];
return res;
}
} lb;
struct DSU {
bst dis[N];
int fa[N], siz[N];
inline void clear(int n) {
iota(fa + 1, fa + n + 1, 1), fill(siz + 1, siz + n + 1, 1);
for (int i = 1; i <= n; ++i)
dis[i].reset();
}
inline int find(int x) {
while (x != fa[x])
x = fa[x];
return x;
}
inline bst dist(int x) {
bst res = dis[x];
while (x != fa[x])
x = fa[x], res ^= dis[x];
return res;
}
inline void merge(int x, int y, bst w, int k) {
int fx = find(x), fy = find(y);
if (fx == fy) {
cir[id[k] = ++tot] = dist(x) ^ dist(y) ^ w, lb.update(cir[tot], tot);
return;
}
if (siz[fx] > siz[fy])
swap(fx, fy);
dis[fx] = dist(x) ^ dist(y) ^ w;
siz[fy] += siz[fx], fa[fx] = fy;
}
} dsu;
inline void writeln(bst x) {
bool lead = false;
for (int i = N - 1; ~i; --i) {
if (x.test(i))
cout << 1, lead = true;
else if (lead)
cout << 0;
}
if (!lead)
cout << 0;
cout << '\n';
}
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m >> q;
dsu.clear(n);
for (int i = 1; i <= m; ++i) {
int u, v;
bst w;
cin >> u >> v >> w;
dsu.merge(u, v, w, 0);
}
writeln(lb.query());
int ext = 0;
while (q--) {
char op[7];
cin >> op;
if (op[0] == 'A') {
int u, v;
cin >> u >> v >> val[++ext];
dsu.merge(u, v, val[ext], ext);
} else if (op[1] == 'h') {
int k;
bst w;
cin >> k >> w;
swap(w, val[k]), lb.update(w ^= val[k], id[k]), cir[id[k]] ^= w;
} else {
int k;
cin >> k;
lb.update(cir[id[k]], id[k]);
}
writeln(lb.query());
}
return 0;
}
P11731 [集训队互测 2015] 最大异或和
给出序列 \(a_{1 \sim n}\) ,每个数都 \(\in [0, 2^m - 1]\) ,\(q\) 次操作,操作有三种:
- 区间异或。
- 区间赋值。
- 求全局异或最大值,即选出若干个数使得异或值最大。
\(n, m, q \le 2000\)
线性基的区间操作考虑在差分序列上单点操作,则操作转化为单点异或和区间赋值为 \(0\) 。
注意到单点操作最多 \(O(q)\) 次,因此考虑暴力对单点做赋值为 \(0\) 的操作,最多只会有 \(O(n + q)\) 次有效操作。
离线后维护一个删除时间即可,使用 bitset
维护前缀线性基,每次尽量保留删除时间最晚的基底。
时间复杂度 \(O(\frac{n^3}{\omega})\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 7;
vector<pair<bitset<N>, int> > upd[N];
bitset<N> a[N], c[N];
int lst[N];
bool qry[N];
int n, m, q;
inline void writeln(bitset<N> k) {
for (int i = m - 1; ~i; --i)
cout << k.test(i);
cout << '\n';
}
struct LinearBasis {
bitset<N> p[N];
int d[N];
inline LinearBasis() {
memset(d, -1, sizeof(d));
}
inline void insert(bitset<N> k, int x) {
for (int i = m - 1; ~i; --i) {
if (!k.test(i))
continue;
if (d[i] == -1) {
p[i] = k, d[i] = x;
break;
}
if (d[i] < x)
swap(k, p[i]), swap(x, d[i]);
k ^= p[i];
}
}
inline void remove(int k) {
for (int i = 0; i < m; ++i)
if (d[i] == k)
d[i] = -1;
}
inline bitset<N> querymax() {
bitset<N> ans;
for (int i = m - 1; ~i; --i)
if (~d[i] && !ans.test(i))
ans ^= p[i];
return ans;
}
} lb;
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n >> m >> q;
for (int i = 1; i <= n; ++i)
cin >> a[i], c[i] = a[i] ^ a[i - 1];
for (int i = 1; i <= q; ++i) {
int op, l, r;
cin >> op;
if (op == 3)
qry[i] = true;
else {
bitset<N> k;
cin >> l >> r >> k;
for (int j = l; j <= r; ++j)
a[j] = (op == 1 ? a[j] ^ k : k);
for (int j = l; j <= min(r + 1, n); ++j) {
bitset<N> now = a[j] ^ a[j - 1];
if (c[j] != now) {
if (!c[j].none())
upd[lst[j]].emplace_back(c[j], i);
c[j] = now, lst[j] = i;
}
}
}
}
for (int i = 1; i <= n; ++i)
if (!c[i].none())
upd[lst[i]].emplace_back(c[i], q + 1);
for (int i = 0; i <= q; ++i) {
lb.remove(i);
for (auto it : upd[i])
lb.insert(it.first, it.second);
if (qry[i])
writeln(lb.querymax());
}
return 0;
}