Skip to main content

problemreductions/models/misc/
register_sufficiency.rs

1//! Register Sufficiency problem implementation.
2//!
3//! Given a directed acyclic graph G = (V, A) representing a computation and a
4//! bound K, determine whether the computation can be performed using at most K
5//! registers. NP-complete even for out-degree <= 2 [Sethi, 1975].
6
7use crate::registry::{FieldInfo, ProblemSchemaEntry};
8use crate::traits::Problem;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "RegisterSufficiency",
14        display_name: "Register Sufficiency",
15        aliases: &[],
16        dimensions: &[],
17        category: crate::registry::ProblemCategory::Misc,
18        module_path: module_path!(),
19        description: "Determine whether a DAG computation can be performed using K or fewer registers",
20        fields: &[
21            FieldInfo { name: "num_vertices", type_name: "usize", description: "Number of vertices n = |V|" },
22            FieldInfo { name: "arcs", type_name: "Vec<(usize, usize)>", description: "Directed arcs (v, u) meaning v depends on u" },
23            FieldInfo { name: "bound", type_name: "usize", description: "Register bound K" },
24        ],
25    }
26}
27
28/// The Register Sufficiency problem.
29///
30/// Given a directed acyclic graph G = (V, A) where arcs represent data
31/// dependencies, and a positive integer K, determine whether there is an
32/// evaluation ordering of all vertices such that at most K registers are
33/// needed at any point during the computation.
34///
35/// # Representation
36///
37/// An arc `(v, u)` means vertex `v` depends on vertex `u` (i.e., `u` must be
38/// in a register when `v` is evaluated). Each variable represents a vertex,
39/// with domain `{0, ..., n-1}` giving its evaluation position (the config
40/// must be a valid permutation).
41///
42/// # Example
43///
44/// ```
45/// use problemreductions::models::misc::RegisterSufficiency;
46/// use problemreductions::{Problem, BruteForce};
47///
48/// // 4 vertices: v2 depends on v0, v3 depends on v0 and v1
49/// let problem = RegisterSufficiency::new(
50///     4,
51///     vec![(2, 0), (3, 0), (3, 1)],
52///     2,
53/// );
54/// let solver = BruteForce::new();
55/// let solution = solver.solve(&problem).unwrap();
56/// assert!(solution.is_some());
57/// ```
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct RegisterSufficiency {
60    /// Number of vertices.
61    num_vertices: usize,
62    /// Directed arcs (v, u) meaning v depends on u.
63    arcs: Vec<(usize, usize)>,
64    /// Register bound K.
65    bound: usize,
66}
67
68impl RegisterSufficiency {
69    /// Create a new Register Sufficiency instance.
70    ///
71    /// # Panics
72    ///
73    /// Panics if any arc index is out of bounds (>= num_vertices),
74    /// or if any arc is a self-loop.
75    pub fn new(num_vertices: usize, arcs: Vec<(usize, usize)>, bound: usize) -> Self {
76        for &(v, u) in &arcs {
77            assert!(
78                v < num_vertices && u < num_vertices,
79                "Arc ({}, {}) out of bounds for {} vertices",
80                v,
81                u,
82                num_vertices
83            );
84            assert!(v != u, "Self-loop ({}, {}) not allowed in a DAG", v, u);
85        }
86        Self {
87            num_vertices,
88            arcs,
89            bound,
90        }
91    }
92
93    /// Get the number of vertices.
94    pub fn num_vertices(&self) -> usize {
95        self.num_vertices
96    }
97
98    /// Get the number of arcs.
99    pub fn num_arcs(&self) -> usize {
100        self.arcs.len()
101    }
102
103    /// Count vertices with no dependents.
104    pub fn num_sinks(&self) -> usize {
105        let mut has_dependent = vec![false; self.num_vertices];
106        for &(_, dependency) in &self.arcs {
107            has_dependent[dependency] = true;
108        }
109        has_dependent.into_iter().filter(|&flag| !flag).count()
110    }
111
112    /// Get the register bound K.
113    pub fn bound(&self) -> usize {
114        self.bound
115    }
116
117    /// Get the arcs.
118    pub fn arcs(&self) -> &[(usize, usize)] {
119        &self.arcs
120    }
121
122    /// Simulate register usage for a given evaluation ordering and return the
123    /// maximum number of registers used, or `None` if the ordering is invalid
124    /// (not a permutation or violates dependencies).
125    pub fn simulate_registers(
126        &self,
127        config: &[usize],
128    ) -> Result<Option<i64>, crate::traits::EvaluationError> {
129        let n = self.num_vertices;
130        if config.len() != n {
131            return Ok(None);
132        }
133
134        // Check valid permutation: each position 0..n-1 used exactly once
135        let mut order = vec![0usize; n]; // order[position] = vertex
136        let mut used = vec![false; n];
137        for (vertex, &position) in config.iter().enumerate() {
138            if position >= n {
139                return Ok(None);
140            }
141            if used[position] {
142                return Ok(None);
143            }
144            used[position] = true;
145            order[position] = vertex;
146        }
147
148        // Build dependency info:
149        // dependents[u] = list of vertices that depend on u (i.e., arcs (v, u))
150        // dependencies[v] = list of vertices that v depends on (i.e., arcs (v, u))
151        let mut dependencies: Vec<Vec<usize>> = vec![vec![]; n];
152        let mut dependents: Vec<Vec<usize>> = vec![vec![]; n];
153        for &(v, u) in &self.arcs {
154            dependencies[v].push(u);
155            dependents[u].push(v);
156        }
157
158        // For each vertex u, compute the latest position among its dependents.
159        // A vertex u must stay in registers until all its dependents have been evaluated.
160        let mut last_use = vec![0usize; n];
161        for u in 0..n {
162            if dependents[u].is_empty() {
163                // Vertex u has no dependents. It stays in registers from its
164                // evaluation step until the end (final outputs must be in S_n).
165                last_use[u] = n; // stays until the end
166            } else {
167                let mut latest = 0;
168                for &v in &dependents[u] {
169                    latest = latest.max(config[v]);
170                }
171                last_use[u] = latest;
172            }
173        }
174
175        let mut max_registers = 0;
176
177        // Simulate: process vertices in evaluation order
178        for step in 0..n {
179            let vertex = order[step];
180
181            // Check dependencies: all dependencies of this vertex must have
182            // been evaluated before this step
183            for &dep in &dependencies[vertex] {
184                if config[dep] >= step {
185                    // Dependency not yet evaluated
186                    return Ok(None);
187                }
188            }
189
190            // Count registers at this step:
191            // A vertex v is in registers if:
192            // - v has been evaluated (config[v] <= step)
193            // - v is still needed (last_use[v] > step, or v is the current vertex)
194            // Actually, more precisely: after evaluating vertex at position `step`,
195            // the register set contains all vertices evaluated so far whose last
196            // use is > step (they're still needed later), plus the current vertex.
197            let reg_count = order[..=step]
198                .iter()
199                .filter(|&&v| last_use[v] > step)
200                .count();
201
202            max_registers = max_registers.max(reg_count);
203        }
204
205        Ok(Some(i64::try_from(max_registers).map_err(|_| {
206            crate::traits::EvaluationError::IntegerOverflow(
207                "converting register-usage count to i64".into(),
208            )
209        })?))
210    }
211
212    /// Exact branch-and-bound solver: finds a topological ordering using at
213    /// most `self.bound` registers, or returns `None` if no such ordering
214    /// exists.  Uses heuristic candidate ordering (prefer vertices that free
215    /// the most registers) so that YES instances typically resolve on the
216    /// first greedy path without backtracking.  For NO instances the full
217    /// search tree must be explored, so prefer the ILP solver path for
218    /// infeasibility proofs.
219    ///
220    /// NOTE: a greedy topological sort is *not* exact — it can miss valid
221    /// orderings.  This method is exact because it backtracks when the
222    /// greedy choice fails.  Do not replace it with a pure greedy solver.
223    pub fn solve_exact(&self) -> Option<Vec<usize>> {
224        let n = self.num_vertices;
225        if n == 0 {
226            return Some(vec![]);
227        }
228
229        let mut dependents: Vec<Vec<usize>> = vec![vec![]; n];
230        let mut dependencies: Vec<Vec<usize>> = vec![vec![]; n];
231        let mut in_degree = vec![0u32; n];
232        for &(v, u) in &self.arcs {
233            in_degree[v] += 1;
234            dependents[u].push(v);
235            dependencies[v].push(u);
236        }
237
238        let mut state = BnBState {
239            n,
240            bound: self.bound,
241            config: vec![0usize; n],
242            live: vec![false; n],
243            live_count: 0,
244            remaining_in_degree: in_degree.clone(),
245            remaining_deps: dependents.iter().map(|d| d.len()).collect(),
246            ready: (0..n).filter(|&v| in_degree[v] == 0).collect(),
247            dependents,
248            dependencies,
249        };
250        state.ready.sort_unstable();
251
252        if state.backtrack(0) {
253            Some(state.config)
254        } else {
255            None
256        }
257    }
258}
259
260struct BnBState {
261    n: usize,
262    bound: usize,
263    config: Vec<usize>,
264    live: Vec<bool>,
265    live_count: usize,
266    remaining_in_degree: Vec<u32>,
267    remaining_deps: Vec<usize>,
268    ready: Vec<usize>,
269    dependents: Vec<Vec<usize>>,
270    dependencies: Vec<Vec<usize>>,
271}
272
273impl BnBState {
274    fn backtrack(&mut self, step: usize) -> bool {
275        if step == self.n {
276            return true;
277        }
278
279        // Heuristic: prefer vertices that free the most registers.
280        let mut candidates = self.ready.clone();
281        candidates.sort_by_key(|&v| {
282            let frees = self.dependencies[v]
283                .iter()
284                .filter(|&&dep| self.remaining_deps[dep] == 1 && self.live[dep])
285                .count();
286            std::cmp::Reverse(frees)
287        });
288
289        for &vertex in &candidates {
290            self.config[vertex] = step;
291
292            let was_live = self.live[vertex];
293            if !was_live {
294                self.live[vertex] = true;
295                self.live_count += 1;
296            }
297
298            let mut freed = Vec::new();
299            for &dep in &self.dependencies[vertex] {
300                self.remaining_deps[dep] -= 1;
301                if self.remaining_deps[dep] == 0 && self.live[dep] {
302                    self.live[dep] = false;
303                    self.live_count -= 1;
304                    freed.push(dep);
305                }
306            }
307
308            if self.live_count <= self.bound {
309                self.ready.retain(|&v| v != vertex);
310                let mut newly_ready = Vec::new();
311                for &dep in &self.dependents[vertex] {
312                    self.remaining_in_degree[dep] -= 1;
313                    if self.remaining_in_degree[dep] == 0 {
314                        self.ready.push(dep);
315                        newly_ready.push(dep);
316                    }
317                }
318
319                if self.backtrack(step + 1) {
320                    return true;
321                }
322
323                for &dep in &newly_ready {
324                    self.ready.retain(|&v| v != dep);
325                }
326                for &dep in &self.dependents[vertex] {
327                    self.remaining_in_degree[dep] += 1;
328                }
329                self.ready.push(vertex);
330                self.ready.sort_unstable();
331            }
332
333            for &dep in &freed {
334                self.live[dep] = true;
335                self.live_count += 1;
336            }
337            for &dep in &self.dependencies[vertex] {
338                self.remaining_deps[dep] += 1;
339            }
340
341            if !was_live {
342                self.live[vertex] = false;
343                self.live_count -= 1;
344            }
345        }
346
347        false
348    }
349}
350
351impl Problem for RegisterSufficiency {
352    const NAME: &'static str = "RegisterSufficiency";
353    type Solution = Vec<usize>;
354    type Value = crate::types::Or;
355
356    crate::problem_parameters![
357        ("bound", bound),
358        ("num_arcs", num_arcs),
359        ("num_sinks", num_sinks),
360        ("num_vertices", num_vertices),
361    ];
362
363    fn variant() -> Vec<(&'static str, &'static str)> {
364        crate::variant_params![]
365    }
366
367    fn evaluate(
368        &self,
369        config: &Self::Solution,
370    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
371        if config.len() != self.num_vertices {
372            return Err(crate::traits::EvaluationError::InvalidConfiguration(
373                "evaluation ordering length does not match the graph vertices".into(),
374            ));
375        }
376        if config.iter().any(|&position| position >= self.num_vertices) {
377            return Err(crate::traits::EvaluationError::InvalidConfiguration(
378                "evaluation ordering contains an out-of-range position".into(),
379            ));
380        }
381        let bound = i64::try_from(self.bound).map_err(|_| {
382            crate::traits::EvaluationError::IntegerOverflow(
383                "converting register bound to i64".into(),
384            )
385        })?;
386        Ok(crate::types::Or(
387            self.simulate_registers(config)?
388                .is_some_and(|max_reg| max_reg <= bound),
389        ))
390    }
391}
392
393impl crate::solvers::BruteForceProblem for RegisterSufficiency {
394    fn dimensions(&self) -> Vec<usize> {
395        vec![self.num_vertices; self.num_vertices]
396    }
397}
398
399crate::declare_variants! {
400    default RegisterSufficiency => "num_vertices ^ 2 * 2 ^ num_vertices",
401}
402
403crate::register_brute_force! {
404    RegisterSufficiency,
405}
406
407#[cfg(feature = "example-db")]
408pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
409    vec![crate::example_db::specs::ModelExampleSpec {
410        id: "register_sufficiency",
411        // Issue #515 example: 7 vertices, 8 arcs, K=3
412        // Arcs (0-indexed): (2,0), (2,1), (3,1), (4,2), (4,3), (5,0), (6,4), (6,5)
413        // Order: v0,v1,v2,v3,v5,v4,v6 -> positions [0,1,2,3,5,4,6]
414        instance: Box::new(RegisterSufficiency::new(
415            7,
416            vec![
417                (2, 0),
418                (2, 1),
419                (3, 1),
420                (4, 2),
421                (4, 3),
422                (5, 0),
423                (6, 4),
424                (6, 5),
425            ],
426            3,
427        )),
428        // Order: v1,v2,v3,v4,v6,v5,v7 (1-indexed) = v0,v1,v2,v3,v5,v4,v6 (0-indexed)
429        // Positions: v0->0, v1->1, v2->2, v3->3, v4->5, v5->4, v6->6
430        optimal_config: serde_json::json!(vec![0, 1, 2, 3, 5, 4, 6]),
431        optimal_value: serde_json::json!(true),
432    }]
433}
434
435#[cfg(test)]
436#[path = "../../unit_tests/models/misc/register_sufficiency.rs"]
437mod tests;