1use crate::lattice::{Lattice, Node, NodeId, NodeType, INVALID_NODE_ID};
37use std::cmp::Ordering;
38use std::collections::BinaryHeap;
39use std::rc::Rc;
40
41#[cfg(feature = "simd")]
43pub mod simd;
44
45#[cfg(feature = "simd")]
46pub use simd::{simd_forward_pass_position, simd_update_node_cost};
47
48#[inline(always)]
52fn saturating_add_chain(a: i32, b: i32, c: i32, d: i32) -> i32 {
53 a.saturating_add(b).saturating_add(c).saturating_add(d)
54}
55
56pub trait ConnectionCost {
61 fn cost(&self, right_id: u16, left_id: u16) -> i32;
72}
73
74#[derive(Debug, Clone, Default)]
78pub struct ZeroConnectionCost;
79
80impl ConnectionCost for ZeroConnectionCost {
81 #[inline(always)]
82 fn cost(&self, _right_id: u16, _left_id: u16) -> i32 {
83 0
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct FixedConnectionCost {
90 pub default_cost: i32,
92}
93
94impl FixedConnectionCost {
95 #[must_use]
97 pub const fn new(cost: i32) -> Self {
98 Self { default_cost: cost }
99 }
100}
101
102impl ConnectionCost for FixedConnectionCost {
103 #[inline(always)]
104 fn cost(&self, _right_id: u16, _left_id: u16) -> i32 {
105 self.default_cost
106 }
107}
108
109impl<T: mecab_ko_dict::Matrix> ConnectionCost for T {
113 #[inline(always)]
114 fn cost(&self, right_id: u16, left_id: u16) -> i32 {
115 self.get(right_id, left_id)
116 }
117}
118
119#[derive(Debug, Clone, Default)]
136pub struct SpacePenalty {
137 penalties: Vec<(u16, i32)>,
140}
141
142impl SpacePenalty {
143 #[must_use]
145 pub fn new() -> Self {
146 Self::default()
147 }
148
149 #[must_use]
154 pub fn korean_default() -> Self {
155 let mut penalties: Vec<(u16, i32)> = (1700u16..1760)
162 .chain(1780..1810)
163 .map(|id| (id, 6000))
164 .collect();
165
166 penalties.sort_unstable_by_key(|&(id, _)| id);
168 Self { penalties }
169 }
170
171 #[must_use]
188 pub fn from_dicrc(config: &str) -> Self {
189 let mut penalties = Vec::new();
190
191 for part in config.split(';') {
192 let parts: Vec<&str> = part.trim().split(',').collect();
193 if parts.len() == 2 {
194 if let (Ok(id), Ok(penalty)) = (
195 parts[0].trim().parse::<u16>(),
196 parts[1].trim().parse::<i32>(),
197 ) {
198 penalties.push((id, penalty));
199 }
200 }
201 }
202
203 penalties.sort_unstable_by_key(|&(id, _)| id);
205 Self { penalties }
206 }
207
208 pub fn add(&mut self, left_id: u16, penalty: i32) {
210 let pos = self.penalties.partition_point(|&(id, _)| id < left_id);
212 self.penalties.insert(pos, (left_id, penalty));
213 }
214
215 #[must_use]
221 #[inline]
222 pub fn get(&self, left_id: u16) -> i32 {
223 self.penalties
225 .binary_search_by_key(&left_id, |&(id, _)| id)
226 .map_or(0, |idx| self.penalties[idx].1)
227 }
228
229 #[must_use]
231 #[inline]
232 pub fn is_empty(&self) -> bool {
233 self.penalties.is_empty()
234 }
235
236 #[must_use]
238 #[inline]
239 pub fn len(&self) -> usize {
240 self.penalties.len()
241 }
242}
243
244#[derive(Debug, Clone)]
248pub struct ViterbiSearcher {
249 pub space_penalty: SpacePenalty,
251}
252
253impl Default for ViterbiSearcher {
254 fn default() -> Self {
255 Self::new()
256 }
257}
258
259impl ViterbiSearcher {
260 #[must_use]
262 pub fn new() -> Self {
263 Self {
264 space_penalty: SpacePenalty::default(),
265 }
266 }
267
268 #[must_use]
270 pub fn with_space_penalty(mut self, penalty: SpacePenalty) -> Self {
271 self.space_penalty = penalty;
272 self
273 }
274
275 pub fn search<C: ConnectionCost>(&self, lattice: &mut Lattice, conn_cost: &C) -> Vec<NodeId> {
301 self.forward_pass(lattice, conn_cost);
303
304 Self::backward_pass(lattice)
306 }
307
308 fn forward_pass<C: ConnectionCost>(&self, lattice: &mut Lattice, conn_cost: &C) {
312 let char_len = lattice.char_len();
313
314 let mut starting_ids: Vec<NodeId> = Vec::new();
318 let mut ending_nodes: Vec<(NodeId, i32, u16)> = Vec::new();
319
320 for pos in 0..=char_len {
322 starting_ids.clear();
324 starting_ids.extend(lattice.nodes_starting_at(pos).map(|n| n.id));
325
326 ending_nodes.clear();
329 ending_nodes.extend(
330 lattice
331 .nodes_ending_at(pos)
332 .map(|n| (n.id, n.total_cost, n.right_id)),
333 );
334
335 for &node_id in &starting_ids {
336 self.update_node_cost_with_endings(lattice, conn_cost, node_id, &ending_nodes);
337 }
338 }
339 }
340
341 #[inline]
346 fn update_node_cost_with_endings<C: ConnectionCost>(
347 &self,
348 lattice: &mut Lattice,
349 conn_cost: &C,
350 node_id: NodeId,
351 ending_nodes: &[(NodeId, i32, u16)],
352 ) {
353 #[cfg(feature = "simd")]
355 if ending_nodes.len() >= 8 {
356 let (best_cost, best_prev) =
357 simd::simd_update_node_cost(lattice, conn_cost, node_id, ending_nodes, &self.space_penalty);
358 if let Some(node) = lattice.node_mut(node_id) {
359 node.total_cost = best_cost;
360 node.prev_node_id = best_prev;
361 }
362 return;
363 }
364
365 let (left_id, word_cost, has_space) = {
367 let Some(node) = lattice.node(node_id) else {
368 return;
369 };
370 (node.left_id, node.word_cost, node.has_space_before)
371 };
372
373 let space_penalty = if has_space {
375 self.space_penalty.get(left_id)
376 } else {
377 0
378 };
379
380 let mut best_cost = i32::MAX;
381 let mut best_prev = INVALID_NODE_ID;
382
383 for &(prev_id, prev_cost, prev_right_id) in ending_nodes {
384 if prev_cost == i32::MAX {
386 continue;
387 }
388
389 let connection = conn_cost.cost(prev_right_id, left_id);
391
392 let total = saturating_add_chain(prev_cost, connection, word_cost, space_penalty);
394
395 if total < best_cost {
396 best_cost = total;
397 best_prev = prev_id;
398 }
399 }
400
401 if let Some(node) = lattice.node_mut(node_id) {
403 node.total_cost = best_cost;
404 node.prev_node_id = best_prev;
405 }
406 }
407
408 #[cfg(test)]
410 #[allow(dead_code)]
411 fn update_node_cost<C: ConnectionCost>(
412 &self,
413 lattice: &mut Lattice,
414 conn_cost: &C,
415 node_id: NodeId,
416 pos: usize,
417 ) {
418 let (left_id, word_cost, has_space) = {
420 let Some(node) = lattice.node(node_id) else {
421 return;
422 };
423 (node.left_id, node.word_cost, node.has_space_before)
424 };
425
426 let ending_nodes: Vec<(NodeId, i32, u16)> = lattice
428 .nodes_ending_at(pos)
429 .map(|n| (n.id, n.total_cost, n.right_id))
430 .collect();
431
432 let mut best_cost = i32::MAX;
433 let mut best_prev = INVALID_NODE_ID;
434
435 for (prev_id, prev_cost, prev_right_id) in ending_nodes {
436 if prev_cost == i32::MAX {
437 continue;
438 }
439
440 let connection = conn_cost.cost(prev_right_id, left_id);
441
442 let space_penalty = if has_space {
443 self.space_penalty.get(left_id)
444 } else {
445 0
446 };
447
448 let total = prev_cost
449 .saturating_add(connection)
450 .saturating_add(word_cost)
451 .saturating_add(space_penalty);
452
453 if total < best_cost {
454 best_cost = total;
455 best_prev = prev_id;
456 }
457 }
458
459 if let Some(node) = lattice.node_mut(node_id) {
461 node.total_cost = best_cost;
462 node.prev_node_id = best_prev;
463 }
464 }
465
466 fn backward_pass(lattice: &Lattice) -> Vec<NodeId> {
470 let mut path = Vec::new();
471 let mut current_id = lattice.eos().id;
472
473 while current_id != INVALID_NODE_ID {
474 if let Some(node) = lattice.node(current_id) {
475 if node.node_type != NodeType::Bos && node.node_type != NodeType::Eos {
477 path.push(current_id);
478 }
479 current_id = node.prev_node_id;
480 } else {
481 break;
482 }
483 }
484
485 path.reverse();
486 path
487 }
488
489 #[must_use]
491 pub fn get_best_cost(&self, lattice: &Lattice) -> i32 {
492 lattice.eos().total_cost
493 }
494
495 #[must_use]
499 pub fn has_valid_path(&self, lattice: &Lattice) -> bool {
500 lattice.eos().total_cost != i32::MAX && lattice.eos().prev_node_id != INVALID_NODE_ID
501 }
502}
503
504#[derive(Debug, Clone)]
513struct PathNode {
514 node_id: NodeId,
516 prev: Option<Rc<Self>>,
518}
519
520impl PathNode {
521 #[allow(clippy::missing_const_for_fn)]
525 fn new(node_id: NodeId, prev: Option<Rc<Self>>) -> Self {
526 Self { node_id, prev }
527 }
528
529 fn to_vec(&self) -> Vec<NodeId> {
531 let mut path = Vec::new();
532 let mut current = Some(self);
533
534 while let Some(node) = current {
535 path.push(node.node_id);
536 current = node.prev.as_ref().map(std::convert::AsRef::as_ref);
537 }
538
539 path.reverse();
540 path
541 }
542}
543
544#[derive(Debug, Clone)]
546struct NbestCandidate {
547 node_id: NodeId,
549 cost: i32,
551 path: Option<Rc<PathNode>>,
553}
554
555impl Eq for NbestCandidate {}
556
557impl PartialEq for NbestCandidate {
558 fn eq(&self, other: &Self) -> bool {
559 self.cost == other.cost
560 }
561}
562
563impl Ord for NbestCandidate {
564 fn cmp(&self, other: &Self) -> Ordering {
565 other.cost.cmp(&self.cost)
567 }
568}
569
570impl PartialOrd for NbestCandidate {
571 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
572 Some(self.cmp(other))
573 }
574}
575
576#[derive(Debug, Clone)]
580pub struct NbestSearcher {
581 viterbi: ViterbiSearcher,
583 max_results: usize,
585}
586
587impl NbestSearcher {
588 #[must_use]
590 pub fn new(n: usize) -> Self {
591 Self {
592 viterbi: ViterbiSearcher::new(),
593 max_results: n,
594 }
595 }
596
597 #[must_use]
599 pub fn with_space_penalty(mut self, penalty: SpacePenalty) -> Self {
600 self.viterbi.space_penalty = penalty;
601 self
602 }
603
604 pub fn search<C: ConnectionCost>(
615 &self,
616 lattice: &mut Lattice,
617 conn_cost: &C,
618 ) -> Vec<(Vec<NodeId>, i32)> {
619 self.viterbi.forward_pass(lattice, conn_cost);
621
622 if !self.viterbi.has_valid_path(lattice) {
624 return Vec::new();
625 }
626
627 if self.max_results == 1 {
629 let path = ViterbiSearcher::backward_pass(lattice);
630 let cost = self.viterbi.get_best_cost(lattice);
631 return vec![(path, cost)];
632 }
633
634 self.search_nbest(lattice, conn_cost)
636 }
637
638 fn search_nbest<C: ConnectionCost>(
645 &self,
646 lattice: &Lattice,
647 _conn_cost: &C,
648 ) -> Vec<(Vec<NodeId>, i32)> {
649 let mut results: Vec<(Vec<NodeId>, i32)> = Vec::new();
650 let mut heap: BinaryHeap<NbestCandidate> = BinaryHeap::new();
651
652 let eos = lattice.eos();
654 if eos.total_cost == i32::MAX {
655 return results;
656 }
657
658 heap.push(NbestCandidate {
659 node_id: eos.id,
660 cost: eos.total_cost,
661 path: None,
662 });
663
664 while let Some(candidate) = heap.pop() {
665 if results.len() >= self.max_results {
666 break;
667 }
668
669 let Some(node) = lattice.node(candidate.node_id) else {
670 continue;
671 };
672
673 let current_path = if node.node_type != NodeType::Bos && node.node_type != NodeType::Eos
675 {
676 Some(Rc::new(PathNode::new(candidate.node_id, candidate.path)))
678 } else {
679 candidate.path
680 };
681
682 if node.node_type == NodeType::Bos {
684 let path_vec = current_path.map_or_else(Vec::new, |path_node| path_node.to_vec());
686 results.push((path_vec, candidate.cost));
687 continue;
688 }
689
690 if node.prev_node_id != INVALID_NODE_ID {
692 heap.push(NbestCandidate {
693 node_id: node.prev_node_id,
694 cost: candidate.cost,
695 path: current_path,
696 });
697 }
698 }
699
700 results
701 }
702}
703
704pub struct ViterbiResult<'a> {
706 lattice: &'a Lattice,
708 path: Vec<NodeId>,
710 total_cost: i32,
712}
713
714impl<'a> ViterbiResult<'a> {
715 #[must_use]
717 pub const fn new(lattice: &'a Lattice, path: Vec<NodeId>, total_cost: i32) -> Self {
718 Self {
719 lattice,
720 path,
721 total_cost,
722 }
723 }
724
725 pub fn nodes(&self) -> impl Iterator<Item = &'a Node> + '_ {
727 self.path.iter().filter_map(|&id| self.lattice.node(id))
728 }
729
730 #[must_use]
732 pub const fn cost(&self) -> i32 {
733 self.total_cost
734 }
735
736 #[must_use]
738 pub fn len(&self) -> usize {
739 self.path.len()
740 }
741
742 #[must_use]
744 pub fn is_empty(&self) -> bool {
745 self.path.is_empty()
746 }
747
748 #[must_use]
750 pub fn surfaces(&self) -> Vec<&str> {
751 self.nodes().map(|n| n.surface.as_ref()).collect()
752 }
753}
754
755#[cfg(test)]
756#[allow(clippy::unwrap_used)]
757mod tests {
758 use super::*;
759 use crate::lattice::NodeBuilder;
760
761 struct TestConnectionCost {
763 costs: std::collections::HashMap<(u16, u16), i32>,
764 default: i32,
765 }
766
767 impl TestConnectionCost {
768 fn new(default: i32) -> Self {
769 Self {
770 costs: std::collections::HashMap::new(),
771 default,
772 }
773 }
774
775 fn set(&mut self, right_id: u16, left_id: u16, cost: i32) {
776 self.costs.insert((right_id, left_id), cost);
777 }
778 }
779
780 impl ConnectionCost for TestConnectionCost {
781 fn cost(&self, right_id: u16, left_id: u16) -> i32 {
782 self.costs
783 .get(&(right_id, left_id))
784 .copied()
785 .unwrap_or(self.default)
786 }
787 }
788
789 #[test]
790 fn test_space_penalty_default() {
791 let penalty = SpacePenalty::default();
792 assert!(penalty.is_empty());
793 assert_eq!(penalty.get(100), 0);
794 }
795
796 #[test]
797 fn test_space_penalty_from_dicrc() {
798 let penalty = SpacePenalty::from_dicrc("100,5000;200,3000;300,1000");
799
800 assert_eq!(penalty.len(), 3);
801 assert_eq!(penalty.get(100), 5000);
802 assert_eq!(penalty.get(200), 3000);
803 assert_eq!(penalty.get(300), 1000);
804 assert_eq!(penalty.get(999), 0); }
806
807 #[test]
808 fn test_space_penalty_korean_default() {
809 let penalty = SpacePenalty::korean_default();
810 assert!(!penalty.is_empty());
811
812 assert!(penalty.get(1785) > 0);
814 }
815
816 #[test]
817 fn test_viterbi_simple_path() {
818 let mut lattice = Lattice::new("AB");
821
822 lattice.add_node(
824 NodeBuilder::new("A", 0, 1)
825 .left_id(1)
826 .right_id(1)
827 .word_cost(100),
828 );
829
830 lattice.add_node(
832 NodeBuilder::new("B", 1, 2)
833 .left_id(2)
834 .right_id(2)
835 .word_cost(200),
836 );
837
838 let conn_cost = ZeroConnectionCost;
839 let searcher = ViterbiSearcher::new();
840
841 let path = searcher.search(&mut lattice, &conn_cost);
842
843 assert_eq!(path.len(), 2);
844
845 let first = lattice.node(path[0]).unwrap();
847 assert_eq!(first.surface.as_ref(), "A");
848
849 let second = lattice.node(path[1]).unwrap();
851 assert_eq!(second.surface.as_ref(), "B");
852
853 let total_cost = searcher.get_best_cost(&lattice);
855 assert_eq!(total_cost, 300); }
857
858 #[test]
859 fn test_viterbi_choose_best_path() {
860 let mut lattice = Lattice::new("AB");
864
865 lattice.add_node(
867 NodeBuilder::new("AB", 0, 2)
868 .left_id(1)
869 .right_id(1)
870 .word_cost(500),
871 );
872
873 lattice.add_node(
875 NodeBuilder::new("A", 0, 1)
876 .left_id(2)
877 .right_id(2)
878 .word_cost(100),
879 );
880
881 lattice.add_node(
883 NodeBuilder::new("B", 1, 2)
884 .left_id(3)
885 .right_id(3)
886 .word_cost(200),
887 );
888
889 let conn_cost = ZeroConnectionCost;
890 let searcher = ViterbiSearcher::new();
891
892 let path = searcher.search(&mut lattice, &conn_cost);
893
894 assert_eq!(path.len(), 2);
896 assert_eq!(lattice.node(path[0]).unwrap().surface.as_ref(), "A");
897 assert_eq!(lattice.node(path[1]).unwrap().surface.as_ref(), "B");
898 }
899
900 #[test]
901 fn test_viterbi_with_connection_cost() {
902 let mut lattice = Lattice::new("AB");
906
907 lattice.add_node(
909 NodeBuilder::new("AB", 0, 2)
910 .left_id(1)
911 .right_id(1)
912 .word_cost(300),
913 );
914
915 lattice.add_node(
917 NodeBuilder::new("A", 0, 1)
918 .left_id(2)
919 .right_id(2)
920 .word_cost(100),
921 );
922
923 lattice.add_node(
925 NodeBuilder::new("B", 1, 2)
926 .left_id(3)
927 .right_id(3)
928 .word_cost(100),
929 );
930
931 let mut conn_cost = TestConnectionCost::new(0);
932 conn_cost.set(2, 3, 500);
934
935 let searcher = ViterbiSearcher::new();
936 let path = searcher.search(&mut lattice, &conn_cost);
937
938 assert_eq!(path.len(), 1);
940 assert_eq!(lattice.node(path[0]).unwrap().surface.as_ref(), "AB");
941 }
942
943 #[test]
944 fn test_viterbi_with_space_penalty() {
945 let mut lattice = Lattice::new("A B");
949 lattice.add_node(
953 NodeBuilder::new("AB", 0, 2)
954 .left_id(1)
955 .right_id(1)
956 .word_cost(500),
957 );
958
959 lattice.add_node(
961 NodeBuilder::new("A", 0, 1)
962 .left_id(2)
963 .right_id(2)
964 .word_cost(100),
965 );
966
967 lattice.add_node(
969 NodeBuilder::new("B", 1, 2)
970 .left_id(100) .right_id(3)
972 .word_cost(100)
973 .has_space_before(true),
974 );
975
976 let mut penalty = SpacePenalty::new();
978 penalty.add(100, 1000);
979
980 let conn_cost = ZeroConnectionCost;
981 let searcher = ViterbiSearcher::new().with_space_penalty(penalty);
982
983 let path = searcher.search(&mut lattice, &conn_cost);
984
985 assert_eq!(path.len(), 1);
987 assert_eq!(lattice.node(path[0]).unwrap().surface.as_ref(), "AB");
988 }
989
990 #[test]
991 fn test_viterbi_korean_example() {
992 let mut lattice = Lattice::new("아버지가");
994
995 lattice.add_node(
997 NodeBuilder::new("아버지", 0, 3)
998 .left_id(1)
999 .right_id(1)
1000 .word_cost(1000),
1001 );
1002 lattice.add_node(
1003 NodeBuilder::new("가", 3, 4)
1004 .left_id(100) .right_id(100)
1006 .word_cost(500),
1007 );
1008
1009 lattice.add_node(
1011 NodeBuilder::new("아버", 0, 2)
1012 .left_id(2)
1013 .right_id(2)
1014 .word_cost(3000),
1015 );
1016 lattice.add_node(
1017 NodeBuilder::new("지가", 2, 4)
1018 .left_id(3)
1019 .right_id(3)
1020 .word_cost(3000),
1021 );
1022
1023 let conn_cost = ZeroConnectionCost;
1024 let searcher = ViterbiSearcher::new();
1025
1026 let path = searcher.search(&mut lattice, &conn_cost);
1027
1028 assert_eq!(path.len(), 2);
1030 assert_eq!(lattice.node(path[0]).unwrap().surface.as_ref(), "아버지");
1031 assert_eq!(lattice.node(path[1]).unwrap().surface.as_ref(), "가");
1032 }
1033
1034 #[test]
1035 fn test_viterbi_empty_lattice() {
1036 let mut lattice = Lattice::new("");
1037
1038 let conn_cost = ZeroConnectionCost;
1039 let searcher = ViterbiSearcher::new();
1040
1041 let path = searcher.search(&mut lattice, &conn_cost);
1042
1043 assert!(path.is_empty());
1045 }
1046
1047 #[test]
1048 fn test_viterbi_no_path() {
1049 let mut lattice = Lattice::new("ABC");
1051
1052 lattice.add_node(
1054 NodeBuilder::new("A", 0, 1)
1055 .left_id(1)
1056 .right_id(1)
1057 .word_cost(100),
1058 );
1059
1060 let conn_cost = ZeroConnectionCost;
1061 let searcher = ViterbiSearcher::new();
1062
1063 let path = searcher.search(&mut lattice, &conn_cost);
1064
1065 assert!(!searcher.has_valid_path(&lattice));
1067 assert!(path.is_empty());
1068 }
1069
1070 #[test]
1071 fn test_nbest_single() {
1072 let mut lattice = Lattice::new("AB");
1073
1074 lattice.add_node(
1075 NodeBuilder::new("A", 0, 1)
1076 .left_id(1)
1077 .right_id(1)
1078 .word_cost(100),
1079 );
1080 lattice.add_node(
1081 NodeBuilder::new("B", 1, 2)
1082 .left_id(2)
1083 .right_id(2)
1084 .word_cost(200),
1085 );
1086
1087 let conn_cost = ZeroConnectionCost;
1088 let searcher = NbestSearcher::new(1);
1089
1090 let results = searcher.search(&mut lattice, &conn_cost);
1091
1092 assert_eq!(results.len(), 1);
1093 assert_eq!(results[0].1, 300); }
1095
1096 #[test]
1097 fn test_viterbi_result_helper() {
1098 let mut lattice = Lattice::new("AB");
1099
1100 let _id1 = lattice.add_node(
1101 NodeBuilder::new("A", 0, 1)
1102 .left_id(1)
1103 .right_id(1)
1104 .word_cost(100),
1105 );
1106 let _id2 = lattice.add_node(
1107 NodeBuilder::new("B", 1, 2)
1108 .left_id(2)
1109 .right_id(2)
1110 .word_cost(200),
1111 );
1112
1113 let conn_cost = ZeroConnectionCost;
1114 let searcher = ViterbiSearcher::new();
1115 let path = searcher.search(&mut lattice, &conn_cost);
1116 let cost = searcher.get_best_cost(&lattice);
1117
1118 let result = ViterbiResult::new(&lattice, path, cost);
1119
1120 assert_eq!(result.len(), 2);
1121 assert_eq!(result.cost(), 300);
1122 assert_eq!(result.surfaces(), vec!["A", "B"]);
1123 }
1124
1125 #[test]
1126 fn test_viterbi_with_dense_matrix() {
1127 use mecab_ko_dict::DenseMatrix;
1128
1129 let mut matrix = DenseMatrix::new(3, 3, 0);
1133
1134 matrix.set(0, 1, 100);
1137 matrix.set(1, 2, 50);
1139 matrix.set(2, 0, 30);
1141
1142 matrix.set(0, 2, 5000);
1144 matrix.set(1, 0, 200);
1146
1147 let mut lattice = Lattice::new("책을");
1148
1149 lattice.add_node(
1151 NodeBuilder::new("책", 0, 1)
1152 .left_id(1) .right_id(1) .word_cost(500),
1155 );
1156
1157 lattice.add_node(
1159 NodeBuilder::new("을", 1, 2)
1160 .left_id(2) .right_id(2) .word_cost(100),
1163 );
1164
1165 let searcher = ViterbiSearcher::new();
1166 let path = searcher.search(&mut lattice, &matrix);
1167
1168 assert!(!path.is_empty());
1170
1171 let result = ViterbiResult::new(&lattice, path, searcher.get_best_cost(&lattice));
1172 assert_eq!(result.surfaces(), vec!["책", "을"]);
1173
1174 assert_eq!(result.cost(), 780);
1177 }
1178}