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
84 changes: 84 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
"ConsecutiveBlockMinimization": [Consecutive Block Minimization],
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
"IntegralFlowHomologousArcs": [Integral Flow with Homologous Arcs],
"IntegralFlowWithMultipliers": [Integral Flow With Multipliers],
"MinMaxMulticenter": [Min-Max Multicenter],
"FlowShopScheduling": [Flow Shop Scheduling],
Expand Down Expand Up @@ -5205,6 +5206,89 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("IntegralFlowHomologousArcs")
let arcs = x.instance.graph.arcs
let sol = x.optimal_config
let source = x.instance.source
let sink = x.instance.sink
let requirement = x.instance.requirement
[
#problem-def("IntegralFlowHomologousArcs")[
Given a directed graph $G = (V, A)$ with source $s in V$, sink $t in V$, arc capacities $c: A -> ZZ^+$, requirement $R in ZZ^+$, and a set $H subset.eq A times A$ of homologous arc pairs, determine whether there exists an integral flow function $f: A -> ZZ_(>= 0)$ such that $f(a) <= c(a)$ for every $a in A$, flow is conserved at every vertex in $V backslash {s, t}$, $f(a) = f(a')$ for every $(a, a') in H$, and the net flow into $t$ is at least $R$.
][
Integral Flow with Homologous Arcs is the single-commodity equality-constrained flow problem listed as ND35 in Garey & Johnson @garey1979. Their catalog records the NP-completeness result attributed to Sahni and notes that the unit-capacity restriction remains hard, while the corresponding non-integral relaxation is polynomial-time equivalent to linear programming @garey1979.

The implementation uses one integer variable per arc, so exhaustive search over the induced configuration space runs in $O((C + 1)^m)$ for $m = |A|$ and $C = max_(a in A) c(a)$#footnote[This is the exact search bound induced by the implemented per-arc domains $f(a) in {0, dots, c(a)}$. In the unit-capacity special case, it simplifies to $O(2^m)$.].

*Example.* The canonical fixture instance has source $s = v_#source$, sink $t = v_#sink$, unit capacities on all eight arcs, requirement $R = #requirement$, and homologous pairs $(a_2, a_5)$ and $(a_4, a_3)$. The stored satisfying configuration routes one unit along $0 -> 1 -> 3 -> 5$ and one unit along $0 -> 2 -> 4 -> 5$. Thus the paired arcs $(1,3)$ and $(2,4)$ both carry 1, while $(1,4)$ and $(2,3)$ both carry 0. Every nonterminal vertex has equal inflow and outflow, and the sink receives two units of flow, so the verifier returns true.

#pred-commands(
"pred create --example " + problem-spec(x) + " -o integral-flow-homologous-arcs.json",
"pred solve integral-flow-homologous-arcs.json --solver brute-force",
"pred evaluate integral-flow-homologous-arcs.json --config " + x.optimal_config.map(str).join(","),
)

#figure(
canvas(length: 1cm, {
import draw: *
let blue = graph-colors.at(0)
let orange = rgb("#f28e2b")
let red = rgb("#e15759")
let gray = luma(185)
let positions = (
(0, 0),
(1.6, 1.1),
(1.6, -1.1),
(3.2, 1.1),
(3.2, -1.1),
(4.8, 0),
)
let labels = (
[$s = v_0$],
[$v_1$],
[$v_2$],
[$v_3$],
[$v_4$],
[$t = v_5$],
)
for (idx, (u, v)) in arcs.enumerate() {
let stroke = if idx == 3 or idx == 4 {
(paint: orange, thickness: 1.3pt, dash: "dashed")
} else if sol.at(idx) == 1 {
(paint: blue, thickness: 1.8pt)
} else {
(paint: gray, thickness: 0.7pt)
}
line(
positions.at(u),
positions.at(v),
stroke: stroke,
mark: (end: "straight", scale: 0.5),
)
}
for (i, pos) in positions.enumerate() {
let fill = if i == source { blue } else if i == sink { red } else { white }
g-node(
pos,
name: "ifha-" + str(i),
fill: fill,
label: if i == source or i == sink {
text(fill: white)[#labels.at(i)]
} else {
labels.at(i)
},
)
}
content((2.4, 1.55), text(8pt, fill: blue)[$f(a_2) = f(a_5) = 1$])
content((2.4, -1.55), text(8pt, fill: orange)[$f(a_4) = f(a_3) = 0$])
}),
caption: [Canonical YES instance for Integral Flow with Homologous Arcs. Solid blue arcs carry the satisfying integral flow; dashed orange arcs form the second homologous pair, constrained to equal zero.],
) <fig:integral-flow-homologous-arcs>
]
]
}

#problem-def("DirectedTwoCommodityIntegralFlow")[
Given a directed graph $G = (V, A)$ with arc capacities $c: A -> ZZ^+$, two source-sink pairs $(s_1, t_1)$ and $(s_2, t_2)$, and requirements $R_1, R_2 in ZZ^+$, determine whether there exist two integral flow functions $f_1, f_2: A -> ZZ_(>= 0)$ such that (1) $f_1(a) + f_2(a) <= c(a)$ for all $a in A$, (2) flow $f_i$ is conserved at every vertex except $s_1, s_2, t_1, t_2$, and (3) the net flow into $t_i$ under $f_i$ is at least $R_i$ for $i in {1, 2}$.
][
Expand Down
7 changes: 6 additions & 1 deletion problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ Flags by problem type:
LongestCircuit --graph, --edge-weights, --bound
BoundedComponentSpanningForest --graph, --weights, --k, --bound
UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
IntegralFlowHomologousArcs --arcs, --capacities, --source, --sink, --requirement, --homologous-pairs
IsomorphicSpanningTree --graph, --tree
KthBestSpanningTree --graph, --edge-weights, --k, --bound
LengthBoundedDisjointPaths --graph, --source, --sink, --num-paths-required, --bound
Expand Down Expand Up @@ -325,6 +326,7 @@ Examples:
pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5
pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1
pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1
pred create IntegralFlowHomologousArcs --arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\"
pred create X3C --universe 9 --sets \"0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8\"
pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3
pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2
Expand Down Expand Up @@ -367,7 +369,7 @@ pub struct CreateArgs {
/// Sink vertex for path-based graph problems and MinimumCutIntoBoundedSets
#[arg(long)]
pub sink: Option<usize>,
/// Required sink inflow for IntegralFlowWithMultipliers
/// Required sink inflow for IntegralFlowHomologousArcs and IntegralFlowWithMultipliers
#[arg(long)]
pub requirement: Option<u64>,
/// Required number of paths for LengthBoundedDisjointPaths
Expand Down Expand Up @@ -536,6 +538,9 @@ pub struct CreateArgs {
/// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0)
#[arg(long)]
pub arcs: Option<String>,
/// Arc-index equality constraints for IntegralFlowHomologousArcs (semicolon-separated, e.g., "2=5;4=3")
#[arg(long)]
pub homologous_pairs: Option<String>,
/// Quantifiers for QBF (comma-separated, E=Exists, A=ForAll, e.g., "E,A,E")
#[arg(long)]
pub quantifiers: Option<String>,
Expand Down
141 changes: 141 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.costs.is_none()
&& args.arc_costs.is_none()
&& args.arcs.is_none()
&& args.homologous_pairs.is_none()
&& args.quantifiers.is_none()
&& args.usage.is_none()
&& args.storage.is_none()
Expand Down Expand Up @@ -151,6 +152,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.sink_2.is_none()
&& args.requirement_1.is_none()
&& args.requirement_2.is_none()
&& args.requirement.is_none()
&& args.homologous_pairs.is_none()
&& args.num_attributes.is_none()
&& args.dependencies.is_none()
&& args.relation_attrs.is_none()
Expand Down Expand Up @@ -528,6 +531,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"UndirectedTwoCommodityIntegralFlow" => {
"--graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1"
},
"IntegralFlowHomologousArcs" => {
"--arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\""
}
"LengthBoundedDisjointPaths" => {
"--graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3"
}
Expand Down Expand Up @@ -750,6 +756,9 @@ fn help_flag_hint(
}
("ShortestCommonSupersequence", "strings") => "symbol lists: \"0,1,2;1,2,0\"",
("MultipleChoiceBranching", "partition") => "semicolon-separated groups: \"0,1;2,3\"",
("IntegralFlowHomologousArcs", "homologous_pairs") => {
"semicolon-separated arc-index equalities: \"2=5;4=3\""
}
("ConsistencyOfDatabaseFrequencyTables", "attribute_domains") => {
"comma-separated domain sizes: 2,3,2"
}
Expand Down Expand Up @@ -3197,6 +3206,83 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// IntegralFlowHomologousArcs
"IntegralFlowHomologousArcs" => {
let usage = "Usage: pred create IntegralFlowHomologousArcs --arcs \"0>1,0>2,1>3,2>3,1>4,2>4,3>5,4>5\" --capacities 1,1,1,1,1,1,1,1 --source 0 --sink 5 --requirement 2 --homologous-pairs \"2=5;4=3\"";
let arcs_str = args.arcs.as_deref().ok_or_else(|| {
anyhow::anyhow!("IntegralFlowHomologousArcs requires --arcs\n\n{usage}")
})?;
let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)
.map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
let capacities: Vec<u64> = if let Some(ref s) = args.capacities {
s.split(',')
.map(|token| {
let trimmed = token.trim();
trimmed
.parse::<u64>()
.with_context(|| format!("Invalid capacity `{trimmed}`\n\n{usage}"))
})
.collect::<Result<Vec<_>>>()?
} else {
vec![1; num_arcs]
};
anyhow::ensure!(
capacities.len() == num_arcs,
"Expected {} capacities but got {}\n\n{}",
num_arcs,
capacities.len(),
usage
);
for (arc_index, &capacity) in capacities.iter().enumerate() {
let fits = usize::try_from(capacity)
.ok()
.and_then(|value| value.checked_add(1))
.is_some();
anyhow::ensure!(
fits,
"capacity {} at arc index {} is too large for this platform\n\n{}",
capacity,
arc_index,
usage
);
}
let num_vertices = graph.num_vertices();
let source = args.source.ok_or_else(|| {
anyhow::anyhow!("IntegralFlowHomologousArcs requires --source\n\n{usage}")
})?;
let sink = args.sink.ok_or_else(|| {
anyhow::anyhow!("IntegralFlowHomologousArcs requires --sink\n\n{usage}")
})?;
let requirement = args.requirement.ok_or_else(|| {
anyhow::anyhow!("IntegralFlowHomologousArcs requires --requirement\n\n{usage}")
})?;
validate_vertex_index("source", source, num_vertices, usage)?;
validate_vertex_index("sink", sink, num_vertices, usage)?;
let homologous_pairs =
parse_homologous_pairs(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
for &(a, b) in &homologous_pairs {
anyhow::ensure!(
a < num_arcs && b < num_arcs,
"homologous pair ({}, {}) references arc >= num_arcs ({})\n\n{}",
a,
b,
num_arcs,
usage
);
}
(
ser(IntegralFlowHomologousArcs::new(
graph,
capacities,
source,
sink,
requirement,
homologous_pairs,
))?,
resolved_variant.clone(),
)
}

// MinimumFeedbackArcSet
"MinimumFeedbackArcSet" => {
let arcs_str = args.arcs.as_deref().ok_or_else(|| {
Expand Down Expand Up @@ -4416,6 +4502,35 @@ fn parse_named_sets(sets_str: Option<&str>, flag: &str) -> Result<Vec<Vec<usize>
.collect()
}

fn parse_homologous_pairs(args: &CreateArgs) -> Result<Vec<(usize, usize)>> {
let pairs = args.homologous_pairs.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"IntegralFlowHomologousArcs requires --homologous-pairs (e.g., \"2=5;4=3\")"
)
})?;

pairs
.split(';')
.filter(|entry| !entry.trim().is_empty())
.map(|entry| {
let entry = entry.trim();
let (left, right) = entry.split_once('=').ok_or_else(|| {
anyhow::anyhow!(
"Invalid homologous pair '{}': expected format u=v (e.g., 2=5)",
entry
)
})?;
let left = left.trim().parse::<usize>().with_context(|| {
format!("Invalid homologous pair '{}': expected format u=v", entry)
})?;
let right = right.trim().parse::<usize>().with_context(|| {
format!("Invalid homologous pair '{}': expected format u=v", entry)
})?;
Ok((left, right))
})
.collect()
}

/// Parse a dependency string as semicolon-separated `lhs>rhs` pairs.
/// E.g., "0,1>2,3;2,3>0,1"
fn parse_deps(s: &str) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
Expand Down Expand Up @@ -6067,6 +6182,7 @@ mod tests {
usage: None,
storage: None,
quantifiers: None,
homologous_pairs: None,
}
}

Expand All @@ -6084,6 +6200,13 @@ mod tests {
assert!(!all_data_flags_empty(&args));
}

#[test]
fn test_all_data_flags_empty_treats_homologous_pairs_as_input() {
let mut args = empty_args();
args.homologous_pairs = Some("2=5;4=3".to_string());
assert!(!all_data_flags_empty(&args));
}

#[test]
fn test_parse_potential_edges() {
let mut args = empty_args();
Expand Down Expand Up @@ -6112,6 +6235,24 @@ mod tests {
assert_eq!(parse_budget(&args).unwrap(), 7);
}

#[test]
fn test_parse_homologous_pairs() {
let mut args = empty_args();
args.homologous_pairs = Some("2=5;4=3".to_string());

assert_eq!(parse_homologous_pairs(&args).unwrap(), vec![(2, 5), (4, 3)]);
}

#[test]
fn test_parse_homologous_pairs_rejects_invalid_token() {
let mut args = empty_args();
args.homologous_pairs = Some("2-5".to_string());

let err = parse_homologous_pairs(&args).unwrap_err().to_string();

assert!(err.contains("u=v"));
}

#[test]
fn test_parse_graph_respects_explicit_num_vertices() {
let mut args = empty_args();
Expand Down
Loading
Loading