problemreductions/models/specialized/
paintshop.rs

1//! Paint Shop problem implementation.
2//!
3//! In the Paint Shop problem, we have a sequence of cars to paint.
4//! Each car appears exactly twice in the sequence and must be painted
5//! one color at its first occurrence and another at its second.
6//! The goal is to minimize color switches between adjacent positions.
7
8use crate::graph_types::SimpleGraph;
9use crate::traits::Problem;
10use crate::types::{EnergyMode, ProblemSize, SolutionSize};
11use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13
14/// The Paint Shop problem.
15///
16/// Given a sequence where each car appears exactly twice, assign colors
17/// (0 or 1) to each car to minimize color switches in the sequence.
18///
19/// # Example
20///
21/// ```
22/// use problemreductions::models::specialized::PaintShop;
23/// use problemreductions::{Problem, Solver, BruteForce};
24///
25/// // Sequence: a, b, a, c, c, b
26/// let problem = PaintShop::new(vec!["a", "b", "a", "c", "c", "b"]);
27///
28/// let solver = BruteForce::new();
29/// let solutions = solver.find_best(&problem);
30///
31/// // The minimum number of color switches
32/// for sol in &solutions {
33///     let switches = problem.count_switches(sol);
34///     println!("Switches: {}", switches);
35/// }
36/// ```
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct PaintShop {
39    /// The sequence of car labels (as indices into unique cars).
40    sequence_indices: Vec<usize>,
41    /// Original car labels.
42    car_labels: Vec<String>,
43    /// Which positions are the first occurrence of each car.
44    is_first: Vec<bool>,
45    /// Number of unique cars.
46    num_cars: usize,
47}
48
49impl PaintShop {
50    /// Create a new Paint Shop problem from string labels.
51    ///
52    /// Each element in the sequence must appear exactly twice.
53    pub fn new<S: AsRef<str>>(sequence: Vec<S>) -> Self {
54        let sequence: Vec<String> = sequence.iter().map(|s| s.as_ref().to_string()).collect();
55        Self::from_strings(sequence)
56    }
57
58    /// Create from a vector of strings.
59    pub fn from_strings(sequence: Vec<String>) -> Self {
60        // Build car-to-index mapping and count occurrences
61        let mut car_count: HashMap<String, usize> = HashMap::new();
62        let mut car_to_index: HashMap<String, usize> = HashMap::new();
63        let mut car_labels: Vec<String> = Vec::new();
64
65        for item in &sequence {
66            let count = car_count.entry(item.clone()).or_insert(0);
67            if *count == 0 {
68                car_to_index.insert(item.clone(), car_labels.len());
69                car_labels.push(item.clone());
70            }
71            *count += 1;
72        }
73
74        // Verify each car appears exactly twice
75        for (car, count) in &car_count {
76            assert_eq!(
77                *count, 2,
78                "Each car must appear exactly twice, but '{}' appears {} times",
79                car, count
80            );
81        }
82
83        // Convert sequence to indices
84        let sequence_indices: Vec<usize> = sequence
85            .iter()
86            .map(|item| car_to_index[item])
87            .collect();
88
89        // Determine which positions are first occurrences
90        let mut seen: HashSet<usize> = HashSet::new();
91        let is_first: Vec<bool> = sequence_indices
92            .iter()
93            .map(|&idx| seen.insert(idx))
94            .collect();
95
96        let num_cars = car_labels.len();
97
98        Self {
99            sequence_indices,
100            car_labels,
101            is_first,
102            num_cars,
103        }
104    }
105
106    /// Get the sequence length.
107    pub fn sequence_len(&self) -> usize {
108        self.sequence_indices.len()
109    }
110
111    /// Get the number of unique cars.
112    pub fn num_cars(&self) -> usize {
113        self.num_cars
114    }
115
116    /// Get the car labels.
117    pub fn car_labels(&self) -> &[String] {
118        &self.car_labels
119    }
120
121    /// Get the coloring of the sequence from a configuration.
122    ///
123    /// Config assigns a color (0 or 1) to each car for its first occurrence.
124    /// The second occurrence gets the opposite color.
125    pub fn get_coloring(&self, config: &[usize]) -> Vec<usize> {
126        self.sequence_indices
127            .iter()
128            .enumerate()
129            .map(|(i, &car_idx)| {
130                let first_color = config.get(car_idx).copied().unwrap_or(0);
131                if self.is_first[i] {
132                    first_color
133                } else {
134                    1 - first_color // Opposite color for second occurrence
135                }
136            })
137            .collect()
138    }
139
140    /// Count the number of color switches in the sequence.
141    pub fn count_switches(&self, config: &[usize]) -> usize {
142        let coloring = self.get_coloring(config);
143        coloring.windows(2).filter(|w| w[0] != w[1]).count()
144    }
145}
146
147impl Problem for PaintShop {
148    const NAME: &'static str = "PaintShop";
149    type GraphType = SimpleGraph;
150    type Weight = i32;
151    type Size = i32;
152
153    fn num_variables(&self) -> usize {
154        self.num_cars
155    }
156
157    fn num_flavors(&self) -> usize {
158        2 // Binary: color 0 or color 1
159    }
160
161    fn problem_size(&self) -> ProblemSize {
162        ProblemSize::new(vec![
163            ("num_cars", self.num_cars),
164            ("sequence_length", self.sequence_indices.len()),
165        ])
166    }
167
168    fn energy_mode(&self) -> EnergyMode {
169        EnergyMode::SmallerSizeIsBetter // Minimize color switches
170    }
171
172    fn solution_size(&self, config: &[usize]) -> SolutionSize<Self::Size> {
173        let switches = self.count_switches(config) as i32;
174        // All configurations are valid (no hard constraints)
175        SolutionSize::valid(switches)
176    }
177}
178
179/// Count color switches in a painted sequence.
180pub fn count_paint_switches(coloring: &[usize]) -> usize {
181    coloring.windows(2).filter(|w| w[0] != w[1]).count()
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use crate::solvers::{BruteForce, Solver};
188
189    #[test]
190    fn test_paintshop_creation() {
191        let problem = PaintShop::new(vec!["a", "b", "a", "b"]);
192        assert_eq!(problem.num_cars(), 2);
193        assert_eq!(problem.sequence_len(), 4);
194        assert_eq!(problem.num_variables(), 2);
195        assert_eq!(problem.num_flavors(), 2);
196    }
197
198    #[test]
199    fn test_is_first() {
200        let problem = PaintShop::new(vec!["a", "b", "a", "b"]);
201        // First occurrence: a at 0, b at 1
202        // Second occurrence: a at 2, b at 3
203        assert_eq!(problem.is_first, vec![true, true, false, false]);
204    }
205
206    #[test]
207    fn test_get_coloring() {
208        let problem = PaintShop::new(vec!["a", "b", "a", "b"]);
209        // Config: a=0, b=1
210        // Sequence: a(0), b(1), a(1-opposite), b(0-opposite)
211        let coloring = problem.get_coloring(&[0, 1]);
212        assert_eq!(coloring, vec![0, 1, 1, 0]);
213
214        // Config: a=1, b=0
215        let coloring = problem.get_coloring(&[1, 0]);
216        assert_eq!(coloring, vec![1, 0, 0, 1]);
217    }
218
219    #[test]
220    fn test_count_switches() {
221        let problem = PaintShop::new(vec!["a", "b", "a", "b"]);
222
223        // Config [0, 1] -> coloring [0, 1, 1, 0] -> 2 switches
224        assert_eq!(problem.count_switches(&[0, 1]), 2);
225
226        // Config [0, 0] -> coloring [0, 0, 1, 1] -> 1 switch
227        assert_eq!(problem.count_switches(&[0, 0]), 1);
228
229        // Config [1, 1] -> coloring [1, 1, 0, 0] -> 1 switch
230        assert_eq!(problem.count_switches(&[1, 1]), 1);
231    }
232
233    #[test]
234    fn test_solution_size() {
235        let problem = PaintShop::new(vec!["a", "b", "a", "b"]);
236
237        let sol = problem.solution_size(&[0, 0]);
238        assert!(sol.is_valid);
239        assert_eq!(sol.size, 1);
240
241        let sol = problem.solution_size(&[0, 1]);
242        assert!(sol.is_valid);
243        assert_eq!(sol.size, 2);
244    }
245
246    #[test]
247    fn test_brute_force_simple() {
248        let problem = PaintShop::new(vec!["a", "b", "a", "b"]);
249        let solver = BruteForce::new();
250
251        let solutions = solver.find_best(&problem);
252        // Optimal has 1 switch: [0,0] or [1,1]
253        for sol in &solutions {
254            assert_eq!(problem.count_switches(sol), 1);
255        }
256    }
257
258    #[test]
259    fn test_brute_force_longer() {
260        // Sequence: a, b, a, c, c, b
261        let problem = PaintShop::new(vec!["a", "b", "a", "c", "c", "b"]);
262        let solver = BruteForce::new();
263
264        let solutions = solver.find_best(&problem);
265        // Find the minimum number of switches
266        let min_switches = problem.count_switches(&solutions[0]);
267        for sol in &solutions {
268            assert_eq!(problem.count_switches(sol), min_switches);
269        }
270    }
271
272    #[test]
273    fn test_count_paint_switches_function() {
274        assert_eq!(count_paint_switches(&[0, 0, 0]), 0);
275        assert_eq!(count_paint_switches(&[0, 1, 0]), 2);
276        assert_eq!(count_paint_switches(&[0, 0, 1, 1]), 1);
277        assert_eq!(count_paint_switches(&[0, 1, 0, 1]), 3);
278    }
279
280    #[test]
281    fn test_energy_mode() {
282        let problem = PaintShop::new(vec!["a", "a"]);
283        assert!(problem.energy_mode().is_minimization());
284    }
285
286    #[test]
287    fn test_problem_size() {
288        let problem = PaintShop::new(vec!["a", "b", "c", "a", "b", "c"]);
289        let size = problem.problem_size();
290        assert_eq!(size.get("num_cars"), Some(3));
291        assert_eq!(size.get("sequence_length"), Some(6));
292    }
293
294    #[test]
295    fn test_single_car() {
296        let problem = PaintShop::new(vec!["a", "a"]);
297        let solver = BruteForce::new();
298
299        let solutions = solver.find_best(&problem);
300        // Both configs give 1 switch: a(0)->a(1) or a(1)->a(0)
301        assert_eq!(solutions.len(), 2);
302        for sol in &solutions {
303            assert_eq!(problem.count_switches(sol), 1);
304        }
305    }
306
307    #[test]
308    fn test_adjacent_same_car() {
309        // Sequence: a, a, b, b
310        let problem = PaintShop::new(vec!["a", "a", "b", "b"]);
311        let solver = BruteForce::new();
312
313        let solutions = solver.find_best(&problem);
314        // Best case: [0,0] -> [0,1,0,1] = 3 switches, or [0,1] -> [0,1,1,0] = 2 switches
315        // Actually: [0,0] -> a=0,a=1,b=0,b=1 = [0,1,0,1] = 3 switches
316        // [0,1] -> a=0,a=1,b=1,b=0 = [0,1,1,0] = 2 switches
317        let min_switches = problem.count_switches(&solutions[0]);
318        assert!(min_switches <= 3);
319    }
320
321    #[test]
322    #[should_panic]
323    fn test_invalid_sequence_single_occurrence() {
324        // This should panic because 'c' only appears once
325        let _ = PaintShop::new(vec!["a", "b", "a", "c"]);
326    }
327
328    #[test]
329    fn test_car_labels() {
330        let problem = PaintShop::new(vec!["car1", "car2", "car1", "car2"]);
331        assert_eq!(problem.car_labels().len(), 2);
332    }
333}