use crate::arc::Arc;
use crate::fst::{Fst, Label, MutableFst, StateId};
use crate::semiring::Semiring;
use crate::Result;
use rustc_hash::{FxHashMap, FxHashSet};
use std::collections::VecDeque;
pub fn minimize_hopcroft<W, F, M>(fst: &F) -> Result<M>
where
W: Semiring + Eq + std::hash::Hash,
F: Fst<W>,
M: MutableFst<W> + Default,
{
let num_states = fst.num_states();
if num_states == 0 {
return Ok(M::default());
}
let reverse_delta = build_reverse_delta(fst);
let all_labels = collect_all_labels(fst);
let mut partition = initial_partition(fst);
let mut state_to_block: Vec<usize> = vec![0; num_states];
for (block_idx, block) in partition.iter().enumerate() {
for &state in block {
state_to_block[state as usize] = block_idx;
}
}
let mut worklist: VecDeque<(usize, Label)> = VecDeque::new();
for block_idx in 0..partition.len() {
for &label in &all_labels {
worklist.push_back((block_idx, label));
}
}
while let Some((splitter_block_idx, label)) = worklist.pop_front() {
if splitter_block_idx >= partition.len() {
continue;
}
let splitter = &partition[splitter_block_idx];
if splitter.is_empty() {
continue;
}
let mut predecessors: FxHashSet<StateId> = FxHashSet::default();
for &target in splitter {
let key = (target, label);
if let Some(sources) = reverse_delta.get(&key) {
for &source in sources {
predecessors.insert(source);
}
}
}
if predecessors.is_empty() {
continue;
}
let mut new_blocks: Vec<FxHashSet<StateId>> = Vec::new();
let mut blocks_to_split: Vec<(usize, FxHashSet<StateId>, FxHashSet<StateId>)> = Vec::new();
let mut preds_by_block: FxHashMap<usize, FxHashSet<StateId>> = FxHashMap::default();
for &pred in &predecessors {
let block_idx = state_to_block[pred as usize];
preds_by_block.entry(block_idx).or_default().insert(pred);
}
for (block_idx, preds_in_block) in preds_by_block {
if block_idx >= partition.len() {
continue;
}
let block = &partition[block_idx];
if preds_in_block.len() < block.len() && !preds_in_block.is_empty() {
let not_preds: FxHashSet<StateId> = block
.iter()
.filter(|s| !preds_in_block.contains(s))
.copied()
.collect();
if !not_preds.is_empty() {
blocks_to_split.push((block_idx, preds_in_block, not_preds));
}
}
}
for (block_idx, preds, not_preds) in blocks_to_split {
let (keep, move_out) = if preds.len() <= not_preds.len() {
(preds, not_preds)
} else {
(not_preds, preds)
};
partition[block_idx] = keep;
let new_block_idx = partition.len();
for &state in &move_out {
state_to_block[state as usize] = new_block_idx;
}
new_blocks.push(move_out);
for &l in &all_labels {
worklist.push_back((new_block_idx, l));
}
}
for block in new_blocks {
partition.push(block);
}
}
build_minimized_fst(fst, &partition, &state_to_block)
}
fn build_reverse_delta<W, F>(fst: &F) -> FxHashMap<(StateId, Label), Vec<StateId>>
where
W: Semiring,
F: Fst<W>,
{
let mut reverse = FxHashMap::default();
for state in 0..fst.num_states() as StateId {
for arc in fst.arcs(state) {
let key = (arc.nextstate, arc.ilabel);
reverse.entry(key).or_insert_with(Vec::new).push(state);
}
}
reverse
}
fn collect_all_labels<W, F>(fst: &F) -> FxHashSet<Label>
where
W: Semiring,
F: Fst<W>,
{
let mut labels = FxHashSet::default();
for state in 0..fst.num_states() as StateId {
for arc in fst.arcs(state) {
labels.insert(arc.ilabel);
}
}
labels
}
fn initial_partition<W, F>(fst: &F) -> Vec<FxHashSet<StateId>>
where
W: Semiring + Eq + std::hash::Hash,
F: Fst<W>,
{
let mut weight_groups: FxHashMap<Option<W>, FxHashSet<StateId>> = FxHashMap::default();
for state in 0..fst.num_states() as StateId {
let final_weight = fst.final_weight(state).cloned();
weight_groups.entry(final_weight).or_default().insert(state);
}
weight_groups.into_values().collect()
}
fn build_minimized_fst<W, F, M>(
fst: &F,
partition: &[FxHashSet<StateId>],
state_to_block: &[usize],
) -> Result<M>
where
W: Semiring,
F: Fst<W>,
M: MutableFst<W> + Default,
{
let mut result = M::default();
let mut block_to_new_state: FxHashMap<usize, StateId> = FxHashMap::default();
for (block_idx, block) in partition.iter().enumerate() {
if !block.is_empty() {
let new_state = result.add_state();
block_to_new_state.insert(block_idx, new_state);
}
}
if let Some(start) = fst.start() {
let start_block = state_to_block[start as usize];
if let Some(&new_start) = block_to_new_state.get(&start_block) {
result.set_start(new_start);
}
}
let mut processed_blocks: FxHashSet<usize> = FxHashSet::default();
for (block_idx, block) in partition.iter().enumerate() {
if block.is_empty() || processed_blocks.contains(&block_idx) {
continue;
}
processed_blocks.insert(block_idx);
let representative = *block.iter().next().unwrap();
let Some(&new_state) = block_to_new_state.get(&block_idx) else {
continue;
};
if let Some(w) = fst.final_weight(representative) {
result.set_final(new_state, w.clone());
}
let mut arc_weights: FxHashMap<(Label, Label, usize), W> = FxHashMap::default();
for arc in fst.arcs(representative) {
let target_block = state_to_block[arc.nextstate as usize];
let arc_key = (arc.ilabel, arc.olabel, target_block);
arc_weights
.entry(arc_key)
.and_modify(|w| w.plus_assign(&arc.weight))
.or_insert_with(|| arc.weight.clone());
}
for ((ilabel, olabel, target_block), weight) in arc_weights {
if let Some(&new_target) = block_to_new_state.get(&target_block) {
result.add_arc(new_state, Arc::new(ilabel, olabel, weight, new_target));
}
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
#[test]
fn test_hopcroft_empty_fst() {
let fst = VectorFst::<TropicalWeight>::new();
let result: VectorFst<TropicalWeight> = minimize_hopcroft(&fst).unwrap();
assert_eq!(result.num_states(), 0);
}
#[test]
fn test_hopcroft_single_state() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
fst.set_start(s0);
fst.set_final(s0, TropicalWeight::one());
let result: VectorFst<TropicalWeight> = minimize_hopcroft(&fst).unwrap();
assert_eq!(result.num_states(), 1);
assert_eq!(result.start(), Some(0));
}
#[test]
fn test_hopcroft_equivalent_states() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
let s3 = fst.add_state();
fst.set_start(s0);
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
fst.add_arc(s0, Arc::new(2, 2, TropicalWeight::one(), s2));
fst.add_arc(s1, Arc::new(1, 1, TropicalWeight::one(), s3));
fst.add_arc(s2, Arc::new(1, 1, TropicalWeight::one(), s3));
fst.set_final(s3, TropicalWeight::one());
let result: VectorFst<TropicalWeight> = minimize_hopcroft(&fst).unwrap();
assert!(result.num_states() <= 3);
}
#[test]
fn test_hopcroft_already_minimal() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
fst.set_start(s0);
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::one(), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::one(), s2));
fst.set_final(s2, TropicalWeight::one());
let result: VectorFst<TropicalWeight> = minimize_hopcroft(&fst).unwrap();
assert_eq!(result.num_states(), 3);
}
#[test]
fn test_hopcroft_preserves_language() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
let s2 = fst.add_state();
let s3 = fst.add_state();
fst.set_start(s0);
fst.add_arc(
s0,
Arc::new(b'a' as u32, b'a' as u32, TropicalWeight::one(), s1),
);
fst.add_arc(
s0,
Arc::new(b'a' as u32, b'a' as u32, TropicalWeight::one(), s2),
);
fst.add_arc(
s1,
Arc::new(b'b' as u32, b'b' as u32, TropicalWeight::one(), s3),
);
fst.add_arc(
s2,
Arc::new(b'b' as u32, b'b' as u32, TropicalWeight::one(), s3),
);
fst.set_final(s3, TropicalWeight::one());
let result: VectorFst<TropicalWeight> = minimize_hopcroft(&fst).unwrap();
assert!(result.start().is_some());
assert!(result.num_states() <= 3);
}
#[test]
fn test_hopcroft_combines_arc_weights() {
let mut fst = VectorFst::<TropicalWeight>::new();
let s0 = fst.add_state();
let s1 = fst.add_state();
fst.set_start(s0);
fst.set_final(s1, TropicalWeight::one());
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1));
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(2.0), s1));
let result: VectorFst<TropicalWeight> = minimize_hopcroft(&fst).unwrap();
assert_eq!(result.num_states(), 2);
if let Some(start) = result.start() {
let arcs: Vec<_> = result.arcs(start).collect();
assert_eq!(arcs.len(), 1, "Should have one combined arc");
assert_eq!(
arcs[0].weight,
TropicalWeight::new(1.0),
"Weight should be min(1.0, 2.0) = 1.0"
);
}
}
}