Skip to content

Commit 07c0da1

Browse files
GiggleLiuisPANN
andauthored
Fix #293: [Model] IntegralFlowBundles (#740)
* Add plan for #293: [Model] IntegralFlowBundles * Add IntegralFlowBundles model * Add IntegralFlowBundles to ILP reduction * Wire IntegralFlowBundles through CLI and example db * Add IntegralFlowBundles paper entry and polish formatting * chore: remove plan file after implementation --------- Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn> Co-authored-by: Xiwei Pan <90967972+isPANN@users.noreply.github.com>
1 parent 40fc86c commit 07c0da1

14 files changed

Lines changed: 1049 additions & 8 deletions

File tree

docs/paper/reductions.typ

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"HamiltonianCircuit": [Hamiltonian Circuit],
7373
"BiconnectivityAugmentation": [Biconnectivity Augmentation],
7474
"HamiltonianPath": [Hamiltonian Path],
75+
"IntegralFlowBundles": [Integral Flow with Bundles],
7576
"LongestCircuit": [Longest Circuit],
7677
"LongestPath": [Longest Path],
7778
"ShortestWeightConstrainedPath": [Shortest Weight-Constrained Path],
@@ -5490,6 +5491,64 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
54905491
) <fig:d2cif>
54915492
]
54925493

5494+
#{
5495+
let x = load-model-example("IntegralFlowBundles")
5496+
let source = x.instance.source
5497+
let sink = x.instance.sink
5498+
[
5499+
#problem-def("IntegralFlowBundles")[
5500+
Given a directed graph $G = (V, A)$, specified vertices $s, t in V$, a family of arc bundles $I_1, dots, I_k subset.eq A$ whose union covers $A$, positive bundle capacities $c_1, dots, c_k$, and a requirement $R in ZZ^+$, determine whether there exists an integral flow $f: A -> ZZ_(>= 0)$ such that (1) $sum_(a in I_j) f(a) <= c_j$ for every bundle $j$, (2) flow is conserved at every vertex in $V backslash {s, t}$, and (3) the net flow into $t$ is at least $R$.
5501+
][
5502+
Integral Flow with Bundles is the shared-capacity single-commodity flow problem listed as ND36 in Garey \& Johnson @garey1979. Sahni introduced it as one of a family of computationally related network problems and showed that the bundled-capacity variant is NP-complete even in a very sparse unit-capacity regime @sahni1974.
5503+
5504+
The implementation keeps one non-negative integer variable per directed arc. Unlike ordinary max-flow, the usable range of an arc is not determined by an intrinsic per-arc capacity; it is bounded instead by the smallest bundle capacity among the bundles that contain that arc. The registered $O(2^m)$ catalog bound therefore reflects the unit-capacity case with $m = |A|$, which is exactly the regime highlighted by Garey \& Johnson and Sahni.#footnote[No exact worst-case algorithm improving on brute-force is claimed here for the bundled-capacity formulation.]
5505+
5506+
*Example.* The canonical YES instance has source $s = v_#source$, sink $t = v_#sink$, and arcs $(0,1)$, $(0,2)$, $(1,3)$, $(2,3)$, $(1,2)$, $(2,1)$. The three bundles are $I_1 = {(0,1), (0,2)}$, $I_2 = {(1,3), (2,1)}$, and $I_3 = {(2,3), (1,2)}$, each with capacity 1. Sending one unit along the path $0 -> 1 -> 3$ yields the flow vector $(1, 0, 1, 0, 0, 0)$: bundle $I_1$ contributes $1 + 0 = 1$, bundle $I_2$ contributes $1 + 0 = 1$, bundle $I_3$ contributes $0 + 0 = 0$, and the only nonterminal vertices $v_1, v_2$ satisfy conservation. If the requirement is raised from $R = 1$ to $R = 2$, the same gadget becomes infeasible because $I_1$ caps the total outflow leaving the source at one unit.
5507+
5508+
#pred-commands(
5509+
"pred create --example IntegralFlowBundles -o integral-flow-bundles.json",
5510+
"pred solve integral-flow-bundles.json",
5511+
"pred evaluate integral-flow-bundles.json --config " + x.optimal_config.map(str).join(","),
5512+
)
5513+
5514+
#figure(
5515+
canvas(length: 1cm, {
5516+
import draw: *
5517+
let blue = graph-colors.at(0)
5518+
let orange = rgb("#f28e2b")
5519+
let teal = rgb("#76b7b2")
5520+
let gray = luma(185)
5521+
let positions = (
5522+
(0, 0),
5523+
(2.2, 1.3),
5524+
(2.2, -1.3),
5525+
(4.4, 0),
5526+
)
5527+
5528+
line(positions.at(0), positions.at(1), stroke: (paint: blue, thickness: 2pt), mark: (end: "straight", scale: 0.5))
5529+
line(positions.at(0), positions.at(2), stroke: (paint: blue.lighten(35%), thickness: 1.0pt), mark: (end: "straight", scale: 0.5))
5530+
line(positions.at(1), positions.at(3), stroke: (paint: orange, thickness: 2pt), mark: (end: "straight", scale: 0.5))
5531+
line(positions.at(2), positions.at(3), stroke: (paint: teal, thickness: 1.0pt), mark: (end: "straight", scale: 0.5))
5532+
line((2.0, 1.0), (3.0, 0.0), (2.0, -1.0), stroke: (paint: teal, thickness: 1.0pt), mark: (end: "straight", scale: 0.5))
5533+
line((2.4, -1.0), (1.4, 0.0), (2.4, 1.0), stroke: (paint: orange, thickness: 1.0pt), mark: (end: "straight", scale: 0.5))
5534+
5535+
for (i, pos) in positions.enumerate() {
5536+
let fill = if i == source { blue } else if i == sink { rgb("#e15759") } else { white }
5537+
g-node(pos, name: "ifb-" + str(i), fill: fill, label: if i == source or i == sink { text(fill: white)[$v_#i$] } else { [$v_#i$] })
5538+
}
5539+
5540+
content((1.0, 1.0), text(8pt, fill: blue)[$I_1, c = 1$])
5541+
content((3.3, 1.0), text(8pt, fill: orange)[$I_2, c = 1$])
5542+
content((3.3, -1.0), text(8pt, fill: teal)[$I_3, c = 1$])
5543+
content((2.2, 1.8), text(8pt)[$f(0,1) = 1$])
5544+
content((3.4, 1.55), text(8pt)[$f(1,3) = 1$])
5545+
}),
5546+
caption: [Canonical YES instance for Integral Flow with Bundles. Thick blue/orange arcs carry the satisfying flow $0 -> 1 -> 3$, while the lighter arcs show the two unused alternatives coupled into bundles $I_1$, $I_2$, and $I_3$.],
5547+
) <fig:integral-flow-bundles>
5548+
]
5549+
]
5550+
}
5551+
54935552
#{
54945553
let x = load-model-example("IntegralFlowWithMultipliers")
54955554
let config = x.optimal_config
@@ -7004,6 +7063,27 @@ The following reductions to Integer Linear Programming are straightforward formu
70047063
_Solution extraction._ For each item $i$, find the unique $j$ with $x_(i j) = 1$; assign item $i$ to bin $j$.
70057064
]
70067065

7066+
#reduction-rule("IntegralFlowBundles", "ILP")[
7067+
The feasibility conditions are already linear: one integer variable per arc, one inequality per bundle, one conservation equality per nonterminal vertex, and one lower bound on sink inflow.
7068+
][
7069+
_Construction._ Given Integral Flow with Bundles instance $(G = (V, A), s, t, (I_j, c_j)_(j=1)^k, R)$ with arc set $A = {a_0, dots, a_(m-1)}$, create one non-negative integer variable $x_i$ for each arc $a_i$. The ILP therefore has $m$ variables.
7070+
7071+
_Bundle constraints._ For every bundle $I_j$, add
7072+
$sum_(a_i in I_j) x_i <= c_j$.
7073+
7074+
_Flow conservation._ For every nonterminal vertex $v in V backslash {s, t}$, add
7075+
$sum_(a_i = (u, v) in A) x_i - sum_(a_i = (v, w) in A) x_i = 0$.
7076+
7077+
_Requirement constraint._ Add the sink inflow lower bound
7078+
$sum_(a_i = (u, t) in A) x_i - sum_(a_i = (t, w) in A) x_i >= R$.
7079+
7080+
_Objective._ Minimize 0. The target is a pure feasibility ILP, so any constant objective works.
7081+
7082+
_Correctness._ ($arrow.r.double$) Any satisfying bundled flow assigns a non-negative integer to each arc, satisfies every bundle inequality by definition, satisfies every nonterminal conservation equality, and yields sink inflow at least $R$, so it is a feasible ILP solution. ($arrow.l.double$) Any feasible ILP solution gives non-negative integral arc values obeying the same bundle, conservation, and sink-inflow constraints, hence it is a satisfying solution to the original Integral Flow with Bundles instance.
7083+
7084+
_Solution extraction._ Identity: read the ILP vector $(x_0, dots, x_(m-1))$ directly as the arc-flow vector of the source problem.
7085+
]
7086+
70077087
#reduction-rule("SequencingToMinimizeWeightedCompletionTime", "ILP")[
70087088
Completion times are natural integer variables, precedence constraints compare those completion times directly, and one binary order variable per task pair enforces that a single machine cannot overlap two jobs.
70097089
][

docs/paper/references.bib

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,8 @@ @article{sahni1974
273273
volume = {3},
274274
number = {4},
275275
pages = {262--279},
276-
year = {1974}
276+
year = {1974},
277+
doi = {10.1137/0203021}
277278
}
278279

279280
@article{jewell1962,

problemreductions-cli/src/cli.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ Flags by problem type:
235235
HamiltonianCircuit, HC --graph
236236
LongestCircuit --graph, --edge-weights, --bound
237237
BoundedComponentSpanningForest --graph, --weights, --k, --bound
238+
IntegralFlowBundles --arcs, --bundles, --bundle-capacities, --source, --sink, --requirement [--num-vertices]
238239
UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
239240
IntegralFlowHomologousArcs --arcs, --capacities, --source, --sink, --requirement, --homologous-pairs
240241
IsomorphicSpanningTree --graph, --tree
@@ -362,6 +363,9 @@ pub struct CreateArgs {
362363
/// Edge capacities for multicommodity flow problems (e.g., 1,1,2)
363364
#[arg(long)]
364365
pub capacities: Option<String>,
366+
/// Bundle capacities for IntegralFlowBundles (e.g., 1,1,1)
367+
#[arg(long)]
368+
pub bundle_capacities: Option<String>,
365369
/// Vertex multipliers in vertex order (e.g., 1,2,3,1)
366370
#[arg(long)]
367371
pub multipliers: Option<String>,
@@ -371,7 +375,7 @@ pub struct CreateArgs {
371375
/// Sink vertex for path-based graph problems and MinimumCutIntoBoundedSets
372376
#[arg(long)]
373377
pub sink: Option<usize>,
374-
/// Required total flow R for IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, and PathConstrainedNetworkFlow
378+
/// Required total flow R for IntegralFlowBundles, IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, and PathConstrainedNetworkFlow
375379
#[arg(long)]
376380
pub requirement: Option<u64>,
377381
/// Required number of paths for LengthBoundedDisjointPaths
@@ -477,6 +481,9 @@ pub struct CreateArgs {
477481
/// Partition groups for arc-index partitions (semicolon-separated, e.g., "0,1;2,3")
478482
#[arg(long)]
479483
pub partition: Option<String>,
484+
/// Arc bundles for IntegralFlowBundles (semicolon-separated groups of arc indices, e.g., "0,1;2,5;3,4")
485+
#[arg(long)]
486+
pub bundles: Option<String>,
480487
/// Universe size for set-system problems such as MinimumHittingSet, MinimumSetCovering, and ComparativeContainment
481488
#[arg(long)]
482489
pub universe: Option<usize>,

problemreductions-cli/src/commands/create.rs

Lines changed: 140 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use problemreductions::models::algebraic::{
1212
};
1313
use problemreductions::models::formula::Quantifier;
1414
use problemreductions::models::graph::{
15-
GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath,
15+
GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IntegralFlowBundles,
1616
LengthBoundedDisjointPaths, LongestCircuit, LongestPath, MinimumCutIntoBoundedSets,
1717
MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching, PathConstrainedNetworkFlow,
1818
SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation,
@@ -50,6 +50,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
5050
&& args.edge_weights.is_none()
5151
&& args.edge_lengths.is_none()
5252
&& args.capacities.is_none()
53+
&& args.bundle_capacities.is_none()
5354
&& args.multipliers.is_none()
5455
&& args.source.is_none()
5556
&& args.sink.is_none()
@@ -88,6 +89,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
8889
&& args.r_weights.is_none()
8990
&& args.s_weights.is_none()
9091
&& args.partition.is_none()
92+
&& args.bundles.is_none()
9193
&& args.universe.is_none()
9294
&& args.biedges.is_none()
9395
&& args.left.is_none()
@@ -520,6 +522,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
520522
"KClique" => "--graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3",
521523
"GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3",
522524
"GeneralizedHex" => "--graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5",
525+
"IntegralFlowBundles" => {
526+
"--arcs \"0>1,0>2,1>3,2>3,1>2,2>1\" --bundles \"0,1;2,5;3,4\" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4"
527+
}
523528
"IntegralFlowWithMultipliers" => {
524529
"--arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2"
525530
}
@@ -776,6 +781,8 @@ fn help_flag_hint(
776781
("ConsistencyOfDatabaseFrequencyTables", "known_values") => {
777782
"semicolon-separated triples: \"0,0,0;3,0,1;1,2,1\""
778783
}
784+
("IntegralFlowBundles", "bundles") => "semicolon-separated groups: \"0,1;2,5;3,4\"",
785+
("IntegralFlowBundles", "bundle_capacities") => "comma-separated capacities: 1,1,1",
779786
("PathConstrainedNetworkFlow", "paths") => {
780787
"semicolon-separated arc-index paths: \"0,2,5,8;1,4,7,9\""
781788
}
@@ -1522,6 +1529,46 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
15221529
)
15231530
}
15241531

1532+
// IntegralFlowBundles (directed graph + bundles + source/sink + requirement)
1533+
"IntegralFlowBundles" => {
1534+
let usage = "Usage: pred create IntegralFlowBundles --arcs \"0>1,0>2,1>3,2>3,1>2,2>1\" --bundles \"0,1;2,5;3,4\" --bundle-capacities 1,1,1 --source 0 --sink 3 --requirement 1 --num-vertices 4";
1535+
let arcs_str = args
1536+
.arcs
1537+
.as_deref()
1538+
.ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles requires --arcs\n\n{usage}"))?;
1539+
let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)
1540+
.map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
1541+
let bundles = parse_bundles(args, num_arcs, usage)?;
1542+
let bundle_capacities = parse_bundle_capacities(args, bundles.len(), usage)?;
1543+
let source = args.source.ok_or_else(|| {
1544+
anyhow::anyhow!("IntegralFlowBundles requires --source\n\n{usage}")
1545+
})?;
1546+
let sink = args
1547+
.sink
1548+
.ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles requires --sink\n\n{usage}"))?;
1549+
let requirement = args.requirement.ok_or_else(|| {
1550+
anyhow::anyhow!("IntegralFlowBundles requires --requirement\n\n{usage}")
1551+
})?;
1552+
validate_vertex_index("source", source, graph.num_vertices(), usage)?;
1553+
validate_vertex_index("sink", sink, graph.num_vertices(), usage)?;
1554+
anyhow::ensure!(
1555+
source != sink,
1556+
"IntegralFlowBundles requires distinct --source and --sink\n\n{usage}"
1557+
);
1558+
1559+
(
1560+
ser(IntegralFlowBundles::new(
1561+
graph,
1562+
source,
1563+
sink,
1564+
bundles,
1565+
bundle_capacities,
1566+
requirement,
1567+
))?,
1568+
resolved_variant.clone(),
1569+
)
1570+
}
1571+
15251572
// LengthBoundedDisjointPaths (graph + source + sink + path count + bound)
15261573
"LengthBoundedDisjointPaths" => {
15271574
let usage = "Usage: pred create 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";
@@ -4469,6 +4516,48 @@ fn parse_capacities(args: &CreateArgs, num_edges: usize, usage: &str) -> Result<
44694516
Ok(capacities)
44704517
}
44714518

4519+
fn parse_bundle_capacities(args: &CreateArgs, num_bundles: usize, usage: &str) -> Result<Vec<u64>> {
4520+
let capacities = args.bundle_capacities.as_deref().ok_or_else(|| {
4521+
anyhow::anyhow!("IntegralFlowBundles requires --bundle-capacities\n\n{usage}")
4522+
})?;
4523+
let capacities: Vec<u64> = capacities
4524+
.split(',')
4525+
.map(|s| {
4526+
let trimmed = s.trim();
4527+
trimmed
4528+
.parse::<u64>()
4529+
.with_context(|| format!("Invalid bundle capacity `{trimmed}`\n\n{usage}"))
4530+
})
4531+
.collect::<Result<Vec<_>>>()?;
4532+
anyhow::ensure!(
4533+
capacities.len() == num_bundles,
4534+
"Expected {} bundle capacities but got {}\n\n{}",
4535+
num_bundles,
4536+
capacities.len(),
4537+
usage
4538+
);
4539+
for (bundle_index, &capacity) in capacities.iter().enumerate() {
4540+
let fits = usize::try_from(capacity)
4541+
.ok()
4542+
.and_then(|value| value.checked_add(1))
4543+
.is_some();
4544+
anyhow::ensure!(
4545+
fits,
4546+
"bundle capacity {} at bundle index {} is too large for this platform\n\n{}",
4547+
capacity,
4548+
bundle_index,
4549+
usage
4550+
);
4551+
anyhow::ensure!(
4552+
capacity > 0,
4553+
"bundle capacity at bundle index {} must be positive\n\n{}",
4554+
bundle_index,
4555+
usage
4556+
);
4557+
}
4558+
Ok(capacities)
4559+
}
4560+
44724561
/// Parse `--couplings` as SpinGlass pairwise couplings (i32), defaulting to all 1s.
44734562
fn parse_couplings(args: &CreateArgs, num_edges: usize) -> Result<Vec<i32>> {
44744563
match &args.couplings {
@@ -4740,6 +4829,54 @@ fn parse_partition_groups(args: &CreateArgs, num_arcs: usize) -> Result<Vec<Vec<
47404829
Ok(partition)
47414830
}
47424831

4832+
fn parse_bundles(args: &CreateArgs, num_arcs: usize, usage: &str) -> Result<Vec<Vec<usize>>> {
4833+
let bundles_str = args
4834+
.bundles
4835+
.as_deref()
4836+
.ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles requires --bundles\n\n{usage}"))?;
4837+
4838+
let bundles: Vec<Vec<usize>> = bundles_str
4839+
.split(';')
4840+
.map(|bundle| {
4841+
let bundle = bundle.trim();
4842+
anyhow::ensure!(
4843+
!bundle.is_empty(),
4844+
"IntegralFlowBundles does not allow empty bundle entries\n\n{usage}"
4845+
);
4846+
bundle
4847+
.split(',')
4848+
.map(|s| {
4849+
s.trim().parse::<usize>().with_context(|| {
4850+
format!("Invalid bundle arc index `{}`\n\n{usage}", s.trim())
4851+
})
4852+
})
4853+
.collect::<Result<Vec<_>>>()
4854+
})
4855+
.collect::<Result<_>>()?;
4856+
4857+
let mut seen_overall = vec![false; num_arcs];
4858+
for (bundle_index, bundle) in bundles.iter().enumerate() {
4859+
let mut seen_in_bundle = BTreeSet::new();
4860+
for &arc_index in bundle {
4861+
anyhow::ensure!(
4862+
arc_index < num_arcs,
4863+
"bundle {bundle_index} references arc {arc_index}, but num_arcs is {num_arcs}\n\n{usage}"
4864+
);
4865+
anyhow::ensure!(
4866+
seen_in_bundle.insert(arc_index),
4867+
"bundle {bundle_index} contains duplicate arc index {arc_index}\n\n{usage}"
4868+
);
4869+
seen_overall[arc_index] = true;
4870+
}
4871+
}
4872+
anyhow::ensure!(
4873+
seen_overall.iter().all(|covered| *covered),
4874+
"bundles must cover every arc at least once\n\n{usage}"
4875+
);
4876+
4877+
Ok(bundles)
4878+
}
4879+
47434880
fn parse_multiple_choice_branching_threshold(args: &CreateArgs, usage: &str) -> Result<i32> {
47444881
let raw_bound = args
47454882
.bound
@@ -6402,6 +6539,7 @@ mod tests {
64026539
edge_weights: None,
64036540
edge_lengths: None,
64046541
capacities: None,
6542+
bundle_capacities: None,
64056543
multipliers: None,
64066544
source: None,
64076545
sink: None,
@@ -6440,6 +6578,7 @@ mod tests {
64406578
r_weights: None,
64416579
s_weights: None,
64426580
partition: None,
6581+
bundles: None,
64436582
universe: None,
64446583
biedges: None,
64456584
left: None,

0 commit comments

Comments
 (0)