Skip to main content

problemreductions/models/misc/
closest_string.rs

1//! Closest String problem implementation.
2//!
3//! Given a finite alphabet `{0, ..., alphabet_size - 1}` and a list of input
4//! strings of equal length `m`, find a center string `c` of length `m` over the
5//! same alphabet that minimizes the maximum Hamming distance from `c` to any
6//! input string.
7
8use crate::registry::{FieldInfo, ProblemSchemaEntry};
9use crate::traits::Problem;
10use crate::types::Min;
11use serde::{Deserialize, Serialize};
12
13inventory::submit! {
14    ProblemSchemaEntry {
15        name: "ClosestString",
16        display_name: "Closest String",
17        aliases: &[],
18        dimensions: &[],
19        category: crate::registry::ProblemCategory::Misc,
20        module_path: module_path!(),
21        description: "Find a center string of fixed length that minimizes the maximum Hamming distance to a list of equal-length input strings",
22        fields: &[
23            FieldInfo {
24                name: "alphabet_size",
25                type_name: "usize",
26                description: "Size q of the finite alphabet {0, ..., q-1}",
27            },
28            FieldInfo {
29                name: "strings",
30                type_name: "Vec<Vec<usize>>",
31                description: "Input strings s_1, ..., s_n over the alphabet, all of equal length m",
32            },
33        ],
34    }
35}
36
37/// The Closest String problem.
38///
39/// Given a finite alphabet `Sigma = {0, ..., q - 1}` and `n` input strings
40/// `s_1, ..., s_n` in `Sigma^m` (all of common length `m`), find a center
41/// string `c` in `Sigma^m` minimizing
42///
43/// `max_{1 <= i <= n} d_H(c, s_i)`,
44///
45/// where `d_H` is the Hamming distance. Every center in the discrete cube is
46/// syntactically feasible; the objective is its worst-case Hamming distance
47/// to the input strings.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct ClosestString {
50    alphabet_size: usize,
51    strings: Vec<Vec<usize>>,
52}
53
54impl ClosestString {
55    /// Create a new `ClosestString` instance.
56    ///
57    /// # Panics
58    ///
59    /// Panics if:
60    /// - `strings` is empty (the problem requires at least one input string),
61    /// - input strings do not all have the same length,
62    /// - `alphabet_size == 0` while any input string is non-empty,
63    /// - any symbol in any input string is `>= alphabet_size`.
64    pub fn new(alphabet_size: usize, strings: Vec<Vec<usize>>) -> Self {
65        assert!(
66            !strings.is_empty(),
67            "ClosestString requires at least one input string"
68        );
69        let string_length = strings[0].len();
70        assert!(
71            strings.iter().all(|s| s.len() == string_length),
72            "all input strings must have the same length"
73        );
74        assert!(
75            alphabet_size > 0 || string_length == 0,
76            "alphabet_size must be > 0 when input strings are non-empty"
77        );
78        assert!(
79            strings
80                .iter()
81                .flat_map(|s| s.iter())
82                .all(|&symbol| symbol < alphabet_size),
83            "input symbols must be less than alphabet_size"
84        );
85        Self {
86            alphabet_size,
87            strings,
88        }
89    }
90
91    /// Returns the alphabet size `q`.
92    pub fn alphabet_size(&self) -> usize {
93        self.alphabet_size
94    }
95
96    /// Returns the input strings.
97    pub fn strings(&self) -> &[Vec<usize>] {
98        &self.strings
99    }
100
101    /// Returns the number of input strings `n`.
102    pub fn num_strings(&self) -> usize {
103        self.strings.len()
104    }
105
106    /// Returns the common string length `m`.
107    pub fn string_length(&self) -> usize {
108        self.strings[0].len()
109    }
110
111    /// Returns the total input length `num_strings * string_length`.
112    pub fn total_length(&self) -> usize {
113        self.num_strings() * self.string_length()
114    }
115}
116
117impl Problem for ClosestString {
118    const NAME: &'static str = "ClosestString";
119    type Solution = Vec<usize>;
120    type Value = Min<i64>;
121
122    crate::problem_parameters![
123        ("alphabet_size", alphabet_size),
124        ("num_strings", num_strings),
125        ("string_length", string_length),
126        ("total_length", total_length),
127    ];
128
129    fn variant() -> Vec<(&'static str, &'static str)> {
130        crate::variant_params![]
131    }
132
133    fn evaluate(
134        &self,
135        config: &Self::Solution,
136    ) -> Result<Min<i64>, crate::traits::EvaluationError> {
137        Ok({
138            let m = self.string_length();
139            if config.len() != m {
140                return Err(crate::traits::EvaluationError::InvalidConfiguration(
141                    "candidate string length does not match the instance strings".into(),
142                ));
143            }
144            if config.iter().any(|&symbol| symbol >= self.alphabet_size) {
145                return Err(crate::traits::EvaluationError::InvalidConfiguration(
146                    "candidate string contains an out-of-range symbol".into(),
147                ));
148            }
149            // Maximum Hamming distance from the center to any input string.
150            let mut max_distance = 0_i64;
151            for string in &self.strings {
152                let distance = i64::try_from(
153                    config
154                        .iter()
155                        .zip(string.iter())
156                        .filter(|(center, target)| center != target)
157                        .count(),
158                )
159                .map_err(|_| {
160                    crate::traits::EvaluationError::IntegerOverflow(
161                        "converting Hamming distance to i64".into(),
162                    )
163                })?;
164                max_distance = max_distance.max(distance);
165            }
166            Min(Some(max_distance))
167        })
168    }
169}
170
171impl crate::solvers::BruteForceProblem for ClosestString {
172    fn dimensions(&self) -> Vec<usize> {
173        vec![self.alphabet_size; self.string_length()]
174    }
175}
176
177crate::declare_variants! {
178    default ClosestString => "alphabet_size ^ string_length",
179}
180
181crate::register_brute_force! {
182    ClosestString,
183}
184
185#[cfg(feature = "example-db")]
186pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
187    vec![crate::example_db::specs::ModelExampleSpec {
188        id: "closest_string",
189        instance: Box::new(ClosestString::new(
190            2,
191            vec![vec![0, 0, 0], vec![0, 1, 1], vec![1, 0, 1], vec![1, 1, 0]],
192        )),
193        optimal_config: serde_json::json!(vec![0, 0, 0]),
194        optimal_value: serde_json::json!(2),
195    }]
196}
197
198#[cfg(test)]
199#[path = "../../unit_tests/models/misc/closest_string.rs"]
200mod tests;