From e1ccef405d625142cefcfbd4e9c81ed4caee36e3 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sat, 28 Mar 2026 20:42:48 +0800 Subject: [PATCH 1/3] Fix #515: [Model] RegisterSufficiency Add the Register Sufficiency decision problem (Garey & Johnson A11 PO1): given a DAG and bound K, determine whether the computation can be performed using at most K registers. Value type Or, category misc. Includes model, unit tests (12), CLI create handler, canonical example, and paper entry with bibliography references (Sethi 1975, Sethi-Ullman 1970, Kessler 1998). Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 28 ++ docs/paper/references.bib | 29 ++ problemreductions-cli/src/cli.rs | 1 + problemreductions-cli/src/commands/create.rs | 33 ++- src/models/misc/mod.rs | 4 + src/models/misc/register_sufficiency.rs | 250 ++++++++++++++++++ src/models/mod.rs | 2 +- .../models/misc/register_sufficiency.rs | 230 ++++++++++++++++ 8 files changed, 575 insertions(+), 2 deletions(-) create mode 100644 src/models/misc/register_sufficiency.rs create mode 100644 src/unit_tests/models/misc/register_sufficiency.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index de79d7edb..f153eba92 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -176,6 +176,7 @@ "QuadraticAssignment": [Quadratic Assignment], "QuantifiedBooleanFormulas": [Quantified Boolean Formulas (QBF)], "RectilinearPictureCompression": [Rectilinear Picture Compression], + "RegisterSufficiency": [Register Sufficiency], "ResourceConstrainedScheduling": [Resource Constrained Scheduling], "RootedTreeStorageAssignment": [Rooted Tree Storage Assignment], "SchedulingWithIndividualDeadlines": [Scheduling With Individual Deadlines], @@ -4088,6 +4089,33 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#{ + let x = load-model-example("RegisterSufficiency") + let n = x.instance.num_vertices + let arcs = x.instance.arcs + let K = x.instance.bound + let sigma = x.optimal_config + // Build evaluation order (position -> vertex) + let order = range(n).map(pos => + range(n).find(v => sigma.at(v) == pos) + ) + [ + #problem-def("RegisterSufficiency")[ + Given a directed acyclic graph $G = (V, A)$ with $n = |V|$ vertices, where each arc $(v, u) in A$ means vertex $v$ depends on vertex $u$, and a positive integer $K$, determine whether there exists an evaluation ordering $v_(pi(0)), v_(pi(1)), dots, v_(pi(n-1))$ of all vertices such that the computation can be performed using at most $K$ registers. A value must reside in a register from the moment it is computed until all vertices that depend on it have been evaluated. + ][ + Register Sufficiency is problem SS19 (also A11 PO1) in Garey & Johnson @garey1979. NP-complete via reduction from 3-SAT @sethi1975. Remains NP-complete even when all vertices have out-degree at most 2. For expression trees (DAGs with tree structure), the Sethi--Ullman algorithm solves the problem in $O(n)$ time @sethiUllman1970. For general DAGs, Kessler's dynamic programming over register states runs in $O(n^2 dot 2^n)$ time @kessler1998. + + *Example.* Let $n = #n$ vertices with arcs (dependencies): #{arcs.map(a => $v_#(a.at(0)) arrow.r v_#(a.at(1))$).join(", ")}. Bound $K = #K$. The evaluation order $pi = (#order.map(v => $v_#v$).join(", "))$ achieves a maximum of $#K$ registers. + + #pred-commands( + "pred create --example RegisterSufficiency -o register-sufficiency.json", + "pred solve register-sufficiency.json", + "pred evaluate register-sufficiency.json --config " + x.optimal_config.map(str).join(","), + ) + ] + ] +} + #{ let x = load-model-example("RuralPostman") let nv = x.instance.graph.num_vertices diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 62192cb04..da17f35bf 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -1,3 +1,32 @@ +@article{sethi1975, + author = {Ravi Sethi}, + title = {Complete Register Allocation Problems}, + journal = {SIAM Journal on Computing}, + volume = {4}, + number = {3}, + pages = {226--248}, + year = {1975}, + doi = {10.1137/0204020} +} + +@article{sethiUllman1970, + author = {Ravi Sethi and Jeffrey D. Ullman}, + title = {The Generation of Optimal Code for Arithmetic Expressions}, + journal = {Journal of the ACM}, + volume = {17}, + number = {4}, + pages = {715--728}, + year = {1970}, + doi = {10.1145/321607.321620} +} + +@phdthesis{kessler1998, + author = {Christoph W. Kessler}, + title = {Scheduling Expression {DAG}s for Minimal Register Need}, + school = {Universit{\"a}t des Saarlandes}, + year = {1998} +} + @inproceedings{wagner1975, author = {Robert A. Wagner}, title = {On the Complexity of the Extended String-to-String Correction Problem}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index fe0b7bd35..1d62e1a03 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -312,6 +312,7 @@ Flags by problem type: StringToStringCorrection --source-string, --target-string, --bound [--alphabet-size] D2CIF --arcs, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 MinimumDummyActivitiesPert --arcs [--num-vertices] + RegisterSufficiency --arcs, --bound [--num-vertices] CBQ --domain-size, --relations, --conjuncts-spec ILP, CircuitSAT (via reduction only) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index d295670c6..0e66a8c26 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -25,7 +25,7 @@ use problemreductions::models::misc::{ ExpectedRetrievalCost, FlowShopScheduling, FrequencyTable, GroupingBySwapping, JobShopScheduling, KnownValue, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, QueryArg, - RectilinearPictureCompression, ResourceConstrainedScheduling, + RectilinearPictureCompression, RegisterSufficiency, ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, @@ -690,6 +690,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } "MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"", "MinimumDummyActivitiesPert" => "--arcs \"0>2,0>3,1>3,1>4,2>5\" --num-vertices 6", + "RegisterSufficiency" => { + "--arcs \"2>0,2>1,3>1,4>2,4>3,5>0,6>4,6>5\" --bound 3 --num-vertices 7" + } "StrongConnectivityAugmentation" => { "--arcs \"0>1,1>2\" --candidate-arcs \"2>0:1\" --bound 1" } @@ -3926,6 +3929,34 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // RegisterSufficiency + "RegisterSufficiency" => { + let usage = "Usage: pred create RegisterSufficiency --arcs \"2>0,2>1,3>1,4>2,4>3,5>0,6>4,6>5\" --bound 3 [--num-vertices N]"; + let arcs_str = args.arcs.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "RegisterSufficiency requires --arcs and --bound\n\n\ + {usage}" + ) + })?; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!( + "RegisterSufficiency requires --bound\n\n\ + {usage}" + ) + })?; + if bound < 0 { + bail!("RegisterSufficiency --bound must be non-negative\n\n{usage}"); + } + let bound = bound as usize; + let (graph, _) = parse_directed_graph(arcs_str, args.num_vertices)?; + let n = graph.num_vertices(); + let arcs = graph.arcs(); + ( + ser(RegisterSufficiency::new(n, arcs, bound))?, + resolved_variant.clone(), + ) + } + // MixedChinesePostman "MixedChinesePostman" => { let usage = "Usage: pred create MixedChinesePostman --graph 0-2,1-3,0-4,4-2 --arcs \"0>1,1>2,2>3,3>0\" --edge-weights 2,3,1,2 --arc-costs 2,3,1,4 [--num-vertices N]"; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 491950d37..473b03c45 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -21,6 +21,7 @@ //! - [`PartiallyOrderedKnapsack`]: Knapsack with precedence constraints //! - [`PrecedenceConstrainedScheduling`]: Schedule unit tasks on processors by deadline //! - [`RectilinearPictureCompression`]: Cover 1-entries with bounded rectangles +//! - [`RegisterSufficiency`]: Evaluate DAG computation with bounded registers //! - [`ResourceConstrainedScheduling`]: Schedule unit-length tasks on processors with resource constraints //! - [`SchedulingWithIndividualDeadlines`]: Meet per-task deadlines on parallel processors //! - [`StackerCrane`]: Minimize the total length of a closed walk through required arcs @@ -82,6 +83,7 @@ pub(crate) mod partially_ordered_knapsack; pub(crate) mod partition; mod precedence_constrained_scheduling; mod rectilinear_picture_compression; +mod register_sufficiency; pub(crate) mod resource_constrained_scheduling; mod scheduling_with_individual_deadlines; mod sequencing_to_minimize_maximum_cumulative_cost; @@ -122,6 +124,7 @@ pub use partially_ordered_knapsack::PartiallyOrderedKnapsack; pub use partition::Partition; pub use precedence_constrained_scheduling::PrecedenceConstrainedScheduling; pub use rectilinear_picture_compression::RectilinearPictureCompression; +pub use register_sufficiency::RegisterSufficiency; pub use resource_constrained_scheduling::ResourceConstrainedScheduling; pub use scheduling_with_individual_deadlines::SchedulingWithIndividualDeadlines; pub use sequencing_to_minimize_maximum_cumulative_cost::SequencingToMinimizeMaximumCumulativeCost; @@ -178,5 +181,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Directed arcs (v, u) meaning v depends on u" }, + FieldInfo { name: "bound", type_name: "usize", description: "Register bound K" }, + ], + } +} + +/// The Register Sufficiency problem. +/// +/// Given a directed acyclic graph G = (V, A) where arcs represent data +/// dependencies, and a positive integer K, determine whether there is an +/// evaluation ordering of all vertices such that at most K registers are +/// needed at any point during the computation. +/// +/// # Representation +/// +/// An arc `(v, u)` means vertex `v` depends on vertex `u` (i.e., `u` must be +/// in a register when `v` is evaluated). Each variable represents a vertex, +/// with domain `{0, ..., n-1}` giving its evaluation position (the config +/// must be a valid permutation). +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::RegisterSufficiency; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 4 vertices: v2 depends on v0, v3 depends on v0 and v1 +/// let problem = RegisterSufficiency::new( +/// 4, +/// vec![(2, 0), (3, 0), (3, 1)], +/// 2, +/// ); +/// let solver = BruteForce::new(); +/// let solution = solver.find_witness(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RegisterSufficiency { + /// Number of vertices. + num_vertices: usize, + /// Directed arcs (v, u) meaning v depends on u. + arcs: Vec<(usize, usize)>, + /// Register bound K. + bound: usize, +} + +impl RegisterSufficiency { + /// Create a new Register Sufficiency instance. + /// + /// # Panics + /// + /// Panics if any arc index is out of bounds (>= num_vertices), + /// or if any arc is a self-loop. + pub fn new(num_vertices: usize, arcs: Vec<(usize, usize)>, bound: usize) -> Self { + for &(v, u) in &arcs { + assert!( + v < num_vertices && u < num_vertices, + "Arc ({}, {}) out of bounds for {} vertices", + v, + u, + num_vertices + ); + assert!(v != u, "Self-loop ({}, {}) not allowed in a DAG", v, u); + } + Self { + num_vertices, + arcs, + bound, + } + } + + /// Get the number of vertices. + pub fn num_vertices(&self) -> usize { + self.num_vertices + } + + /// Get the number of arcs. + pub fn num_arcs(&self) -> usize { + self.arcs.len() + } + + /// Get the register bound K. + pub fn bound(&self) -> usize { + self.bound + } + + /// Get the arcs. + pub fn arcs(&self) -> &[(usize, usize)] { + &self.arcs + } + + /// Simulate register usage for a given evaluation ordering and return the + /// maximum number of registers used, or `None` if the ordering is invalid + /// (not a permutation or violates dependencies). + pub fn simulate_registers(&self, config: &[usize]) -> Option { + let n = self.num_vertices; + if config.len() != n { + return None; + } + + // Check valid permutation: each position 0..n-1 used exactly once + let mut order = vec![0usize; n]; // order[position] = vertex + let mut used = vec![false; n]; + for (vertex, &position) in config.iter().enumerate() { + if position >= n { + return None; + } + if used[position] { + return None; + } + used[position] = true; + order[position] = vertex; + } + + // Build dependency info: + // dependents[u] = list of vertices that depend on u (i.e., arcs (v, u)) + // dependencies[v] = list of vertices that v depends on (i.e., arcs (v, u)) + let mut dependencies: Vec> = vec![vec![]; n]; + let mut dependents: Vec> = vec![vec![]; n]; + for &(v, u) in &self.arcs { + dependencies[v].push(u); + dependents[u].push(v); + } + + // For each vertex u, compute the latest position among its dependents. + // A vertex u must stay in registers until all its dependents have been evaluated. + let mut last_use = vec![0usize; n]; + for u in 0..n { + if dependents[u].is_empty() { + // Vertex u has no dependents. It stays in registers from its + // evaluation step until the end (final outputs must be in S_n). + last_use[u] = n; // stays until the end + } else { + let mut latest = 0; + for &v in &dependents[u] { + latest = latest.max(config[v]); + } + last_use[u] = latest; + } + } + + let mut max_registers = 0; + + // Simulate: process vertices in evaluation order + for step in 0..n { + let vertex = order[step]; + + // Check dependencies: all dependencies of this vertex must have + // been evaluated before this step + for &dep in &dependencies[vertex] { + if config[dep] >= step { + // Dependency not yet evaluated + return None; + } + } + + // Count registers at this step: + // A vertex v is in registers if: + // - v has been evaluated (config[v] <= step) + // - v is still needed (last_use[v] > step, or v is the current vertex) + // Actually, more precisely: after evaluating vertex at position `step`, + // the register set contains all vertices evaluated so far whose last + // use is > step (they're still needed later), plus the current vertex. + let reg_count = order[..=step] + .iter() + .filter(|&&v| last_use[v] > step) + .count(); + + max_registers = max_registers.max(reg_count); + } + + Some(max_registers) + } +} + +impl Problem for RegisterSufficiency { + const NAME: &'static str = "RegisterSufficiency"; + type Value = crate::types::Or; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![self.num_vertices; self.num_vertices] + } + + fn evaluate(&self, config: &[usize]) -> crate::types::Or { + crate::types::Or( + self.simulate_registers(config) + .is_some_and(|max_reg| max_reg <= self.bound), + ) + } +} + +crate::declare_variants! { + default RegisterSufficiency => "num_vertices ^ 2 * 2 ^ num_vertices", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "register_sufficiency", + // Issue #515 example: 7 vertices, 8 arcs, K=3 + // Arcs (0-indexed): (2,0), (2,1), (3,1), (4,2), (4,3), (5,0), (6,4), (6,5) + // Order: v0,v1,v2,v3,v5,v4,v6 -> positions [0,1,2,3,5,4,6] + instance: Box::new(RegisterSufficiency::new( + 7, + vec![ + (2, 0), + (2, 1), + (3, 1), + (4, 2), + (4, 3), + (5, 0), + (6, 4), + (6, 5), + ], + 3, + )), + // Order: v1,v2,v3,v4,v6,v5,v7 (1-indexed) = v0,v1,v2,v3,v5,v4,v6 (0-indexed) + // Positions: v0->0, v1->1, v2->2, v3->3, v4->5, v5->4, v6->6 + optimal_config: vec![0, 1, 2, 3, 5, 4, 6], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/register_sufficiency.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index a3e77f0a1..1518a076a 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -41,7 +41,7 @@ pub use misc::{ ExpectedRetrievalCost, Factoring, FlowShopScheduling, GroupingBySwapping, JobShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, Partition, PrecedenceConstrainedScheduling, QueryArg, RectilinearPictureCompression, - ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, + RegisterSufficiency, ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, StackerCrane, StaffScheduling, diff --git a/src/unit_tests/models/misc/register_sufficiency.rs b/src/unit_tests/models/misc/register_sufficiency.rs new file mode 100644 index 000000000..c4cd025fc --- /dev/null +++ b/src/unit_tests/models/misc/register_sufficiency.rs @@ -0,0 +1,230 @@ +use super::*; +use crate::solvers::BruteForce; +use crate::traits::Problem; + +#[test] +fn test_register_sufficiency_basic() { + let problem = RegisterSufficiency::new( + 7, + vec![ + (2, 0), + (2, 1), + (3, 1), + (4, 2), + (4, 3), + (5, 0), + (6, 4), + (6, 5), + ], + 3, + ); + assert_eq!(problem.num_vertices(), 7); + assert_eq!(problem.num_arcs(), 8); + assert_eq!(problem.bound(), 3); + assert_eq!( + problem.arcs(), + &[ + (2, 0), + (2, 1), + (3, 1), + (4, 2), + (4, 3), + (5, 0), + (6, 4), + (6, 5) + ] + ); + assert_eq!(problem.dims(), vec![7; 7]); + assert_eq!( + ::NAME, + "RegisterSufficiency" + ); + assert_eq!(::variant(), vec![]); +} + +#[test] +fn test_register_sufficiency_evaluate_valid() { + // Issue #515 example: 7 vertices, 8 arcs, K=3 + let problem = RegisterSufficiency::new( + 7, + vec![ + (2, 0), + (2, 1), + (3, 1), + (4, 2), + (4, 3), + (5, 0), + (6, 4), + (6, 5), + ], + 3, + ); + // Order: v0,v1,v2,v3,v5,v4,v6 (0-indexed) + // Positions: v0->0, v1->1, v2->2, v3->3, v4->5, v5->4, v6->6 + let config = vec![0, 1, 2, 3, 5, 4, 6]; + assert!(problem.evaluate(&config)); + + // Verify register count + let max_reg = problem.simulate_registers(&config).unwrap(); + assert_eq!(max_reg, 3); +} + +#[test] +fn test_register_sufficiency_evaluate_invalid_permutation() { + let problem = RegisterSufficiency::new(4, vec![(2, 0), (3, 0), (3, 1)], 2); + // Not a permutation: position 0 used twice + assert!(!problem.evaluate(&[0, 0, 1, 2])); + // Wrong length + assert!(!problem.evaluate(&[0, 1, 2])); + assert!(!problem.evaluate(&[0, 1, 2, 3, 4])); + // Position out of range + assert!(!problem.evaluate(&[0, 1, 2, 4])); +} + +#[test] +fn test_register_sufficiency_evaluate_invalid_dependency() { + // v2 depends on v0, v3 depends on v0 and v1 + let problem = RegisterSufficiency::new(4, vec![(2, 0), (3, 0), (3, 1)], 4); + // v2 at position 0, v0 at position 1 -> v2 evaluated before its dependency v0 + assert!(!problem.evaluate(&[1, 2, 0, 3])); +} + +#[test] +fn test_register_sufficiency_evaluate_exceeds_bound() { + // Issue example with K=2 (should fail - minimum is 3) + let problem = RegisterSufficiency::new( + 7, + vec![ + (2, 0), + (2, 1), + (3, 1), + (4, 2), + (4, 3), + (5, 0), + (6, 4), + (6, 5), + ], + 2, + ); + // Same valid ordering but K=2 is too small + let config = vec![0, 1, 2, 3, 5, 4, 6]; + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_register_sufficiency_brute_force() { + // Small instance: 4 vertices, v2 depends on v0, v3 depends on v1 + let problem = RegisterSufficiency::new(4, vec![(2, 0), (3, 1)], 2); + let solver = BruteForce::new(); + let solution = solver + .find_witness(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_register_sufficiency_brute_force_all() { + let problem = RegisterSufficiency::new(4, vec![(2, 0), (3, 1)], 2); + let solver = BruteForce::new(); + let solutions = solver.find_all_witnesses(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_register_sufficiency_unsatisfiable() { + // Chain: v3 depends on v2, v2 depends on v1, v1 depends on v0 + // Plus: v3 also depends on v0 + // This requires 3 registers (v0 must stay alive until v3) + // With K=1, impossible + let problem = RegisterSufficiency::new(4, vec![(1, 0), (2, 1), (3, 2), (3, 0)], 1); + let solver = BruteForce::new(); + assert!(solver.find_witness(&problem).is_none()); +} + +#[test] +fn test_register_sufficiency_serialization() { + let problem = RegisterSufficiency::new( + 7, + vec![ + (2, 0), + (2, 1), + (3, 1), + (4, 2), + (4, 3), + (5, 0), + (6, 4), + (6, 5), + ], + 3, + ); + let json = serde_json::to_value(&problem).unwrap(); + let restored: RegisterSufficiency = serde_json::from_value(json).unwrap(); + assert_eq!(restored.num_vertices(), problem.num_vertices()); + assert_eq!(restored.num_arcs(), problem.num_arcs()); + assert_eq!(restored.bound(), problem.bound()); + assert_eq!(restored.arcs(), problem.arcs()); +} + +#[test] +fn test_register_sufficiency_empty() { + let problem = RegisterSufficiency::new(0, vec![], 0); + assert_eq!(problem.num_vertices(), 0); + assert_eq!(problem.dims(), Vec::::new()); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_register_sufficiency_single_vertex() { + let problem = RegisterSufficiency::new(1, vec![], 1); + assert!(problem.evaluate(&[0])); + // K=0 should fail (vertex needs one register) + let problem_k0 = RegisterSufficiency::new(1, vec![], 0); + assert!(!problem_k0.evaluate(&[0])); +} + +#[test] +fn test_register_sufficiency_paper_example() { + // The issue example: 7 vertices, 8 arcs, K=3 + let problem = RegisterSufficiency::new( + 7, + vec![ + (2, 0), + (2, 1), + (3, 1), + (4, 2), + (4, 3), + (5, 0), + (6, 4), + (6, 5), + ], + 3, + ); + + // The order from the issue: v1,v2,v3,v4,v6,v5,v7 (1-indexed) + // = v0,v1,v2,v3,v5,v4,v6 (0-indexed) + // Positions: v0->0, v1->1, v2->2, v3->3, v4->5, v5->4, v6->6 + let config = vec![0, 1, 2, 3, 5, 4, 6]; + assert!(problem.evaluate(&config)); + assert_eq!(problem.simulate_registers(&config).unwrap(), 3); + + // Verify K=2 is impossible using brute force + let problem_k2 = RegisterSufficiency::new( + 7, + vec![ + (2, 0), + (2, 1), + (3, 1), + (4, 2), + (4, 3), + (5, 0), + (6, 4), + (6, 5), + ], + 2, + ); + let solver = BruteForce::new(); + assert!(solver.find_witness(&problem_k2).is_none()); +} From 64b2c17c9d58893d997da1ead9b8be901b77eb82 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 29 Mar 2026 21:22:49 +0800 Subject: [PATCH 2/3] Fix pred solve command in paper to use --solver brute-force RegisterSufficiency has no ILP reduction path yet, so the default solver fails. Use --solver brute-force consistent with other models lacking an ILP path (e.g., PathConstrainedNetworkFlow, RootedTreeArrangement). Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 037415cd2..d6900203f 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -4111,7 +4111,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], #pred-commands( "pred create --example RegisterSufficiency -o register-sufficiency.json", - "pred solve register-sufficiency.json", + "pred solve register-sufficiency.json --solver brute-force", "pred evaluate register-sufficiency.json --config " + x.optimal_config.map(str).join(","), ) ] From 37708a347747d51530f2f2492e4899edb031c6ba Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Sun, 29 Mar 2026 21:25:10 +0800 Subject: [PATCH 3/3] Add CLI regression test for RegisterSufficiency create Covers the documented create flow with --arcs, --bound, --num-vertices flags and verifies the output JSON structure. Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/tests/cli_tests.rs | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 158e71584..448f86c7b 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -4088,6 +4088,38 @@ fn test_create_rectilinear_picture_compression_rejects_ragged_matrix() { ); } +#[test] +fn test_create_register_sufficiency() { + let output_file = std::env::temp_dir().join("pred_test_create_register_sufficiency.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "RegisterSufficiency", + "--arcs", + "2>0,2>1,3>1,4>2,4>3,5>0,6>4,6>5", + "--bound", + "3", + "--num-vertices", + "7", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "RegisterSufficiency"); + assert_eq!(json["data"]["num_vertices"], 7); + assert_eq!(json["data"]["bound"], 3); + assert_eq!(json["data"]["arcs"].as_array().unwrap().len(), 8); + std::fs::remove_file(&output_file).ok(); +} + #[test] fn test_create_help_uses_generic_matrix_and_k_descriptions() { let output = pred().args(["create", "--help"]).output().unwrap();