Skip to content

Commit 73d7aaa

Browse files
authored
Merge pull request #46 from yongqli/master
Compress trie even further by 1.9% using bipartite matching
2 parents 94f16bd + 5aed899 commit 73d7aaa

3 files changed

Lines changed: 192 additions & 140 deletions

File tree

generate/src/main.rs

Lines changed: 104 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ mod parse;
2323
mod write;
2424

2525
use crate::parse::parse_xid_properties;
26-
use std::collections::{BTreeMap as Map, VecDeque};
26+
use std::collections::BTreeMap as Map;
2727
use std::fs;
2828
use std::io::{self, Write};
2929
use std::path::Path;
@@ -86,58 +86,120 @@ fn main() {
8686
index_continue.pop();
8787
}
8888

89-
let mut halfchunkmap = Map::new();
90-
for chunk in &dense {
91-
let mut front = [0u8; CHUNK / 2];
92-
let mut back = [0u8; CHUNK / 2];
93-
front.copy_from_slice(&chunk[..CHUNK / 2]);
94-
back.copy_from_slice(&chunk[CHUNK / 2..]);
95-
halfchunkmap
96-
.entry(front)
97-
.or_insert_with(VecDeque::new)
98-
.push_back(back);
89+
// Compress the LEAF array by overlapping chunks at half-chunk boundaries.
90+
//
91+
// If chunk i's back half equals chunk j's front half, placing them
92+
// adjacently saves 32 bytes. We find the maximum number of such overlaps
93+
// by modeling this as a bipartite matching problem (left side = back
94+
// halves, right side = front halves) and solving with Kuhn's algorithm.
95+
96+
let num_chunks = dense.len();
97+
98+
let front_of: Vec<[u8; CHUNK / 2]> = dense
99+
.iter()
100+
.map(|c| c[..CHUNK / 2].try_into().unwrap())
101+
.collect();
102+
let back_of: Vec<[u8; CHUNK / 2]> = dense
103+
.iter()
104+
.map(|c| c[CHUNK / 2..].try_into().unwrap())
105+
.collect();
106+
107+
// Build index from front-half value to chunk indices for efficient lookup.
108+
let mut chunks_by_front: Map<[u8; CHUNK / 2], Vec<usize>> = Map::new();
109+
for (j, front) in front_of.iter().enumerate() {
110+
chunks_by_front.entry(*front).or_default().push(j);
111+
}
112+
113+
// adj_list[i] = chunks whose front half matches chunk i's back half,
114+
// meaning they can follow chunk i with a 32-byte overlap. Exclude
115+
// self-edges (the all-zeros and all-ones chunks have front == back).
116+
let adj_list: Vec<Vec<usize>> = (0..num_chunks)
117+
.map(|i| {
118+
chunks_by_front
119+
.get(&back_of[i])
120+
.map(|js| js.iter().copied().filter(|&j| j != i).collect())
121+
.unwrap_or_default()
122+
})
123+
.collect();
124+
125+
// Maximum bipartite matching via Kuhn's algorithm (augmenting paths).
126+
// prev_of[j] = Some(i) means chunk i is matched to precede chunk j.
127+
let mut prev_of: Vec<Option<usize>> = vec![None; num_chunks];
128+
129+
// DFS for an augmenting path from `src`. If found, augments the matching
130+
// in-place (rehoming existing matches to preserve validity) and returns true.
131+
fn try_kuhn(
132+
src: usize,
133+
adj_list: &[Vec<usize>],
134+
visited: &mut [bool],
135+
prev_of: &mut [Option<usize>],
136+
) -> bool {
137+
for &dst in &adj_list[src] {
138+
if !visited[dst] {
139+
visited[dst] = true;
140+
// If dst is free, or its current match can be rehomed, claim dst.
141+
if prev_of[dst].is_none_or(|prev| try_kuhn(prev, adj_list, visited, prev_of)) {
142+
prev_of[dst] = Some(src);
143+
return true;
144+
}
145+
}
146+
}
147+
false
148+
}
149+
150+
// Try every left vertex. A failed attempt stays failed because later
151+
// rounds only shrink the set of free right vertices (Berge's theorem).
152+
for i in 0..num_chunks {
153+
let mut visited = vec![false; num_chunks];
154+
try_kuhn(i, &adj_list, &mut visited, &mut prev_of);
155+
}
156+
157+
// Invert the matching into a forward map for chain traversal.
158+
let mut next_of: Vec<Option<usize>> = vec![None; num_chunks];
159+
for (j, &prev) in prev_of.iter().enumerate() {
160+
if let Some(prev) = prev {
161+
next_of[prev] = Some(j);
162+
}
163+
}
164+
165+
// Chunk 0 (all zeros) is special and must be laid out first at halfdense position 0,
166+
// because the runtime defaults to index 0 for codepoints beyond the trie.
167+
// Remove any incoming edge so chunk 0 becomes a chain start.
168+
if let Some(prev) = prev_of[0] {
169+
next_of[prev] = None;
170+
prev_of[0] = None;
99171
}
100172

173+
// Lay out chains into halfdense, starting with chunk 0's chain.
101174
let mut halfdense = Vec::<u8>::new();
102175
let mut dense_to_halfdense = Map::<u8, u8>::new();
103-
for chunk in &dense {
104-
let original_pos = chunkmap[chunk];
105-
if dense_to_halfdense.contains_key(&original_pos) {
106-
continue;
107-
}
108-
let mut front = [0u8; CHUNK / 2];
109-
let mut back = [0u8; CHUNK / 2];
110-
front.copy_from_slice(&chunk[..CHUNK / 2]);
111-
back.copy_from_slice(&chunk[CHUNK / 2..]);
176+
177+
for start in (0..num_chunks).filter(|&i| prev_of[i].is_none()) {
112178
dense_to_halfdense.insert(
113-
original_pos,
114-
match u8::try_from(halfdense.len() / (CHUNK / 2)) {
115-
Ok(byte) => byte,
116-
Err(_) => panic!("exceeded 256 half-chunks"),
117-
},
179+
start as u8,
180+
u8::try_from(halfdense.len() / (CHUNK / 2)).expect("exceeded 256 half-chunks"),
118181
);
119-
halfdense.extend_from_slice(&front);
120-
halfdense.extend_from_slice(&back);
121-
while let Some(next) = halfchunkmap.get_mut(&back).and_then(VecDeque::pop_front) {
122-
let mut concat = empty_chunk;
123-
concat[..CHUNK / 2].copy_from_slice(&back);
124-
concat[CHUNK / 2..].copy_from_slice(&next);
125-
let original_pos = chunkmap[&concat];
126-
if dense_to_halfdense.contains_key(&original_pos) {
127-
continue;
128-
}
182+
halfdense.extend_from_slice(&front_of[start]);
183+
halfdense.extend_from_slice(&back_of[start]);
184+
185+
// Write the rest of the chain: each chunk's front half overlaps the
186+
// previous chunk's back half, so only append the back half.
187+
let mut curr = start;
188+
while let Some(next) = next_of[curr] {
129189
dense_to_halfdense.insert(
130-
original_pos,
131-
match u8::try_from(halfdense.len() / (CHUNK / 2) - 1) {
132-
Ok(byte) => byte,
133-
Err(_) => panic!("exceeded 256 half-chunks"),
134-
},
190+
next as u8,
191+
u8::try_from(halfdense.len() / (CHUNK / 2) - 1).expect("exceeded 256 half-chunks"),
135192
);
136-
halfdense.extend_from_slice(&next);
137-
back = next;
193+
halfdense.extend_from_slice(&back_of[next]);
194+
curr = next;
138195
}
139196
}
140197

198+
// Each chunk can be both a predecessor (back half) and a successor
199+
// (front half), so next_of can form cycles with no chain start.
200+
// We broke chunk 0's cycle above; verify no others exist.
201+
assert_eq!(dense_to_halfdense.len(), num_chunks, "not all chunks were laid out");
202+
141203
for index in &mut index_start {
142204
*index = dense_to_halfdense[index];
143205
}

0 commit comments

Comments
 (0)