Skip to main content

problemreductions/models/misc/
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::registry::{FieldInfo, ProblemSchemaEntry};
9use crate::traits::Problem;
10use crate::types::Min;
11use serde::{Deserialize, Serialize};
12use std::collections::{HashMap, HashSet};
13
14inventory::submit! {
15    ProblemSchemaEntry {
16        name: "PaintShop",
17        display_name: "Paint Shop",
18        aliases: &[],
19        dimensions: &[],
20        module_path: module_path!(),
21        description: "Minimize color changes in paint shop sequence",
22        fields: &[
23            FieldInfo { name: "sequence", type_name: "Vec<String>", description: "Car labels (each must appear exactly twice)" },
24        ],
25    }
26}
27
28/// The Paint Shop problem.
29///
30/// Given a sequence where each car appears exactly twice, assign colors
31/// (0 or 1) to each car to minimize color switches in the sequence.
32///
33/// # Example
34///
35/// ```
36/// use problemreductions::models::misc::PaintShop;
37/// use problemreductions::{Problem, Solver, BruteForce};
38///
39/// // Sequence: a, b, a, c, c, b
40/// let problem = PaintShop::new(vec!["a", "b", "a", "c", "c", "b"]);
41///
42/// let solver = BruteForce::new();
43/// let solutions = solver.find_all_witnesses(&problem);
44///
45/// // The minimum number of color switches
46/// for sol in &solutions {
47///     let switches = problem.count_switches(sol);
48///     println!("Switches: {}", switches);
49/// }
50/// ```
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct PaintShop {
53    /// The sequence of car labels (as indices into unique cars).
54    sequence_indices: Vec<usize>,
55    /// Original car labels.
56    car_labels: Vec<String>,
57    /// Which positions are the first occurrence of each car.
58    is_first: Vec<bool>,
59    /// Number of unique cars.
60    num_cars: usize,
61}
62
63impl PaintShop {
64    /// Create a new Paint Shop problem from string labels.
65    ///
66    /// Each element in the sequence must appear exactly twice.
67    pub fn new<S: AsRef<str>>(sequence: Vec<S>) -> Self {
68        let sequence: Vec<String> = sequence.iter().map(|s| s.as_ref().to_string()).collect();
69        Self::from_strings(sequence)
70    }
71
72    /// Create from a vector of strings.
73    pub fn from_strings(sequence: Vec<String>) -> Self {
74        // Build car-to-index mapping and count occurrences
75        let mut car_count: HashMap<String, usize> = HashMap::new();
76        let mut car_to_index: HashMap<String, usize> = HashMap::new();
77        let mut car_labels: Vec<String> = Vec::new();
78
79        for item in &sequence {
80            let count = car_count.entry(item.clone()).or_insert(0);
81            if *count == 0 {
82                car_to_index.insert(item.clone(), car_labels.len());
83                car_labels.push(item.clone());
84            }
85            *count += 1;
86        }
87
88        // Verify each car appears exactly twice
89        for (car, count) in &car_count {
90            assert_eq!(
91                *count, 2,
92                "Each car must appear exactly twice, but '{}' appears {} times",
93                car, count
94            );
95        }
96
97        // Convert sequence to indices
98        let sequence_indices: Vec<usize> = sequence.iter().map(|item| car_to_index[item]).collect();
99
100        // Determine which positions are first occurrences
101        let mut seen: HashSet<usize> = HashSet::new();
102        let is_first: Vec<bool> = sequence_indices
103            .iter()
104            .map(|&idx| seen.insert(idx))
105            .collect();
106
107        let num_cars = car_labels.len();
108
109        Self {
110            sequence_indices,
111            car_labels,
112            is_first,
113            num_cars,
114        }
115    }
116
117    /// Get the sequence length.
118    pub fn sequence_len(&self) -> usize {
119        self.sequence_indices.len()
120    }
121
122    /// Get the sequence length (alias for `sequence_len()`).
123    pub fn num_sequence(&self) -> usize {
124        self.sequence_len()
125    }
126
127    /// Get the number of unique cars.
128    pub fn num_cars(&self) -> usize {
129        self.num_cars
130    }
131
132    /// Get the car labels.
133    pub fn car_labels(&self) -> &[String] {
134        &self.car_labels
135    }
136
137    /// Get the sequence as car indices.
138    pub fn sequence_indices(&self) -> &[usize] {
139        &self.sequence_indices
140    }
141
142    /// Get whether each position is the first occurrence of its car.
143    pub fn is_first(&self) -> &[bool] {
144        &self.is_first
145    }
146
147    /// Get the coloring of the sequence from a configuration.
148    ///
149    /// Config assigns a color (0 or 1) to each car for its first occurrence.
150    /// The second occurrence gets the opposite color.
151    pub fn get_coloring(&self, config: &[usize]) -> Vec<usize> {
152        self.sequence_indices
153            .iter()
154            .enumerate()
155            .map(|(i, &car_idx)| {
156                let first_color = config.get(car_idx).copied().unwrap_or(0);
157                if self.is_first[i] {
158                    first_color
159                } else {
160                    1 - first_color // Opposite color for second occurrence
161                }
162            })
163            .collect()
164    }
165
166    /// Count the number of color switches in the sequence.
167    pub fn count_switches(&self, config: &[usize]) -> usize {
168        let coloring = self.get_coloring(config);
169        coloring.windows(2).filter(|w| w[0] != w[1]).count()
170    }
171}
172
173/// Count color switches in a painted sequence.
174#[cfg(test)]
175pub(crate) fn count_paint_switches(coloring: &[usize]) -> usize {
176    coloring.windows(2).filter(|w| w[0] != w[1]).count()
177}
178
179impl Problem for PaintShop {
180    const NAME: &'static str = "PaintShop";
181    type Value = Min<i32>;
182
183    fn dims(&self) -> Vec<usize> {
184        vec![2; self.num_cars]
185    }
186
187    fn evaluate(&self, config: &[usize]) -> Min<i32> {
188        // All configurations are valid (no hard constraints).
189        Min(Some(self.count_switches(config) as i32))
190    }
191
192    fn variant() -> Vec<(&'static str, &'static str)> {
193        crate::variant_params![]
194    }
195}
196
197crate::declare_variants! {
198    default PaintShop => "2^num_cars",
199}
200
201#[cfg(feature = "example-db")]
202pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
203    vec![crate::example_db::specs::ModelExampleSpec {
204        id: "paintshop",
205        instance: Box::new(PaintShop::new(vec!["A", "B", "A", "C", "B", "C"])),
206        optimal_config: vec![0, 0, 1],
207        optimal_value: serde_json::json!(2),
208    }]
209}
210
211#[cfg(test)]
212#[path = "../../unit_tests/models/misc/paintshop.rs"]
213mod tests;