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:
- Find the shortest path using Dijkstra
- 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
- For each node in path k-1:
- 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
shortest_path_single- Convenience function for single shortest pathshortest_distance- Compute path weight sumsNaturallyOrderedSemiring- Required trait
Examples found in repository?
More 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}