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
80 changes: 80 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"HamiltonianCircuit": [Hamiltonian Circuit],
"BiconnectivityAugmentation": [Biconnectivity Augmentation],
"HamiltonianPath": [Hamiltonian Path],
"IntegralFlowBundles": [Integral Flow with Bundles],
"LongestCircuit": [Longest Circuit],
"LongestPath": [Longest Path],
"ShortestWeightConstrainedPath": [Shortest Weight-Constrained Path],
Expand Down Expand Up @@ -5490,6 +5491,64 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
) <fig:d2cif>
]

#{
let x = load-model-example("IntegralFlowBundles")
let source = x.instance.source
let sink = x.instance.sink
[
#problem-def("IntegralFlowBundles")[
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$.
][
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.

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

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

#pred-commands(
"pred create --example IntegralFlowBundles -o integral-flow-bundles.json",
"pred solve integral-flow-bundles.json",
"pred evaluate integral-flow-bundles.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 teal = rgb("#76b7b2")
let gray = luma(185)
let positions = (
(0, 0),
(2.2, 1.3),
(2.2, -1.3),
(4.4, 0),
)

line(positions.at(0), positions.at(1), stroke: (paint: blue, thickness: 2pt), mark: (end: "straight", scale: 0.5))
line(positions.at(0), positions.at(2), stroke: (paint: blue.lighten(35%), thickness: 1.0pt), mark: (end: "straight", scale: 0.5))
line(positions.at(1), positions.at(3), stroke: (paint: orange, thickness: 2pt), mark: (end: "straight", scale: 0.5))
line(positions.at(2), positions.at(3), stroke: (paint: teal, thickness: 1.0pt), mark: (end: "straight", scale: 0.5))
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))
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))

for (i, pos) in positions.enumerate() {
let fill = if i == source { blue } else if i == sink { rgb("#e15759") } else { white }
g-node(pos, name: "ifb-" + str(i), fill: fill, label: if i == source or i == sink { text(fill: white)[$v_#i$] } else { [$v_#i$] })
}

content((1.0, 1.0), text(8pt, fill: blue)[$I_1, c = 1$])
content((3.3, 1.0), text(8pt, fill: orange)[$I_2, c = 1$])
content((3.3, -1.0), text(8pt, fill: teal)[$I_3, c = 1$])
content((2.2, 1.8), text(8pt)[$f(0,1) = 1$])
content((3.4, 1.55), text(8pt)[$f(1,3) = 1$])
}),
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$.],
) <fig:integral-flow-bundles>
]
]
}

#{
let x = load-model-example("IntegralFlowWithMultipliers")
let config = x.optimal_config
Expand Down Expand Up @@ -7004,6 +7063,27 @@ The following reductions to Integer Linear Programming are straightforward formu
_Solution extraction._ For each item $i$, find the unique $j$ with $x_(i j) = 1$; assign item $i$ to bin $j$.
]

#reduction-rule("IntegralFlowBundles", "ILP")[
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.
][
_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.

_Bundle constraints._ For every bundle $I_j$, add
$sum_(a_i in I_j) x_i <= c_j$.

_Flow conservation._ For every nonterminal vertex $v in V backslash {s, t}$, add
$sum_(a_i = (u, v) in A) x_i - sum_(a_i = (v, w) in A) x_i = 0$.

_Requirement constraint._ Add the sink inflow lower bound
$sum_(a_i = (u, t) in A) x_i - sum_(a_i = (t, w) in A) x_i >= R$.

_Objective._ Minimize 0. The target is a pure feasibility ILP, so any constant objective works.

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

_Solution extraction._ Identity: read the ILP vector $(x_0, dots, x_(m-1))$ directly as the arc-flow vector of the source problem.
]

#reduction-rule("SequencingToMinimizeWeightedCompletionTime", "ILP")[
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.
][
Expand Down
3 changes: 2 additions & 1 deletion docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,8 @@ @article{sahni1974
volume = {3},
number = {4},
pages = {262--279},
year = {1974}
year = {1974},
doi = {10.1137/0203021}
}

@article{jewell1962,
Expand Down
9 changes: 8 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:
HamiltonianCircuit, HC --graph
LongestCircuit --graph, --edge-weights, --bound
BoundedComponentSpanningForest --graph, --weights, --k, --bound
IntegralFlowBundles --arcs, --bundles, --bundle-capacities, --source, --sink, --requirement [--num-vertices]
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
Expand Down Expand Up @@ -362,6 +363,9 @@ pub struct CreateArgs {
/// Edge capacities for multicommodity flow problems (e.g., 1,1,2)
#[arg(long)]
pub capacities: Option<String>,
/// Bundle capacities for IntegralFlowBundles (e.g., 1,1,1)
#[arg(long)]
pub bundle_capacities: Option<String>,
/// Vertex multipliers in vertex order (e.g., 1,2,3,1)
#[arg(long)]
pub multipliers: Option<String>,
Expand All @@ -371,7 +375,7 @@ pub struct CreateArgs {
/// Sink vertex for path-based graph problems and MinimumCutIntoBoundedSets
#[arg(long)]
pub sink: Option<usize>,
/// Required total flow R for IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, and PathConstrainedNetworkFlow
/// Required total flow R for IntegralFlowBundles, IntegralFlowHomologousArcs, IntegralFlowWithMultipliers, and PathConstrainedNetworkFlow
#[arg(long)]
pub requirement: Option<u64>,
/// Required number of paths for LengthBoundedDisjointPaths
Expand Down Expand Up @@ -477,6 +481,9 @@ pub struct CreateArgs {
/// Partition groups for arc-index partitions (semicolon-separated, e.g., "0,1;2,3")
#[arg(long)]
pub partition: Option<String>,
/// Arc bundles for IntegralFlowBundles (semicolon-separated groups of arc indices, e.g., "0,1;2,5;3,4")
#[arg(long)]
pub bundles: Option<String>,
/// Universe size for set-system problems such as MinimumHittingSet, MinimumSetCovering, and ComparativeContainment
#[arg(long)]
pub universe: Option<usize>,
Expand Down
141 changes: 140 additions & 1 deletion problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use problemreductions::models::algebraic::{
};
use problemreductions::models::formula::Quantifier;
use problemreductions::models::graph::{
GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath,
GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IntegralFlowBundles,
LengthBoundedDisjointPaths, LongestCircuit, LongestPath, MinimumCutIntoBoundedSets,
MinimumMultiwayCut, MixedChinesePostman, MultipleChoiceBranching, PathConstrainedNetworkFlow,
SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation,
Expand Down Expand Up @@ -50,6 +50,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.edge_weights.is_none()
&& args.edge_lengths.is_none()
&& args.capacities.is_none()
&& args.bundle_capacities.is_none()
&& args.multipliers.is_none()
&& args.source.is_none()
&& args.sink.is_none()
Expand Down Expand Up @@ -88,6 +89,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.r_weights.is_none()
&& args.s_weights.is_none()
&& args.partition.is_none()
&& args.bundles.is_none()
&& args.universe.is_none()
&& args.biedges.is_none()
&& args.left.is_none()
Expand Down Expand Up @@ -520,6 +522,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"KClique" => "--graph 0-1,0-2,1-3,2-3,2-4,3-4 --k 3",
"GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3",
"GeneralizedHex" => "--graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5",
"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"
}
"IntegralFlowWithMultipliers" => {
"--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"
}
Expand Down Expand Up @@ -776,6 +781,8 @@ fn help_flag_hint(
("ConsistencyOfDatabaseFrequencyTables", "known_values") => {
"semicolon-separated triples: \"0,0,0;3,0,1;1,2,1\""
}
("IntegralFlowBundles", "bundles") => "semicolon-separated groups: \"0,1;2,5;3,4\"",
("IntegralFlowBundles", "bundle_capacities") => "comma-separated capacities: 1,1,1",
("PathConstrainedNetworkFlow", "paths") => {
"semicolon-separated arc-index paths: \"0,2,5,8;1,4,7,9\""
}
Expand Down Expand Up @@ -1522,6 +1529,46 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// IntegralFlowBundles (directed graph + bundles + source/sink + requirement)
"IntegralFlowBundles" => {
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";
let arcs_str = args
.arcs
.as_deref()
.ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles 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 bundles = parse_bundles(args, num_arcs, usage)?;
let bundle_capacities = parse_bundle_capacities(args, bundles.len(), usage)?;
let source = args.source.ok_or_else(|| {
anyhow::anyhow!("IntegralFlowBundles requires --source\n\n{usage}")
})?;
let sink = args
.sink
.ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles requires --sink\n\n{usage}"))?;
let requirement = args.requirement.ok_or_else(|| {
anyhow::anyhow!("IntegralFlowBundles requires --requirement\n\n{usage}")
})?;
validate_vertex_index("source", source, graph.num_vertices(), usage)?;
validate_vertex_index("sink", sink, graph.num_vertices(), usage)?;
anyhow::ensure!(
source != sink,
"IntegralFlowBundles requires distinct --source and --sink\n\n{usage}"
);

(
ser(IntegralFlowBundles::new(
graph,
source,
sink,
bundles,
bundle_capacities,
requirement,
))?,
resolved_variant.clone(),
)
}

// LengthBoundedDisjointPaths (graph + source + sink + path count + bound)
"LengthBoundedDisjointPaths" => {
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";
Expand Down Expand Up @@ -4469,6 +4516,48 @@ fn parse_capacities(args: &CreateArgs, num_edges: usize, usage: &str) -> Result<
Ok(capacities)
}

fn parse_bundle_capacities(args: &CreateArgs, num_bundles: usize, usage: &str) -> Result<Vec<u64>> {
let capacities = args.bundle_capacities.as_deref().ok_or_else(|| {
anyhow::anyhow!("IntegralFlowBundles requires --bundle-capacities\n\n{usage}")
})?;
let capacities: Vec<u64> = capacities
.split(',')
.map(|s| {
let trimmed = s.trim();
trimmed
.parse::<u64>()
.with_context(|| format!("Invalid bundle capacity `{trimmed}`\n\n{usage}"))
})
.collect::<Result<Vec<_>>>()?;
anyhow::ensure!(
capacities.len() == num_bundles,
"Expected {} bundle capacities but got {}\n\n{}",
num_bundles,
capacities.len(),
usage
);
for (bundle_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,
"bundle capacity {} at bundle index {} is too large for this platform\n\n{}",
capacity,
bundle_index,
usage
);
anyhow::ensure!(
capacity > 0,
"bundle capacity at bundle index {} must be positive\n\n{}",
bundle_index,
usage
);
}
Ok(capacities)
}

/// Parse `--couplings` as SpinGlass pairwise couplings (i32), defaulting to all 1s.
fn parse_couplings(args: &CreateArgs, num_edges: usize) -> Result<Vec<i32>> {
match &args.couplings {
Expand Down Expand Up @@ -4740,6 +4829,54 @@ fn parse_partition_groups(args: &CreateArgs, num_arcs: usize) -> Result<Vec<Vec<
Ok(partition)
}

fn parse_bundles(args: &CreateArgs, num_arcs: usize, usage: &str) -> Result<Vec<Vec<usize>>> {
let bundles_str = args
.bundles
.as_deref()
.ok_or_else(|| anyhow::anyhow!("IntegralFlowBundles requires --bundles\n\n{usage}"))?;

let bundles: Vec<Vec<usize>> = bundles_str
.split(';')
.map(|bundle| {
let bundle = bundle.trim();
anyhow::ensure!(
!bundle.is_empty(),
"IntegralFlowBundles does not allow empty bundle entries\n\n{usage}"
);
bundle
.split(',')
.map(|s| {
s.trim().parse::<usize>().with_context(|| {
format!("Invalid bundle arc index `{}`\n\n{usage}", s.trim())
})
})
.collect::<Result<Vec<_>>>()
})
.collect::<Result<_>>()?;

let mut seen_overall = vec![false; num_arcs];
for (bundle_index, bundle) in bundles.iter().enumerate() {
let mut seen_in_bundle = BTreeSet::new();
for &arc_index in bundle {
anyhow::ensure!(
arc_index < num_arcs,
"bundle {bundle_index} references arc {arc_index}, but num_arcs is {num_arcs}\n\n{usage}"
);
anyhow::ensure!(
seen_in_bundle.insert(arc_index),
"bundle {bundle_index} contains duplicate arc index {arc_index}\n\n{usage}"
);
seen_overall[arc_index] = true;
}
}
anyhow::ensure!(
seen_overall.iter().all(|covered| *covered),
"bundles must cover every arc at least once\n\n{usage}"
);

Ok(bundles)
}

fn parse_multiple_choice_branching_threshold(args: &CreateArgs, usage: &str) -> Result<i32> {
let raw_bound = args
.bound
Expand Down Expand Up @@ -6402,6 +6539,7 @@ mod tests {
edge_weights: None,
edge_lengths: None,
capacities: None,
bundle_capacities: None,
multipliers: None,
source: None,
sink: None,
Expand Down Expand Up @@ -6440,6 +6578,7 @@ mod tests {
r_weights: None,
s_weights: None,
partition: None,
bundles: None,
universe: None,
biedges: None,
left: None,
Expand Down
Loading
Loading