Skip to content
Open
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
41 changes: 41 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"ClosestVectorProblem": [Closest Vector Problem],
"SubsetSum": [Subset Sum],
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
"SteinerTreeInGraphs": [Steiner Tree in Graphs],
)

// Definition label: "def:<ProblemName>" — each definition block must have a matching label
Expand Down Expand Up @@ -570,6 +571,46 @@ caption: [A directed graph with FVS $S = {v_0}$ (blue, $w(S) = 1$). Removing $v_
) <fig:fvs-example>
]

#problem-def("SteinerTreeInGraphs")[
Given an undirected graph $G = (V, E)$ with edge weights $w: E -> RR_(>= 0)$ and a set of terminal vertices $R subset.eq V$, find a subtree $T$ of $G$ that spans all terminals in $R$ and minimizes the total edge weight $sum_(e in T) w(e)$.
][
A classical NP-complete problem from Karp's list (as "Steiner Tree in Graphs," Garey & Johnson ND12) @karp1972. Central to network design, VLSI layout, and phylogenetic reconstruction. The problem generalizes minimum spanning tree (where $R = V$) and shortest path (where $|R| = 2$). The Dreyfus--Wagner dynamic programming algorithm @dreyfuswagner1971 solves it in $O(3^k dot n + 2^k dot n^2 + n^3)$ time, where $k = |R|$ and $n = |V|$. Bjorklund et al. @bjorklund2007 achieved $O^*(2^k)$ using subset convolution over the Mobius algebra, and Nederlof @nederlof2009 gave an $O^*(2^k)$ polynomial-space algorithm.

*Example.* Consider a graph $G$ with $n = 6$ vertices and $|E| = 7$ edges. The terminals are $R = {v_0, v_3, v_5}$ (blue). The optimal Steiner tree uses Steiner vertex $v_2$ (gray, dashed border) and edges ${v_0, v_2}$, ${v_2, v_3}$, ${v_2, v_5}$ with total weight $2 + 1 + 2 = 5$. The direct path $v_0 -> v_1 -> v_3$ plus $v_3 -> v_4 -> v_5$ would cost $3 + 2 + 3 + 1 = 9$.

#figure({
// Graph: 6 vertices arranged in two rows
let verts = ((0, 1), (1.5, 1), (3, 1), (1.5, -0.5), (3, -0.5), (4.5, 0.25))
let edges = ((0, 1), (0, 2), (1, 3), (2, 3), (2, 5), (3, 4), (4, 5))
let weights = ("3", "2", "4", "1", "2", "3", "1")
let terminals = (0, 3, 5)
let steiner-verts = (2,)
let tree-edges = ((0, 2), (2, 3), (2, 5)) // optimal Steiner tree
canvas(length: 1cm, {
// Draw edges
for (idx, (u, v)) in edges.enumerate() {
let on-tree = tree-edges.any(t => (t.at(0) == u and t.at(1) == v) or (t.at(0) == v and t.at(1) == u))
g-edge(verts.at(u), verts.at(v),
stroke: if on-tree { 2pt + graph-colors.at(0) } else { 1pt + luma(200) })
let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2
let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2
draw.content((mx, my), text(7pt, fill: luma(80))[#weights.at(idx)])
}
// Draw vertices
for (k, pos) in verts.enumerate() {
let is-terminal = terminals.contains(k)
let is-steiner = steiner-verts.contains(k)
g-node(pos, name: "v" + str(k),
fill: if is-terminal { graph-colors.at(0) } else if is-steiner { luma(220) } else { white },
stroke: if is-steiner { (dash: "dashed", paint: graph-colors.at(0)) } else { 1pt + black },
label: if is-terminal { text(fill: white)[$v_#k$] } else { [$v_#k$] })
}
})
},
caption: [Steiner Tree: terminals $R = {v_0, v_3, v_5}$ (blue), Steiner vertex $v_2$ (dashed). Optimal tree (blue edges) has weight 5.],
) <fig:steiner-tree-example>
]

== Set Problems

#problem-def("MaximumSetPacking")[
Expand Down
31 changes: 31 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -456,3 +456,34 @@ @article{cygan2014
note = {Conference version: STOC 2014},
doi = {10.1137/140990255}
}

@article{dreyfuswagner1971,
author = {Stuart E. Dreyfus and Robert A. Wagner},
title = {The Steiner Problem in Graphs},
journal = {Networks},
volume = {1},
number = {3},
pages = {195--207},
year = {1971},
doi = {10.1002/net.3230010302}
}

@inproceedings{bjorklund2007,
author = {Andreas Bj\"{o}rklund and Thore Husfeldt and Petteri Kaski and Mikko Koivisto},
title = {Fourier Meets M\"{o}bius: Fast Subset Convolution},
booktitle = {Proceedings of the 39th ACM Symposium on Theory of Computing (STOC)},
pages = {67--74},
year = {2007},
doi = {10.1145/1250790.1250801}
}

@inproceedings{nederlof2009,
author = {Jesper Nederlof},
title = {Fast Polynomial-Space Algorithms Using {M\"{o}bius} Inversion: Improving on {Steiner} Tree and Related Problems},
booktitle = {Proceedings of the 36th International Colloquium on Automata, Languages and Programming (ICALP)},
series = {LNCS},
volume = {5555},
pages = {713--725},
year = {2009},
doi = {10.1007/978-3-642-02927-1_59}
}
4 changes: 4 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ Flags by problem type:
BMF --matrix (0/1), --rank
CVP --basis, --target-vec [--bounds]
FVS --arcs [--weights] [--num-vertices]
SteinerTreeInGraphs --graph, --edge-weights, --terminals
ILP, CircuitSAT (via reduction only)

Geometry graph variants (use slash notation, e.g., MIS/KingsSubgraph):
Expand Down Expand Up @@ -332,6 +333,9 @@ pub struct CreateArgs {
/// Directed arcs for directed graph problems (e.g., 0>1,1>2,2>0)
#[arg(long)]
pub arcs: Option<String>,
/// Terminal vertices for SteinerTreeInGraphs (comma-separated, e.g., 0,3,5)
#[arg(long)]
pub terminals: Option<String>,
}

#[derive(clap::Args)]
Expand Down
55 changes: 54 additions & 1 deletion problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.target_vec.is_none()
&& args.bounds.is_none()
&& args.arcs.is_none()
&& args.terminals.is_none()
}

fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
Expand Down Expand Up @@ -81,6 +82,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
"--graph 0-1,1-2,2-3 --edge-weights 1,1,1"
}
"SteinerTreeInGraphs" => "--graph 0-1,1-2,2-3 --edge-weights 1,1,1 --terminals 0,3",
"Satisfiability" => "--num-vars 3 --clauses \"1,2;-1,3\"",
"KSatisfiability" => "--num-vars 3 --clauses \"1,2,3;-1,2,-3\" --k 3",
"QUBO" => "--matrix \"1,0.5;0.5,2\"",
Expand Down Expand Up @@ -227,6 +229,21 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
(data, resolved_variant.clone())
}

// SteinerTreeInGraphs (graph + edge weights + terminals)
"SteinerTreeInGraphs" => {
let (graph, _) = parse_graph(args).map_err(|e| {
anyhow::anyhow!(
"{e}\n\nUsage: pred create SteinerTreeInGraphs --graph 0-1,1-2,2-3 --terminals 0,3 [--edge-weights 1,1,1]"
)
})?;
let edge_weights = parse_edge_weights(args, graph.num_edges())?;
let terminals = parse_terminals(args)?;
(
ser(SteinerTreeInGraphs::new(graph, terminals, edge_weights))?,
resolved_variant.clone(),
)
}

// KColoring
"KColoring" => {
let (graph, _) = parse_graph(args).map_err(|e| {
Expand Down Expand Up @@ -785,6 +802,23 @@ fn parse_fields_f64(args: &CreateArgs, num_vertices: usize) -> Result<Vec<f64>>
}
}

/// Parse `--terminals` as comma-separated terminal vertex indices.
fn parse_terminals(args: &CreateArgs) -> Result<Vec<usize>> {
let terminals_str = args.terminals.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"SteinerTreeInGraphs requires --terminals (comma-separated vertex indices, e.g., 0,3,5)"
)
})?;
terminals_str
.split(',')
.map(|s| {
s.trim()
.parse::<usize>()
.map_err(|e| anyhow::anyhow!("Invalid terminal vertex: {}", e))
})
.collect()
}

/// Parse `--clauses` as semicolon-separated clauses of comma-separated literals.
/// E.g., "1,2;-1,3;2,-3"
fn parse_clauses(args: &CreateArgs) -> Result<Vec<CNFClause>> {
Expand Down Expand Up @@ -994,6 +1028,25 @@ fn create_random(
(data, variant)
}

// SteinerTreeInGraphs
"SteinerTreeInGraphs" => {
let edge_prob = args.edge_prob.unwrap_or(0.5);
if !(0.0..=1.0).contains(&edge_prob) {
bail!("--edge-prob must be between 0.0 and 1.0");
}
let graph = util::create_random_graph(num_vertices, edge_prob, args.seed);
let num_edges = graph.num_edges();
let edge_weights = vec![1i32; num_edges];
// Use first half of vertices as terminals (at least 2)
let num_terminals = std::cmp::max(2, num_vertices / 2);
let terminals: Vec<usize> = (0..num_terminals).collect();
let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]);
(
ser(SteinerTreeInGraphs::new(graph, terminals, edge_weights))?,
variant,
)
}

// SpinGlass
"SpinGlass" => {
let edge_prob = args.edge_prob.unwrap_or(0.5);
Expand Down Expand Up @@ -1026,7 +1079,7 @@ fn create_random(
_ => bail!(
"Random generation is not supported for {canonical}. \
Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman)"
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, SteinerTreeInGraphs)"
),
};

Expand Down
2 changes: 2 additions & 0 deletions problemreductions-cli/src/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ pub fn load_problem(
"MaxCut" => deser_opt::<MaxCut<SimpleGraph, i32>>(data),
"MaximalIS" => deser_opt::<MaximalIS<SimpleGraph, i32>>(data),
"TravelingSalesman" => deser_opt::<TravelingSalesman<SimpleGraph, i32>>(data),
"SteinerTreeInGraphs" => deser_opt::<SteinerTreeInGraphs<SimpleGraph, i32>>(data),
"KColoring" => match variant.get("k").map(|s| s.as_str()) {
Some("K3") => deser_sat::<KColoring<K3, SimpleGraph>>(data),
_ => deser_sat::<KColoring<KN, SimpleGraph>>(data),
Expand Down Expand Up @@ -274,6 +275,7 @@ pub fn serialize_any_problem(
"MaxCut" => try_ser::<MaxCut<SimpleGraph, i32>>(any),
"MaximalIS" => try_ser::<MaximalIS<SimpleGraph, i32>>(any),
"TravelingSalesman" => try_ser::<TravelingSalesman<SimpleGraph, i32>>(any),
"SteinerTreeInGraphs" => try_ser::<SteinerTreeInGraphs<SimpleGraph, i32>>(any),
"KColoring" => match variant.get("k").map(|s| s.as_str()) {
Some("K3") => try_ser::<KColoring<K3, SimpleGraph>>(any),
_ => try_ser::<KColoring<KN, SimpleGraph>>(any),
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/problem_name.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ pub fn resolve_alias(input: &str) -> String {
"knapsack" => "Knapsack".to_string(),
"fvs" | "minimumfeedbackvertexset" => "MinimumFeedbackVertexSet".to_string(),
"subsetsum" => "SubsetSum".to_string(),
"steinertreeingraphs" => "SteinerTreeInGraphs".to_string(),
_ => input.to_string(), // pass-through for exact names
}
}
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ pub mod prelude {
pub use crate::models::graph::{BicliqueCover, GraphPartitioning, SpinGlass};
pub use crate::models::graph::{
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, TravelingSalesman,
MinimumDominatingSet, MinimumFeedbackVertexSet, MinimumVertexCover, SteinerTreeInGraphs,
TravelingSalesman,
};
pub use crate::models::misc::{BinPacking, Factoring, Knapsack, PaintShop, SubsetSum};
pub use crate::models::set::{MaximumSetPacking, MinimumSetCovering};
Expand Down
3 changes: 3 additions & 0 deletions src/models/graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
//! - [`TravelingSalesman`]: Traveling Salesman (minimum weight Hamiltonian cycle)
//! - [`SpinGlass`]: Ising model Hamiltonian
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs
//! - [`SteinerTreeInGraphs`]: Minimum weight Steiner tree connecting terminal vertices

pub(crate) mod biclique_cover;
pub(crate) mod graph_partitioning;
Expand All @@ -27,6 +28,7 @@ pub(crate) mod minimum_dominating_set;
pub(crate) mod minimum_feedback_vertex_set;
pub(crate) mod minimum_vertex_cover;
pub(crate) mod spin_glass;
pub(crate) mod steiner_tree_in_graphs;
pub(crate) mod traveling_salesman;

pub use biclique_cover::BicliqueCover;
Expand All @@ -41,4 +43,5 @@ pub use minimum_dominating_set::MinimumDominatingSet;
pub use minimum_feedback_vertex_set::MinimumFeedbackVertexSet;
pub use minimum_vertex_cover::MinimumVertexCover;
pub use spin_glass::SpinGlass;
pub use steiner_tree_in_graphs::SteinerTreeInGraphs;
pub use traveling_salesman::TravelingSalesman;
Loading
Loading