Skip to content

Commit 6a4f5c7

Browse files
isPANNclaude
andauthored
feat: add FeasibleBasisExtension model (#530) (#818)
* feat: add FeasibleBasisExtension model (closes #530) Add the Feasible Basis Extension problem: given m x n integer matrix A, vector a_bar, and required columns S, determine if a feasible basis extending S exists. Uses exact rational arithmetic (Bareiss + rational back-substitution) to avoid floating-point issues. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix paper citation, Bareiss overflow, and solve command - Add missing Murty1972 BibTeX entry (fixes make paper failure) - Correct paper: "3-SAT" → "Hamiltonian Circuit", @murty1980 → @Murty1972 - Widen Bareiss elimination to i128 to prevent overflow on large coefficients - Fix pred solve command to use --solver brute-force (no ILP path exists) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 006d468 commit 6a4f5c7

File tree

8 files changed

+744
-2
lines changed

8 files changed

+744
-2
lines changed

docs/paper/reductions.typ

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@
154154
"ConsecutiveBlockMinimization": [Consecutive Block Minimization],
155155
"ConsecutiveOnesMatrixAugmentation": [Consecutive Ones Matrix Augmentation],
156156
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
157+
"FeasibleBasisExtension": [Feasible Basis Extension],
157158
"SparseMatrixCompression": [Sparse Matrix Compression],
158159
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
159160
"IntegralFlowHomologousArcs": [Integral Flow with Homologous Arcs],
@@ -6607,6 +6608,101 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
66076608
]
66086609
}
66096610

6611+
#{
6612+
let x = load-model-example("FeasibleBasisExtension")
6613+
let A = x.instance.matrix
6614+
let m = A.len()
6615+
let n = A.at(0).len()
6616+
let rhs = x.instance.rhs
6617+
let S = x.instance.required_columns
6618+
let cfg = x.optimal_config
6619+
// Free column indices (those not in S)
6620+
let free-cols = range(n).filter(j => j not in S)
6621+
// Selected free columns from config
6622+
let selected = cfg.enumerate().filter(((i, v)) => v == 1).map(((i, v)) => free-cols.at(i))
6623+
// Full basis: required + selected
6624+
let basis = S + selected
6625+
[
6626+
#problem-def("FeasibleBasisExtension")[
6627+
Given an $m times n$ integer matrix $A$ with $m < n$, a column vector $overline(a) in bb(Z)^m$, and a subset $S$ of column indices with $|S| < m$, determine whether there exists a _feasible basis_ $B$ --- a set of $m$ column indices including $S$ --- such that the $m times m$ submatrix $A_B$ is nonsingular and $A_B^(-1) overline(a) >= 0$ (componentwise).
6628+
][
6629+
The Feasible Basis Extension problem arises in linear programming theory and the study of simplex method pivoting rules. It was shown NP-complete by Murty @Murty1972 via a reduction from Hamiltonian Circuit, establishing that determining whether a partial basis can be extended to a feasible one is computationally intractable in general. The problem is closely related to the question of whether a given linear program has a feasible basic solution containing specified variables. The best known exact algorithm is brute-force enumeration of all $binom(n - |S|, m - |S|)$ candidate extensions, testing each for nonsingularity and non-negativity of the solution in $O(m^3)$ time.#footnote[No algorithm improving on brute-force enumeration is known for the general Feasible Basis Extension problem.]
6630+
6631+
*Example.* Consider the $#m times #n$ matrix $A = mat(#A.map(row => row.map(v => str(v)).join(", ")).join("; "))$ with $overline(a) = (#rhs.map(str).join(", "))^top$ and required columns $S = \{#S.map(str).join(", ")\}$. We need $#(m - S.len())$ additional column from the free set $\{#free-cols.map(str).join(", ")\}$. Selecting column #selected.at(0) gives basis $B = \{#basis.map(str).join(", ")\}$, which yields $A_B^(-1) overline(a) = (4, 5, 3)^top >= 0$. Column 4 makes $A_B$ singular, and column 5 produces a negative component.
6632+
6633+
#pred-commands(
6634+
"pred create --example " + problem-spec(x) + " -o feasible-basis-extension.json",
6635+
"pred solve feasible-basis-extension.json --solver brute-force",
6636+
"pred evaluate feasible-basis-extension.json --config " + x.optimal_config.map(str).join(","),
6637+
)
6638+
6639+
#figure(
6640+
canvas(length: 0.7cm, {
6641+
import draw: *
6642+
let cell-size = 0.9
6643+
let gap = 0.15
6644+
// Draw the matrix
6645+
for i in range(m) {
6646+
for j in range(n) {
6647+
let val = A.at(i).at(j)
6648+
let is-basis = j in basis
6649+
let is-required = j in S
6650+
let f = if is-required {
6651+
graph-colors.at(1).transparentize(50%)
6652+
} else if is-basis {
6653+
graph-colors.at(0).transparentize(30%)
6654+
} else {
6655+
white
6656+
}
6657+
rect(
6658+
(j * cell-size, -i * cell-size),
6659+
(j * cell-size + cell-size - gap, -i * cell-size - cell-size + gap),
6660+
fill: f,
6661+
stroke: 0.3pt + luma(180),
6662+
)
6663+
content(
6664+
(j * cell-size + (cell-size - gap) / 2, -i * cell-size - (cell-size - gap) / 2),
6665+
text(8pt, str(val)),
6666+
)
6667+
}
6668+
}
6669+
// Column labels
6670+
for j in range(n) {
6671+
let label-color = if j in S { graph-colors.at(1) } else if j in basis { graph-colors.at(0) } else { black }
6672+
content(
6673+
(j * cell-size + (cell-size - gap) / 2, 0.4),
6674+
text(7pt, fill: label-color)[$c_#j$],
6675+
)
6676+
}
6677+
// Row labels
6678+
for i in range(m) {
6679+
content(
6680+
(-0.5, -i * cell-size - (cell-size - gap) / 2),
6681+
text(7pt)[$r_#(i + 1)$],
6682+
)
6683+
}
6684+
// RHS vector
6685+
let rhs-x = n * cell-size + 0.6
6686+
content((rhs-x + (cell-size - gap) / 2, 0.4), text(7pt, weight: "bold")[$overline(a)$])
6687+
for i in range(m) {
6688+
rect(
6689+
(rhs-x, -i * cell-size),
6690+
(rhs-x + cell-size - gap, -i * cell-size - cell-size + gap),
6691+
fill: luma(240),
6692+
stroke: 0.3pt + luma(180),
6693+
)
6694+
content(
6695+
(rhs-x + (cell-size - gap) / 2, -i * cell-size - (cell-size - gap) / 2),
6696+
text(8pt, str(rhs.at(i))),
6697+
)
6698+
}
6699+
}),
6700+
caption: [Feasible Basis Extension instance ($#m times #n$). Orange columns are required ($S = \{#S.map(str).join(", ")\}$), blue column is the selected extension. Together they form a nonsingular basis with non-negative solution.],
6701+
) <fig:feasible-basis-extension>
6702+
]
6703+
]
6704+
}
6705+
66106706
// Completeness check: warn about problem types in JSON but missing from paper
66116707
#{
66126708
let json-models = {

docs/paper/references.bib

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1454,3 +1454,13 @@ @techreport{plaisted1976
14541454
number = {STAN-CS-76-583},
14551455
year = {1976}
14561456
}
1457+
1458+
@article{Murty1972,
1459+
author = {Murty, Katta G.},
1460+
title = {A fundamental problem in linear inequalities with applications to the travelling salesman problem},
1461+
journal = {Mathematical Programming},
1462+
year = {1972},
1463+
volume = {3},
1464+
pages = {326--370},
1465+
doi = {10.1007/BF01584550}
1466+
}

problemreductions-cli/src/cli.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ Flags by problem type:
272272
ConsecutiveOnesMatrixAugmentation --matrix (0/1), --bound
273273
ConsecutiveOnesSubmatrix --matrix (0/1), --k
274274
SparseMatrixCompression --matrix (0/1), --bound
275+
FeasibleBasisExtension --matrix (JSON 2D i64), --rhs, --required-columns
275276
SteinerTree --graph, --edge-weights, --terminals
276277
MultipleCopyFileAllocation --graph, --usage, --storage
277278
AcyclicPartition --arcs [--weights] [--arc-costs] --weight-bound --cost-bound [--num-vertices]
@@ -734,6 +735,12 @@ pub struct CreateArgs {
734735
/// Query attribute index for PrimeAttributeName
735736
#[arg(long)]
736737
pub query: Option<usize>,
738+
/// Right-hand side vector for FeasibleBasisExtension (comma-separated, e.g., "7,5,3")
739+
#[arg(long)]
740+
pub rhs: Option<String>,
741+
/// Required column indices for FeasibleBasisExtension (comma-separated, e.g., "0,1")
742+
#[arg(long)]
743+
pub required_columns: Option<String>,
737744
/// Number of groups for SumOfSquaresPartition
738745
#[arg(long)]
739746
pub num_groups: Option<usize>,

problemreductions-cli/src/commands/create.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use anyhow::{bail, Context, Result};
99
use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample};
1010
use problemreductions::models::algebraic::{
1111
ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesMatrixAugmentation,
12-
ConsecutiveOnesSubmatrix, SparseMatrixCompression, BMF,
12+
ConsecutiveOnesSubmatrix, FeasibleBasisExtension, SparseMatrixCompression, BMF,
1313
};
1414
use problemreductions::models::formula::Quantifier;
1515
use problemreductions::models::graph::{
@@ -193,6 +193,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
193193
&& args.conjuncts_spec.is_none()
194194
&& args.deps.is_none()
195195
&& args.query.is_none()
196+
&& args.rhs.is_none()
197+
&& args.required_columns.is_none()
196198
}
197199

198200
fn emit_problem_output(output: &ProblemJsonOutput, out: &OutputConfig) -> Result<()> {
@@ -766,6 +768,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
766768
"StringToStringCorrection" => {
767769
"--source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2"
768770
}
771+
"FeasibleBasisExtension" => {
772+
"--matrix '[[1,0,1,2,-1,0],[0,1,0,1,1,2],[0,0,1,1,0,1]]' --rhs '7,5,3' --required-columns '0,1'"
773+
}
769774
_ => "",
770775
}
771776
}
@@ -893,6 +898,9 @@ fn help_flag_hint(
893898
}
894899
("ConsecutiveOnesSubmatrix", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"",
895900
("SparseMatrixCompression", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"",
901+
("FeasibleBasisExtension", "matrix") => "JSON 2D integer array: '[[1,0,1],[0,1,0]]'",
902+
("FeasibleBasisExtension", "rhs") => "comma-separated integers: \"7,5,3\"",
903+
("FeasibleBasisExtension", "required_columns") => "comma-separated column indices: \"0,1\"",
896904
("TimetableDesign", "craftsman_avail") | ("TimetableDesign", "task_avail") => {
897905
"semicolon-separated 0/1 rows: \"1,1,0;0,1,1\""
898906
}
@@ -2831,6 +2839,53 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
28312839
)
28322840
}
28332841

2842+
// FeasibleBasisExtension
2843+
"FeasibleBasisExtension" => {
2844+
let usage = "Usage: pred create FeasibleBasisExtension --matrix '[[1,0,1],[0,1,0]]' --rhs '7,5' --required-columns '0'";
2845+
let matrix_str = args.matrix.as_deref().ok_or_else(|| {
2846+
anyhow::anyhow!(
2847+
"FeasibleBasisExtension requires --matrix (JSON 2D i64 array), --rhs, and --required-columns\n\n{usage}"
2848+
)
2849+
})?;
2850+
let matrix: Vec<Vec<i64>> = serde_json::from_str(matrix_str).map_err(|err| {
2851+
anyhow::anyhow!(
2852+
"FeasibleBasisExtension requires --matrix as a JSON 2D integer array (e.g., '[[1,0,1],[0,1,0]]')\n\n{usage}\n\nFailed to parse --matrix: {err}"
2853+
)
2854+
})?;
2855+
let rhs_str = args.rhs.as_deref().ok_or_else(|| {
2856+
anyhow::anyhow!(
2857+
"FeasibleBasisExtension requires --rhs (comma-separated integers)\n\n{usage}"
2858+
)
2859+
})?;
2860+
let rhs: Vec<i64> = rhs_str
2861+
.split(',')
2862+
.map(|s| s.trim().parse::<i64>())
2863+
.collect::<Result<Vec<_>, _>>()
2864+
.map_err(|err| {
2865+
anyhow::anyhow!(
2866+
"Failed to parse --rhs as comma-separated integers: {err}\n\n{usage}"
2867+
)
2868+
})?;
2869+
let required_str = args.required_columns.as_deref().unwrap_or("");
2870+
let required_columns: Vec<usize> = if required_str.is_empty() {
2871+
vec![]
2872+
} else {
2873+
required_str
2874+
.split(',')
2875+
.map(|s| s.trim().parse::<usize>())
2876+
.collect::<Result<Vec<_>, _>>()
2877+
.map_err(|err| {
2878+
anyhow::anyhow!(
2879+
"Failed to parse --required-columns as comma-separated indices: {err}\n\n{usage}"
2880+
)
2881+
})?
2882+
};
2883+
(
2884+
ser(FeasibleBasisExtension::new(matrix, rhs, required_columns))?,
2885+
resolved_variant.clone(),
2886+
)
2887+
}
2888+
28342889
// LongestCommonSubsequence
28352890
"LongestCommonSubsequence" => {
28362891
let usage =
@@ -7612,6 +7667,8 @@ mod tests {
76127667
storage: None,
76137668
quantifiers: None,
76147669
homologous_pairs: None,
7670+
rhs: None,
7671+
required_columns: None,
76157672
}
76167673
}
76177674

0 commit comments

Comments
 (0)