Skip to content

Commit e770c59

Browse files
authored
Fix #292: [Model] IntegralFlowHomologousArcs (#739)
* Add plan for #292: [Model] IntegralFlowHomologousArcs * Implement #292: [Model] IntegralFlowHomologousArcs * chore: remove plan file after implementation * fix: resolve formatting after merge with main * fix: simplify homologous-pair iteration and add non-unit-capacity test
1 parent 638c7f7 commit e770c59

11 files changed

Lines changed: 794 additions & 12 deletions

File tree

docs/paper/reductions.typ

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@
131131
"ConsecutiveBlockMinimization": [Consecutive Block Minimization],
132132
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
133133
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
134+
"IntegralFlowHomologousArcs": [Integral Flow with Homologous Arcs],
134135
"IntegralFlowWithMultipliers": [Integral Flow With Multipliers],
135136
"MinMaxMulticenter": [Min-Max Multicenter],
136137
"FlowShopScheduling": [Flow Shop Scheduling],
@@ -5205,6 +5206,89 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
52055206
]
52065207
}
52075208

5209+
#{
5210+
let x = load-model-example("IntegralFlowHomologousArcs")
5211+
let arcs = x.instance.graph.arcs
5212+
let sol = x.optimal_config
5213+
let source = x.instance.source
5214+
let sink = x.instance.sink
5215+
let requirement = x.instance.requirement
5216+
[
5217+
#problem-def("IntegralFlowHomologousArcs")[
5218+
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$.
5219+
][
5220+
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.
5221+
5222+
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)$.].
5223+
5224+
*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.
5225+
5226+
#pred-commands(
5227+
"pred create --example " + problem-spec(x) + " -o integral-flow-homologous-arcs.json",
5228+
"pred solve integral-flow-homologous-arcs.json --solver brute-force",
5229+
"pred evaluate integral-flow-homologous-arcs.json --config " + x.optimal_config.map(str).join(","),
5230+
)
5231+
5232+
#figure(
5233+
canvas(length: 1cm, {
5234+
import draw: *
5235+
let blue = graph-colors.at(0)
5236+
let orange = rgb("#f28e2b")
5237+
let red = rgb("#e15759")
5238+
let gray = luma(185)
5239+
let positions = (
5240+
(0, 0),
5241+
(1.6, 1.1),
5242+
(1.6, -1.1),
5243+
(3.2, 1.1),
5244+
(3.2, -1.1),
5245+
(4.8, 0),
5246+
)
5247+
let labels = (
5248+
[$s = v_0$],
5249+
[$v_1$],
5250+
[$v_2$],
5251+
[$v_3$],
5252+
[$v_4$],
5253+
[$t = v_5$],
5254+
)
5255+
for (idx, (u, v)) in arcs.enumerate() {
5256+
let stroke = if idx == 3 or idx == 4 {
5257+
(paint: orange, thickness: 1.3pt, dash: "dashed")
5258+
} else if sol.at(idx) == 1 {
5259+
(paint: blue, thickness: 1.8pt)
5260+
} else {
5261+
(paint: gray, thickness: 0.7pt)
5262+
}
5263+
line(
5264+
positions.at(u),
5265+
positions.at(v),
5266+
stroke: stroke,
5267+
mark: (end: "straight", scale: 0.5),
5268+
)
5269+
}
5270+
for (i, pos) in positions.enumerate() {
5271+
let fill = if i == source { blue } else if i == sink { red } else { white }
5272+
g-node(
5273+
pos,
5274+
name: "ifha-" + str(i),
5275+
fill: fill,
5276+
label: if i == source or i == sink {
5277+
text(fill: white)[#labels.at(i)]
5278+
} else {
5279+
labels.at(i)
5280+
},
5281+
)
5282+
}
5283+
content((2.4, 1.55), text(8pt, fill: blue)[$f(a_2) = f(a_5) = 1$])
5284+
content((2.4, -1.55), text(8pt, fill: orange)[$f(a_4) = f(a_3) = 0$])
5285+
}),
5286+
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.],
5287+
) <fig:integral-flow-homologous-arcs>
5288+
]
5289+
]
5290+
}
5291+
52085292
#problem-def("DirectedTwoCommodityIntegralFlow")[
52095293
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}$.
52105294
][

problemreductions-cli/src/cli.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ Flags by problem type:
235235
LongestCircuit --graph, --edge-weights, --bound
236236
BoundedComponentSpanningForest --graph, --weights, --k, --bound
237237
UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
238+
IntegralFlowHomologousArcs --arcs, --capacities, --source, --sink, --requirement, --homologous-pairs
238239
IsomorphicSpanningTree --graph, --tree
239240
KthBestSpanningTree --graph, --edge-weights, --k, --bound
240241
LengthBoundedDisjointPaths --graph, --source, --sink, --num-paths-required, --bound
@@ -325,6 +326,7 @@ Examples:
325326
pred create BiconnectivityAugmentation --graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5
326327
pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1
327328
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
329+
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\"
328330
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\"
329331
pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3
330332
pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2
@@ -367,7 +369,7 @@ pub struct CreateArgs {
367369
/// Sink vertex for path-based graph problems and MinimumCutIntoBoundedSets
368370
#[arg(long)]
369371
pub sink: Option<usize>,
370-
/// Required sink inflow for IntegralFlowWithMultipliers
372+
/// Required sink inflow for IntegralFlowHomologousArcs and IntegralFlowWithMultipliers
371373
#[arg(long)]
372374
pub requirement: Option<u64>,
373375
/// Required number of paths for LengthBoundedDisjointPaths
@@ -536,6 +538,9 @@ pub struct CreateArgs {
536538
/// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0)
537539
#[arg(long)]
538540
pub arcs: Option<String>,
541+
/// Arc-index equality constraints for IntegralFlowHomologousArcs (semicolon-separated, e.g., "2=5;4=3")
542+
#[arg(long)]
543+
pub homologous_pairs: Option<String>,
539544
/// Quantifiers for QBF (comma-separated, E=Exists, A=ForAll, e.g., "E,A,E")
540545
#[arg(long)]
541546
pub quantifiers: Option<String>,

problemreductions-cli/src/commands/create.rs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
109109
&& args.costs.is_none()
110110
&& args.arc_costs.is_none()
111111
&& args.arcs.is_none()
112+
&& args.homologous_pairs.is_none()
112113
&& args.quantifiers.is_none()
113114
&& args.usage.is_none()
114115
&& args.storage.is_none()
@@ -151,6 +152,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
151152
&& args.sink_2.is_none()
152153
&& args.requirement_1.is_none()
153154
&& args.requirement_2.is_none()
155+
&& args.requirement.is_none()
156+
&& args.homologous_pairs.is_none()
154157
&& args.num_attributes.is_none()
155158
&& args.dependencies.is_none()
156159
&& args.relation_attrs.is_none()
@@ -528,6 +531,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
528531
"UndirectedTwoCommodityIntegralFlow" => {
529532
"--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"
530533
},
534+
"IntegralFlowHomologousArcs" => {
535+
"--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\""
536+
}
531537
"LengthBoundedDisjointPaths" => {
532538
"--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"
533539
}
@@ -750,6 +756,9 @@ fn help_flag_hint(
750756
}
751757
("ShortestCommonSupersequence", "strings") => "symbol lists: \"0,1,2;1,2,0\"",
752758
("MultipleChoiceBranching", "partition") => "semicolon-separated groups: \"0,1;2,3\"",
759+
("IntegralFlowHomologousArcs", "homologous_pairs") => {
760+
"semicolon-separated arc-index equalities: \"2=5;4=3\""
761+
}
753762
("ConsistencyOfDatabaseFrequencyTables", "attribute_domains") => {
754763
"comma-separated domain sizes: 2,3,2"
755764
}
@@ -3197,6 +3206,83 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
31973206
)
31983207
}
31993208

3209+
// IntegralFlowHomologousArcs
3210+
"IntegralFlowHomologousArcs" => {
3211+
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\"";
3212+
let arcs_str = args.arcs.as_deref().ok_or_else(|| {
3213+
anyhow::anyhow!("IntegralFlowHomologousArcs requires --arcs\n\n{usage}")
3214+
})?;
3215+
let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)
3216+
.map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
3217+
let capacities: Vec<u64> = if let Some(ref s) = args.capacities {
3218+
s.split(',')
3219+
.map(|token| {
3220+
let trimmed = token.trim();
3221+
trimmed
3222+
.parse::<u64>()
3223+
.with_context(|| format!("Invalid capacity `{trimmed}`\n\n{usage}"))
3224+
})
3225+
.collect::<Result<Vec<_>>>()?
3226+
} else {
3227+
vec![1; num_arcs]
3228+
};
3229+
anyhow::ensure!(
3230+
capacities.len() == num_arcs,
3231+
"Expected {} capacities but got {}\n\n{}",
3232+
num_arcs,
3233+
capacities.len(),
3234+
usage
3235+
);
3236+
for (arc_index, &capacity) in capacities.iter().enumerate() {
3237+
let fits = usize::try_from(capacity)
3238+
.ok()
3239+
.and_then(|value| value.checked_add(1))
3240+
.is_some();
3241+
anyhow::ensure!(
3242+
fits,
3243+
"capacity {} at arc index {} is too large for this platform\n\n{}",
3244+
capacity,
3245+
arc_index,
3246+
usage
3247+
);
3248+
}
3249+
let num_vertices = graph.num_vertices();
3250+
let source = args.source.ok_or_else(|| {
3251+
anyhow::anyhow!("IntegralFlowHomologousArcs requires --source\n\n{usage}")
3252+
})?;
3253+
let sink = args.sink.ok_or_else(|| {
3254+
anyhow::anyhow!("IntegralFlowHomologousArcs requires --sink\n\n{usage}")
3255+
})?;
3256+
let requirement = args.requirement.ok_or_else(|| {
3257+
anyhow::anyhow!("IntegralFlowHomologousArcs requires --requirement\n\n{usage}")
3258+
})?;
3259+
validate_vertex_index("source", source, num_vertices, usage)?;
3260+
validate_vertex_index("sink", sink, num_vertices, usage)?;
3261+
let homologous_pairs =
3262+
parse_homologous_pairs(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
3263+
for &(a, b) in &homologous_pairs {
3264+
anyhow::ensure!(
3265+
a < num_arcs && b < num_arcs,
3266+
"homologous pair ({}, {}) references arc >= num_arcs ({})\n\n{}",
3267+
a,
3268+
b,
3269+
num_arcs,
3270+
usage
3271+
);
3272+
}
3273+
(
3274+
ser(IntegralFlowHomologousArcs::new(
3275+
graph,
3276+
capacities,
3277+
source,
3278+
sink,
3279+
requirement,
3280+
homologous_pairs,
3281+
))?,
3282+
resolved_variant.clone(),
3283+
)
3284+
}
3285+
32003286
// MinimumFeedbackArcSet
32013287
"MinimumFeedbackArcSet" => {
32023288
let arcs_str = args.arcs.as_deref().ok_or_else(|| {
@@ -4416,6 +4502,35 @@ fn parse_named_sets(sets_str: Option<&str>, flag: &str) -> Result<Vec<Vec<usize>
44164502
.collect()
44174503
}
44184504

4505+
fn parse_homologous_pairs(args: &CreateArgs) -> Result<Vec<(usize, usize)>> {
4506+
let pairs = args.homologous_pairs.as_deref().ok_or_else(|| {
4507+
anyhow::anyhow!(
4508+
"IntegralFlowHomologousArcs requires --homologous-pairs (e.g., \"2=5;4=3\")"
4509+
)
4510+
})?;
4511+
4512+
pairs
4513+
.split(';')
4514+
.filter(|entry| !entry.trim().is_empty())
4515+
.map(|entry| {
4516+
let entry = entry.trim();
4517+
let (left, right) = entry.split_once('=').ok_or_else(|| {
4518+
anyhow::anyhow!(
4519+
"Invalid homologous pair '{}': expected format u=v (e.g., 2=5)",
4520+
entry
4521+
)
4522+
})?;
4523+
let left = left.trim().parse::<usize>().with_context(|| {
4524+
format!("Invalid homologous pair '{}': expected format u=v", entry)
4525+
})?;
4526+
let right = right.trim().parse::<usize>().with_context(|| {
4527+
format!("Invalid homologous pair '{}': expected format u=v", entry)
4528+
})?;
4529+
Ok((left, right))
4530+
})
4531+
.collect()
4532+
}
4533+
44194534
/// Parse a dependency string as semicolon-separated `lhs>rhs` pairs.
44204535
/// E.g., "0,1>2,3;2,3>0,1"
44214536
fn parse_deps(s: &str) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
@@ -6067,6 +6182,7 @@ mod tests {
60676182
usage: None,
60686183
storage: None,
60696184
quantifiers: None,
6185+
homologous_pairs: None,
60706186
}
60716187
}
60726188

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

6203+
#[test]
6204+
fn test_all_data_flags_empty_treats_homologous_pairs_as_input() {
6205+
let mut args = empty_args();
6206+
args.homologous_pairs = Some("2=5;4=3".to_string());
6207+
assert!(!all_data_flags_empty(&args));
6208+
}
6209+
60876210
#[test]
60886211
fn test_parse_potential_edges() {
60896212
let mut args = empty_args();
@@ -6112,6 +6235,24 @@ mod tests {
61126235
assert_eq!(parse_budget(&args).unwrap(), 7);
61136236
}
61146237

6238+
#[test]
6239+
fn test_parse_homologous_pairs() {
6240+
let mut args = empty_args();
6241+
args.homologous_pairs = Some("2=5;4=3".to_string());
6242+
6243+
assert_eq!(parse_homologous_pairs(&args).unwrap(), vec![(2, 5), (4, 3)]);
6244+
}
6245+
6246+
#[test]
6247+
fn test_parse_homologous_pairs_rejects_invalid_token() {
6248+
let mut args = empty_args();
6249+
args.homologous_pairs = Some("2-5".to_string());
6250+
6251+
let err = parse_homologous_pairs(&args).unwrap_err().to_string();
6252+
6253+
assert!(err.contains("u=v"));
6254+
}
6255+
61156256
#[test]
61166257
fn test_parse_graph_respects_explicit_num_vertices() {
61176258
let mut args = empty_args();

0 commit comments

Comments
 (0)