Skip to content

Commit d922f80

Browse files
committed
2 parents b951c22 + 54e54c2 commit d922f80

22 files changed

Lines changed: 1449 additions & 31 deletions

File tree

.github/workflows/docs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ jobs:
4141
- name: Build mdBook
4242
run: mdbook build
4343

44-
- name: Build examples and PDF
45-
run: make examples && typst compile --root docs docs/paper/reductions.typ book/reductions.pdf
44+
- name: Build PDF
45+
run: typst compile --root . docs/paper/reductions.typ book/reductions.pdf
4646

4747
- name: Build rustdoc
4848
run: RUSTDOCFLAGS="--default-theme=dark" cargo doc --features ilp-highs --no-deps

docs/paper/reductions.typ

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"MaximumClique": [Maximum Clique],
7676
"MaximumSetPacking": [Maximum Set Packing],
7777
"MinimumSetCovering": [Minimum Set Covering],
78+
"SetBasis": [Set Basis],
7879
"SpinGlass": [Spin Glass],
7980
"QUBO": [QUBO],
8081
"ILP": [Integer Linear Programming],
@@ -100,6 +101,7 @@
100101
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
101102
"ShortestCommonSupersequence": [Shortest Common Supersequence],
102103
"MinimumSumMulticenter": [Minimum Sum Multicenter],
104+
"SteinerTree": [Steiner Tree],
103105
"SubgraphIsomorphism": [Subgraph Isomorphism],
104106
"PartitionIntoTriangles": [Partition Into Triangles],
105107
"FlowShopScheduling": [Flow Shop Scheduling],
@@ -786,6 +788,72 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co
786788
]
787789
]
788790
}
791+
#{
792+
let x = load-model-example("SteinerTree")
793+
let nv = graph-num-vertices(x.instance)
794+
let ne = graph-num-edges(x.instance)
795+
let edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1)))
796+
let weights = x.instance.edge_weights
797+
let terminals = x.instance.terminals
798+
let sol = x.optimal.at(0)
799+
let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i)
800+
let tree-edges = tree-edge-indices.map(i => edges.at(i))
801+
let cost = sol.metric.Valid
802+
// Steiner vertices: in tree but not terminals
803+
let tree-verts = tree-edges.map(e => (e.at(0), e.at(1))).fold((), (acc, pair) => {
804+
let (u, v) = pair
805+
let acc2 = if acc.contains(u) { acc } else { acc + (u,) }
806+
if acc2.contains(v) { acc2 } else { acc2 + (v,) }
807+
})
808+
let steiner-verts = tree-verts.filter(v => not terminals.contains(v))
809+
[
810+
#problem-def("SteinerTree")[
811+
Given an undirected graph $G = (V, E)$ with edge weights $w: E -> RR_(>= 0)$ and a set of terminal vertices $T subset.eq V$ with $|T| >= 2$, find a tree $S = (V_S, E_S)$ in $G$ such that $T subset.eq V_S$, minimizing $sum_(e in E_S) w(e)$. Vertices in $V_S backslash T$ are called _Steiner vertices_.
812+
][
813+
One of Karp's 21 NP-complete problems @karp1972, foundational in network design with applications in telecommunications backbone routing, VLSI chip interconnect, pipeline planning, and phylogenetic tree construction. When $T = V$, the problem reduces to the minimum spanning tree (polynomial). The NP-hardness arises from choosing which Steiner vertices to include.
814+
815+
The best known exact algorithm runs in $O^*(3^(|T|) dot n + 2^(|T|) dot n^2)$ time via Dreyfus--Wagner dynamic programming over terminal subsets @dreyfuswagner1971. Byrka _et al._ achieved a $ln(4) + epsilon approx 1.39$-approximation @byrka2013; the classic 2-approximation uses the minimum spanning tree of the terminal distance graph.
816+
817+
// Find the unique direct terminal-terminal edge (both endpoints in T, not in the optimal tree)
818+
#let terminal-set = terminals
819+
#let direct-tt-edges = edges.enumerate().filter(((i, e)) => {
820+
terminal-set.contains(e.at(0)) and terminal-set.contains(e.at(1)) and not tree-edge-indices.contains(i)
821+
})
822+
#let tt-edge = direct-tt-edges.at(0)
823+
#let tt-idx = tt-edge.at(0)
824+
#let tt-u = tt-edge.at(1).at(0)
825+
#let tt-v = tt-edge.at(1).at(1)
826+
827+
*Example.* Consider $G$ with $n = #nv$ vertices, $m = #ne$ edges, and terminals $T = {#terminals.map(t => $v_#t$).join(", ")}$. The optimal Steiner tree uses edges ${#tree-edges.map(e => $(v_#(e.at(0)), v_#(e.at(1)))$).join(", ")}$ with Steiner vertices ${#steiner-verts.map(v => $v_#v$).join(", ")}$ acting as relay points. The total cost is #tree-edge-indices.map(i => $#(weights.at(i))$).join($+$) $= #cost$. Note the only direct terminal--terminal edge $(v_#tt-u, v_#tt-v)$ has weight #weights.at(tt-idx), equaling the entire Steiner tree cost.
828+
829+
#figure({
830+
// Layout: v0 top-left, v1 top-center, v2 top-right, v3 bottom-center, v4 bottom-right
831+
let verts = ((0, 1.2), (1.2, 1.2), (2.4, 1.2), (1.2, 0), (2.4, 0))
832+
canvas(length: 1cm, {
833+
for (idx, (u, v)) in edges.enumerate() {
834+
let on-tree = tree-edge-indices.contains(idx)
835+
g-edge(verts.at(u), verts.at(v),
836+
stroke: if on-tree { 2pt + graph-colors.at(0) } else { 1pt + luma(200) })
837+
let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2
838+
let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2
839+
let dx = if u == 0 and v == 3 { -0.3 } else if u == 2 and v == 3 { 0.3 } else { 0 }
840+
let dy = if u == 0 and v == 1 { 0.2 } else if u == 1 and v == 2 { 0.2 } else if u == 2 and v == 4 { 0.3 } else { 0 }
841+
draw.content((mx + dx, my + dy), text(7pt, fill: luma(80))[#weights.at(idx)])
842+
}
843+
for (k, pos) in verts.enumerate() {
844+
let is-terminal = terminals.contains(k)
845+
g-node(pos, name: "v" + str(k),
846+
fill: if is-terminal { graph-colors.at(0) } else { white },
847+
stroke: if is-terminal { none } else { 1pt + graph-colors.at(0) },
848+
label: text(fill: if is-terminal { white } else { black })[$v_#k$])
849+
}
850+
})
851+
},
852+
caption: [Steiner tree on #nv vertices with terminals $T = {#terminals.map(t => $v_#t$).join(", ")}$ (filled blue). Steiner vertices #steiner-verts.map(v => $v_#v$).join(", ") (outlined) relay connections. Blue edges form the optimal tree with cost #cost.],
853+
) <fig:steiner-tree>
854+
]
855+
]
856+
}
789857
#problem-def("OptimalLinearArrangement")[
790858
Given an undirected graph $G=(V,E)$ and a non-negative integer $K$, is there a bijection $f: V -> {0, 1, dots, |V|-1}$ such that $sum_({u,v} in E) |f(u) - f(v)| <= K$?
791859
][
@@ -1040,6 +1108,43 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS
10401108
]
10411109
}
10421110

1111+
#{
1112+
let x = load-model-example("SetBasis")
1113+
let coll = x.instance.collection
1114+
let m = coll.len()
1115+
let U-size = x.instance.universe_size
1116+
let k = x.instance.k
1117+
let sample = x.samples.at(0)
1118+
let sat-count = x.optimal.len()
1119+
let basis = range(k).map(i =>
1120+
range(U-size).filter(j => sample.config.at(i * U-size + j) == 1)
1121+
)
1122+
let fmt-set(s) = "${" + s.map(e => str(e + 1)).join(", ") + "}$"
1123+
[
1124+
#problem-def("SetBasis")[
1125+
Given finite set $S$, collection $cal(C)$ of subsets of $S$, and integer $k$, does there exist a family $cal(B) = {B_1, ..., B_k}$ with each $B_i subset.eq S$ such that for every $C in cal(C)$ there exists $cal(B)_C subset.eq cal(B)$ with $union.big_(B in cal(B)_C) B = C$?
1126+
][
1127+
The Set Basis problem was shown NP-complete by Stockmeyer @stockmeyer1975setbasis and appears as SP7 in Garey & Johnson @garey1979. It asks for an exact union-based description of a family of sets, unlike Set Cover which only requires covering the underlying universe. Applications include data compression, database schema design, and Boolean function minimization. The library's decision encoding uses $k |S|$ membership bits, so brute-force over those bits gives an $O^*(2^(k |S|))$ exact algorithm#footnote[This is the direct search bound induced by the encoding implemented here; we are not aware of a faster general exact worst-case algorithm for this representation.].
1128+
1129+
*Example.* Let $S = {1, 2, 3, 4}$, $k = #k$, and $cal(C) = {#range(m).map(i => $C_#(i + 1)$).join(", ")}$ with #coll.enumerate().map(((i, s)) => $C_#(i + 1) = #fmt-set(s)$).join(", "). The sample basis from the issue is $cal(B) = {#range(k).map(i => $B_#(i + 1)$).join(", ")}$ with #basis.enumerate().map(((i, s)) => $B_#(i + 1) = #fmt-set(s)$).join(", "). Then $C_1 = B_1 union B_2$, $C_2 = B_2 union B_3$, $C_3 = B_1 union B_3$, and $C_4 = B_1 union B_2 union B_3$. There are #sat-count satisfying encodings in total: the singleton basis can be permuted in $3! = 6$ ways, and the three pair sets $C_1, C_2, C_3$ also form a basis with another six row permutations.
1130+
1131+
#figure(
1132+
canvas(length: 1cm, {
1133+
let elems = ((-0.9, 0.2), (0.0, -0.5), (0.9, 0.2), (1.8, -0.5))
1134+
for i in range(k) {
1135+
let positions = basis.at(i).map(e => elems.at(e))
1136+
sregion(positions, pad: 0.28, label: [$B_#(i + 1)$], ..sregion-selected)
1137+
}
1138+
for (idx, pos) in elems.enumerate() {
1139+
selem(pos, label: [#(idx + 1)], fill: if idx < 3 { black } else { luma(160) })
1140+
}
1141+
}),
1142+
caption: [Set Basis example: the singleton basis $cal(B) = {#range(k).map(i => $B_#(i + 1)$).join(", ")}$ reconstructs every target set in $cal(C)$; element $4$ is unused by the target family.],
1143+
) <fig:set-basis>
1144+
]
1145+
]
1146+
}
1147+
10431148
== Optimization Problems
10441149

10451150
#{

docs/paper/references.bib

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,28 @@ @article{alber2004
459459
doi = {10.1016/j.jalgor.2003.10.001}
460460
}
461461

462+
@article{dreyfuswagner1971,
463+
author = {S. E. Dreyfus and R. A. Wagner},
464+
title = {The Steiner Problem in Graphs},
465+
journal = {Networks},
466+
volume = {1},
467+
number = {3},
468+
pages = {195--207},
469+
year = {1971},
470+
doi = {10.1002/net.3230010302}
471+
}
472+
473+
@article{byrka2013,
474+
author = {Jarosław Byrka and Fabrizio Grandoni and Thomas Rothvoß and Laura Sanità},
475+
title = {Steiner Tree Approximation via Iterative Randomized Rounding},
476+
journal = {Journal of the ACM},
477+
volume = {60},
478+
number = {1},
479+
pages = {1--33},
480+
year = {2013},
481+
doi = {10.1145/2432622.2432628}
482+
}
483+
462484
@article{horowitz1974,
463485
author = {Ellis Horowitz and Sartaj Sahni},
464486
title = {Computing Partitions with Applications to the Knapsack Problem},
@@ -540,6 +562,15 @@ @article{arora2009
540562
doi = {10.1145/1502793.1502794}
541563
}
542564

565+
@techreport{stockmeyer1975setbasis,
566+
author = {Larry J. Stockmeyer},
567+
title = {The Set Basis Problem Is NP-Complete},
568+
institution = {IBM Thomas J. Watson Research Center},
569+
number = {RC 5431},
570+
address = {Yorktown Heights, New York},
571+
year = {1975}
572+
}
573+
543574
@article{cygan2014,
544575
author = {Marek Cygan and Daniel Lokshtanov and Marcin Pilipczuk and Micha{\l} Pilipczuk and Saket Saurabh},
545576
title = {Minimum Bisection Is Fixed Parameter Tractable},

docs/src/cli.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ pred create MIS --graph 0-1,1-2,2-3 -o problem.json
4545
# Create a weighted instance (variant auto-upgrades to i32)
4646
pred create MIS --graph 0-1,1-2,2-3 --weights 3,1,2,1 -o weighted.json
4747

48+
# Create a Steiner Tree instance
49+
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
50+
4851
# Or start from a canonical model example
4952
pred create --example MIS/SimpleGraph/i32 -o example.json
5053

@@ -272,6 +275,7 @@ pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json
272275
pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json
273276
pred create SpinGlass --graph 0-1,1-2 -o sg.json
274277
pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json
278+
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
275279
pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json
276280
pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json
277281
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" -o x3c.json

docs/src/reductions/problem_schemas.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,27 @@
594594
}
595595
]
596596
},
597+
{
598+
"name": "SetBasis",
599+
"description": "Determine whether a collection of sets admits a basis of size k under union",
600+
"fields": [
601+
{
602+
"name": "universe_size",
603+
"type_name": "usize",
604+
"description": "Size of the ground set S"
605+
},
606+
{
607+
"name": "collection",
608+
"type_name": "Vec<Vec<usize>>",
609+
"description": "Collection C of target subsets of S"
610+
},
611+
{
612+
"name": "k",
613+
"type_name": "usize",
614+
"description": "Required number of basis sets"
615+
}
616+
]
617+
},
597618
{
598619
"name": "ShortestCommonSupersequence",
599620
"description": "Find a common supersequence of bounded length for a set of strings",
@@ -636,6 +657,27 @@
636657
}
637658
]
638659
},
660+
{
661+
"name": "SteinerTree",
662+
"description": "Find minimum weight tree connecting terminal vertices",
663+
"fields": [
664+
{
665+
"name": "graph",
666+
"type_name": "G",
667+
"description": "The underlying graph G=(V,E)"
668+
},
669+
{
670+
"name": "edge_weights",
671+
"type_name": "Vec<W>",
672+
"description": "Edge weights w: E -> R"
673+
},
674+
{
675+
"name": "terminals",
676+
"type_name": "Vec<usize>",
677+
"description": "Terminal vertices T that must be connected"
678+
}
679+
]
680+
},
639681
{
640682
"name": "SubgraphIsomorphism",
641683
"description": "Determine if host graph G contains a subgraph isomorphic to pattern graph H",

docs/src/reductions/reduction_graph.json

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,13 @@
464464
"doc_path": "models/formula/struct.Satisfiability.html",
465465
"complexity": "2^num_variables"
466466
},
467+
{
468+
"name": "SetBasis",
469+
"variant": {},
470+
"category": "set",
471+
"doc_path": "models/set/struct.SetBasis.html",
472+
"complexity": "2^(basis_size * universe_size)"
473+
},
467474
{
468475
"name": "ShortestCommonSupersequence",
469476
"variant": {},
@@ -491,6 +498,26 @@
491498
"doc_path": "models/graph/struct.SpinGlass.html",
492499
"complexity": "2^num_spins"
493500
},
501+
{
502+
"name": "SteinerTree",
503+
"variant": {
504+
"graph": "SimpleGraph",
505+
"weight": "One"
506+
},
507+
"category": "graph",
508+
"doc_path": "models/graph/struct.SteinerTree.html",
509+
"complexity": "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2"
510+
},
511+
{
512+
"name": "SteinerTree",
513+
"variant": {
514+
"graph": "SimpleGraph",
515+
"weight": "i32"
516+
},
517+
"category": "graph",
518+
"doc_path": "models/graph/struct.SteinerTree.html",
519+
"complexity": "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2"
520+
},
494521
{
495522
"name": "SubgraphIsomorphism",
496523
"variant": {},
@@ -549,7 +576,7 @@
549576
},
550577
{
551578
"source": 4,
552-
"target": 54,
579+
"target": 55,
553580
"overhead": [
554581
{
555582
"field": "num_spins",
@@ -713,7 +740,7 @@
713740
},
714741
{
715742
"source": 21,
716-
"target": 56,
743+
"target": 59,
717744
"overhead": [
718745
{
719746
"field": "num_elements",
@@ -769,7 +796,7 @@
769796
},
770797
{
771798
"source": 25,
772-
"target": 54,
799+
"target": 55,
773800
"overhead": [
774801
{
775802
"field": "num_spins",
@@ -1215,7 +1242,7 @@
12151242
},
12161243
{
12171244
"source": 49,
1218-
"target": 53,
1245+
"target": 54,
12191246
"overhead": [
12201247
{
12211248
"field": "num_spins",
@@ -1300,7 +1327,7 @@
13001327
"doc_path": "rules/sat_minimumdominatingset/index.html"
13011328
},
13021329
{
1303-
"source": 53,
1330+
"source": 54,
13041331
"target": 49,
13051332
"overhead": [
13061333
{
@@ -1311,7 +1338,7 @@
13111338
"doc_path": "rules/spinglass_qubo/index.html"
13121339
},
13131340
{
1314-
"source": 54,
1341+
"source": 55,
13151342
"target": 25,
13161343
"overhead": [
13171344
{
@@ -1326,8 +1353,8 @@
13261353
"doc_path": "rules/spinglass_maxcut/index.html"
13271354
},
13281355
{
1329-
"source": 54,
1330-
"target": 53,
1356+
"source": 55,
1357+
"target": 54,
13311358
"overhead": [
13321359
{
13331360
"field": "num_spins",
@@ -1341,7 +1368,7 @@
13411368
"doc_path": "rules/spinglass_casts/index.html"
13421369
},
13431370
{
1344-
"source": 57,
1371+
"source": 60,
13451372
"target": 12,
13461373
"overhead": [
13471374
{
@@ -1356,7 +1383,7 @@
13561383
"doc_path": "rules/travelingsalesman_ilp/index.html"
13571384
},
13581385
{
1359-
"source": 57,
1386+
"source": 60,
13601387
"target": 49,
13611388
"overhead": [
13621389
{

0 commit comments

Comments
 (0)