Skip to content

Commit 9803205

Browse files
authored
Merge pull request #623 from CodingThrust/issue-255-steiner-tree
Fix #255: Add SteinerTreeInGraphs model
1 parent 3618171 commit 9803205

9 files changed

Lines changed: 669 additions & 6 deletions

File tree

docs/paper/reductions.typ

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
"Partition": [Partition],
117117
"MinimumFeedbackArcSet": [Minimum Feedback Arc Set],
118118
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
119+
"SteinerTreeInGraphs": [Steiner Tree in Graphs],
119120
"MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets],
120121
"MultipleChoiceBranching": [Multiple Choice Branching],
121122
"PartitionIntoPathsOfLength2": [Partition into Paths of Length 2],
@@ -1518,6 +1519,58 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
15181519
*Example.* Consider the graph $G$ with $n = 9$ vertices and edges ${0,1}, {1,2}, {3,4}, {4,5}, {6,7}, {7,8}$ (plus cross-edges ${0,3}, {2,5}, {3,6}, {5,8}$). Setting $q = 3$, the partition $V_1 = {0,1,2}$, $V_2 = {3,4,5}$, $V_3 = {6,7,8}$ is valid: $V_1$ contains edges ${0,1}, {1,2}$ (path $0 dash.em 1 dash.em 2$), $V_2$ contains ${3,4}, {4,5}$, and $V_3$ contains ${6,7}, {7,8}$.
15191520
]
15201521

1522+
#{
1523+
let x = load-model-example("SteinerTreeInGraphs")
1524+
let nv = graph-num-vertices(x.instance)
1525+
let edges = x.instance.graph.edges
1526+
let ne = edges.len()
1527+
let terminals = x.instance.terminals
1528+
let weights = x.instance.edge_weights
1529+
let sol = (config: x.optimal_config, metric: x.optimal_value)
1530+
let opt-weight = sol.metric.Valid
1531+
// Derive tree edges from optimal config
1532+
let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i)
1533+
let tree-edges = tree-edge-indices.map(i => edges.at(i))
1534+
// Steiner vertices: non-terminal vertices that appear in tree edges
1535+
let steiner-verts = range(nv).filter(v => not terminals.contains(v) and tree-edges.any(e => e.at(0) == v or e.at(1) == v))
1536+
[
1537+
#problem-def("SteinerTreeInGraphs")[
1538+
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)$.
1539+
][
1540+
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.
1541+
1542+
*Example.* Consider a graph $G$ with $n = #nv$ vertices and $|E| = #ne$ edges. The terminals are $R = {#terminals.map(i => $v_#i$).join(", ")}$ (blue). The optimal Steiner tree uses Steiner vertex #steiner-verts.map(i => $v_#i$).join(", ") (gray, dashed border) and edges #tree-edges.map(e => [$\{v_#(e.at(0)), v_#(e.at(1))\}$]).join(", ") with total weight #tree-edge-indices.map(i => str(weights.at(i))).join(" + ") $= #opt-weight$.
1543+
1544+
#figure({
1545+
// Graph: 6 vertices arranged in two rows (layout positions)
1546+
let verts = ((0, 1), (1.5, 1), (3, 1), (1.5, -0.5), (3, -0.5), (4.5, 0.25))
1547+
canvas(length: 1cm, {
1548+
// Draw edges
1549+
for (idx, (u, v)) in edges.enumerate() {
1550+
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))
1551+
g-edge(verts.at(u), verts.at(v),
1552+
stroke: if on-tree { 2pt + graph-colors.at(0) } else { 1pt + luma(200) })
1553+
let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2
1554+
let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2
1555+
draw.content((mx, my), text(7pt, fill: luma(80))[#weights.at(idx)])
1556+
}
1557+
// Draw vertices
1558+
for (k, pos) in verts.enumerate() {
1559+
let is-terminal = terminals.contains(k)
1560+
let is-steiner = steiner-verts.contains(k)
1561+
g-node(pos, name: "v" + str(k),
1562+
fill: if is-terminal { graph-colors.at(0) } else if is-steiner { luma(220) } else { white },
1563+
stroke: if is-steiner { (dash: "dashed", paint: graph-colors.at(0)) } else { 1pt + black },
1564+
label: if is-terminal { text(fill: white)[$v_#k$] } else { [$v_#k$] })
1565+
}
1566+
})
1567+
},
1568+
caption: [Steiner Tree: terminals $R = {#terminals.map(i => $v_#i$).join(", ")}$ (blue), Steiner vertex #steiner-verts.map(i => $v_#i$).join(", ") (dashed). Optimal tree (blue edges) has weight #opt-weight.],
1569+
) <fig:steiner-tree-example>
1570+
]
1571+
]
1572+
}
1573+
15211574
#{
15221575
let x = load-model-example("MinimumSumMulticenter")
15231576
let nv = graph-num-vertices(x.instance)

docs/paper/references.bib

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,37 @@ @article{cygan2014
793793
doi = {10.1137/140990255}
794794
}
795795

796+
@article{dreyfuswagner1971,
797+
author = {Stuart E. Dreyfus and Robert A. Wagner},
798+
title = {The Steiner Problem in Graphs},
799+
journal = {Networks},
800+
volume = {1},
801+
number = {3},
802+
pages = {195--207},
803+
year = {1971},
804+
doi = {10.1002/net.3230010302}
805+
}
806+
807+
@inproceedings{bjorklund2007,
808+
author = {Andreas Bj\"{o}rklund and Thore Husfeldt and Petteri Kaski and Mikko Koivisto},
809+
title = {Fourier Meets M\"{o}bius: Fast Subset Convolution},
810+
booktitle = {Proceedings of the 39th ACM Symposium on Theory of Computing (STOC)},
811+
pages = {67--74},
812+
year = {2007},
813+
doi = {10.1145/1250790.1250801}
814+
}
815+
816+
@inproceedings{nederlof2009,
817+
author = {Jesper Nederlof},
818+
title = {Fast Polynomial-Space Algorithms Using {M\"{o}bius} Inversion: Improving on {Steiner} Tree and Related Problems},
819+
booktitle = {Proceedings of the 36th International Colloquium on Automata, Languages and Programming (ICALP)},
820+
series = {LNCS},
821+
volume = {5555},
822+
pages = {713--725},
823+
year = {2009},
824+
doi = {10.1007/978-3-642-02927-1_59}
825+
}
826+
796827
@article{johnson1983,
797828
author = {David S. Johnson and Kenneth A. Niemi},
798829
title = {On Knapsacks, Partitions, and a New Dynamic Programming Technique for Trees},

problemreductions-cli/src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ Flags by problem type:
265265
LCS --strings, --bound [--alphabet-size]
266266
FAS --arcs [--weights] [--num-vertices]
267267
FVS --arcs [--weights] [--num-vertices]
268+
SteinerTreeInGraphs --graph, --edge-weights, --terminals
268269
PartitionIntoPathsOfLength2 --graph
269270
ResourceConstrainedScheduling --num-processors, --resource-bounds, --resource-requirements, --deadline
270271
PartiallyOrderedKnapsack --sizes, --values, --capacity, --precedences

problemreductions-cli/src/commands/create.rs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use problemreductions::models::algebraic::{
1313
use problemreductions::models::graph::{
1414
GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath,
1515
LengthBoundedDisjointPaths, MinimumCutIntoBoundedSets, MinimumMultiwayCut,
16-
MultipleChoiceBranching, SteinerTree, StrongConnectivityAugmentation,
16+
MultipleChoiceBranching, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation,
1717
};
1818
use problemreductions::models::misc::{
1919
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery,
@@ -389,6 +389,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
389389
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
390390
"--graph 0-1,1-2,2-3 --edge-weights 1,1,1"
391391
}
392+
"SteinerTreeInGraphs" => "--graph 0-1,1-2,2-3 --edge-weights 1,1,1 --terminals 0,3",
392393
"BiconnectivityAugmentation" => {
393394
"--graph 0-1,1-2,2-3 --potential-edges 0-2:3,0-3:4,1-3:2 --budget 5"
394395
}
@@ -1209,6 +1210,21 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
12091210
(data, resolved_variant.clone())
12101211
}
12111212

1213+
// SteinerTreeInGraphs (graph + edge weights + terminals)
1214+
"SteinerTreeInGraphs" => {
1215+
let (graph, _) = parse_graph(args).map_err(|e| {
1216+
anyhow::anyhow!(
1217+
"{e}\n\nUsage: pred create SteinerTreeInGraphs --graph 0-1,1-2,2-3 --terminals 0,3 [--edge-weights 1,1,1]"
1218+
)
1219+
})?;
1220+
let edge_weights = parse_edge_weights(args, graph.num_edges())?;
1221+
let terminals = parse_terminals(args, graph.num_vertices())?;
1222+
(
1223+
ser(SteinerTreeInGraphs::new(graph, terminals, edge_weights))?,
1224+
resolved_variant.clone(),
1225+
)
1226+
}
1227+
12121228
// RuralPostman
12131229
"RuralPostman" => {
12141230
reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?;
@@ -4361,6 +4377,25 @@ fn create_random(
43614377
(data, variant)
43624378
}
43634379

4380+
// SteinerTreeInGraphs
4381+
"SteinerTreeInGraphs" => {
4382+
let edge_prob = args.edge_prob.unwrap_or(0.5);
4383+
if !(0.0..=1.0).contains(&edge_prob) {
4384+
bail!("--edge-prob must be between 0.0 and 1.0");
4385+
}
4386+
let graph = util::create_random_graph(num_vertices, edge_prob, args.seed);
4387+
let num_edges = graph.num_edges();
4388+
let edge_weights = vec![1i32; num_edges];
4389+
// Use first half of vertices as terminals (at least 2)
4390+
let num_terminals = std::cmp::max(2, num_vertices / 2);
4391+
let terminals: Vec<usize> = (0..num_terminals).collect();
4392+
let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]);
4393+
(
4394+
ser(SteinerTreeInGraphs::new(graph, terminals, edge_weights))?,
4395+
variant,
4396+
)
4397+
}
4398+
43644399
// SteinerTree
43654400
"SteinerTree" => {
43664401
anyhow::ensure!(
@@ -4441,7 +4476,7 @@ fn create_random(
44414476
"Random generation is not supported for {canonical}. \
44424477
Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \
44434478
MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, TravelingSalesman, \
4444-
HamiltonianCircuit, SteinerTree, OptimalLinearArrangement, HamiltonianPath, GeneralizedHex)"
4479+
SteinerTreeInGraphs, HamiltonianCircuit, SteinerTree, OptimalLinearArrangement, HamiltonianPath, GeneralizedHex)"
44454480
),
44464481
};
44474482

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ pub mod prelude {
5656
MinimumCutIntoBoundedSets, MinimumDominatingSet, MinimumFeedbackArcSet,
5757
MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover,
5858
MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement,
59-
PartitionIntoPathsOfLength2, PartitionIntoTriangles, RuralPostman, TravelingSalesman,
60-
UndirectedTwoCommodityIntegralFlow,
59+
PartitionIntoPathsOfLength2, PartitionIntoTriangles, RuralPostman, SteinerTreeInGraphs,
60+
TravelingSalesman, UndirectedTwoCommodityIntegralFlow,
6161
};
6262
pub use crate::models::misc::{
6363
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation,

src/models/graph/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
//! - [`HamiltonianPath`]: Hamiltonian path (simple path visiting every vertex)
2323
//! - [`PartitionIntoPathsOfLength2`]: Partition vertices into triples with at least two edges each
2424
//! - [`BicliqueCover`]: Biclique cover on bipartite graphs
25+
//! - [`SteinerTreeInGraphs`]: Minimum weight Steiner tree connecting terminal vertices
2526
//! - [`BalancedCompleteBipartiteSubgraph`]: Balanced biclique decision problem
2627
//! - [`BiconnectivityAugmentation`]: Biconnectivity augmentation with weighted potential edges
2728
//! - [`BoundedComponentSpanningForest`]: Partition vertices into bounded-weight connected components
@@ -71,6 +72,7 @@ pub(crate) mod partition_into_triangles;
7172
pub(crate) mod rural_postman;
7273
pub(crate) mod spin_glass;
7374
pub(crate) mod steiner_tree;
75+
pub(crate) mod steiner_tree_in_graphs;
7476
pub(crate) mod strong_connectivity_augmentation;
7577
pub(crate) mod subgraph_isomorphism;
7678
pub(crate) mod traveling_salesman;
@@ -109,6 +111,7 @@ pub use partition_into_triangles::PartitionIntoTriangles;
109111
pub use rural_postman::RuralPostman;
110112
pub use spin_glass::SpinGlass;
111113
pub use steiner_tree::SteinerTree;
114+
pub use steiner_tree_in_graphs::SteinerTreeInGraphs;
112115
pub use strong_connectivity_augmentation::StrongConnectivityAugmentation;
113116
pub use subgraph_isomorphism::SubgraphIsomorphism;
114117
pub use traveling_salesman::TravelingSalesman;
@@ -146,6 +149,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
146149
specs.extend(partition_into_triangles::canonical_model_example_specs());
147150
specs.extend(partition_into_paths_of_length_2::canonical_model_example_specs());
148151
specs.extend(steiner_tree::canonical_model_example_specs());
152+
specs.extend(steiner_tree_in_graphs::canonical_model_example_specs());
149153
specs.extend(directed_two_commodity_integral_flow::canonical_model_example_specs());
150154
specs.extend(undirected_two_commodity_integral_flow::canonical_model_example_specs());
151155
specs.extend(strong_connectivity_augmentation::canonical_model_example_specs());

0 commit comments

Comments
 (0)