perf(operators): make PMX and OX crossovers O(n) with value-indexed lookups
Both crossovers had an O(n) inner scan making them O(n^2): PMX located the value to swap with `child.iter().position`, OX tested segment membership with `segment.contains`. Both operate on strict permutations of 0..n, so a value-indexed table (PMX: position kept in sync across swaps; OX: a static membership bitmap) gives O(1) lookups. debug_asserts document the range assumption, consistent with ERX and CX. pmx_crossover_vary n=100: 21_507 -> 5_900 (-73%, 3.65x); n=30 -18%. order_crossover_vary n=100: 18_265 -> 7_389 (-60%, 2.47x); n=30 -29%. All four permutation crossovers are now O(n). Bit-identical for valid permutations -- all 606 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -372,12 +372,20 @@ fn ox_child(donor: &[usize], filler: &[usize], lo: usize, hi: usize) -> Vec<usiz
|
|||||||
let mut child = vec![0_usize; n];
|
let mut child = vec![0_usize; n];
|
||||||
let segment = &donor[lo..hi];
|
let segment = &donor[lo..hi];
|
||||||
child[lo..hi].copy_from_slice(segment);
|
child[lo..hi].copy_from_slice(segment);
|
||||||
|
// Membership bitmap for the donor segment. OX operates on strict
|
||||||
|
// permutations of 0..n, so a value-indexed bitmap replaces the
|
||||||
|
// O(segment) `contains` scan inside the fill loop with an O(1) check.
|
||||||
|
let mut in_segment = vec![false; n];
|
||||||
|
for &v in segment {
|
||||||
|
debug_assert!(v < n, "OrderCrossover requires strict permutations of 0..n");
|
||||||
|
in_segment[v] = true;
|
||||||
|
}
|
||||||
let mut fill_pos = hi % n;
|
let mut fill_pos = hi % n;
|
||||||
let mut filler_pos = hi % n;
|
let mut filler_pos = hi % n;
|
||||||
let mut placed = hi - lo;
|
let mut placed = hi - lo;
|
||||||
while placed < n {
|
while placed < n {
|
||||||
let v = filler[filler_pos];
|
let v = filler[filler_pos];
|
||||||
if !segment.contains(&v) {
|
if !in_segment[v] {
|
||||||
child[fill_pos] = v;
|
child[fill_pos] = v;
|
||||||
fill_pos = (fill_pos + 1) % n;
|
fill_pos = (fill_pos + 1) % n;
|
||||||
placed += 1;
|
placed += 1;
|
||||||
@@ -452,16 +460,29 @@ fn pmx_child(donor: &[usize], base: &[usize], lo: usize, hi: usize) -> Vec<usize
|
|||||||
// Start from a copy of `base`; for each position k in [lo, hi), swap so
|
// Start from a copy of `base`; for each position k in [lo, hi), swap so
|
||||||
// that child[k] == donor[k]. Each swap preserves the permutation.
|
// that child[k] == donor[k]. Each swap preserves the permutation.
|
||||||
let mut child = base.to_vec();
|
let mut child = base.to_vec();
|
||||||
|
let n = child.len();
|
||||||
|
// pos[value] = current index of that value in `child`. PMX operates on
|
||||||
|
// strict permutations of 0..n, so this value-indexed table replaces the
|
||||||
|
// O(n) `position` scan with an O(1) lookup, kept in sync across swaps.
|
||||||
|
let mut pos = vec![0_usize; n];
|
||||||
|
for (i, &v) in child.iter().enumerate() {
|
||||||
|
debug_assert!(
|
||||||
|
v < n,
|
||||||
|
"PartiallyMappedCrossover requires strict permutations of 0..n",
|
||||||
|
);
|
||||||
|
pos[v] = i;
|
||||||
|
}
|
||||||
for k in lo..hi {
|
for k in lo..hi {
|
||||||
let v = donor[k];
|
let v = donor[k];
|
||||||
if child[k] == v {
|
if child[k] == v {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let cur = child
|
let cur = pos[v];
|
||||||
.iter()
|
let displaced = child[k];
|
||||||
.position(|&x| x == v)
|
|
||||||
.expect("permutation invariant: every value must appear");
|
|
||||||
child.swap(k, cur);
|
child.swap(k, cur);
|
||||||
|
// child[k] is now `v`; child[cur] is now `displaced`.
|
||||||
|
pos[v] = k;
|
||||||
|
pos[displaced] = cur;
|
||||||
}
|
}
|
||||||
child
|
child
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user