Integers are vectors over and XOR is vector addition. Any set of numbers spans a subspace of dimension (the bit width), represented by a basis of at most numbers. Insertion is Gaussian elimination, one vector at a time.
Structure
struct XorBasis {
static const int B = 60;
ll b[B] = {}; int rank = 0;
bool insert(ll x) { // true if x enlarged the span
for (int i = B - 1; i >= 0 && x; i--) if (x >> i & 1) {
if (!b[i]) { b[i] = x; rank++; return true; }
x ^= b[i];
}
return false;
}
bool contains(ll x) const {
for (int i = B - 1; i >= 0; i--) if (x >> i & 1) { if (!b[i]) return false; x ^= b[i]; }
return true;
}
ll maxXor(ll x = 0) const { for (int i = B - 1; i >= 0; i--) x = max(x, x ^ b[i]); return x; }
ll minXor(ll x) const { for (int i = B - 1; i >= 0; i--) x = min(x, x ^ b[i]); return x; }
};b[i] has highest bit (row echelon form). Insertion and queries are .
Proof: insertion preserves the span,
containsis correct, andmaxXoris optimalSpan preservation.
insert(x)only ever replacesxbyx ^ b[i](XORing by an existing basis vector, which doesn’t change the span since is already in it) or adds the final nonzero remainder as a new basis vector with a pivot bit no other vector has. So at every point after inserting equals ‘s span: either reduced to (already in the span) or the leftover is a genuinely new vector, linearly independent of the current basis because its leading bit is zero in every existing (, by the row-echelon invariant) and nonzero in itself, so it cannot be written as a combination of the others. By induction over insertions,balways spans exactly the same subspace as all inserted vectors, andrankcounts the dimension.contains. Reducing against the basis top-down XORs out bit using whenever has bit set; if that pivot is missing (), no combination of the basis (all of which are zero at bit or higher pivots only — row echelon) can cancel that bit, so . If every encountered bit has a pivot, reduces to exactly , which happens iff is a linear combination of the basis vectors used, i.e. .maxXor. Greedily taking whenever it increases , scanning pivots from the highest bit down: once a pivot’s bit is fixed to in the running answer (either because already had it, or XORing sets it), no later (lower-pivot) operation can clear a higher bit, since every for has all bits equal to zero in row-echelon form below the point where ‘s pivot sits (reduced form has distinct pivots, and the standard greedy variant used here, while not fully reduced, still never reintroduces a cleared higher bit, since later have pivot so XORing them can only change bits ). So the greedy choice at each pivot is locally optimal and never sacrifices a higher bit for a lower one, which is exactly what maximizes the integer value bit by bit from the top.Tested against full subset-XOR enumeration for up to 7: span size ,
containsmatching exact reachability, every reachable value hit exactly times, andmaxXor()matching the brute-force maximum — all confirmed.
What it answers
| Question | Answer |
|---|---|
| maximum subset XOR (optionally XORed with ) | greedy from the top bit |
| minimum XOR of with an element of the span | greedy minimising |
| is a subset XOR? | reduce to 0 |
| number of distinct subset XORs | |
| number of subsets (of inputs) with XOR , for reachable | (each value hit equally often, including the empty subset for ) |
| -th smallest subset XOR | fully reduce, then read in binary |
| minimum non-zero subset XOR | smallest vector after full reduction (if , then 0 is reachable non-trivially) |
| merge two bases | insert one into the other, |
| rank of a set over | rank |
Full reduction: for each pivot , clear bit from every other basis vector. Then the reduced vectors sorted by pivot are , and the -th smallest element of the span (0-indexed, counting 0) is .
Proof: distinct subset XORs, each hit by exactly subsets, and the -th-smallest readout
Counting. The map sending a subset (as an indicator vector) to its XOR is -linear, and its image is exactly , which has dimension (the basis rank), so — that is the count of distinct subset XORs. By the rank-nullity theorem, has dimension , so ; since is a group homomorphism from to , every fiber for in the image is a coset of (translate any one solution by every kernel element), hence has the same size — this is why reachable values are hit equally often, not just on average. -th smallest. After full reduction, the basis vectors (sorted by pivot, lowest first) have disjoint pivot bits with no vector sharing another’s pivot (by construction of full reduction), so the map for is injective (two different differ in some bit , and ‘s pivot bit is only ever toggled by including , since no other touches that bit after full reduction) and its image is exactly the span (every span element is some XOR-combination of the basis, and combinations correspond bijectively to subsets of the basis once the basis is linearly independent). Since XOR-combining more/fewer high-pivot vectors changes the result monotonically in the same direction as binary place value (the highest pivot included dominates all lower bits, by row-echelon structure), the combinations sorted by ‘s integer value are exactly the span sorted by integer value, so ‘s binary digits directly select the -th smallest.
Range and path queries
Prefix basis with timestamps (max subset XOR of online): insert into the prefix- basis, keeping at each pivot the vector with the latest index (swap when the incoming index is newer, then continue reducing the older one). A query on uses only pivots with index . build, per query, and it’s persistent (store one basis per prefix, memory).
ll b[B]; int pos[B];
void insert(ll x, int idx) {
for (int i = B - 1; i >= 0; i--) if (x >> i & 1) {
if (!b[i]) { b[i] = x; pos[i] = idx; return; }
if (pos[i] < idx) swap(b[i], x), swap(pos[i], idx);
x ^= b[i];
}
}
// query [l, r] on the prefix-r snapshot: use b[i] only if pos[i] >= lThe same timestamp trick works on trees: the basis of the root-to- path keeps the deepest vectors, and a path query – merges two such bases restricted to depth depth(LCA). Alternatives: segment tree of bases ( per query), binary lifting, or offline divide and conquer.
Deletion: offline with a segment tree over time and an undo stack (record which b[i] was set). Online deletion of the most recently inserted element with the timestamp trick is also possible (“basis with deletion by time”).
Cycle space of a graph
Edge weights XORed around cycles form a linear space. Build a spanning tree with root XOR distances ; for each non-tree edge insert . Then:
- the maximum XOR of a (not necessarily simple) path from to is
maxXor(d[u] ^ d[v]); - the minimum is
minXor(d[u] ^ d[v]); - the number of distinct path XOR values between and is .
Walks may repeat edges, which is why every cycle can be added for free (go to the cycle, around it, and back).
Proof: the reachable path-XOR values from to are exactly
Fix a spanning tree with root distances (XOR of tree-edge weights from the root). Any walk from to can be decomposed into the tree path (XOR value , since the tree path’s XOR telescopes to the two root-distances) composed with some number of detours, each of which walks from a vertex on the current path out to some edge and back. A detour using a non-tree edge : walking tree-path to , crossing , then tree-path back from to the same point XORs in — exactly the fundamental cycle vector stored in the basis — because the tree-path portions from the detour’s start back to itself cancel (any edge traversed twice XORs to zero), leaving only the non-tree edge’s contribution combined with the root-distance difference it induces. Conversely, every basis vector (fundamental cycle) is realizable as such a detour, and detours compose by XOR (doing two detours back to back XORs their two cycle values, since the connecting tree-walk between them also cancels in pairs). So the set of all walk-XOR values from to is exactly , a coset of the cycle space — hence
maxXor(d[u]^d[v])andminXor(d[u]^d[v])give the true max/min over all walks (not just simple paths), and the count of distinct values is .
Over other fields
The same row-echelon insertion works over with vectors of dimension in per insertion: normalise the pivot with an inverse and eliminate. The span has elements and every reachable vector has representations. Gaussian elimination in general is in Gaussian Elimination. Beyond ‘s subset-XOR questions, the same machinery solves “is this modular linear system consistent, and how many solutions” and toggle puzzles like Lights Out (each move is a vector over , the reachable configurations are the span).
Matroid view
Linearly independent sets form a matroid. So “choose items maximising total weight such that their values are XOR-independent” is solved by sorting by weight and inserting greedily; the kept items are exactly those whose insertion enlarged the span — the same reason Kruskal’s algorithm is correct for spanning forests (a graphic matroid): both are “greedy is optimal for max-weight independent set in a matroid.”
Trie vs basis
“Choose a subset” means a basis; “choose one or two elements” means a binary trie (Binary Trie).
| XOR basis | Binary trie | |
|---|---|---|
| Represents | the span of the set | the elements of the set |
| Max XOR over a subset | ✔ | ✘ |
| Max XOR with a single element | ✘ | ✔ |
| Count elements with a property | ✘ | ✔ |
| Deletion | hard (offline / timestamp trick above) | ✔ |
| Memory | ||
| Merge two structures |