Skip to main content

problemreductions/models/misc/
maximum_likelihood_ranking.rs

1//! Maximum Likelihood Ranking problem implementation.
2//!
3//! Given an n x n antisymmetric comparison matrix A where a_ij + a_ji = c
4//! (constant) for every pair and a_ii = 0, find a permutation pi minimizing
5//! the total disagreement cost: sum over all position pairs (i > j) of
6//! a_{pi(i), pi(j)}.  Entries may be negative (e.g. c = 0 gives a
7//! skew-symmetric matrix).
8
9use 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/// The Maximum Likelihood Ranking problem.
29///
30/// Given an n x n antisymmetric comparison matrix A where a_ij + a_ji = c
31/// (constant) for every pair and a_ii = 0, find a permutation pi that
32/// minimizes the total disagreement cost: sum_{i > j} a_{pi(i), pi(j)}.
33/// Entries may be negative (e.g. c = 0 gives a skew-symmetric matrix).
34///
35/// Each item is assigned a rank position (0-indexed). The configuration
36/// maps item -> rank: `config[item] = rank`. The permutation pi maps
37/// rank -> item (the inverse of config).
38///
39/// # Example
40///
41/// ```
42/// use problemreductions::models::misc::MaximumLikelihoodRanking;
43/// use problemreductions::{Problem, Solver, BruteForce};
44///
45/// let matrix = vec![
46///     vec![0, 4, 3, 5],
47///     vec![1, 0, 4, 3],
48///     vec![2, 1, 0, 4],
49///     vec![0, 2, 1, 0],
50/// ];
51/// let problem = MaximumLikelihoodRanking::new(matrix);
52/// let solver = BruteForce::new();
53/// let solution = solver.find_witness(&problem);
54/// assert!(solution.is_some());
55/// ```
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct MaximumLikelihoodRanking {
58    matrix: Vec<Vec<i32>>,
59}
60
61impl MaximumLikelihoodRanking {
62    /// Create a new MaximumLikelihoodRanking instance.
63    ///
64    /// # Panics
65    /// Panics if the matrix is not square, if any diagonal element is nonzero,
66    /// or if the pairwise sums `a_ij + a_ji` are not the same constant for
67    /// all `i != j`.
68    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    /// Returns the comparison matrix.
103    pub fn matrix(&self) -> &Vec<Vec<i32>> {
104        &self.matrix
105    }
106
107    /// Returns the number of items to rank.
108    pub fn num_items(&self) -> usize {
109        self.matrix.len()
110    }
111
112    /// Returns the constant pairwise comparison count `c`.
113    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        // Validate config length
139        if config.len() != n {
140            return Min(None);
141        }
142
143        // Validate permutation: all values must be distinct and in 0..n
144        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        // config[item] = rank position of item
153        // Disagreement cost: for all pairs of items (a, b) where a is
154        // ranked AFTER b (config[a] > config[b]), add matrix[a][b].
155        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    // 4 items with comparison matrix.
175    // Optimal ranking: [0, 1, 2, 3] (identity) gives cost 7.
176    // Let's verify: items ranked in order 0,1,2,3.
177    // Disagreement = sum over (a,b) where config[a] > config[b] of matrix[a][b]
178    // = matrix[1][0] + matrix[2][0] + matrix[2][1] + matrix[3][0] + matrix[3][1] + matrix[3][2]
179    // = 1 + 2 + 1 + 0 + 2 + 1 = 7
180    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;