perf(operators): make ERX neighbor removal O(degree) per step (397K -> 140K instr)

Edge Recombination Crossover scrubbed `current` from every one of the n
adjacency lists on each step of the walk -- an O(n^2) pass. The parent-tour
adjacency relation is symmetric (b in adj[a] iff a in adj[b]), so `current`
only ever appears in the lists of its own neighbors. Taking adj[current]
out with mem::take and retaining only over those lists is O(degree).

edge_recombination_crossover_vary n=100: 397_214 -> 140_403 (-65%, 2.83x);
n=30: 62_708 -> 39_987 (-36%). Output bit-identical -- all 606 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 06:29:23 -06:00
co-authored by Claude Opus 4.7
parent eb4c8a6a9f
commit 3f104a4395
+9 -4
View File
@@ -642,14 +642,19 @@ fn erx_child(p1: &[usize], p2: &[usize], start: usize, rng: &mut Rng) -> Vec<usi
for _ in 0..n { for _ in 0..n {
child.push(current); child.push(current);
visited[current] = true; visited[current] = true;
// Remove `current` from every adjacency list so it isn't picked again. // Remove `current` from the adjacency lists it appears in. The
for list in adj.iter_mut() { // parent-tour adjacency relation is symmetric (`b ∈ adj[a]` iff
list.retain(|&x| x != current); // `a ∈ adj[b]`), so `current` appears only in the lists of its own
// neighbors — taking `adj[current]` out and scrubbing just those
// lists is O(degree), not O(n) over every list.
let current_adj = std::mem::take(&mut adj[current]);
for &nb in &current_adj {
adj[nb].retain(|&x| x != current);
} }
if child.len() == n { if child.len() == n {
break; break;
} }
let neighbors: Vec<usize> = adj[current] let neighbors: Vec<usize> = current_adj
.iter() .iter()
.copied() .copied()
.filter(|&c| !visited[c]) .filter(|&c| !visited[c])