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 category: crate::registry::ProblemCategory::Misc,
21 module_path: module_path!(),
22 description: "Find a ranking minimizing total pairwise disagreement cost",
23 fields: &[
24 FieldInfo { name: "matrix", type_name: "Vec<Vec<i64>>", description: "Antisymmetric comparison matrix A (a_ij + a_ji = c, a_ii = 0)" },
25 ],
26 }
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct MaximumLikelihoodRanking {
59 matrix: Vec<Vec<i64>>,
60}
61
62impl MaximumLikelihoodRanking {
63 pub fn new(matrix: Vec<Vec<i64>>) -> Self {
70 let n = matrix.len();
71 for (i, row) in matrix.iter().enumerate() {
72 assert_eq!(
73 row.len(),
74 n,
75 "matrix must be square: row {i} has length {} but expected {n}",
76 row.len()
77 );
78 assert_eq!(
79 row[i], 0,
80 "diagonal entries must be zero: matrix[{i}][{i}] = {}",
81 row[i]
82 );
83 }
84
85 let mut comparison_count = None;
86 for (i, row) in matrix.iter().enumerate() {
87 for (j, &entry) in row.iter().enumerate().skip(i + 1) {
88 let pair_sum = entry + matrix[j][i];
89 match comparison_count {
90 None => comparison_count = Some(pair_sum),
91 Some(expected) => assert_eq!(
92 pair_sum,
93 expected,
94 "all off-diagonal pairs must have the same comparison count: matrix[{i}][{j}] + matrix[{j}][{i}] = {pair_sum}, expected {expected}"
95 ),
96 }
97 }
98 }
99
100 Self { matrix }
101 }
102
103 pub fn matrix(&self) -> &Vec<Vec<i64>> {
105 &self.matrix
106 }
107
108 pub fn num_items(&self) -> usize {
110 self.matrix.len()
111 }
112
113 pub fn comparison_count(&self) -> i64 {
115 if self.matrix.len() < 2 {
116 0
117 } else {
118 self.matrix[0][1] + self.matrix[1][0]
119 }
120 }
121}
122
123impl Problem for MaximumLikelihoodRanking {
124 const NAME: &'static str = "MaximumLikelihoodRanking";
125 type Solution = Vec<usize>;
126 type Value = Min<i64>;
127
128 crate::problem_parameters![("num_items", num_items),];
129
130 fn variant() -> Vec<(&'static str, &'static str)> {
131 crate::variant_params![]
132 }
133
134 fn evaluate(
135 &self,
136 config: &Self::Solution,
137 ) -> Result<Min<i64>, crate::traits::EvaluationError> {
138 Ok({
139 let n = self.num_items();
140
141 if config.len() != n {
143 return Err(crate::traits::EvaluationError::InvalidConfiguration(
144 "ranking length does not match the number of alternatives".into(),
145 ));
146 }
147
148 if config.iter().any(|&rank| rank >= n) {
149 return Err(crate::traits::EvaluationError::InvalidConfiguration(
150 "ranking contains an out-of-range position".into(),
151 ));
152 }
153
154 let mut seen = vec![false; n];
156 for &rank in config {
157 if rank >= n || seen[rank] {
158 return Ok(Min(None));
159 }
160 seen[rank] = true;
161 }
162
163 let mut cost: i64 = 0;
167 for a in 0..n {
168 for b in 0..n {
169 if a != b && config[a] > config[b] {
170 cost = cost.checked_add(self.matrix[a][b]).ok_or_else(|| {
171 crate::traits::EvaluationError::IntegerOverflow(
172 "summing ranking disagreement costs".into(),
173 )
174 })?;
175 }
176 }
177 }
178
179 Min(Some(cost))
180 })
181 }
182}
183
184impl crate::solvers::BruteForceProblem for MaximumLikelihoodRanking {
185 fn dimensions(&self) -> Vec<usize> {
186 let n = self.num_items();
187 vec![n; n]
188 }
189}
190
191crate::declare_variants! {
192 default MaximumLikelihoodRanking => "num_items * num_items * 2^num_items",
193}
194
195crate::register_brute_force! {
196 MaximumLikelihoodRanking,
197}
198
199#[cfg(feature = "example-db")]
200pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
201 let matrix = vec![
208 vec![0, 4, 3, 5],
209 vec![1, 0, 4, 3],
210 vec![2, 1, 0, 4],
211 vec![0, 2, 1, 0],
212 ];
213 vec![crate::example_db::specs::ModelExampleSpec {
214 id: "maximum_likelihood_ranking",
215 instance: Box::new(MaximumLikelihoodRanking::new(matrix)),
216 optimal_config: serde_json::json!(vec![0, 1, 2, 3]),
217 optimal_value: serde_json::json!(7),
218 }]
219}
220
221#[cfg(test)]
222#[path = "../../unit_tests/models/misc/maximum_likelihood_ranking.rs"]
223mod tests;