Skip to main content

shortest_path

Function shortest_path 

Source
pub fn shortest_path<W, F, M>(fst: &F, config: ShortestPathConfig) -> Result<M>
Expand description

Find k-shortest paths using Yen’s algorithm

Computes the k shortest loopless paths from the start state to any final state in the FST using Yen’s algorithm. Returns an FST containing all k paths.

§Algorithm: Yen’s K-Shortest Paths

Algorithm Details:

  • Base: Iterative Dijkstra with edge removal
  • Time Complexity: O(k × |V| × (|E| + |V| log |V|))
  • Space Complexity: O(k × |V| + |V|²)
  • Path Property: All returned paths are loopless (no repeated states)
  • Ordering: Paths returned in strictly increasing weight order

Algorithm Steps:

  1. Find the shortest path using Dijkstra
  2. For each subsequent path k = 2..n:
    • For each node in path k-1:
      • Temporarily remove edges that would duplicate previous paths
      • Find shortest path from that node (spur node) to any final state
      • Add candidate path to priority queue
    • Select best candidate as path k
  3. Build result FST containing all k paths

§Semiring Requirements

The input FST must use a NaturallyOrderedSemiring that provides:

  • Total ordering for path weight comparison
  • Monotonic path weight accumulation
  • Well-defined shortest path semantics

§Configuration

  • nshortest: Number of shortest paths to find (default: 1)
  • unique: If true, only return paths with unique input/output sequences (default: false)

§Examples

§Single Shortest Path

use arcweight::prelude::*;

// Create FST: 0 --a/0.5--> 1 --b/0.3--> 2(final)
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.set_final(s2, TropicalWeight::one());
fst.add_arc(s0, Arc::new('a' as u32, 'a' as u32, TropicalWeight::new(0.5), s1));
fst.add_arc(s1, Arc::new('b' as u32, 'b' as u32, TropicalWeight::new(0.3), s2));

// Find single shortest path
let config = ShortestPathConfig::default();
let shortest: VectorFst<TropicalWeight> = shortest_path(&fst, config)?;

// Result contains path "ab" with total weight 0.8
assert!(shortest.num_states() > 0);

§K-Best Paths

use arcweight::prelude::*;

// FST with multiple paths of different costs
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());

// Add multiple arcs with different weights
fst.add_arc(s0, Arc::new('a' as u32, 'a' as u32, TropicalWeight::new(1.0), s1));
fst.add_arc(s0, Arc::new('b' as u32, 'b' as u32, TropicalWeight::new(2.0), s1));
fst.add_arc(s0, Arc::new('c' as u32, 'c' as u32, TropicalWeight::new(3.0), s1));

// Find top 3 shortest paths
let config = ShortestPathConfig {
    nshortest: 3,
    unique: false,
};

let k_best: VectorFst<TropicalWeight> = shortest_path(&fst, config)?;
// Result FST contains all 3 paths: a (1.0), b (2.0), c (3.0)

§Unique Paths Only

use arcweight::prelude::*;

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.set_final(s2, TropicalWeight::one());

// Two paths with same input/output but different intermediate states
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1));
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::new(1.0), s2));

fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(0.5), s2)); // Direct path, same I/O

// Find unique paths only
let config = ShortestPathConfig {
    nshortest: 5,
    unique: true,  // Filter duplicate input/output sequences
};

let unique_paths: VectorFst<TropicalWeight> = shortest_path(&fst, config)?;

§Complex Network

use arcweight::prelude::*;

// Complex FST with multiple paths through different routes
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();
let s4 = fst.add_state();

fst.set_start(s0);
fst.set_final(s4, TropicalWeight::one());

// Create diamond pattern with multiple paths
fst.add_arc(s0, Arc::new(1, 1, TropicalWeight::new(1.0), s1)); // Top route
fst.add_arc(s1, Arc::new(2, 2, TropicalWeight::new(1.0), s3));
fst.add_arc(s3, Arc::new(3, 3, TropicalWeight::new(1.0), s4));

fst.add_arc(s0, Arc::new(4, 4, TropicalWeight::new(2.0), s2)); // Bottom route
fst.add_arc(s2, Arc::new(5, 5, TropicalWeight::new(1.0), s3));
fst.add_arc(s3, Arc::new(3, 3, TropicalWeight::new(1.0), s4));

// Find 10 best paths
let config = ShortestPathConfig {
    nshortest: 10,
    unique: false,
};

let paths: VectorFst<TropicalWeight> = shortest_path(&fst, config)?;

§Performance Characteristics

  • Time per path: O(|V| × (|E| + |V| log |V|))
  • Total time: O(k × |V| × (|E| + |V| log |V|))
  • Memory: O(k × |V|) for storing k paths + O(|V|²) for edge exclusion tracking
  • Optimality: Guaranteed to find k shortest loopless paths in order

§Errors

Returns Error::Algorithm if:

  • The input FST has no start state
  • No paths exist to any final state
  • Memory allocation fails during computation
  • Weight computation overflows or becomes infinite

§References

[1] Yen, J. Y. 1971. Finding the k shortest loopless paths in a network. Management Science 17, 11 (July 1971), 712-716. DOI: https://doi.org/10.1287/mnsc.17.11.712

[2] Mohri, M. and Riley, M. 2002. An efficient algorithm for the n-best-strings problem. In Proceedings of ICSLP 2002, 1313-1316.

§See Also

Examples found in repository?
examples/string_alignment.rs (line 317)
312fn compute_alignment_fst(source: &str, target: &str) -> Result<Alignment> {
313    // Build alignment FST
314    let alignment_fst = build_alignment_fst(source, target, 1.0, 1.0, 1.0);
315
316    // Find shortest path
317    let shortest = shortest_path(&alignment_fst, ShortestPathConfig::default())?;
318
319    // Extract alignment from shortest path
320    extract_alignment_from_path(source, target, &shortest)
321}
More examples
Hide additional examples
examples/edit_distance.rs (line 135)
117fn compute_edit_distance(
118    source: &str,
119    target: &str,
120    insertion_cost: f32,
121    deletion_cost: f32,
122    substitution_cost: f32,
123) -> Result<f32> {
124    // Build edit distance FST that directly computes the distance
125    let edit_fst = build_edit_distance_fst(
126        source,
127        target,
128        insertion_cost,
129        deletion_cost,
130        substitution_cost,
131    );
132
133    // Find shortest path from start to final state
134    let config = ShortestPathConfig::default();
135    let shortest: VectorFst<TropicalWeight> = shortest_path(&edit_fst, config)?;
136
137    // Get the cost from the shortest path
138    if let Some(start) = shortest.start() {
139        // Check if there's a path to a final state
140        let mut stack = vec![(start, 0.0)];
141        let mut visited = std::collections::HashSet::new();
142
143        while let Some((state, cost)) = stack.pop() {
144            if visited.contains(&state) {
145                continue;
146            }
147            visited.insert(state);
148
149            if shortest.is_final(state) {
150                return Ok(cost);
151            }
152
153            for arc in shortest.arcs(state) {
154                stack.push((arc.nextstate, cost + arc.weight.value()));
155            }
156        }
157    }
158
159    // If no path found, return infinity
160    Ok(f32::INFINITY)
161}
examples/spell_checking.rs (line 211)
195fn find_spelling_corrections(
196    dict_fst: &VectorFst<TropicalWeight>,
197    target: &str,
198    max_distance: usize,
199) -> Result<Vec<(String, f32)>> {
200    // Build edit distance FST
201    let edit_fst = build_edit_distance_fst(target, max_distance);
202
203    // Compose dictionary with edit distance FST
204    let composed: VectorFst<TropicalWeight> = compose_default(dict_fst, &edit_fst)?;
205
206    // Find shortest paths
207    let config = ShortestPathConfig {
208        nshortest: 10,
209        ..Default::default()
210    };
211    let shortest: VectorFst<TropicalWeight> = shortest_path(&composed, config)?;
212
213    // Extract words and distances
214    let mut results = Vec::new();
215
216    if let Some(start) = shortest.start() {
217        extract_paths(&shortest, start, &mut Vec::new(), 0.0, &mut results);
218    }
219
220    // Deduplicate results and keep the best score for each word
221    let mut word_scores: HashMap<String, f32> = HashMap::new();
222    for (word, score) in results {
223        word_scores
224            .entry(word)
225            .and_modify(|e| *e = e.min(score))
226            .or_insert(score);
227    }
228
229    // Convert back to vec and sort by distance
230    let mut final_results: Vec<(String, f32)> = word_scores.into_iter().collect();
231    final_results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
232    Ok(final_results)
233}