Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@
"ConsecutiveBlockMinimization": [Consecutive Block Minimization],
"ConsecutiveOnesMatrixAugmentation": [Consecutive Ones Matrix Augmentation],
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
"FeasibleBasisExtension": [Feasible Basis Extension],
"SparseMatrixCompression": [Sparse Matrix Compression],
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
"IntegralFlowHomologousArcs": [Integral Flow with Homologous Arcs],
Expand Down Expand Up @@ -6607,6 +6608,101 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("FeasibleBasisExtension")
let A = x.instance.matrix
let m = A.len()
let n = A.at(0).len()
let rhs = x.instance.rhs
let S = x.instance.required_columns
let cfg = x.optimal_config
// Free column indices (those not in S)
let free-cols = range(n).filter(j => j not in S)
// Selected free columns from config
let selected = cfg.enumerate().filter(((i, v)) => v == 1).map(((i, v)) => free-cols.at(i))
// Full basis: required + selected
let basis = S + selected
[
#problem-def("FeasibleBasisExtension")[
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).
][
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.]

*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.

#pred-commands(
"pred create --example " + problem-spec(x) + " -o feasible-basis-extension.json",
"pred solve feasible-basis-extension.json --solver brute-force",
"pred evaluate feasible-basis-extension.json --config " + x.optimal_config.map(str).join(","),
)

#figure(
canvas(length: 0.7cm, {
import draw: *
let cell-size = 0.9
let gap = 0.15
// Draw the matrix
for i in range(m) {
for j in range(n) {
let val = A.at(i).at(j)
let is-basis = j in basis
let is-required = j in S
let f = if is-required {
graph-colors.at(1).transparentize(50%)
} else if is-basis {
graph-colors.at(0).transparentize(30%)
} else {
white
}
rect(
(j * cell-size, -i * cell-size),
(j * cell-size + cell-size - gap, -i * cell-size - cell-size + gap),
fill: f,
stroke: 0.3pt + luma(180),
)
content(
(j * cell-size + (cell-size - gap) / 2, -i * cell-size - (cell-size - gap) / 2),
text(8pt, str(val)),
)
}
}
// Column labels
for j in range(n) {
let label-color = if j in S { graph-colors.at(1) } else if j in basis { graph-colors.at(0) } else { black }
content(
(j * cell-size + (cell-size - gap) / 2, 0.4),
text(7pt, fill: label-color)[$c_#j$],
)
}
// Row labels
for i in range(m) {
content(
(-0.5, -i * cell-size - (cell-size - gap) / 2),
text(7pt)[$r_#(i + 1)$],
)
}
// RHS vector
let rhs-x = n * cell-size + 0.6
content((rhs-x + (cell-size - gap) / 2, 0.4), text(7pt, weight: "bold")[$overline(a)$])
for i in range(m) {
rect(
(rhs-x, -i * cell-size),
(rhs-x + cell-size - gap, -i * cell-size - cell-size + gap),
fill: luma(240),
stroke: 0.3pt + luma(180),
)
content(
(rhs-x + (cell-size - gap) / 2, -i * cell-size - (cell-size - gap) / 2),
text(8pt, str(rhs.at(i))),
)
}
}),
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.],
) <fig:feasible-basis-extension>
]
]
}

// Completeness check: warn about problem types in JSON but missing from paper
#{
let json-models = {
Expand Down
10 changes: 10 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -1454,3 +1454,13 @@ @techreport{plaisted1976
number = {STAN-CS-76-583},
year = {1976}
}

@article{Murty1972,
author = {Murty, Katta G.},
title = {A fundamental problem in linear inequalities with applications to the travelling salesman problem},
journal = {Mathematical Programming},
year = {1972},
volume = {3},
pages = {326--370},
doi = {10.1007/BF01584550}
}
7 changes: 7 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ Flags by problem type:
ConsecutiveOnesMatrixAugmentation --matrix (0/1), --bound
ConsecutiveOnesSubmatrix --matrix (0/1), --k
SparseMatrixCompression --matrix (0/1), --bound
FeasibleBasisExtension --matrix (JSON 2D i64), --rhs, --required-columns
SteinerTree --graph, --edge-weights, --terminals
MultipleCopyFileAllocation --graph, --usage, --storage
AcyclicPartition --arcs [--weights] [--arc-costs] --weight-bound --cost-bound [--num-vertices]
Expand Down Expand Up @@ -734,6 +735,12 @@ pub struct CreateArgs {
/// Query attribute index for PrimeAttributeName
#[arg(long)]
pub query: Option<usize>,
/// Right-hand side vector for FeasibleBasisExtension (comma-separated, e.g., "7,5,3")
#[arg(long)]
pub rhs: Option<String>,
/// Required column indices for FeasibleBasisExtension (comma-separated, e.g., "0,1")
#[arg(long)]
pub required_columns: Option<String>,
/// Number of groups for SumOfSquaresPartition
#[arg(long)]
pub num_groups: Option<usize>,
Expand Down
59 changes: 58 additions & 1 deletion problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use anyhow::{bail, Context, Result};
use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample};
use problemreductions::models::algebraic::{
ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesMatrixAugmentation,
ConsecutiveOnesSubmatrix, SparseMatrixCompression, BMF,
ConsecutiveOnesSubmatrix, FeasibleBasisExtension, SparseMatrixCompression, BMF,
};
use problemreductions::models::formula::Quantifier;
use problemreductions::models::graph::{
Expand Down Expand Up @@ -193,6 +193,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.conjuncts_spec.is_none()
&& args.deps.is_none()
&& args.query.is_none()
&& args.rhs.is_none()
&& args.required_columns.is_none()
}

fn emit_problem_output(output: &ProblemJsonOutput, out: &OutputConfig) -> Result<()> {
Expand Down Expand Up @@ -766,6 +768,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"StringToStringCorrection" => {
"--source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2"
}
"FeasibleBasisExtension" => {
"--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'"
}
_ => "",
}
}
Expand Down Expand Up @@ -893,6 +898,9 @@ fn help_flag_hint(
}
("ConsecutiveOnesSubmatrix", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"",
("SparseMatrixCompression", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"",
("FeasibleBasisExtension", "matrix") => "JSON 2D integer array: '[[1,0,1],[0,1,0]]'",
("FeasibleBasisExtension", "rhs") => "comma-separated integers: \"7,5,3\"",
("FeasibleBasisExtension", "required_columns") => "comma-separated column indices: \"0,1\"",
("TimetableDesign", "craftsman_avail") | ("TimetableDesign", "task_avail") => {
"semicolon-separated 0/1 rows: \"1,1,0;0,1,1\""
}
Expand Down Expand Up @@ -2831,6 +2839,53 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// FeasibleBasisExtension
"FeasibleBasisExtension" => {
let usage = "Usage: pred create FeasibleBasisExtension --matrix '[[1,0,1],[0,1,0]]' --rhs '7,5' --required-columns '0'";
let matrix_str = args.matrix.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"FeasibleBasisExtension requires --matrix (JSON 2D i64 array), --rhs, and --required-columns\n\n{usage}"
)
})?;
let matrix: Vec<Vec<i64>> = serde_json::from_str(matrix_str).map_err(|err| {
anyhow::anyhow!(
"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}"
)
})?;
let rhs_str = args.rhs.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"FeasibleBasisExtension requires --rhs (comma-separated integers)\n\n{usage}"
)
})?;
let rhs: Vec<i64> = rhs_str
.split(',')
.map(|s| s.trim().parse::<i64>())
.collect::<Result<Vec<_>, _>>()
.map_err(|err| {
anyhow::anyhow!(
"Failed to parse --rhs as comma-separated integers: {err}\n\n{usage}"
)
})?;
let required_str = args.required_columns.as_deref().unwrap_or("");
let required_columns: Vec<usize> = if required_str.is_empty() {
vec![]
} else {
required_str
.split(',')
.map(|s| s.trim().parse::<usize>())
.collect::<Result<Vec<_>, _>>()
.map_err(|err| {
anyhow::anyhow!(
"Failed to parse --required-columns as comma-separated indices: {err}\n\n{usage}"
)
})?
};
(
ser(FeasibleBasisExtension::new(matrix, rhs, required_columns))?,
resolved_variant.clone(),
)
}

// LongestCommonSubsequence
"LongestCommonSubsequence" => {
let usage =
Expand Down Expand Up @@ -7612,6 +7667,8 @@ mod tests {
storage: None,
quantifiers: None,
homologous_pairs: None,
rhs: None,
required_columns: None,
}
}

Expand Down
Loading
Loading