Skip to content

Commit 3ee17e6

Browse files
GiggleLiuisPANNclaude
authored
Fix #419: [Model] ConsecutiveOnesMatrixAugmentation (#756)
* Add plan for #419: [Model] ConsecutiveOnesMatrixAugmentation * Implement #419: [Model] ConsecutiveOnesMatrixAugmentation * chore: remove plan file after implementation * Fix CLI panic on negative bound, paper solve command, and test coverage - Use try_new() instead of new() in CLI create path to avoid panic on negative --bound input - Add --solver brute-force to paper pred solve command (no ILP path) - Add CLI regression test for negative bound - Add unit test for all-zero row branch - Apply cargo fmt fixes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fed65e9 commit 3ee17e6

10 files changed

Lines changed: 481 additions & 5 deletions

File tree

docs/paper/reductions.typ

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
138138
"ConjunctiveBooleanQuery": [Conjunctive Boolean Query],
139139
"ConsecutiveBlockMinimization": [Consecutive Block Minimization],
140+
"ConsecutiveOnesMatrixAugmentation": [Consecutive Ones Matrix Augmentation],
140141
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
141142
"SparseMatrixCompression": [Sparse Matrix Compression],
142143
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
@@ -6106,6 +6107,55 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
61066107
]
61076108
}
61086109

6110+
#{
6111+
let x = load-model-example("ConsecutiveOnesMatrixAugmentation")
6112+
let A = x.instance.matrix
6113+
let m = A.len()
6114+
let n = if m > 0 { A.at(0).len() } else { 0 }
6115+
let K = x.instance.bound
6116+
let perm = x.optimal_config
6117+
let A-int = A.map(row => row.map(v => if v { 1 } else { 0 }))
6118+
let reordered = A.map(row => perm.map(c => if row.at(c) { 1 } else { 0 }))
6119+
let total-flips = 0
6120+
for row in reordered {
6121+
let first = none
6122+
let last = none
6123+
let count = 0
6124+
for (j, value) in row.enumerate() {
6125+
if value == 1 {
6126+
if first == none {
6127+
first = j
6128+
}
6129+
last = j
6130+
count += 1
6131+
}
6132+
}
6133+
if first != none and last != none {
6134+
total-flips += last - first + 1 - count
6135+
}
6136+
}
6137+
[
6138+
#problem-def("ConsecutiveOnesMatrixAugmentation")[
6139+
Given an $m times n$ binary matrix $A$ and a nonnegative integer $K$, determine whether there exists a matrix $A'$, obtained from $A$ by changing at most $K$ zero entries to one, such that some permutation of the columns of $A'$ has the consecutive ones property.
6140+
][
6141+
Consecutive Ones Matrix Augmentation is problem SR16 in Garey & Johnson @garey1979. It asks whether a binary matrix can be repaired by a bounded number of augmenting flips so that every row's 1-entries become contiguous after reordering the columns. This setting appears in information retrieval and DNA physical mapping, where matrices close to the consecutive ones property can still encode useful interval structure. Booth and Lueker showed that testing whether a matrix already has the consecutive ones property is polynomial-time via PQ-trees @booth1976, but allowing bounded augmentation makes the decision problem NP-complete @booth1975. The direct exhaustive search tries all $n!$ column permutations and, for each one, computes the minimum augmentation cost by filling the holes between the first and last 1 in every row#footnote[No algorithm improving on brute-force permutation enumeration is known for the general problem in this repository's supported setting.].
6142+
6143+
*Example.* Consider the $#m times #n$ matrix $A = mat(#A-int.map(row => row.map(v => str(v)).join(", ")).join("; "))$ with $K = #K$. Under the permutation $pi = (#perm.map(p => str(p)).join(", "))$, the reordered rows are #reordered.enumerate().map(((i, row)) => [$r_#(i + 1) = (#row.map(v => str(v)).join(", "))$]).join(", "). The first row becomes $(1, 0, 1, 0, 1)$, so filling the two interior gaps yields $(1, 1, 1, 1, 1)$. The other three rows already have consecutive 1-entries under the same order, so the total augmentation cost is #total-flips and #total-flips $<= #K$, making the instance satisfiable.
6144+
6145+
#pred-commands(
6146+
"pred create --example ConsecutiveOnesMatrixAugmentation -o consecutive-ones-matrix-augmentation.json",
6147+
"pred solve consecutive-ones-matrix-augmentation.json --solver brute-force",
6148+
"pred evaluate consecutive-ones-matrix-augmentation.json --config " + x.optimal_config.map(str).join(","),
6149+
)
6150+
6151+
#figure(
6152+
align(center, math.equation([$A = #math.mat(..A-int.map(row => row.map(v => [#v])))$])),
6153+
caption: [The canonical $#m times #n$ example matrix for Consecutive Ones Matrix Augmentation. The permutation $pi = (#perm.map(p => str(p)).join(", "))$ makes only the first row need augmentation, and exactly two zero-to-one flips suffice.],
6154+
) <fig:coma-example>
6155+
]
6156+
]
6157+
}
6158+
61096159
#{
61106160
let x = load-model-example("ConsecutiveOnesSubmatrix")
61116161
let A = x.instance.matrix

problemreductions-cli/src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ Flags by problem type:
268268
PartialFeedbackEdgeSet --graph, --budget, --max-cycle-length [--num-vertices]
269269
BMF --matrix (0/1), --rank
270270
ConsecutiveBlockMinimization --matrix (JSON 2D bool), --bound-k
271+
ConsecutiveOnesMatrixAugmentation --matrix (0/1), --bound
271272
ConsecutiveOnesSubmatrix --matrix (0/1), --k
272273
SparseMatrixCompression --matrix (0/1), --bound
273274
SteinerTree --graph, --edge-weights, --terminals

problemreductions-cli/src/commands/create.rs

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ use crate::util;
88
use anyhow::{bail, Context, Result};
99
use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample};
1010
use problemreductions::models::algebraic::{
11-
ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesSubmatrix,
12-
SparseMatrixCompression, BMF,
11+
ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesMatrixAugmentation,
12+
ConsecutiveOnesSubmatrix, SparseMatrixCompression, BMF,
1313
};
1414
use problemreductions::models::formula::Quantifier;
1515
use problemreductions::models::graph::{
@@ -693,6 +693,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
693693
"ConsecutiveBlockMinimization" => {
694694
"--matrix '[[true,false,true],[false,true,true]]' --bound 2"
695695
}
696+
"ConsecutiveOnesMatrixAugmentation" => {
697+
"--matrix \"1,0,0,1,1;1,1,0,0,0;0,1,1,0,1;0,0,1,1,0\" --bound 2"
698+
}
696699
"SparseMatrixCompression" => {
697700
"--matrix \"1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0\" --bound 2"
698701
}
@@ -739,6 +742,7 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String {
739742
("PrimeAttributeName", "dependencies") => return "deps".to_string(),
740743
("PrimeAttributeName", "query_attribute") => return "query".to_string(),
741744
("MixedChinesePostman", "arc_weights") => return "arc-costs".to_string(),
745+
("ConsecutiveOnesMatrixAugmentation", "bound") => return "bound".to_string(),
742746
("ConsecutiveOnesSubmatrix", "bound") => return "bound".to_string(),
743747
("SparseMatrixCompression", "bound_k") => return "bound".to_string(),
744748
("StackerCrane", "edges") => return "graph".to_string(),
@@ -824,6 +828,9 @@ fn help_flag_hint(
824828
("PathConstrainedNetworkFlow", "paths") => {
825829
"semicolon-separated arc-index paths: \"0,2,5,8;1,4,7,9\""
826830
}
831+
("ConsecutiveOnesMatrixAugmentation", "matrix") => {
832+
"semicolon-separated 0/1 rows: \"1,0;0,1\""
833+
}
827834
("ConsecutiveOnesSubmatrix", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"",
828835
("SparseMatrixCompression", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"",
829836
("TimetableDesign", "craftsman_avail") | ("TimetableDesign", "task_avail") => {
@@ -2817,6 +2824,22 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
28172824
)
28182825
}
28192826

2827+
// ConsecutiveOnesMatrixAugmentation
2828+
"ConsecutiveOnesMatrixAugmentation" => {
2829+
let matrix = parse_bool_matrix(args)?;
2830+
let bound = args.bound.ok_or_else(|| {
2831+
anyhow::anyhow!(
2832+
"ConsecutiveOnesMatrixAugmentation requires --matrix and --bound\n\n\
2833+
Usage: pred create ConsecutiveOnesMatrixAugmentation --matrix \"1,0,0,1,1;1,1,0,0,0;0,1,1,0,1;0,0,1,1,0\" --bound 2"
2834+
)
2835+
})?;
2836+
(
2837+
ser(ConsecutiveOnesMatrixAugmentation::try_new(matrix, bound)
2838+
.map_err(|e| anyhow::anyhow!(e))?)?,
2839+
resolved_variant.clone(),
2840+
)
2841+
}
2842+
28202843
// SparseMatrixCompression
28212844
"SparseMatrixCompression" => {
28222845
let matrix = parse_bool_matrix(args)?;
@@ -7994,4 +8017,80 @@ mod tests {
79948017
let err = create(&args, &out).unwrap_err().to_string();
79958018
assert!(err.contains("bound >= 1"));
79968019
}
8020+
8021+
#[test]
8022+
fn test_create_consecutive_ones_matrix_augmentation_json() {
8023+
use crate::dispatch::ProblemJsonOutput;
8024+
8025+
let mut args = empty_args();
8026+
args.problem = Some("ConsecutiveOnesMatrixAugmentation".to_string());
8027+
args.matrix = Some("1,0,0,1,1;1,1,0,0,0;0,1,1,0,1;0,0,1,1,0".to_string());
8028+
args.bound = Some(2);
8029+
8030+
let output_path =
8031+
std::env::temp_dir().join(format!("coma-create-{}.json", std::process::id()));
8032+
let out = OutputConfig {
8033+
output: Some(output_path.clone()),
8034+
quiet: true,
8035+
json: false,
8036+
auto_json: false,
8037+
};
8038+
8039+
create(&args, &out).unwrap();
8040+
8041+
let json = std::fs::read_to_string(&output_path).unwrap();
8042+
let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap();
8043+
assert_eq!(created.problem_type, "ConsecutiveOnesMatrixAugmentation");
8044+
assert!(created.variant.is_empty());
8045+
assert_eq!(
8046+
created.data,
8047+
serde_json::json!({
8048+
"matrix": [
8049+
[true, false, false, true, true],
8050+
[true, true, false, false, false],
8051+
[false, true, true, false, true],
8052+
[false, false, true, true, false],
8053+
],
8054+
"bound": 2,
8055+
})
8056+
);
8057+
8058+
let _ = std::fs::remove_file(output_path);
8059+
}
8060+
8061+
#[test]
8062+
fn test_create_consecutive_ones_matrix_augmentation_requires_bound() {
8063+
let mut args = empty_args();
8064+
args.problem = Some("ConsecutiveOnesMatrixAugmentation".to_string());
8065+
args.matrix = Some("1,0;0,1".to_string());
8066+
8067+
let out = OutputConfig {
8068+
output: None,
8069+
quiet: true,
8070+
json: false,
8071+
auto_json: false,
8072+
};
8073+
8074+
let err = create(&args, &out).unwrap_err().to_string();
8075+
assert!(err.contains("ConsecutiveOnesMatrixAugmentation requires --matrix and --bound"));
8076+
assert!(err.contains("Usage: pred create ConsecutiveOnesMatrixAugmentation"));
8077+
}
8078+
8079+
#[test]
8080+
fn test_create_consecutive_ones_matrix_augmentation_negative_bound() {
8081+
let mut args = empty_args();
8082+
args.problem = Some("ConsecutiveOnesMatrixAugmentation".to_string());
8083+
args.matrix = Some("1,0;0,1".to_string());
8084+
args.bound = Some(-1);
8085+
8086+
let out = OutputConfig {
8087+
output: None,
8088+
quiet: true,
8089+
json: false,
8090+
auto_json: false,
8091+
};
8092+
8093+
let err = create(&args, &out).unwrap_err().to_string();
8094+
assert!(err.contains("nonnegative"));
8095+
}
79978096
}

src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@ pub mod variant;
4242
/// Prelude module for convenient imports.
4343
pub mod prelude {
4444
// Problem types
45-
pub use crate::models::algebraic::{QuadraticAssignment, SparseMatrixCompression, BMF, QUBO};
45+
pub use crate::models::algebraic::{
46+
ConsecutiveOnesMatrixAugmentation, QuadraticAssignment, SparseMatrixCompression, BMF, QUBO,
47+
};
4648
pub use crate::models::formula::{
4749
CNFClause, CircuitSAT, KSatisfiability, NAESatisfiability, QuantifiedBooleanFormulas,
4850
Satisfiability,
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//! Consecutive Ones Matrix Augmentation problem implementation.
2+
//!
3+
//! Given an m x n binary matrix A and a nonnegative integer K, determine
4+
//! whether there exists a permutation of the columns and at most K zero-to-one
5+
//! augmentations such that every row has consecutive 1s.
6+
7+
use crate::registry::{FieldInfo, ProblemSchemaEntry};
8+
use crate::traits::{Problem, SatisfactionProblem};
9+
use serde::{Deserialize, Serialize};
10+
11+
inventory::submit! {
12+
ProblemSchemaEntry {
13+
name: "ConsecutiveOnesMatrixAugmentation",
14+
display_name: "Consecutive Ones Matrix Augmentation",
15+
aliases: &[],
16+
dimensions: &[],
17+
module_path: module_path!(),
18+
description: "Augment a binary matrix with at most K zero-to-one flips so some column permutation has the consecutive ones property",
19+
fields: &[
20+
FieldInfo { name: "matrix", type_name: "Vec<Vec<bool>>", description: "m x n binary matrix A" },
21+
FieldInfo { name: "bound", type_name: "i64", description: "Upper bound K on zero-to-one augmentations" },
22+
],
23+
}
24+
}
25+
26+
#[derive(Debug, Clone, Serialize, Deserialize)]
27+
pub struct ConsecutiveOnesMatrixAugmentation {
28+
matrix: Vec<Vec<bool>>,
29+
bound: i64,
30+
}
31+
32+
impl ConsecutiveOnesMatrixAugmentation {
33+
pub fn new(matrix: Vec<Vec<bool>>, bound: i64) -> Self {
34+
Self::try_new(matrix, bound).unwrap_or_else(|err| panic!("{err}"))
35+
}
36+
37+
pub fn try_new(matrix: Vec<Vec<bool>>, bound: i64) -> Result<Self, String> {
38+
let num_cols = matrix.first().map_or(0, Vec::len);
39+
if matrix.iter().any(|row| row.len() != num_cols) {
40+
return Err("all matrix rows must have the same length".to_string());
41+
}
42+
if bound < 0 {
43+
return Err("bound must be nonnegative".to_string());
44+
}
45+
Ok(Self { matrix, bound })
46+
}
47+
48+
pub fn matrix(&self) -> &[Vec<bool>] {
49+
&self.matrix
50+
}
51+
52+
pub fn bound(&self) -> i64 {
53+
self.bound
54+
}
55+
56+
pub fn num_rows(&self) -> usize {
57+
self.matrix.len()
58+
}
59+
60+
pub fn num_cols(&self) -> usize {
61+
self.matrix.first().map_or(0, Vec::len)
62+
}
63+
64+
fn validate_permutation(&self, config: &[usize]) -> bool {
65+
if config.len() != self.num_cols() {
66+
return false;
67+
}
68+
69+
let mut seen = vec![false; self.num_cols()];
70+
for &col in config {
71+
if col >= self.num_cols() || seen[col] {
72+
return false;
73+
}
74+
seen[col] = true;
75+
}
76+
true
77+
}
78+
79+
fn row_augmentation_cost(row: &[bool], config: &[usize]) -> usize {
80+
let mut first_one = None;
81+
let mut last_one = None;
82+
let mut one_count = 0usize;
83+
84+
for (position, &col) in config.iter().enumerate() {
85+
if row[col] {
86+
first_one.get_or_insert(position);
87+
last_one = Some(position);
88+
one_count += 1;
89+
}
90+
}
91+
92+
match (first_one, last_one) {
93+
(Some(first), Some(last)) => last - first + 1 - one_count,
94+
_ => 0,
95+
}
96+
}
97+
98+
fn total_augmentation_cost(&self, config: &[usize]) -> Option<usize> {
99+
if !self.validate_permutation(config) {
100+
return None;
101+
}
102+
103+
let mut total = 0usize;
104+
for row in &self.matrix {
105+
total += Self::row_augmentation_cost(row, config);
106+
if total > self.bound as usize {
107+
return Some(total);
108+
}
109+
}
110+
111+
Some(total)
112+
}
113+
}
114+
115+
impl Problem for ConsecutiveOnesMatrixAugmentation {
116+
const NAME: &'static str = "ConsecutiveOnesMatrixAugmentation";
117+
type Metric = bool;
118+
119+
fn dims(&self) -> Vec<usize> {
120+
vec![self.num_cols(); self.num_cols()]
121+
}
122+
123+
fn evaluate(&self, config: &[usize]) -> bool {
124+
self.total_augmentation_cost(config)
125+
.is_some_and(|cost| cost <= self.bound as usize)
126+
}
127+
128+
fn variant() -> Vec<(&'static str, &'static str)> {
129+
crate::variant_params![]
130+
}
131+
132+
fn num_variables(&self) -> usize {
133+
self.num_cols()
134+
}
135+
}
136+
137+
impl SatisfactionProblem for ConsecutiveOnesMatrixAugmentation {}
138+
139+
crate::declare_variants! {
140+
default sat ConsecutiveOnesMatrixAugmentation => "factorial(num_cols) * num_rows * num_cols",
141+
}
142+
143+
#[cfg(feature = "example-db")]
144+
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
145+
vec![crate::example_db::specs::ModelExampleSpec {
146+
id: "consecutive_ones_matrix_augmentation",
147+
instance: Box::new(ConsecutiveOnesMatrixAugmentation::new(
148+
vec![
149+
vec![true, false, false, true, true],
150+
vec![true, true, false, false, false],
151+
vec![false, true, true, false, true],
152+
vec![false, false, true, true, false],
153+
],
154+
2,
155+
)),
156+
optimal_config: vec![0, 1, 4, 2, 3],
157+
optimal_value: serde_json::json!(true),
158+
}]
159+
}
160+
161+
#[cfg(test)]
162+
#[path = "../../unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs"]
163+
mod tests;

0 commit comments

Comments
 (0)