Skip to content
Merged
52 changes: 52 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"MaxCut": [Max-Cut],
"GraphPartitioning": [Graph Partitioning],
"HamiltonianPath": [Hamiltonian Path],
"UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow],
"LengthBoundedDisjointPaths": [Length-Bounded Disjoint Paths],
"IsomorphicSpanningTree": [Isomorphic Spanning Tree],
"KColoring": [$k$-Coloring],
Expand Down Expand Up @@ -637,6 +638,57 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co
]
]
}
#{
let x = load-model-example("UndirectedTwoCommodityIntegralFlow")
let sample = x.samples.at(0)
let satisfying_count = x.optimal.len()
let source1 = x.instance.source_1
let source2 = x.instance.source_2
let sink1 = x.instance.sink_1
[
#problem-def("UndirectedTwoCommodityIntegralFlow")[
Given an undirected graph $G = (V, E)$, specified terminals $s_1, s_2, t_1, t_2 in V$, edge capacities $c: E -> ZZ^+$, and requirements $R_1, R_2 in ZZ^+$, determine whether there exist two integral flow functions $f_1, f_2$ that orient each used edge for each commodity, respect the shared edge capacities, conserve flow at every vertex in $V backslash {s_1, s_2, t_1, t_2}$, and deliver at least $R_i$ units of net flow into $t_i$ for each commodity $i in {1, 2}$.
][
Undirected Two-Commodity Integral Flow is the undirected counterpart of the classical two-commodity integral flow problem from Garey \& Johnson (ND39) @garey1979. Even, Itai, and Shamir proved that it remains NP-complete even when every capacity is 1, but becomes polynomial-time solvable when all capacities are even, giving a rare parity-driven complexity dichotomy @evenItaiShamir1976.

The implementation uses four variables per undirected edge ${u, v}$: $f_1(u, v)$, $f_1(v, u)$, $f_2(u, v)$, and $f_2(v, u)$. In the unit-capacity regime, each edge has exactly five meaningful local states: unused, commodity 1 in either direction, or commodity 2 in either direction, which matches the catalog bound $O(5^m)$ for $m = |E|$.

*Example.* Consider the graph with edges $(0, 2)$, $(1, 2)$, and $(2, 3)$, capacities $(1, 1, 2)$, sources $s_1 = v_#source1$, $s_2 = v_#source2$, and shared sink $t_1 = t_2 = v_#sink1$. The sample configuration in the fixture database sets $f_1(0, 2) = 1$, $f_2(1, 2) = 1$, and $f_1(2, 3) = f_2(2, 3) = 1$, with all reverse-direction variables zero. The only nonterminal vertex is $v_2$, where each commodity has one unit of inflow and one unit of outflow, so conservation holds. Vertex $v_3$ receives one unit of net inflow from each commodity, and the shared edge $(2,3)$ uses its full capacity 2. The fixture database contains exactly #satisfying_count satisfying configurations for this instance: the one shown below and the symmetric variant that swaps which commodity uses the two left edges.

#figure(
canvas(length: 1cm, {
import draw: *
let blue = graph-colors.at(0)
let teal = rgb("#76b7b2")
let gray = luma(190)
let verts = ((0, 1.2), (0, -1.2), (2.0, 0), (4.0, 0))
let labels = (
[$s_1 = v_0$],
[$s_2 = v_1$],
[$v_2$],
[$t_1 = t_2 = v_3$],
)
let edges = ((0, 2), (1, 2), (2, 3))
for (u, v) in edges {
g-edge(verts.at(u), verts.at(v), stroke: 1pt + gray)
}
g-edge(verts.at(0), verts.at(2), stroke: 1.8pt + blue)
g-edge(verts.at(1), verts.at(2), stroke: (paint: teal, thickness: 1.8pt, dash: "dashed"))
g-edge(verts.at(2), verts.at(3), stroke: 1.8pt + blue)
g-edge(verts.at(2), verts.at(3), stroke: (paint: teal, thickness: 1.8pt, dash: "dashed"))
for (i, pos) in verts.enumerate() {
let fill = if i == 0 { blue } else if i == 1 { teal } else if i == 3 { rgb("#e15759") } else { white }
g-node(pos, name: "utcif-" + str(i), fill: fill, label: if i == 2 { labels.at(i) } else { text(fill: white)[#labels.at(i)] })
}
content((1.0, 0.95), text(8pt, fill: gray)[$c = 1$])
content((1.0, -0.95), text(8pt, fill: gray)[$c = 1$])
content((3.0, 0.35), text(8pt, fill: gray)[$c = 2$])
}),
caption: [Canonical shared-capacity YES instance for Undirected Two-Commodity Integral Flow. Solid blue carries commodity 1 and dashed teal carries commodity 2; both commodities share the edge $(v_2, v_3)$ of capacity 2.],
) <fig:undirected-two-commodity-integral-flow>
]
]
}
#{
let x = load-model-example("IsomorphicSpanningTree")
let g-edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1)))
Expand Down
11 changes: 11 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,17 @@ @article{gareyJohnsonStockmeyer1976
year = {1976}
}

@article{evenItaiShamir1976,
author = {Shimon Even and Alon Itai and Adi Shamir},
title = {On the Complexity of Timetable and Multicommodity Flow Problems},
journal = {SIAM Journal on Computing},
volume = {5},
number = {4},
pages = {691--703},
year = {1976},
doi = {10.1137/0205048}
}

@article{glover2019,
author = {Fred Glover and Gary Kochenberger and Yu Du},
title = {Quantum Bridge Analytics {I}: a tutorial on formulating and using {QUBO} models},
Expand Down
1 change: 1 addition & 0 deletions docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json
pred create SpinGlass --graph 0-1,1-2 -o sg.json
pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json
pred create SteinerTree --graph 0-1,0-3,1-2,1-3,2-3,2-4,3-4 --edge-weights 2,5,2,1,5,6,1 --terminals 0,2,4 -o steiner.json
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 -o utcif.json
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 -o lbdp.json
pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json
pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json
Expand Down
46 changes: 46 additions & 0 deletions docs/src/reductions/problem_schemas.json
Original file line number Diff line number Diff line change
Expand Up @@ -756,5 +756,51 @@
"description": "Edge weights w: E -> R"
}
]
},
{
"name": "UndirectedTwoCommodityIntegralFlow",
"description": "Determine whether two integral commodities can satisfy sink demands in an undirected capacitated graph",
"fields": [
{
"name": "graph",
"type_name": "SimpleGraph",
"description": "Undirected graph G=(V,E)"
},
{
"name": "capacities",
"type_name": "Vec<u64>",
"description": "Edge capacities c(e) in graph edge order"
},
{
"name": "source_1",
"type_name": "usize",
"description": "Source vertex s_1 for commodity 1"
},
{
"name": "sink_1",
"type_name": "usize",
"description": "Sink vertex t_1 for commodity 1"
},
{
"name": "source_2",
"type_name": "usize",
"description": "Source vertex s_2 for commodity 2"
},
{
"name": "sink_2",
"type_name": "usize",
"description": "Sink vertex t_2 for commodity 2"
},
{
"name": "requirement_1",
"type_name": "u64",
"description": "Required net inflow R_1 at sink t_1"
},
{
"name": "requirement_2",
"type_name": "u64",
"description": "Required net inflow R_2 at sink t_2"
}
]
}
]
7 changes: 7 additions & 0 deletions docs/src/reductions/reduction_graph.json
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,13 @@
"category": "graph",
"doc_path": "models/graph/struct.TravelingSalesman.html",
"complexity": "2^num_vertices"
},
{
"name": "UndirectedTwoCommodityIntegralFlow",
"variant": {},
"category": "graph",
"doc_path": "models/graph/struct.UndirectedTwoCommodityIntegralFlow.html",
"complexity": "5^num_edges"
}
],
"edges": [
Expand Down
23 changes: 23 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ Flags by problem type:
KColoring --graph, --k
PartitionIntoTriangles --graph
GraphPartitioning --graph
UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
IsomorphicSpanningTree --graph, --tree
LengthBoundedDisjointPaths --graph, --source, --sink, --num-paths-required, --bound
Factoring --target, --m, --n
Expand Down Expand Up @@ -266,6 +267,7 @@ Examples:
pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5
pred create MIS --random --num-vertices 10 --edge-prob 0.3
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 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")]
pub struct CreateArgs {
Expand All @@ -290,6 +292,9 @@ pub struct CreateArgs {
/// Edge weights (e.g., 2,3,1) [default: all 1s]
#[arg(long)]
pub edge_weights: Option<String>,
/// Edge capacities for multicommodity flow problems (e.g., 1,1,2)
#[arg(long)]
pub capacities: Option<String>,
/// Source vertex for path-based graph problems
#[arg(long)]
pub source: Option<usize>,
Expand Down Expand Up @@ -344,6 +349,24 @@ pub struct CreateArgs {
/// Radius for UnitDiskGraph [default: 1.0]
#[arg(long)]
pub radius: Option<f64>,
/// Source vertex s_1 for commodity 1
#[arg(long)]
pub source_1: Option<usize>,
/// Sink vertex t_1 for commodity 1
#[arg(long)]
pub sink_1: Option<usize>,
/// Source vertex s_2 for commodity 2
#[arg(long)]
pub source_2: Option<usize>,
/// Sink vertex t_2 for commodity 2
#[arg(long)]
pub sink_2: Option<usize>,
/// Required flow R_1 for commodity 1
#[arg(long)]
pub requirement_1: Option<u64>,
/// Required flow R_2 for commodity 2
#[arg(long)]
pub requirement_2: Option<u64>,
/// Item sizes for BinPacking (comma-separated, e.g., "3,3,2,2")
#[arg(long)]
pub sizes: Option<String>,
Expand Down
115 changes: 115 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
args.graph.is_none()
&& args.weights.is_none()
&& args.edge_weights.is_none()
&& args.capacities.is_none()
&& args.source.is_none()
&& args.sink.is_none()
&& args.num_paths_required.is_none()
Expand All @@ -44,6 +45,12 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.seed.is_none()
&& args.positions.is_none()
&& args.radius.is_none()
&& args.source_1.is_none()
&& args.sink_1.is_none()
&& args.source_2.is_none()
&& args.sink_2.is_none()
&& args.requirement_1.is_none()
&& args.requirement_2.is_none()
&& args.sizes.is_none()
&& args.capacity.is_none()
&& args.sequence.is_none()
Expand Down Expand Up @@ -199,11 +206,13 @@ fn create_from_example(args: &CreateArgs, out: &OutputConfig) -> Result<()> {

fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
match type_name {
"SimpleGraph" => "edge list: 0-1,1-2,2-3",
"G" => match graph_type {
Some("KingsSubgraph" | "TriangularSubgraph") => "integer positions: \"0,0;1,0;1,1\"",
Some("UnitDiskGraph") => "float positions: \"0.0,0.0;1.0,0.0\"",
_ => "edge list: 0-1,1-2,2-3",
},
"Vec<u64>" => "comma-separated integers: 1,1,2",
"Vec<W>" => "comma-separated: 1,2,3",
"Vec<CNFClause>" => "semicolon-separated clauses: \"1,2;-1,3\"",
"Vec<Vec<W>>" => "semicolon-separated rows: \"1,0.5;0.5,2\"",
Expand Down Expand Up @@ -246,6 +255,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
},
"GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3",
"HamiltonianPath" => "--graph 0-1,1-2,2-3",
"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"
},
"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 @@ -485,6 +497,57 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
(ser(HamiltonianPath::new(graph))?, resolved_variant.clone())
}

// UndirectedTwoCommodityIntegralFlow (graph + capacities + terminals + requirements)
"UndirectedTwoCommodityIntegralFlow" => {
let usage = "Usage: 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";
let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
let capacities = parse_capacities(args, graph.num_edges(), usage)?;
let num_vertices = graph.num_vertices();
let source_1 = args.source_1.ok_or_else(|| {
anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --source-1\n\n{usage}")
})?;
let sink_1 = args.sink_1.ok_or_else(|| {
anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --sink-1\n\n{usage}")
})?;
let source_2 = args.source_2.ok_or_else(|| {
anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --source-2\n\n{usage}")
})?;
let sink_2 = args.sink_2.ok_or_else(|| {
anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --sink-2\n\n{usage}")
})?;
let requirement_1 = args.requirement_1.ok_or_else(|| {
anyhow::anyhow!(
"UndirectedTwoCommodityIntegralFlow requires --requirement-1\n\n{usage}"
)
})?;
let requirement_2 = args.requirement_2.ok_or_else(|| {
anyhow::anyhow!(
"UndirectedTwoCommodityIntegralFlow requires --requirement-2\n\n{usage}"
)
})?;
for (label, vertex) in [
("source-1", source_1),
("sink-1", sink_1),
("source-2", source_2),
("sink-2", sink_2),
] {
validate_vertex_index(label, vertex, num_vertices, usage)?;
}
(
ser(UndirectedTwoCommodityIntegralFlow::new(
graph,
capacities,
source_1,
sink_1,
source_2,
sink_2,
requirement_1,
requirement_2,
))?,
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 @@ -1523,6 +1586,58 @@ fn parse_edge_weights(args: &CreateArgs, num_edges: usize) -> Result<Vec<i32>> {
}
}

fn validate_vertex_index(
label: &str,
vertex: usize,
num_vertices: usize,
usage: &str,
) -> Result<()> {
if vertex < num_vertices {
return Ok(());
}

bail!("{label} must be less than num_vertices ({num_vertices})\n\n{usage}");
}

/// Parse `--capacities` as edge capacities (u64).
fn parse_capacities(args: &CreateArgs, num_edges: usize, usage: &str) -> Result<Vec<u64>> {
let capacities = args.capacities.as_deref().ok_or_else(|| {
anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --capacities\n\n{usage}")
})?;
let capacities: Vec<u64> = capacities
.split(',')
.map(|s| {
let trimmed = s.trim();
trimmed
.parse::<u64>()
.with_context(|| format!("Invalid capacity `{trimmed}`\n\n{usage}"))
})
.collect::<Result<Vec<_>>>()?;
if capacities.len() != num_edges {
bail!(
"Expected {} capacities but got {}\n\n{}",
num_edges,
capacities.len(),
usage
);
}
for (edge_index, &capacity) in capacities.iter().enumerate() {
let fits = usize::try_from(capacity)
.ok()
.and_then(|value| value.checked_add(1))
.is_some();
if !fits {
bail!(
"capacity {} at edge index {} is too large for this platform\n\n{}",
capacity,
edge_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
8 changes: 8 additions & 0 deletions problemreductions-cli/src/problem_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,14 @@ mod tests {
assert_eq!(spec.variant_values, vec!["SimpleGraph", "f64"]);
}

#[test]
fn test_resolve_alias_pass_through_undirected_two_commodity_integral_flow() {
assert_eq!(
resolve_alias("UndirectedTwoCommodityIntegralFlow"),
"UndirectedTwoCommodityIntegralFlow"
);
}

#[test]
fn test_parse_problem_spec_ksat_alias() {
let spec = parse_problem_spec("KSAT").unwrap();
Expand Down
Loading
Loading