Skip to main content

problemreductions/models/algebraic/
quadratic_assignment.rs

1//! Quadratic Assignment Problem (QAP) implementation.
2//!
3//! The QAP assigns facilities to locations to minimize the total cost,
4//! where cost depends on both inter-facility flows and inter-location distances.
5
6use crate::registry::{FieldInfo, ProblemSchemaEntry};
7use crate::traits::Problem;
8use crate::types::Min;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "QuadraticAssignment",
14        display_name: "Quadratic Assignment",
15        aliases: &["QAP"],
16        dimensions: &[],
17        module_path: module_path!(),
18        description: "Minimize total cost of assigning facilities to locations",
19        fields: &[
20            FieldInfo { name: "cost_matrix", type_name: "Vec<Vec<i64>>", description: "Flow/cost matrix between facilities" },
21            FieldInfo { name: "distance_matrix", type_name: "Vec<Vec<i64>>", description: "Distance matrix between locations" },
22        ],
23    }
24}
25
26/// The Quadratic Assignment Problem (QAP).
27///
28/// Given n facilities and m locations, a cost matrix C (n x n) representing
29/// flows between facilities, and a distance matrix D (m x m) representing
30/// distances between locations, find an injective assignment of facilities
31/// to locations that minimizes:
32///
33/// f(p) = sum_{i != j} C[i][j] * D[p(i)][p(j)]
34///
35/// where p is an injective mapping from facilities to locations (a permutation when n == m).
36///
37/// # Example
38///
39/// ```
40/// use problemreductions::models::algebraic::QuadraticAssignment;
41/// use problemreductions::{Problem, Solver, BruteForce};
42///
43/// let cost_matrix = vec![
44///     vec![0, 1, 2],
45///     vec![1, 0, 3],
46///     vec![2, 3, 0],
47/// ];
48/// let distance_matrix = vec![
49///     vec![0, 5, 8],
50///     vec![5, 0, 3],
51///     vec![8, 3, 0],
52/// ];
53/// let problem = QuadraticAssignment::new(cost_matrix, distance_matrix);
54///
55/// let solver = BruteForce::new();
56/// let best = solver.find_witness(&problem);
57/// assert!(best.is_some());
58/// ```
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct QuadraticAssignment {
61    /// Cost/flow matrix between facilities (n x n).
62    cost_matrix: Vec<Vec<i64>>,
63    /// Distance matrix between locations (m x m).
64    distance_matrix: Vec<Vec<i64>>,
65}
66
67impl QuadraticAssignment {
68    /// Create a new Quadratic Assignment Problem.
69    ///
70    /// # Arguments
71    /// * `cost_matrix` - n x n matrix of flows/costs between facilities
72    /// * `distance_matrix` - m x m matrix of distances between locations
73    ///
74    /// # Panics
75    /// Panics if either matrix is not square, or if num_facilities > num_locations.
76    pub fn new(cost_matrix: Vec<Vec<i64>>, distance_matrix: Vec<Vec<i64>>) -> Self {
77        let n = cost_matrix.len();
78        for row in &cost_matrix {
79            assert_eq!(row.len(), n, "cost_matrix must be square");
80        }
81        let m = distance_matrix.len();
82        for row in &distance_matrix {
83            assert_eq!(row.len(), m, "distance_matrix must be square");
84        }
85        assert!(
86            n <= m,
87            "num_facilities ({n}) must be <= num_locations ({m})"
88        );
89        Self {
90            cost_matrix,
91            distance_matrix,
92        }
93    }
94
95    /// Get the cost/flow matrix.
96    pub fn cost_matrix(&self) -> &[Vec<i64>] {
97        &self.cost_matrix
98    }
99
100    /// Get the distance matrix.
101    pub fn distance_matrix(&self) -> &[Vec<i64>] {
102        &self.distance_matrix
103    }
104
105    /// Get the number of facilities.
106    pub fn num_facilities(&self) -> usize {
107        self.cost_matrix.len()
108    }
109
110    /// Get the number of locations.
111    pub fn num_locations(&self) -> usize {
112        self.distance_matrix.len()
113    }
114}
115
116impl Problem for QuadraticAssignment {
117    const NAME: &'static str = "QuadraticAssignment";
118    type Value = Min<i64>;
119
120    fn dims(&self) -> Vec<usize> {
121        vec![self.num_locations(); self.num_facilities()]
122    }
123
124    fn evaluate(&self, config: &[usize]) -> Min<i64> {
125        let n = self.num_facilities();
126        let m = self.num_locations();
127
128        // Check config length matches number of facilities
129        if config.len() != n {
130            return Min(None);
131        }
132
133        // Check that all assignments are valid locations
134        for &loc in config {
135            if loc >= m {
136                return Min(None);
137            }
138        }
139
140        // Check injectivity: no two facilities assigned to the same location
141        let mut used = vec![false; m];
142        for &loc in config {
143            if used[loc] {
144                return Min(None);
145            }
146            used[loc] = true;
147        }
148
149        // Compute objective: sum_{i != j} cost_matrix[i][j] * distance_matrix[config[i]][config[j]]
150        let mut total: i64 = 0;
151        for i in 0..n {
152            for j in 0..n {
153                if i != j {
154                    total += self.cost_matrix[i][j] * self.distance_matrix[config[i]][config[j]];
155                }
156            }
157        }
158
159        Min(Some(total))
160    }
161
162    fn variant() -> Vec<(&'static str, &'static str)> {
163        crate::variant_params![]
164    }
165}
166
167crate::declare_variants! {
168    default QuadraticAssignment => "factorial(num_facilities)",
169}
170
171#[cfg(feature = "example-db")]
172pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
173    vec![crate::example_db::specs::ModelExampleSpec {
174        id: "quadratic_assignment",
175        instance: Box::new(QuadraticAssignment::new(
176            vec![
177                vec![0, 5, 2, 0],
178                vec![5, 0, 0, 3],
179                vec![2, 0, 0, 4],
180                vec![0, 3, 4, 0],
181            ],
182            vec![
183                vec![0, 4, 1, 1],
184                vec![4, 0, 3, 4],
185                vec![1, 3, 0, 4],
186                vec![1, 4, 4, 0],
187            ],
188        )),
189        optimal_config: vec![3, 0, 1, 2],
190        optimal_value: serde_json::json!(56),
191    }]
192}
193
194#[cfg(test)]
195#[path = "../../unit_tests/models/algebraic/quadratic_assignment.rs"]
196mod tests;