problemreductions/models/misc/
maximum_likelihood_ranking.rs1use crate::registry::{FieldInfo, ProblemSchemaEntry};
10use crate::traits::Problem;
11use crate::types::Min;
12use serde::{Deserialize, Serialize};
13
14inventory::submit! {
15 ProblemSchemaEntry {
16 name: "MaximumLikelihoodRanking",
17 display_name: "Maximum Likelihood Ranking",
18 aliases: &[],
19 dimensions: &[],
20 module_path: module_path!(),
21 description: "Find a ranking minimizing total pairwise disagreement cost",
22 fields: &[
23 FieldInfo { name: "matrix", type_name: "Vec<Vec<i32>>", description: "Antisymmetric comparison matrix A (a_ij + a_ji = c, a_ii = 0)" },
24 ],
25 }
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct MaximumLikelihoodRanking {
58 matrix: Vec<Vec<i32>>,
59}
60
61impl MaximumLikelihoodRanking {
62 pub fn new(matrix: Vec<Vec<i32>>) -> Self {
69 let n = matrix.len();
70 for (i, row) in matrix.iter().enumerate() {
71 assert_eq!(
72 row.len(),
73 n,
74 "matrix must be square: row {i} has length {} but expected {n}",
75 row.len()
76 );
77 assert_eq!(
78 row[i], 0,
79 "diagonal entries must be zero: matrix[{i}][{i}] = {}",
80 row[i]
81 );
82 }
83
84 let mut comparison_count = None;
85 for (i, row) in matrix.iter().enumerate() {
86 for (j, &entry) in row.iter().enumerate().skip(i + 1) {
87 let pair_sum = entry + matrix[j][i];
88 match comparison_count {
89 None => comparison_count = Some(pair_sum),
90 Some(expected) => assert_eq!(
91 pair_sum,
92 expected,
93 "all off-diagonal pairs must have the same comparison count: matrix[{i}][{j}] + matrix[{j}][{i}] = {pair_sum}, expected {expected}"
94 ),
95 }
96 }
97 }
98
99 Self { matrix }
100 }
101
102 pub fn matrix(&self) -> &Vec<Vec<i32>> {
104 &self.matrix
105 }
106
107 pub fn num_items(&self) -> usize {
109 self.matrix.len()
110 }
111
112 pub fn comparison_count(&self) -> i32 {
114 if self.matrix.len() < 2 {
115 0
116 } else {
117 self.matrix[0][1] + self.matrix[1][0]
118 }
119 }
120}
121
122impl Problem for MaximumLikelihoodRanking {
123 const NAME: &'static str = "MaximumLikelihoodRanking";
124 type Value = Min<i64>;
125
126 fn variant() -> Vec<(&'static str, &'static str)> {
127 crate::variant_params![]
128 }
129
130 fn dims(&self) -> Vec<usize> {
131 let n = self.num_items();
132 vec![n; n]
133 }
134
135 fn evaluate(&self, config: &[usize]) -> Min<i64> {
136 let n = self.num_items();
137
138 if config.len() != n {
140 return Min(None);
141 }
142
143 let mut seen = vec![false; n];
145 for &rank in config {
146 if rank >= n || seen[rank] {
147 return Min(None);
148 }
149 seen[rank] = true;
150 }
151
152 let mut cost: i64 = 0;
156 for a in 0..n {
157 for b in 0..n {
158 if a != b && config[a] > config[b] {
159 cost += self.matrix[a][b] as i64;
160 }
161 }
162 }
163
164 Min(Some(cost))
165 }
166}
167
168crate::declare_variants! {
169 default MaximumLikelihoodRanking => "num_items * num_items * 2^num_items",
170}
171
172#[cfg(feature = "example-db")]
173pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
174 let matrix = vec![
181 vec![0, 4, 3, 5],
182 vec![1, 0, 4, 3],
183 vec![2, 1, 0, 4],
184 vec![0, 2, 1, 0],
185 ];
186 vec![crate::example_db::specs::ModelExampleSpec {
187 id: "maximum_likelihood_ranking",
188 instance: Box::new(MaximumLikelihoodRanking::new(matrix)),
189 optimal_config: vec![0, 1, 2, 3],
190 optimal_value: serde_json::json!(7),
191 }]
192}
193
194#[cfg(test)]
195#[path = "../../unit_tests/models/misc/maximum_likelihood_ranking.rs"]
196mod tests;