More than Linear Basis
线性基 (Linear Basis)
在线性代数中, 基 (basis) (也称为基底) 是描述, 刻画向量空间的基本工具. 向量空间的基是它的一个特殊的子集, 基的元素称为基向量. 向量空间中任意一个元素, 都可以唯一地表示成基向量的线性组合. 如果基中元素个数有限, 就称向量空间为有限维向量空间, 将元素的个数称作向量空间的维数. ----Wikipedia
定义
OI 中, 线性基特指一种线性空间的基. 这种线性空间的向量的每一维坐标是 或 , 每维坐标可以看作整数的每个二进制位. 加法定义是按位异或, 数乘定义没有意义, 因为因数只能是 或 .
性质
由于线性基内所有向量线性无关. 所以线性基的大小即为线性空间中线性无关的向量个数, 不会超过 个.
如果把线性基的元素放一起形成矩阵, 进行初等变换后一定还是原线性空间的线性基. 而且我们用初等变换可以把线性基变成最高位的 互不相同的数集.
算法
假设现在有 个数, ,..., , 求它们生成的线性空间 的基 .
我们先考虑从定义出发, 如果我们把所有给定的数字看成是行向量, 然后组成过一个 行 列的矩阵, 高斯消元后非全零的行即为所求的线性基. 对这个矩阵进行高斯消元的复杂度是 . 如果 时我们可以通过用整数表示行向量来优化行异或为 , 所以复杂度优化到 . 当 超出 字节后可以进行 bitset
优化, 达到 .
可以每次插入一个行向量就进行一轮初等变换, 出现全零行就将其删除, 维持这个矩阵为已经插入的行向量生成的线性空间的基 , 并且从大到小有序排列. 每次插入一个行向量需要进行 次行异或. 因为 , 所以这样总共需要 次行异或. 当 时, 复杂度为 , 否则是 .
因为行向量从大到小排列, 且高斯消元使得它们最高位互不相同, 所以矩阵每行最高位不同. 对于一个新的行向量 , 从上往下扫描矩阵, 如果发现了不存在和 最高位相同的行向量, 那么直接把 插入矩阵这一行中. 如果存在, 那么使 被这个行向量异或. 的最高位一定会减小, 这时继续刚才的操作, 直到把 作为行向量插入 或 变成 为止.
为了方便操作, 使得矩阵第 行对应最高位为 的行向量.
n = RD(), Bin[0] = 1;
for (unsigned i(1); i <= 50; ++i) Bin[i] = (Bin[i - 1] << 1);
for (unsigned i(1); i <= n; ++i) {
A = RD();
for (unsigned j(50); ~j; --j) if (A & Bin[j]) {
if (B[j]) A ^= B[j];
else { B[j] = A; break; }
}
}
判断向量是否属于
如果把 插入 时 被异或成 , 说明它属于 .
求 中最大向量
只要从大到小枚举线性基然后贪心即可, 每一位能赋成 就赋成 .
for (unsigned i(50); ~i; --i) if ((B[i]) && (!(Ans & Bin[i]))) Ans ^= B[i];
printf("%llu\n", Ans);
求 中最小非零向量
线性基的最小元素即为最小非零向量.
求 中第 小的非零向量
我们把 高斯消元后, 得到的矩阵中一定能使得每一行的最高位的 是当前列唯一的 .
所以要想凑出线性空间中的向量 . 如果 中的某一位 是某个 中元素的最高位的 , 那么这个 只能由它提供; 如果 中某一位 是某个 中元素的最高位的 , 那么这个元素一定不在异或出 的元素集合中, 否则 的这一位就变成 了.
根据上面的规则, 只要知道 的所有这 个位置的值, 就可以构造出唯一的 . 所以 .
的前 小的元素, 可以生成 个 中的元素. 因为任意这 个元素异或一个 中的前 小的元素之外的元素, 都会变大, 因此可以知道这 个元素即为 中最小的 个元素.
所以对于 来说, 如果它的最高位是第 位, 那么 中第 小的元素 的最高位一定等于 中第 小的元素的最高位. 然后就得到了子问题: 求 中前 的元素生成的 个元素中第 小的元素 . 即为 异或 中第 小元素的值. 扫描一遍 , 过程中维护 即可得到答案.
求并
我们只要把 的所有元素插入 中即可.
求交
对于两个集合 , 定义运算 , 若 , 当且仅当 .
我们要对 进行初等变换, 使得 为 , 的交 的基 . 为了构造, 建一个临时集合 , 把 复制进去, 然后尝试把 的元素往 里面插.
如果 的元素 插入的时候没有被异或为 , 说明它不可以被 中的元素表示. 于是将其插入 .
如果 可以被 异或为 , 且可以仅被 的元素异或为 , 说明 属于 , 将 直接放入 中即可.
如果 需要被 已经插入 中的元素和 中的元素共同异或为 , 那么我们用 已经插入 的元素异或 不会对线性基的性质产生任何影响, 所以可以转化为上一种情况, 把调整后的 插入 即可.
最后求出的 即为 的基.
在线删除
我们前面求的是 集合生成的线性空间 的基 , 那么如果需要支持删除数字, 如何维护 呢?
我们设 为 中最高位为 的元素.
对于每个插入时没有异或为 的数字 , 我们认为它为线性空间增加了一维, 异或后插入到了位置 . 这时我们给这一维赋一个空闲的维度编号 . 这个编号不能是最高位位置 , 原因在最后解释.
所以我们单独开一个名为 的 bitset
, 表示这个维度编号空闲. 每次开启新维度时, 就是扫描这个数组得到的.
记一个数组 表示 生成的维度的编号, 如果 没有生成任何维度则 .
如果把每个 的 作为 个维度的单位向量, 那么对于线性空间内每个数, 都可以由这些单位向量中的一部分异或而来的.
每个 维护一个集合 , 表示 是由哪些维度的单位向量异或而来的.
每个 维护一个集合 , 表示 是由哪些维度的单位向量异或而来的.
还要记录一个 bitset
, 表示 已经被删除了.
这时假设要删除的是 , 那么可以按 分为两类.
这就说明 的插入并没有给 带来新的元素, 那么 删除之后 也不会变化, 所以它的基 也不会变化, 我们什么都不用做.
我们这时候要尝试找出一个未被删除且 的数 , 满足 .
存在
如果我们设 表示维度集合 内所有维度的单位向量的异或和. 则有
那么对于 中的任意元素 , 一定有集合 使得 . 如果 , 则删除 不会对生成 产生影响. 如果 那么就有:
因为 和 都不含 , 所以它们都可以在 不存在的时候结合 凑出 . 也就是说删除 也不会使得 发生任何变化, 所有需要 参与生成的元素都可以由 生成, 所以我们不对 进行任何修改.
但是我们需要把 维度的单位向量改成 , 这样会使 和 都发生变化.
因为对于包含 的集合 , 在这个维度单位向量变化后, 会变成 . 为了使所有这种 的 仍然等于原来的 .
我们如果想对它进行修正, 就要使 再异或一个 .
体现在 上就是使得 变成 . 这个修正操作对每个包含 的 和 都要进行.
由于我们已经把 选为维度 的单位向量, 所以将 赋值为 .
不存在
说明删掉 会让维度 消失, 我们需要在 中删掉一个元素, 并且把 变成 .
如果 中任意元素 都可以表示为 .
减少一维意味着减少一半的元素, 也就是删除所有 的 .
因为所有 都是 的元素异或出来的, 如果所有 都满足 , 那么自然就无法生成 的 了.
解法1
先考虑把所有 的所有 都异或 . 这时可以在 中清除所有 , 但是这时 的元素不是线性无关的. 所以需要进行一轮高斯消元.
解法2
重新审视线性基的性质, 我们发现可以选一个 , 使其满足 , 然后把所有满足 的 都异或 , 这样也可以消除所有 中的 .
如果我们选择 的时候让 尽可能小, 那么最后异或结束后除了 变成 以外, 其它的元素因为最高位比 高, 所以最高位没有发生变化, 所以在 中的位置也不会变化. 为了方便, 我们这样选择 .
注意在异或过程中同步维护 .
编号单独维护的原因
如果用 最高位作为维度编号, 在这组数据中会出问题:
Insert 3
Insert 2
Delete 3
Insert 1
生成了维度 , 插入了新元素 , 同时 .
生成了维度 , 插入了新元素 , 同时 .
如果删除 , 会删除维度 , 这时所有 的集合中, 最小的是 , 所以使得 , 都异或 , 同时 , 异或 .
得到了 , , , .
虽然插入 之前线性基不存在最高位为 的元素, 但是在此之前最高位为 的元素可能插入并删除了, 在这个元素存在的时候,
如果这个时候插入 , 会生成新的维度, 插入新元素 , 最高位为 , 但是维度 仍然存在, 所以这样编号会和已经存在的维度 冲突.
复杂度分析
我们在插入 的时候需要维护大小为 的集合 , 大小为 的向量 , 进行 次异或. 如果最后出现了新的维度, 需要遍历大小为 的 bitset
, 复杂度 .
删除 时, 如果 , 则 .
然后我要遍历 个集合来寻找 , .
如果存在 , 那么我需要将 个大小为 的集合异或一个同样大小的集合, .
如果没有 , 那么我需要遍历 个大小为 的集合并且选出集合进行异或, 复杂度
如果操作数是 , 则总复杂度是 . 如果认为 , 则复杂度为
代码实现
模板题, 可惜时间复杂度过不了.
map<unsigned, vector<unsigned> > a;
bitset<500005> Del;
bitset<32> Exi;
unsigned Set[500005], Bel[500005], BSet[32], Bin[32], B[32], m, n;
unsigned C, D, Rk, Line;
unsigned Cnt(0), Ans(0);
int A;
void Ins(int i, unsigned Val) {
unsigned Highest(0x3f3f3f3f);
for (unsigned j(30); ~j; --j) if (Val & Bin[j]) {
if (B[j]) Val ^= B[j], Set[i] ^= BSet[j];
else { B[j] = Val, Highest = j; break; }
}
if (Highest ^ 0x3f3f3f3f) for (unsigned j(0); j <= 30; ++j)
if (!Exi[j]) { BSet[Highest] = (Set[i] | Bin[j]), Exi[Bel[i] = j] = 1; break; }
}
void Delt(int i) {
Del[i] = 1;
if (Bel[i] == 0x3f3f3f3f) return;
unsigned z(0x3f3f3f3f), Line(Bel[i]);
for (int x(0); x < Cnt; x++) if ((!Del[x]) && (Bel[x] == 0x3f3f3f3f) && (Set[x] & Bin[Line])) { z = x; break; }
if (z ^ 0x3f3f3f3f) {// Replace i by z
Bel[z] = Line;
unsigned t(Set[z] ^ Bin[Line]);
for (unsigned x(0); x < Cnt; ++x) if (Set[x] & Bin[Line]) Set[x] ^= t;
for (unsigned x(0); x <= 30; ++x) if (BSet[x] & Bin[Line]) BSet[x] ^= t;
return;
}
unsigned XPos(0x3f3f3f3f), Xor, XSet;
for (unsigned x(0); x <= 30; ++x) if (BSet[x] & Bin[Line]) {
if (XPos == 0x3f3f3f3f) XPos = x, Xor = B[x], XSet = BSet[x];
B[x] ^= Xor, BSet[x] ^= XSet;
}
Exi[Line] = 0;
}
signed main() {
memset(Bel, 0x3f, sizeof(Bel));
Bin[0] = 1;
for (unsigned i(1); i <= 30; ++i) Bin[i] = (Bin[i - 1] << 1);
n = RD();
for (unsigned i(1); i <= n; ++i) {
if (!(i % 10000)) fprintf(stderr, "Now %u\n", i);
A = RDsg(), Ans = 0;
if (A < 0) D = -A, Delt(a[D].back()), a[D].pop_back();
else D = A, a[D].push_back(Cnt), Ins(Cnt++, D);
for (unsigned j(30); ~j; --j) if (B[j] && (!(Ans & Bin[j]))) Ans ^= B[j];
printf("%u\n", Ans);
}
return Wild_Donkey;
}
离线删除
线段树
可以对时间构造线段树, 每个插入的元素看成一个区间修改, 区间就是它存在的时间区间, 区间修改会遇到 个节点, 给这些节点的 vector
中压入这个元素即可.
插入结束后对线段树每个节点求出自己 vector
中所有元素的线性基.
每次查询的时候, 在线段树上单点查询的时候合并 个线性基, 相当于 次插入, 复杂度是 . 如果按时间顺序回答询问, 每次用回滚的方式代替从头开始合并, 则可以均摊所有询问最多会合并 次, 需要 次插入, 复杂度 .
我们同样可以在线段树构造完之后, DFS 使得每个节点的线性基变成父亲和自己线性基合并的结果. 这样每个时间的答案便可以在 的时间内处理出来.
下面这份代码可以通过前面提到的模板题.
map<unsigned, vector<unsigned> > a;
unsigned Bin[32], B[31], n, D, CL, CR;
unsigned Cnt(0), Ans(0);
int A;
inline void Ins(unsigned x) {
for (unsigned i(30); (~i) && x; --i) if (x >> i) {
if (B[i]) x ^= B[i];
else { B[i] = x; return; }
}
}
struct Node {
vector <unsigned> Val;
Node* LS, * RS;
inline void Chg(unsigned L, unsigned R) {
if ((CL <= L) && (R <= CR)) { Val.push_back(D); return; }
unsigned Mid((L + R) >> 1);
if (CL <= Mid) LS->Chg(L, Mid);
if (CR > Mid) RS->Chg(Mid + 1, R);
}
inline void DFS() {
unsigned Bf[31];
memcpy(Bf, B, sizeof(B));
for (auto i : Val) Ins(i);
if (!LS) {
Ans = 0;
for (unsigned i(30); ~i; --i) if (B[i] && (!(Ans & Bin[i]))) Ans ^= B[i];
printf("%u\n", Ans);
}
else LS->DFS(), RS->DFS();
memcpy(B, Bf, sizeof(B));
return;
}
}N[1000005], * CntN(N);
inline void Build(Node* x, unsigned L, unsigned R) {
if (L == R) return;
unsigned Mid((L + R) >> 1);
Build(x->LS = ++CntN, L, Mid);
Build(x->RS = ++CntN, Mid + 1, R);
}
signed main() {
Bin[0] = 1;
for (unsigned i(1); i <= 30; ++i) Bin[i] = (Bin[i - 1] << 1);
n = RD(), Build(N, 1, n);
for (unsigned i(1); i <= n; ++i) {
A = RDsg(), Ans = 0;
if (A < 0) D = -A, CL = a[D].back(), CR = i - 1, N->Chg(1, n), a[D].pop_back();
else D = A, a[D].push_back(i);
}
for (auto i : a) { D = i.first; for (auto j : i.second) CL = j, CR = n, N->Chg(1, n); }
fprintf(stderr, "Done\n");
N->DFS();
return Wild_Donkey;
}
无线段树
我们发现在线删除时, 最耗费时间的是维护 , 如果能想办法避免维护 , 时间复杂度就会得到非常大的优化.
分析 的作用, 发现它被用来寻找 , 也就是 的可以代替 的元素. 如果想办法消除 存在情况就可以省略这个判断了.
另需维护数组 使得最高位为第 位的
具体措施是处理出每个元素的删除时间, 然后在插入 时, 如果 生成新的维度, 则按原来的写法维护.
如果不能生成新的维度, 我们求出 (注意这里不维护所有数字的 , 仅求出 对应的 作为临时变量使用), 找出所有维度 中单位向量删除时间最早的单位向量 . 如果 在 之后删除, 则什么也不做, 否则直接用 作为维度 的单位向量, 进行替换操作.
复杂度: 在原来的基础上去掉维护和查询 的复杂度, 复杂度为 .
struct Num {
unsigned In, Out, Val;
}List[500005];
map<unsigned, vector<unsigned> > a;
bitset<32> Exi;
unsigned Move[500005], Bel[500005], BSet[32], Bin[32], B[32], b[32], m, n;
unsigned C(0), D, Rk;
unsigned Cnt(0), Ans(0);
int A;
void Delt(int x) {
if (Bel[x] == 0x3f3f3f3f) return;
unsigned Line(Bel[x]), XPos(0x3f3f3f3f), Xor, XSet;
for (unsigned i(0); i <= 30; ++i) if (BSet[i] & Bin[Line]) {
if (XPos == 0x3f3f3f3f) XPos = i, Xor = B[i], XSet = BSet[i];
B[i] ^= Xor, BSet[i] ^= XSet;
}
for (unsigned i(0); i <= 30; ++i) if (b[i] == x) { b[i] = b[XPos]; break; }
b[XPos] = Exi[Line] = 0, Bel[x] = 0x3f3f3f3f;
}
signed main() {
memset(Bel, 0x3f, sizeof(Bel));
Bin[0] = 1;
for (unsigned i(1); i <= 30; ++i) Bin[i] = (Bin[i - 1] << 1);
n = RD();
for (unsigned i(1); i <= n; ++i) {
A = RDsg(), Ans = 0;
Num* Cur;
if (A < 0) Cur = List + a[D = -A].back(), Cur->Out = i - 1, Move[i] = Cur - List, a[D].pop_back();
else D = A, a[D].push_back(++Cnt), List[Move[i] = Cnt] = Num{ i, 0, D };
}
for (auto i : a) { D = i.first; for (auto j : i.second) List[j].Out = n; }
for (unsigned i(1); i <= n; ++i) {
if (!(i % 50000)) fprintf(stderr, "Now %u\n", i);
unsigned x(Move[i]);
Num* Cur(List + x);
Ans = 0, D = Cur->Val;
if ((Cur->In) ^ i) Delt(x);
else {
unsigned Highest(0x3f3f3f3f), TmpS(0), Line(0), TmpD(D);
for (unsigned j(30); ~j; --j) if (D & Bin[j]) {
if (B[j]) D ^= B[j], TmpS ^= BSet[j];
else { b[j] = x, B[j] = D, Highest = j; break; }
}
if (Highest ^ 0x3f3f3f3f) {
for (unsigned j(0); j <= 30; ++j) if (!Exi[j]) { Exi[Line = Bel[x] = j] = 1; break; }
BSet[Highest] = (TmpS | Bin[Line]);
}
else {
unsigned Last(0x3f3f3f3f);
for (unsigned j(0); j <= 30; ++j) if (b[j] && (TmpS & Bin[Bel[b[j]]]) && (List[b[j]].Out < Last))
Last = List[b[j]].Out, Line = Bel[b[j]], Highest = j;
if (Last < Cur->Out) {
Bel[b[Highest]] = 0x3f3f3f3f, Bel[b[Highest] = x] = Line, TmpS ^= Bin[Line];
for (unsigned j(0); j <= 30; ++j) if (BSet[j] & Bin[Line]) BSet[j] ^= TmpS;
}
}
}
for (unsigned j(30); ~j; --j) if (B[j] && (!(Ans & Bin[j]))) Ans ^= B[j];
printf("%u\n", Ans);
}
return Wild_Donkey;
}
优化
是否可以通过插入的特判避免 的维护, 使得程序更简洁.
考虑 的作用, 在删除一个维度 的时候, 需要把所有被维度 的单位向量 生成的 都异或上最小的 . 的作用就是找出所有这些 . 如果能使每次找出的 都只有 一个, 只要让它自己变成 即可, 那么我们便不需要维护 了.
避免了 的互相异或, 无需维护任何维度集合, 所以维度编号就不用记录了. 我们可以把 作为 插入时新建的维度的编号, 也就是最高位位置.
为了实现每次删除 时, 只有 是需要单位向量 参与才能生成. 需要保证线性基中每个 只需要不比 删除得早的维度的单位向量参与生成.
由于避免了 的相互异或, 的元素都是从高位一路异或过来才插入的, 所以只要一个时刻内, 满足任意 只需要 参与即可生成, 那么任何后来插入的 都不会需要 的 参与即可生成. 显然 为空的时候满足这个条件, 所以任何时刻 都能满足这个条件.
接下来我们只需要满足维度 删除时间比任意 都晚, 这样因为前面的性质, 就可以满足 可以被删除时间不比 早的维度 的单位向量即可生成.
为了使得 删除时间比 的 晚, 我们需要在插入时进行一些操作.
如果这时向线性基内插入 , 过程中记录 表示 异或一些 的 得到的结果. 遇到 和 最高位相同, 按 和 的删除时间分类讨论:
-
更早删除
那么直接用 异或 , 然后用新的 继续这个过程讨论后面的元素. -
更早删除
这时候 可以只用 和 生成, 所以有资格作为 , 我们使 作为维度 的单位向量, 而 则是作为 中别的元素的单位向量.
实现上是交换 和 的数值, 然后交换 和 的数值, 就可以用新的 和 继续这个过程的讨论. 我们只是把 暂时拿到了外面, 并不是把它删除, 因为原来的维度 仍然存在, 只是编号不知道变成什么了, 并且 也参与生成其它 的 .
这个过程直到 或 其中任何一个变成 结束. 如果 先为 , 说明 产生了新的维度, 赋值 , . 如果 先变成 , 则没有新维度产生, 直接跳出.
过程复杂度不变, 但是得到了简化.
map<unsigned, unsigned> a;
unsigned Val[500005], Out[500005], Bin[32], B[32], b[32], n;
unsigned C(0), D, Rk;
unsigned Cnt(0), Ans(0);
int A;
signed main() {
n = RD(), Bin[0] = 1; for (unsigned i(1); i <= 30; ++i) Bin[i] = (Bin[i - 1] << 1);
memset(Out, 0x3f, (n + 1) << 2);
for (unsigned i(1); i <= n; ++i) {
A = RDsg(), Ans = 0;
if (A < 0) Out[Val[i] = Out[i] = a[D = -A]] = i - 1;
else a[Val[i] = D = A] = i;
}
for (unsigned i(1); i <= n; ++i) if (Out[i] == 0x3f3f3f3f) Out[i] = n;
for (unsigned i(1); i <= n; ++i) {
if (!(i % 50000)) fprintf(stderr, "Now %u\n", i);
Ans = 0, D = Val[i];
if (i > Out[i]) { for (unsigned j(0); j <= 30; ++j) if (b[j] == D) { B[j] = b[j] = 0; break; } }
else {
unsigned Cur(i);
for (unsigned j(30); (~j) && D; --j) if (D & Bin[j]) {
if (B[j]) { if (Out[b[j]] < Out[Cur]) swap(D, B[j]), swap(Cur, b[j]); D ^= B[j]; }
else { b[j] = Cur, B[j] = D; break; }
}
}
for (unsigned j(30); ~j; --j) if (B[j] && (!(Ans & Bin[j]))) Ans ^= B[j];
printf("%u\n", Ans);
}
return Wild_Donkey;
}
例题
ABC223H
给一个序列, 每次询问一个区间 的数字通过异或是否可以得到一个给出的数字 .
很容易想到用线段树, 每个节点存这个区间的线性基. 每次查询时把 个线性基合并起来, 直接查询 是否存在于这个线性基的线性空间里. 单次查询复杂度 , 不能通过.
我们如果查询的是后缀而非区间, 一个贪心的思想是只要能用较短的后缀生成的数字, 那么比这个长的后缀就同样可以生成. 也就是说我们尝试使用尽可能靠后的元素生成 . 如果存在两个元素, 插入当前线性基后得到的线性空间相同, 那么靠后面的元素相对于前面的元素更有价值.
思考优化过的线性基离线删除的算法, 相当于是让每一对 满足 比 先删除. 用尽可能删除晚的元素代替同位置的删除早的元素, 贪心地维护这个线性基.
推广到区间修改上, 就是一边从左往右插入, 一边在这个长度为 前缀生成的线性空间的线性基上回答 的询问. 如果生成过程中出现了需要使用 而 的情况, 则回答是否定的. 如果无法生成 , 则答案仍是否定的. 其余情况答案是肯定的.
struct Query {
unsigned long long Val;
unsigned L, R, Num;
inline const char operator < (const Query& x) const { return R < x.R; }
}Q[200005];
unsigned long long a[400005], B[60];
unsigned Pos[60], C, D, m, n;
unsigned Cnt(0), Tmp(0);
bitset<200005> Ans;
signed main() {
n = RD(), m = RD();
for (unsigned i(1); i <= n; ++i) a[i] = RDll();
for (unsigned i(1); i <= m; ++i) Q[i].L = RD(), Q[i].R = RD(), Q[i].Val = RDll(), Q[i].Num = i;
sort(Q + 1, Q + m + 1);
for (unsigned i(1), j(1); j <= m; ++j) {
while (i <= Q[j].R) {
unsigned long long A(a[i]);
unsigned P(i++);
for (unsigned k(59); (~k) && A; --k) if (A >> k) {
if (!B[k]) { Pos[k] = P, B[k] = A;break; }
if (P > Pos[k]) swap(P, Pos[k]), swap(A, B[k]);
A ^= B[k];
}
}
unsigned long long A(Q[j].Val);
unsigned P(Q[j].L);
for (unsigned k(59); ~k; --k) if (A >> k) {
if (P > Pos[k]) break;
A ^= B[k]; if (!A) { Ans[Q[j].Num] = 1; break; }
}
}
for (unsigned i(1); i <= m; ++i) printf(Ans[i] ? "Yes\n" : "No\n");
return Wild_Donkey;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
2020-02-18 ybt1224 最大子矩阵(一个做了4遍的励志故事)