From c6e64513cce69a42e69a13bd1d72b553e8245b9b Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 06:46:39 +0800 Subject: [PATCH 1/9] Add plan for #249: [Model] KthBestSpanningTree --- .../2026-03-16-kth-best-spanning-tree.md | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 docs/plans/2026-03-16-kth-best-spanning-tree.md diff --git a/docs/plans/2026-03-16-kth-best-spanning-tree.md b/docs/plans/2026-03-16-kth-best-spanning-tree.md new file mode 100644 index 000000000..e367e375c --- /dev/null +++ b/docs/plans/2026-03-16-kth-best-spanning-tree.md @@ -0,0 +1,219 @@ +# KthBestSpanningTree Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `KthBestSpanningTree` satisfaction model, wire it into the registry/CLI/example database/paper, and implement the issue #249 example semantics. + +**Architecture:** Model `KthBestSpanningTree` as a `SimpleGraph` plus per-edge weights, a target multiplicity `k`, and a weight bound `B`. Each configuration is `k` consecutive binary edge-selection blocks of length `num_edges`; `evaluate()` accepts exactly those assignments where every block is a distinct spanning tree whose total weight is at most `B`. + +**Tech Stack:** Rust, serde, petgraph-backed `SimpleGraph`, registry macros (`inventory::submit!`, `declare_variants!`), CLI `pred create`, Typst paper. + +**Notes:** The issue comment thread already settled two design points that this plan follows: `dims()` uses `k * num_edges` binary variables, and the registry complexity uses the general-case bound `2^(num_edges * k)`. There is no open associated rule issue whose title mentions `KthBestSpanningTree`; carry that orphan-model warning into the PR summary instead of blocking execution. + +--- + +## Batch 1: Model, Registry, CLI, Tests + +### Task 1: Add failing model tests for the issue semantics + +**Files:** +- Create: `src/unit_tests/models/graph/kth_best_spanning_tree.rs` + +**Step 1: Write the failing test** + +Add focused tests that reference the new type before it exists: +- `test_kthbestspanningtree_creation` +- `test_kthbestspanningtree_evaluation_yes_instance` +- `test_kthbestspanningtree_evaluation_rejects_duplicate_trees` +- `test_kthbestspanningtree_evaluation_rejects_overweight_tree` +- `test_kthbestspanningtree_solver_yes_and_no_examples` +- `test_kthbestspanningtree_serialization` +- `test_kthbestspanningtree_paper_example` + +Use the issue’s two worked instances as the source of truth: +- YES instance: 5-vertex, 8-edge graph with `k = 3`, `bound = 12` +- NO instance: 4-vertex path with `k = 2`, `bound = 3` + +**Step 2: Run test to verify it fails** + +Run: `cargo test kthbestspanningtree --lib` +Expected: FAIL because `KthBestSpanningTree` does not exist yet. + +**Step 3: Write minimal implementation** + +Create `src/models/graph/kth_best_spanning_tree.rs` with: +- `ProblemSchemaEntry` for `graph = SimpleGraph`, `weight = i32` +- `#[derive(Debug, Clone, Serialize, Deserialize)] pub struct KthBestSpanningTree` +- Constructor `new(graph, weights, k, bound)` validating edge-weight count and positive `k` +- Getters `graph()`, `weights()`, `k()`, `bound()`, `num_vertices()`, `num_edges()` +- Helper predicates for one block: + - length/range check for binary edge choices + - exact `n - 1` selected-edge count + - selected-edge subgraph connectivity + - total selected weight `<= bound` +- Pairwise distinctness check across the `k` blocks +- `Problem` implementation with `dims() = vec![2; self.k * self.graph.num_edges()]` +- `variant() = crate::variant_params![W]` +- `impl SatisfactionProblem` +- `declare_variants! { default sat KthBestSpanningTree => "2^(num_edges * k)", }` +- `#[cfg(feature = "example-db")] canonical_model_example_specs()` using the issue YES instance +- test link at the bottom + +Prefer the `RuralPostman` pattern for `bound: W::Sum` rather than `bound: W`, and only register the `i32` variant because the issue requires integer edge weights. + +**Step 4: Run test to verify it passes** + +Run: `cargo test kthbestspanningtree --lib` +Expected: PASS for the new targeted model tests. + +**Step 5: Commit** + +Run: +```bash +git add src/models/graph/kth_best_spanning_tree.rs src/unit_tests/models/graph/kth_best_spanning_tree.rs +git commit -m "Add KthBestSpanningTree model" +``` + +### Task 2: Register the model across the crate and example-db + +**Files:** +- Modify: `src/models/graph/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` +- Modify: `src/unit_tests/trait_consistency.rs` + +**Step 1: Write the failing test** + +Add a trait-consistency entry for a tiny positive instance so the new model is checked by the shared trait suite. + +**Step 2: Run test to verify it fails** + +Run: `cargo test trait_consistency --lib` +Expected: FAIL before the new type is exported and referenced consistently. + +**Step 3: Write minimal implementation** + +Wire the type through the graph module, top-level model exports, and prelude exports. In `trait_consistency.rs`, add one `check_problem_trait(...)` call using a 3-vertex triangle with `k = 1`, plus any minimal imports needed. + +Also add the new example-db spec to `src/models/graph/mod.rs` so `build_model_examples()` includes the canonical example. + +**Step 4: Run test to verify it passes** + +Run: `cargo test trait_consistency --lib` +Expected: PASS with the new model included. + +**Step 5: Commit** + +Run: +```bash +git add src/models/graph/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/trait_consistency.rs +git commit -m "Register KthBestSpanningTree across the crate" +``` + +### Task 3: Add CLI creation support for the new model + +**Files:** +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/src/cli.rs` + +**Step 1: Write the failing test** + +Use a direct CLI smoke command as the behavior check: +- `pred create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` + +No dedicated Rust test file is required if the smoke command is the verification target for this task. + +**Step 2: Run test to verify it fails** + +Run: `cargo run -p problemreductions-cli -- create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` +Expected: FAIL with unknown problem or unsupported create-path errors before the CLI wiring exists. + +**Step 3: Write minimal implementation** + +Update CLI creation support by: +- Adding `KthBestSpanningTree` to the help text table in `problemreductions-cli/src/cli.rs` +- Adding an `example_for()` entry in `problemreductions-cli/src/commands/create.rs` +- Adding a `match` arm that parses: + - `--graph` + - `--edge-weights` + - `--k` + - `--bound` +- Constructing `KthBestSpanningTree::new(graph, edge_weights, k, bound)` + +Use the existing `RuralPostman` and weighted-graph parsing helpers as the template. + +**Step 4: Run test to verify it passes** + +Run: `cargo run -p problemreductions-cli -- create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` +Expected: PASS and emit valid problem JSON. + +**Step 5: Commit** + +Run: +```bash +git add problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs +git commit -m "Add CLI support for KthBestSpanningTree" +``` + +## Batch 2: Paper Entry and Paper-Aligned Verification + +### Task 4: Add the paper entry and align the paper example test + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `src/unit_tests/models/graph/kth_best_spanning_tree.rs` + +**Step 1: Write the failing test** + +Ensure `test_kthbestspanningtree_paper_example` uses the exact same instance and witness that the paper will show. If the current test uses the issue example already, tighten it so it also checks the claimed count of satisfying witnesses from brute force. + +**Step 2: Run test to verify it fails** + +Run: `cargo test test_kthbestspanningtree_paper_example --lib` +Expected: FAIL until the test and paper example agree on one concrete instance. + +**Step 3: Write minimal implementation** + +In `docs/paper/reductions.typ`: +- Add `"KthBestSpanningTree": [Kth Best Spanning Tree],` to `display-name` +- Add a `problem-def("KthBestSpanningTree")[...][...]` entry near the other graph problems +- Load the canonical example with `load-model-example("KthBestSpanningTree")` +- Use the issue’s YES instance as the worked example, including: + - the graph and edge weights + - `k = 3`, `B = 12` + - three distinct spanning trees under the bound + - the explanation that the witness is accepted because all three blocks are valid, distinct, and under the bound +- Cite Garey & Johnson and mention the comment-thread correction about Eppstein’s bounds in prose instead of restating the incorrect issue draft + +Then update `test_kthbestspanningtree_paper_example` so it matches the exact paper instance and asserted satisfying-witness count. + +**Step 4: Run test to verify it passes** + +Run: +- `cargo test test_kthbestspanningtree_paper_example --lib` +- `make paper` + +Expected: both PASS. + +**Step 5: Commit** + +Run: +```bash +git add docs/paper/reductions.typ src/unit_tests/models/graph/kth_best_spanning_tree.rs +git commit -m "Document KthBestSpanningTree in the paper" +``` + +## Final Verification + +After all tasks: + +1. Run targeted verification first: + - `cargo test kthbestspanningtree --lib` + - `cargo test trait_consistency --lib` + - `cargo test example_db --lib` + - `cargo run -p problemreductions-cli -- create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` +2. Run repo verification: + - `make test` + - `make clippy` + - `make paper` +3. Run `review-implementation` after code lands to catch structural omissions before the final implementation commit. From f8a6cf9e0e6f06d38bd54cbcc0a5ed09fe9d097f Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 07:11:07 +0800 Subject: [PATCH 2/9] Implement #249: [Model] KthBestSpanningTree --- docs/paper/reductions.typ | 36 +++ docs/paper/references.bib | 22 ++ docs/src/reductions/problem_schemas.json | 26 ++ docs/src/reductions/reduction_graph.json | 171 ++++++----- problemreductions-cli/src/cli.rs | 1 + problemreductions-cli/src/commands/create.rs | 28 ++ src/example_db/fixtures/examples.json | 1 + src/lib.rs | 4 +- src/models/graph/kth_best_spanning_tree.rs | 277 ++++++++++++++++++ src/models/graph/mod.rs | 4 + src/models/mod.rs | 11 +- .../models/graph/kth_best_spanning_tree.rs | 127 ++++++++ src/unit_tests/trait_consistency.rs | 9 + 13 files changed, 629 insertions(+), 88 deletions(-) create mode 100644 src/models/graph/kth_best_spanning_tree.rs create mode 100644 src/unit_tests/models/graph/kth_best_spanning_tree.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 1c0e5f785..be7fd2d68 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -71,6 +71,7 @@ "UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow], "LengthBoundedDisjointPaths": [Length-Bounded Disjoint Paths], "IsomorphicSpanningTree": [Isomorphic Spanning Tree], + "KthBestSpanningTree": [Kth Best Spanning Tree], "KColoring": [$k$-Coloring], "MinimumDominatingSet": [Minimum Dominating Set], "MaximumMatching": [Maximum Matching], @@ -851,6 +852,41 @@ is feasible: each set induces a connected subgraph, the component weights are $2 ] ] } +#{ + let x = load-model-example("KthBestSpanningTree") + let edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1))) + let m = edges.len() + let sol = x.optimal.at(0).config + let tree1 = sol.enumerate().filter(((i, v)) => i < m and v == 1).map(((i, _)) => edges.at(i)) + let blue = graph-colors.at(0) + let gray = luma(190) + [ + #problem-def("KthBestSpanningTree")[ + Given an undirected graph $G = (V, E)$ with edge weights $w: E -> ZZ_(gt.eq 0)$, a positive integer $k$, and a bound $B in ZZ_(gt.eq 0)$, determine whether there exist $k$ distinct spanning trees $T_1, dots, T_k subset.eq E$ such that $sum_(e in T_i) w(e) lt.eq B$ for every $i$. + ][ + Kth Best Spanning Tree is catalogued as ND9 in Garey and Johnson @garey1979 and is marked there with an asterisk because the general problem is NP-hard but not known to lie in NP. For any fixed value of $k$, Lawler's $k$-best enumeration framework gives a polynomial-time algorithm when combined with minimum-spanning-tree subroutines @lawler1972. For output-sensitive enumeration, Eppstein gave an algorithm that lists the $k$ smallest spanning trees of a weighted graph in $O(m log beta(m, n) + k^2)$ time @eppstein1992. + + Variables: $k |E|$ binary values grouped into $k$ consecutive edge-selection blocks. Entry $x_(i, e) = 1$ means edge $e$ belongs to the $i$-th candidate tree. A configuration is satisfying exactly when each block selects a spanning tree, every selected tree has total weight at most $B$, and the $k$ blocks encode pairwise distinct edge sets. + + *Example.* Consider the 5-vertex graph with weighted edges ${(0,1): 2, (0,2): 3, (1,2): 1, (1,3): 4, (2,3): 2, (2,4): 5, (3,4): 3, (0,4): 6}$. With $k = 3$ and $B = 12$, the spanning trees $T_1 = {(0,1), (1,2), (2,3), (3,4)}$, $T_2 = {(0,1), (1,2), (1,3), (3,4)}$, and $T_3 = {(0,2), (1,2), (2,3), (3,4)}$ have weights $8$, $10$, and $9$, respectively. They are all distinct and all satisfy the bound, so this instance is a YES-instance. + + #figure({ + canvas(length: 1cm, { + let pos = ((0.0, 1.2), (1.1, 2.0), (2.3, 1.2), (1.8, 0.0), (0.3, 0.0)) + for (u, v) in edges { + let in-tree1 = tree1.any(e => (e.at(0) == u and e.at(1) == v) or (e.at(0) == v and e.at(1) == u)) + g-edge(pos.at(u), pos.at(v), stroke: if in-tree1 { 2pt + blue } else { 1pt + gray }) + } + for (idx, p) in pos.enumerate() { + g-node(p, name: "v" + str(idx), fill: white, label: $v_#idx$) + } + }) + }, + caption: [Kth Best Spanning Tree example graph. Blue edges show $T_1 = {(0,1), (1,2), (2,3), (3,4)}$, one of the three bounded spanning trees used to certify the YES-instance for $k = 3$ and $B = 12$.], + ) + ] + ] +} #{ let x = load-model-example("KColoring") let nv = graph-num-vertices(x.instance) diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 34fc92a17..9b6597657 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -734,3 +734,25 @@ @article{papadimitriou1982 year = {1982}, doi = {10.1145/322307.322309} } + +@article{lawler1972, + author = {Eugene L. Lawler}, + title = {A Procedure for Computing the $K$ Best Solutions to Discrete Optimization Problems and Its Application to the Shortest Path Problem}, + journal = {Management Science}, + volume = {18}, + number = {7}, + pages = {401--405}, + year = {1972}, + doi = {10.1287/mnsc.18.7.401} +} + +@article{eppstein1992, + author = {David Eppstein}, + title = {Finding the $k$ Smallest Spanning Trees}, + journal = {BIT}, + volume = {32}, + number = {2}, + pages = {237--248}, + year = {1992}, + doi = {10.1007/BF01994880} +} diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 7cd236022..5b64d6b0c 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -368,6 +368,32 @@ } ] }, + { + "name": "KthBestSpanningTree", + "description": "Do there exist k distinct spanning trees with total weight at most B?", + "fields": [ + { + "name": "graph", + "type_name": "SimpleGraph", + "description": "The underlying graph G=(V,E)" + }, + { + "name": "weights", + "type_name": "Vec", + "description": "Edge weights w(e) for each edge in E" + }, + { + "name": "k", + "type_name": "usize", + "description": "Number of distinct spanning trees required" + }, + { + "name": "bound", + "type_name": "W::Sum", + "description": "Upper bound B on each spanning tree weight" + } + ] + }, { "name": "LengthBoundedDisjointPaths", "description": "Find J internally vertex-disjoint s-t paths of length at most K", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index 756c52b70..b1a34eb96 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -239,6 +239,15 @@ "doc_path": "models/misc/struct.Knapsack.html", "complexity": "2^(num_items / 2)" }, + { + "name": "KthBestSpanningTree", + "variant": { + "weight": "i32" + }, + "category": "graph", + "doc_path": "models/graph/struct.KthBestSpanningTree.html", + "complexity": "2^(num_edges * k)" + }, { "name": "LengthBoundedDisjointPaths", "variant": { @@ -661,7 +670,7 @@ }, { "source": 7, - "target": 63, + "target": 64, "overhead": [ { "field": "num_spins", @@ -721,7 +730,7 @@ }, { "source": 16, - "target": 56, + "target": 57, "overhead": [ { "field": "num_vars", @@ -762,7 +771,7 @@ }, { "source": 23, - "target": 56, + "target": 57, "overhead": [ { "field": "num_vars", @@ -788,7 +797,7 @@ }, { "source": 24, - "target": 56, + "target": 57, "overhead": [ { "field": "num_vars", @@ -814,7 +823,7 @@ }, { "source": 25, - "target": 56, + "target": 57, "overhead": [ { "field": "num_vars", @@ -825,7 +834,7 @@ }, { "source": 25, - "target": 68, + "target": 69, "overhead": [ { "field": "num_elements", @@ -836,7 +845,7 @@ }, { "source": 26, - "target": 58, + "target": 59, "overhead": [ { "field": "num_clauses", @@ -855,7 +864,7 @@ }, { "source": 27, - "target": 56, + "target": 57, "overhead": [ { "field": "num_vars", @@ -865,7 +874,7 @@ "doc_path": "rules/knapsack_qubo/index.html" }, { - "source": 29, + "source": 30, "target": 16, "overhead": [ { @@ -880,8 +889,8 @@ "doc_path": "rules/longestcommonsubsequence_ilp/index.html" }, { - "source": 30, - "target": 63, + "source": 31, + "target": 64, "overhead": [ { "field": "num_spins", @@ -895,7 +904,7 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 32, + "source": 33, "target": 16, "overhead": [ { @@ -910,8 +919,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 32, - "target": 36, + "source": 33, + "target": 37, "overhead": [ { "field": "num_vertices", @@ -925,8 +934,8 @@ "doc_path": "rules/maximumclique_maximumindependentset/index.html" }, { - "source": 33, - "target": 34, + "source": 34, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -940,8 +949,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 38, + "source": 34, + "target": 39, "overhead": [ { "field": "num_vertices", @@ -955,8 +964,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 34, - "target": 39, + "source": 35, + "target": 40, "overhead": [ { "field": "num_vertices", @@ -970,8 +979,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 35, - "target": 33, + "source": 36, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -985,8 +994,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 35, - "target": 36, + "source": 36, + "target": 37, "overhead": [ { "field": "num_vertices", @@ -1000,8 +1009,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 35, - "target": 37, + "source": 36, + "target": 38, "overhead": [ { "field": "num_vertices", @@ -1015,8 +1024,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 35, - "target": 41, + "source": 36, + "target": 42, "overhead": [ { "field": "num_sets", @@ -1030,8 +1039,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 36, - "target": 32, + "source": 37, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -1045,8 +1054,8 @@ "doc_path": "rules/maximumindependentset_maximumclique/index.html" }, { - "source": 36, - "target": 43, + "source": 37, + "target": 44, "overhead": [ { "field": "num_sets", @@ -1060,8 +1069,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 36, - "target": 51, + "source": 37, + "target": 52, "overhead": [ { "field": "num_vertices", @@ -1075,8 +1084,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 37, - "target": 39, + "source": 38, + "target": 40, "overhead": [ { "field": "num_vertices", @@ -1090,8 +1099,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 38, - "target": 35, + "source": 39, + "target": 36, "overhead": [ { "field": "num_vertices", @@ -1105,8 +1114,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 38, - "target": 39, + "source": 39, + "target": 40, "overhead": [ { "field": "num_vertices", @@ -1120,8 +1129,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 39, - "target": 36, + "source": 40, + "target": 37, "overhead": [ { "field": "num_vertices", @@ -1135,7 +1144,7 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 40, + "source": 41, "target": 16, "overhead": [ { @@ -1150,8 +1159,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 40, - "target": 43, + "source": 41, + "target": 44, "overhead": [ { "field": "num_sets", @@ -1165,8 +1174,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 41, - "target": 35, + "source": 42, + "target": 36, "overhead": [ { "field": "num_vertices", @@ -1180,8 +1189,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 41, - "target": 43, + "source": 42, + "target": 44, "overhead": [ { "field": "num_sets", @@ -1195,8 +1204,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 42, - "target": 56, + "source": 43, + "target": 57, "overhead": [ { "field": "num_vars", @@ -1206,7 +1215,7 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 43, + "source": 44, "target": 16, "overhead": [ { @@ -1221,8 +1230,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 43, - "target": 36, + "source": 44, + "target": 37, "overhead": [ { "field": "num_vertices", @@ -1236,8 +1245,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 43, - "target": 42, + "source": 44, + "target": 43, "overhead": [ { "field": "num_sets", @@ -1251,7 +1260,7 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 44, + "source": 45, "target": 16, "overhead": [ { @@ -1266,7 +1275,7 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 48, + "source": 49, "target": 16, "overhead": [ { @@ -1281,8 +1290,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 51, - "target": 36, + "source": 52, + "target": 37, "overhead": [ { "field": "num_vertices", @@ -1296,8 +1305,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 51, - "target": 48, + "source": 52, + "target": 49, "overhead": [ { "field": "num_sets", @@ -1311,7 +1320,7 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 56, + "source": 57, "target": 16, "overhead": [ { @@ -1326,8 +1335,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 56, - "target": 62, + "source": 57, + "target": 63, "overhead": [ { "field": "num_spins", @@ -1337,7 +1346,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 58, + "source": 59, "target": 7, "overhead": [ { @@ -1352,7 +1361,7 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 58, + "source": 59, "target": 20, "overhead": [ { @@ -1367,7 +1376,7 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 58, + "source": 59, "target": 25, "overhead": [ { @@ -1382,8 +1391,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 58, - "target": 35, + "source": 59, + "target": 36, "overhead": [ { "field": "num_vertices", @@ -1397,8 +1406,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 58, - "target": 44, + "source": 59, + "target": 45, "overhead": [ { "field": "num_vertices", @@ -1412,8 +1421,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 62, - "target": 56, + "source": 63, + "target": 57, "overhead": [ { "field": "num_vars", @@ -1423,8 +1432,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 63, - "target": 30, + "source": 64, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1438,8 +1447,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 63, - "target": 62, + "source": 64, + "target": 63, "overhead": [ { "field": "num_spins", @@ -1453,7 +1462,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 69, + "source": 70, "target": 16, "overhead": [ { @@ -1468,8 +1477,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 69, - "target": 56, + "source": 70, + "target": 57, "overhead": [ { "field": "num_vars", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 1a9037e1d..7eb933509 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -227,6 +227,7 @@ Flags by problem type: BoundedComponentSpanningForest --graph, --weights, --k, --bound UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 IsomorphicSpanningTree --graph, --tree + KthBestSpanningTree --graph, --edge-weights, --k, --bound LengthBoundedDisjointPaths --graph, --source, --sink, --num-paths-required, --bound Factoring --target, --m, --n BinPacking --sizes, --capacity diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 973005907..25f0d2d43 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -265,6 +265,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "--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" } "IsomorphicSpanningTree" => "--graph 0-1,1-2,0-2 --tree 0-1,1-2", + "KthBestSpanningTree" => "--graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3", "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { "--graph 0-1,1-2,2-3 --edge-weights 1,1,1" } @@ -780,6 +781,33 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // KthBestSpanningTree (weighted graph + k + bound) + "KthBestSpanningTree" => { + let (graph, _) = parse_graph(args).map_err(|e| { + anyhow::anyhow!( + "{e}\n\nUsage: pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3" + ) + })?; + let edge_weights = parse_edge_weights(args, graph.num_edges())?; + let (k, _variant) = + util::validate_k_param(&resolved_variant, args.k, None, "KthBestSpanningTree")?; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!( + "KthBestSpanningTree requires --bound\n\n\ + Usage: pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3" + ) + })? as i32; + ( + ser(problemreductions::models::graph::KthBestSpanningTree::new( + graph, + edge_weights, + k, + bound, + ))?, + resolved_variant.clone(), + ) + } + // Graph problems with edge weights "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { let (graph, _) = parse_graph(args).map_err(|e| { diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index d8dd0fb6e..eea339aef 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -15,6 +15,7 @@ {"problem":"IsomorphicSpanningTree","variant":{},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"tree":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[0,1,2,3],"metric":true}],"optimal":[{"config":[0,1,2,3],"metric":true},{"config":[0,1,3,2],"metric":true},{"config":[0,2,1,3],"metric":true},{"config":[0,2,3,1],"metric":true},{"config":[0,3,1,2],"metric":true},{"config":[0,3,2,1],"metric":true},{"config":[1,0,2,3],"metric":true},{"config":[1,0,3,2],"metric":true},{"config":[1,2,0,3],"metric":true},{"config":[1,2,3,0],"metric":true},{"config":[1,3,0,2],"metric":true},{"config":[1,3,2,0],"metric":true},{"config":[2,0,1,3],"metric":true},{"config":[2,0,3,1],"metric":true},{"config":[2,1,0,3],"metric":true},{"config":[2,1,3,0],"metric":true},{"config":[2,3,0,1],"metric":true},{"config":[2,3,1,0],"metric":true},{"config":[3,0,1,2],"metric":true},{"config":[3,0,2,1],"metric":true},{"config":[3,1,0,2],"metric":true},{"config":[3,1,2,0],"metric":true},{"config":[3,2,0,1],"metric":true},{"config":[3,2,1,0],"metric":true}]}, {"problem":"KColoring","variant":{"graph":"SimpleGraph","k":"K3"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"num_colors":3},"samples":[{"config":[0,1,1,0,2],"metric":true}],"optimal":[{"config":[0,1,1,0,2],"metric":true},{"config":[0,1,1,2,0],"metric":true},{"config":[0,1,2,0,1],"metric":true},{"config":[0,2,1,0,2],"metric":true},{"config":[0,2,2,0,1],"metric":true},{"config":[0,2,2,1,0],"metric":true},{"config":[1,0,0,1,2],"metric":true},{"config":[1,0,0,2,1],"metric":true},{"config":[1,0,2,1,0],"metric":true},{"config":[1,2,0,1,2],"metric":true},{"config":[1,2,2,0,1],"metric":true},{"config":[1,2,2,1,0],"metric":true},{"config":[2,0,0,1,2],"metric":true},{"config":[2,0,0,2,1],"metric":true},{"config":[2,0,1,2,0],"metric":true},{"config":[2,1,0,2,1],"metric":true},{"config":[2,1,1,0,2],"metric":true},{"config":[2,1,1,2,0],"metric":true}]}, {"problem":"KSatisfiability","variant":{"k":"K3"},"instance":{"clauses":[{"literals":[1,2,3]},{"literals":[-1,-2,3]},{"literals":[1,-2,-3]}],"num_vars":3},"samples":[{"config":[1,0,1],"metric":true}],"optimal":[{"config":[0,0,1],"metric":true},{"config":[0,1,0],"metric":true},{"config":[1,0,0],"metric":true},{"config":[1,0,1],"metric":true},{"config":[1,1,1],"metric":true}]}, + {"problem":"KthBestSpanningTree","variant":{"weight":"i32"},"instance":{"bound":12,"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null],[0,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"k":3,"weights":[2,3,1,4,2,5,3,6]},"samples":[{"config":[1,0,1,0,1,0,1,0,1,0,1,1,0,0,1,0,0,1,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[1,0,1,0,1,0,1,0,1,0,1,1,0,0,1,0,0,1,1,0,1,0,1,0],"metric":true}]}, {"problem":"LengthBoundedDisjointPaths","variant":{"graph":"SimpleGraph"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,6,null],[0,2,null],[2,3,null],[3,6,null],[0,4,null],[4,5,null],[5,6,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null]}},"max_length":3,"num_paths_required":2,"sink":6,"source":0},"samples":[{"config":[1,1,0,0,0,0,1,1,0,1,1,0,0,1],"metric":true}],"optimal":[{"config":[1,0,0,0,1,1,1,1,0,1,1,0,0,1],"metric":true},{"config":[1,0,0,0,1,1,1,1,1,0,0,0,0,1],"metric":true},{"config":[1,0,1,1,0,0,1,1,0,0,0,1,1,1],"metric":true},{"config":[1,0,1,1,0,0,1,1,1,0,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,0,1,1,0,0,0,1,1,1],"metric":true},{"config":[1,1,0,0,0,0,1,1,0,1,1,0,0,1],"metric":true}]}, {"problem":"MaxCut","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,1,1,1,1,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,0,1,0],"metric":{"Valid":5}}],"optimal":[{"config":[0,1,1,0,0],"metric":{"Valid":5}},{"config":[0,1,1,0,1],"metric":{"Valid":5}},{"config":[1,0,0,1,0],"metric":{"Valid":5}},{"config":[1,0,0,1,1],"metric":{"Valid":5}}]}, {"problem":"MaximalIS","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[2,3,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"weights":[1,1,1,1,1]},"samples":[{"config":[0,1,0,1,0],"metric":{"Valid":2}},{"config":[1,0,1,0,1],"metric":{"Valid":3}}],"optimal":[{"config":[1,0,1,0,1],"metric":{"Valid":3}}]}, diff --git a/src/lib.rs b/src/lib.rs index 68b747161..3ff1fe8c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,8 +47,8 @@ pub mod prelude { pub use crate::models::graph::{ BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation, BoundedComponentSpanningForest, DirectedTwoCommodityIntegralFlow, GraphPartitioning, - HamiltonianPath, IsomorphicSpanningTree, LengthBoundedDisjointPaths, SpinGlass, - SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, + HamiltonianPath, IsomorphicSpanningTree, KthBestSpanningTree, LengthBoundedDisjointPaths, + SpinGlass, SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, diff --git a/src/models/graph/kth_best_spanning_tree.rs b/src/models/graph/kth_best_spanning_tree.rs new file mode 100644 index 000000000..80e001bf6 --- /dev/null +++ b/src/models/graph/kth_best_spanning_tree.rs @@ -0,0 +1,277 @@ +//! Kth Best Spanning Tree problem implementation. +//! +//! Given a weighted graph, determine whether it contains `k` distinct spanning +//! trees whose total weights are all at most a prescribed bound. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{Problem, SatisfactionProblem}; +use crate::types::WeightElement; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; + +inventory::submit! { + ProblemSchemaEntry { + name: "KthBestSpanningTree", + display_name: "Kth Best Spanning Tree", + aliases: &[], + dimensions: &[VariantDimension::new("weight", "i32", &["i32"])], + module_path: module_path!(), + description: "Do there exist k distinct spanning trees with total weight at most B?", + fields: &[ + FieldInfo { name: "graph", type_name: "SimpleGraph", description: "The underlying graph G=(V,E)" }, + FieldInfo { name: "weights", type_name: "Vec", description: "Edge weights w(e) for each edge in E" }, + FieldInfo { name: "k", type_name: "usize", description: "Number of distinct spanning trees required" }, + FieldInfo { name: "bound", type_name: "W::Sum", description: "Upper bound B on each spanning tree weight" }, + ], + } +} + +/// Kth Best Spanning Tree. +/// +/// Given an undirected graph `G = (V, E)`, non-negative edge weights `w(e)`, +/// a positive integer `k`, and a bound `B`, determine whether there are `k` +/// distinct spanning trees of `G` whose total weights are all at most `B`. +/// +/// # Representation +/// +/// A configuration is `k` consecutive binary blocks of length `|E|`. +/// Each block selects the edges of one candidate spanning tree. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KthBestSpanningTree { + graph: SimpleGraph, + weights: Vec, + k: usize, + bound: W::Sum, +} + +impl KthBestSpanningTree { + /// Create a new KthBestSpanningTree instance. + /// + /// # Panics + /// + /// Panics if the number of weights does not match the number of edges, or + /// if `k` is zero. + pub fn new(graph: SimpleGraph, weights: Vec, k: usize, bound: W::Sum) -> Self { + assert_eq!( + weights.len(), + graph.num_edges(), + "weights length must match graph num_edges" + ); + assert!(k > 0, "k must be positive"); + + Self { + graph, + weights, + k, + bound, + } + } + + /// Get the underlying graph. + pub fn graph(&self) -> &SimpleGraph { + &self.graph + } + + /// Get the edge weights. + pub fn weights(&self) -> &[W] { + &self.weights + } + + /// Get the requested number of trees. + pub fn k(&self) -> usize { + self.k + } + + /// Get the weight bound. + pub fn bound(&self) -> &W::Sum { + &self.bound + } + + /// Get the number of vertices. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of edges. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + /// Check whether the problem uses a non-unit weight type. + pub fn is_weighted(&self) -> bool { + !W::IS_UNIT + } + + /// Check whether a configuration satisfies the problem. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + self.evaluate_config(config) + } + + fn block_is_valid_tree(&self, block: &[usize], edges: &[(usize, usize)]) -> bool { + if block.len() != edges.len() || block.iter().any(|&value| value > 1) { + return false; + } + + let num_vertices = self.graph.num_vertices(); + let selected_count = block.iter().filter(|&&value| value == 1).count(); + if selected_count != num_vertices.saturating_sub(1) { + return false; + } + + let mut total_weight = W::Sum::zero(); + let mut adjacency = vec![Vec::new(); num_vertices]; + let mut start = None; + + for (idx, &selected) in block.iter().enumerate() { + if selected == 0 { + continue; + } + total_weight += self.weights[idx].to_sum(); + let (u, v) = edges[idx]; + adjacency[u].push(v); + adjacency[v].push(u); + if start.is_none() { + start = Some(u); + } + } + + if total_weight > self.bound { + return false; + } + + if num_vertices <= 1 { + return true; + } + + let start = match start { + Some(vertex) => vertex, + None => return false, + }; + + let mut visited = vec![false; num_vertices]; + let mut queue = VecDeque::new(); + visited[start] = true; + queue.push_back(start); + + while let Some(vertex) = queue.pop_front() { + for &neighbor in &adjacency[vertex] { + if !visited[neighbor] { + visited[neighbor] = true; + queue.push_back(neighbor); + } + } + } + + visited.into_iter().all(|seen| seen) + } + + fn blocks_are_pairwise_distinct(&self, config: &[usize], block_size: usize) -> bool { + if block_size == 0 { + return self.k == 1; + } + + let blocks: Vec<&[usize]> = config.chunks_exact(block_size).collect(); + for left in 0..blocks.len() { + for right in (left + 1)..blocks.len() { + if blocks[left] == blocks[right] { + return false; + } + } + } + true + } + + fn evaluate_config(&self, config: &[usize]) -> bool { + let block_size = self.graph.num_edges(); + let expected_len = self.k * block_size; + if config.len() != expected_len { + return false; + } + + if block_size == 0 { + return self.k == 1 && self.block_is_valid_tree(config, &[]); + } + + let edges = self.graph.edges(); + let blocks = config.chunks_exact(block_size); + if !blocks.remainder().is_empty() { + return false; + } + + if !self.blocks_are_pairwise_distinct(config, block_size) { + return false; + } + + config + .chunks_exact(block_size) + .all(|block| self.block_is_valid_tree(block, &edges)) + } +} + +impl Problem for KthBestSpanningTree +where + W: WeightElement + crate::variant::VariantParam, +{ + const NAME: &'static str = "KthBestSpanningTree"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![W] + } + + fn dims(&self) -> Vec { + vec![2; self.k * self.graph.num_edges()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.evaluate_config(config) + } +} + +impl SatisfactionProblem for KthBestSpanningTree where + W: WeightElement + crate::variant::VariantParam +{ +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "kth_best_spanning_tree_i32", + build: || { + let graph = SimpleGraph::new( + 5, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (0, 4), + ], + ); + let problem = KthBestSpanningTree::new(graph, vec![2, 3, 1, 4, 2, 5, 3, 6], 3, 12); + let witness = vec![ + 1, 0, 1, 0, 1, 0, 1, 0, // + 1, 0, 1, 1, 0, 0, 1, 0, // + 0, 1, 1, 0, 1, 0, 1, 0, + ]; + crate::example_db::specs::explicit_example( + problem, + vec![witness.clone()], + vec![witness], + ) + }, + }] +} + +crate::declare_variants! { + default sat KthBestSpanningTree => "2^(num_edges * k)", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/kth_best_spanning_tree.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index b8da19aa7..3e7ea9751 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -10,6 +10,7 @@ //! - [`MaxCut`]: Maximum cut on weighted graphs //! - [`GraphPartitioning`]: Minimum bisection (balanced graph partitioning) //! - [`IsomorphicSpanningTree`]: Isomorphic spanning tree (satisfaction) +//! - [`KthBestSpanningTree`]: K distinct bounded spanning trees (satisfaction) //! - [`KColoring`]: K-vertex coloring //! - [`PartitionIntoTriangles`]: Partition vertices into triangles //! - [`MaximumMatching`]: Maximum weight matching @@ -42,6 +43,7 @@ pub(crate) mod graph_partitioning; pub(crate) mod hamiltonian_path; pub(crate) mod isomorphic_spanning_tree; pub(crate) mod kcoloring; +pub(crate) mod kth_best_spanning_tree; pub(crate) mod length_bounded_disjoint_paths; pub(crate) mod max_cut; pub(crate) mod maximal_is; @@ -74,6 +76,7 @@ pub use graph_partitioning::GraphPartitioning; pub use hamiltonian_path::HamiltonianPath; pub use isomorphic_spanning_tree::IsomorphicSpanningTree; pub use kcoloring::KColoring; +pub use kth_best_spanning_tree::KthBestSpanningTree; pub use length_bounded_disjoint_paths::LengthBoundedDisjointPaths; pub use max_cut::MaxCut; pub use maximal_is::MaximalIS; @@ -106,6 +109,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec KthBestSpanningTree { + let graph = SimpleGraph::new( + 5, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (0, 4), + ], + ); + let weights = vec![2, 3, 1, 4, 2, 5, 3, 6]; + KthBestSpanningTree::new(graph, weights, 3, 12) +} + +fn no_instance() -> KthBestSpanningTree { + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let weights = vec![1, 1, 1]; + KthBestSpanningTree::new(graph, weights, 2, 3) +} + +fn yes_witness_config() -> Vec { + vec![ + 1, 0, 1, 0, 1, 0, 1, 0, // {0,1}, {1,2}, {2,3}, {3,4} + 1, 0, 1, 1, 0, 0, 1, 0, // {0,1}, {1,2}, {1,3}, {3,4} + 0, 1, 1, 0, 1, 0, 1, 0, // {0,2}, {1,2}, {2,3}, {3,4} + ] +} + +fn duplicate_tree_config() -> Vec { + vec![ + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, + ] +} + +fn overweight_tree_config() -> Vec { + vec![ + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, + 0, // {0,1}, {0,2}, {1,3}, {2,4} => 14 + ] +} + +#[test] +fn test_kthbestspanningtree_creation() { + let problem = yes_instance(); + + assert_eq!(problem.dims(), vec![2; 24]); + assert_eq!(problem.num_vertices(), 5); + assert_eq!(problem.num_edges(), 8); + assert_eq!(problem.k(), 3); + assert_eq!(problem.weights(), &[2, 3, 1, 4, 2, 5, 3, 6]); + assert_eq!(*problem.bound(), 12); + assert!(problem.is_weighted()); + assert_eq!(KthBestSpanningTree::::NAME, "KthBestSpanningTree"); +} + +#[test] +fn test_kthbestspanningtree_evaluation_yes_instance() { + let problem = yes_instance(); + assert!(problem.evaluate(&yes_witness_config())); +} + +#[test] +fn test_kthbestspanningtree_evaluation_rejects_duplicate_trees() { + let problem = yes_instance(); + assert!(!problem.evaluate(&duplicate_tree_config())); +} + +#[test] +fn test_kthbestspanningtree_evaluation_rejects_overweight_tree() { + let problem = yes_instance(); + assert!(!problem.evaluate(&overweight_tree_config())); +} + +#[test] +fn test_kthbestspanningtree_solver_yes_instance() { + let problem = yes_instance(); + let solver = BruteForce::new(); + + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + assert!(problem.evaluate(&solution.unwrap())); +} + +#[test] +fn test_kthbestspanningtree_solver_no_instance() { + let problem = no_instance(); + let solver = BruteForce::new(); + + assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_all_satisfying(&problem).is_empty()); +} + +#[test] +fn test_kthbestspanningtree_serialization() { + let problem = yes_instance(); + let json = serde_json::to_string(&problem).unwrap(); + let restored: KthBestSpanningTree = serde_json::from_str(&json).unwrap(); + + assert_eq!(restored.num_vertices(), problem.num_vertices()); + assert_eq!(restored.num_edges(), problem.num_edges()); + assert_eq!(restored.k(), problem.k()); + assert_eq!(restored.weights(), problem.weights()); + assert_eq!(restored.bound(), problem.bound()); + assert!(restored.evaluate(&yes_witness_config())); +} + +#[test] +fn test_kthbestspanningtree_paper_example() { + let problem = yes_instance(); + let witness = yes_witness_config(); + + assert!(problem.evaluate(&witness)); + + let solver = BruteForce::new(); + let all = solver.find_all_satisfying(&problem); + assert_eq!(all.len(), 4_896); + assert!(all.iter().any(|config| config == &witness)); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 5c50c3745..2374113c6 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -102,6 +102,15 @@ fn test_all_problems_implement_trait_correctly() { ), "StrongConnectivityAugmentation", ); + check_problem_trait( + &KthBestSpanningTree::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + vec![1, 1, 1], + 1, + 2, + ), + "KthBestSpanningTree", + ); } #[test] From 56c5c4e35a29b79a4d1c8b7e8afe27c21ae4ebc6 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 07:11:23 +0800 Subject: [PATCH 3/9] chore: remove plan file after implementation --- .../2026-03-16-kth-best-spanning-tree.md | 219 ------------------ 1 file changed, 219 deletions(-) delete mode 100644 docs/plans/2026-03-16-kth-best-spanning-tree.md diff --git a/docs/plans/2026-03-16-kth-best-spanning-tree.md b/docs/plans/2026-03-16-kth-best-spanning-tree.md deleted file mode 100644 index e367e375c..000000000 --- a/docs/plans/2026-03-16-kth-best-spanning-tree.md +++ /dev/null @@ -1,219 +0,0 @@ -# KthBestSpanningTree Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `KthBestSpanningTree` satisfaction model, wire it into the registry/CLI/example database/paper, and implement the issue #249 example semantics. - -**Architecture:** Model `KthBestSpanningTree` as a `SimpleGraph` plus per-edge weights, a target multiplicity `k`, and a weight bound `B`. Each configuration is `k` consecutive binary edge-selection blocks of length `num_edges`; `evaluate()` accepts exactly those assignments where every block is a distinct spanning tree whose total weight is at most `B`. - -**Tech Stack:** Rust, serde, petgraph-backed `SimpleGraph`, registry macros (`inventory::submit!`, `declare_variants!`), CLI `pred create`, Typst paper. - -**Notes:** The issue comment thread already settled two design points that this plan follows: `dims()` uses `k * num_edges` binary variables, and the registry complexity uses the general-case bound `2^(num_edges * k)`. There is no open associated rule issue whose title mentions `KthBestSpanningTree`; carry that orphan-model warning into the PR summary instead of blocking execution. - ---- - -## Batch 1: Model, Registry, CLI, Tests - -### Task 1: Add failing model tests for the issue semantics - -**Files:** -- Create: `src/unit_tests/models/graph/kth_best_spanning_tree.rs` - -**Step 1: Write the failing test** - -Add focused tests that reference the new type before it exists: -- `test_kthbestspanningtree_creation` -- `test_kthbestspanningtree_evaluation_yes_instance` -- `test_kthbestspanningtree_evaluation_rejects_duplicate_trees` -- `test_kthbestspanningtree_evaluation_rejects_overweight_tree` -- `test_kthbestspanningtree_solver_yes_and_no_examples` -- `test_kthbestspanningtree_serialization` -- `test_kthbestspanningtree_paper_example` - -Use the issue’s two worked instances as the source of truth: -- YES instance: 5-vertex, 8-edge graph with `k = 3`, `bound = 12` -- NO instance: 4-vertex path with `k = 2`, `bound = 3` - -**Step 2: Run test to verify it fails** - -Run: `cargo test kthbestspanningtree --lib` -Expected: FAIL because `KthBestSpanningTree` does not exist yet. - -**Step 3: Write minimal implementation** - -Create `src/models/graph/kth_best_spanning_tree.rs` with: -- `ProblemSchemaEntry` for `graph = SimpleGraph`, `weight = i32` -- `#[derive(Debug, Clone, Serialize, Deserialize)] pub struct KthBestSpanningTree` -- Constructor `new(graph, weights, k, bound)` validating edge-weight count and positive `k` -- Getters `graph()`, `weights()`, `k()`, `bound()`, `num_vertices()`, `num_edges()` -- Helper predicates for one block: - - length/range check for binary edge choices - - exact `n - 1` selected-edge count - - selected-edge subgraph connectivity - - total selected weight `<= bound` -- Pairwise distinctness check across the `k` blocks -- `Problem` implementation with `dims() = vec![2; self.k * self.graph.num_edges()]` -- `variant() = crate::variant_params![W]` -- `impl SatisfactionProblem` -- `declare_variants! { default sat KthBestSpanningTree => "2^(num_edges * k)", }` -- `#[cfg(feature = "example-db")] canonical_model_example_specs()` using the issue YES instance -- test link at the bottom - -Prefer the `RuralPostman` pattern for `bound: W::Sum` rather than `bound: W`, and only register the `i32` variant because the issue requires integer edge weights. - -**Step 4: Run test to verify it passes** - -Run: `cargo test kthbestspanningtree --lib` -Expected: PASS for the new targeted model tests. - -**Step 5: Commit** - -Run: -```bash -git add src/models/graph/kth_best_spanning_tree.rs src/unit_tests/models/graph/kth_best_spanning_tree.rs -git commit -m "Add KthBestSpanningTree model" -``` - -### Task 2: Register the model across the crate and example-db - -**Files:** -- Modify: `src/models/graph/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` -- Modify: `src/unit_tests/trait_consistency.rs` - -**Step 1: Write the failing test** - -Add a trait-consistency entry for a tiny positive instance so the new model is checked by the shared trait suite. - -**Step 2: Run test to verify it fails** - -Run: `cargo test trait_consistency --lib` -Expected: FAIL before the new type is exported and referenced consistently. - -**Step 3: Write minimal implementation** - -Wire the type through the graph module, top-level model exports, and prelude exports. In `trait_consistency.rs`, add one `check_problem_trait(...)` call using a 3-vertex triangle with `k = 1`, plus any minimal imports needed. - -Also add the new example-db spec to `src/models/graph/mod.rs` so `build_model_examples()` includes the canonical example. - -**Step 4: Run test to verify it passes** - -Run: `cargo test trait_consistency --lib` -Expected: PASS with the new model included. - -**Step 5: Commit** - -Run: -```bash -git add src/models/graph/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/trait_consistency.rs -git commit -m "Register KthBestSpanningTree across the crate" -``` - -### Task 3: Add CLI creation support for the new model - -**Files:** -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/src/cli.rs` - -**Step 1: Write the failing test** - -Use a direct CLI smoke command as the behavior check: -- `pred create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` - -No dedicated Rust test file is required if the smoke command is the verification target for this task. - -**Step 2: Run test to verify it fails** - -Run: `cargo run -p problemreductions-cli -- create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` -Expected: FAIL with unknown problem or unsupported create-path errors before the CLI wiring exists. - -**Step 3: Write minimal implementation** - -Update CLI creation support by: -- Adding `KthBestSpanningTree` to the help text table in `problemreductions-cli/src/cli.rs` -- Adding an `example_for()` entry in `problemreductions-cli/src/commands/create.rs` -- Adding a `match` arm that parses: - - `--graph` - - `--edge-weights` - - `--k` - - `--bound` -- Constructing `KthBestSpanningTree::new(graph, edge_weights, k, bound)` - -Use the existing `RuralPostman` and weighted-graph parsing helpers as the template. - -**Step 4: Run test to verify it passes** - -Run: `cargo run -p problemreductions-cli -- create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` -Expected: PASS and emit valid problem JSON. - -**Step 5: Commit** - -Run: -```bash -git add problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs -git commit -m "Add CLI support for KthBestSpanningTree" -``` - -## Batch 2: Paper Entry and Paper-Aligned Verification - -### Task 4: Add the paper entry and align the paper example test - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `src/unit_tests/models/graph/kth_best_spanning_tree.rs` - -**Step 1: Write the failing test** - -Ensure `test_kthbestspanningtree_paper_example` uses the exact same instance and witness that the paper will show. If the current test uses the issue example already, tighten it so it also checks the claimed count of satisfying witnesses from brute force. - -**Step 2: Run test to verify it fails** - -Run: `cargo test test_kthbestspanningtree_paper_example --lib` -Expected: FAIL until the test and paper example agree on one concrete instance. - -**Step 3: Write minimal implementation** - -In `docs/paper/reductions.typ`: -- Add `"KthBestSpanningTree": [Kth Best Spanning Tree],` to `display-name` -- Add a `problem-def("KthBestSpanningTree")[...][...]` entry near the other graph problems -- Load the canonical example with `load-model-example("KthBestSpanningTree")` -- Use the issue’s YES instance as the worked example, including: - - the graph and edge weights - - `k = 3`, `B = 12` - - three distinct spanning trees under the bound - - the explanation that the witness is accepted because all three blocks are valid, distinct, and under the bound -- Cite Garey & Johnson and mention the comment-thread correction about Eppstein’s bounds in prose instead of restating the incorrect issue draft - -Then update `test_kthbestspanningtree_paper_example` so it matches the exact paper instance and asserted satisfying-witness count. - -**Step 4: Run test to verify it passes** - -Run: -- `cargo test test_kthbestspanningtree_paper_example --lib` -- `make paper` - -Expected: both PASS. - -**Step 5: Commit** - -Run: -```bash -git add docs/paper/reductions.typ src/unit_tests/models/graph/kth_best_spanning_tree.rs -git commit -m "Document KthBestSpanningTree in the paper" -``` - -## Final Verification - -After all tasks: - -1. Run targeted verification first: - - `cargo test kthbestspanningtree --lib` - - `cargo test trait_consistency --lib` - - `cargo test example_db --lib` - - `cargo run -p problemreductions-cli -- create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` -2. Run repo verification: - - `make test` - - `make clippy` - - `make paper` -3. Run `review-implementation` after code lands to catch structural omissions before the final implementation commit. From c22f66961d3d8d429198b0a613ceb02bae71586d Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 12:49:57 +0800 Subject: [PATCH 4/9] fix: address KthBestSpanningTree review feedback --- docs/src/cli.md | 1 + problemreductions-cli/src/util.rs | 20 ++++++ problemreductions-cli/tests/cli_tests.rs | 25 ++++++++ .../models/graph/kth_best_spanning_tree.rs | 64 ++++++++++++++++++- 4 files changed, 107 insertions(+), 3 deletions(-) diff --git a/docs/src/cli.md b/docs/src/cli.md index 404c03ef1..5309b8852 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -342,6 +342,7 @@ pred create MIS --graph 0-1,1-2,2-3 --weights 2,1,3,1 -o problem.json pred create SAT --num-vars 3 --clauses "1,2;-1,3" -o sat.json pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json +pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3 -o kth.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 MinimumMultiwayCut --graph 0-1,1-2,2-3,3-0 --terminals 0,2 --edge-weights 3,1,2,4 -o mmc.json diff --git a/problemreductions-cli/src/util.rs b/problemreductions-cli/src/util.rs index 4991d63d8..9f2ffb115 100644 --- a/problemreductions-cli/src/util.rs +++ b/problemreductions-cli/src/util.rs @@ -65,6 +65,10 @@ pub fn validate_k_param( }, }; + if effective_k == 0 { + bail!("{problem_name}: --k must be positive"); + } + // Build the variant map with the effective k let mut variant = resolved_variant.clone(); variant.insert("k".to_string(), k_variant_str(effective_k).to_string()); @@ -282,3 +286,19 @@ pub fn parse_edge_pairs(s: &str) -> Result> { }) .collect() } + +#[cfg(test)] +mod tests { + use super::validate_k_param; + use std::collections::BTreeMap; + + #[test] + fn test_validate_k_param_rejects_zero() { + let err = validate_k_param(&BTreeMap::new(), Some(0), None, "KthBestSpanningTree") + .expect_err("k=0 should be rejected before problem construction"); + assert!( + err.to_string().contains("positive"), + "unexpected error message: {err}" + ); + } +} diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index bd96bfeed..cd71652f3 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -2425,6 +2425,31 @@ fn test_create_kcoloring_missing_k() { assert!(stderr.contains("--k")); } +#[test] +fn test_create_kth_best_spanning_tree_rejects_zero_k() { + let output = pred() + .args([ + "create", + "KthBestSpanningTree", + "--graph", + "0-1,1-2,0-2", + "--edge-weights", + "2,3,1", + "--k", + "0", + "--bound", + "3", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("must be positive"), + "expected positive-k validation error, got: {stderr}" + ); +} + #[test] fn test_create_length_bounded_disjoint_paths_rejects_equal_terminals() { let output = pred() diff --git a/src/unit_tests/models/graph/kth_best_spanning_tree.rs b/src/unit_tests/models/graph/kth_best_spanning_tree.rs index c50845f9e..a146397cb 100644 --- a/src/unit_tests/models/graph/kth_best_spanning_tree.rs +++ b/src/unit_tests/models/graph/kth_best_spanning_tree.rs @@ -27,6 +27,12 @@ fn no_instance() -> KthBestSpanningTree { KthBestSpanningTree::new(graph, weights, 2, 3) } +fn small_yes_instance() -> KthBestSpanningTree { + let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let weights = vec![1, 1, 1]; + KthBestSpanningTree::new(graph, weights, 2, 2) +} + fn yes_witness_config() -> Vec { vec![ 1, 0, 1, 0, 1, 0, 1, 0, // {0,1}, {1,2}, {2,3}, {3,4} @@ -53,6 +59,8 @@ fn test_kthbestspanningtree_creation() { let problem = yes_instance(); assert_eq!(problem.dims(), vec![2; 24]); + assert_eq!(problem.graph().num_vertices(), 5); + assert_eq!(problem.graph().num_edges(), 8); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 8); assert_eq!(problem.k(), 3); @@ -66,6 +74,7 @@ fn test_kthbestspanningtree_creation() { fn test_kthbestspanningtree_evaluation_yes_instance() { let problem = yes_instance(); assert!(problem.evaluate(&yes_witness_config())); + assert!(problem.is_valid_solution(&yes_witness_config())); } #[test] @@ -80,6 +89,20 @@ fn test_kthbestspanningtree_evaluation_rejects_overweight_tree() { assert!(!problem.evaluate(&overweight_tree_config())); } +#[test] +fn test_kthbestspanningtree_evaluation_rejects_wrong_length_config() { + let problem = yes_instance(); + assert!(!problem.evaluate(&yes_witness_config()[..23])); +} + +#[test] +fn test_kthbestspanningtree_evaluation_rejects_nonbinary_value() { + let problem = yes_instance(); + let mut config = yes_witness_config(); + config[0] = 2; + assert!(!problem.evaluate(&config)); +} + #[test] fn test_kthbestspanningtree_solver_yes_instance() { let problem = yes_instance(); @@ -99,6 +122,16 @@ fn test_kthbestspanningtree_solver_no_instance() { assert!(solver.find_all_satisfying(&problem).is_empty()); } +#[test] +fn test_kthbestspanningtree_small_exhaustive_search() { + let problem = small_yes_instance(); + let solver = BruteForce::new(); + + let all = solver.find_all_satisfying(&problem); + assert_eq!(all.len(), 6); + assert!(all.iter().all(|config| problem.evaluate(config))); +} + #[test] fn test_kthbestspanningtree_serialization() { let problem = yes_instance(); @@ -121,7 +154,32 @@ fn test_kthbestspanningtree_paper_example() { assert!(problem.evaluate(&witness)); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); - assert_eq!(all.len(), 4_896); - assert!(all.iter().any(|config| config == &witness)); + let solution = solver.find_satisfying(&problem).unwrap(); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_kthbestspanningtree_single_vertex_accepts_single_empty_tree() { + let problem = KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]), vec![], 1, 0); + assert!(problem.evaluate(&[])); + assert!(problem.is_valid_solution(&[])); +} + +#[test] +fn test_kthbestspanningtree_single_vertex_rejects_multiple_empty_trees() { + let problem = KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]), vec![], 2, 0); + assert!(!problem.evaluate(&[])); +} + +#[test] +#[should_panic(expected = "weights length must match graph num_edges")] +fn test_kthbestspanningtree_creation_rejects_weight_length_mismatch() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let _ = KthBestSpanningTree::new(graph, vec![1], 1, 2); +} + +#[test] +#[should_panic(expected = "k must be positive")] +fn test_kthbestspanningtree_creation_rejects_zero_k() { + let _ = KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]), vec![], 0, 0); } From 65ab98ecc8f5ebdaf768fab33ff3ac231d4aa617 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 13:00:55 +0800 Subject: [PATCH 5/9] fix: clarify edge-weight create help --- problemreductions-cli/src/commands/create.rs | 37 +++++++++++++++++ problemreductions-cli/tests/cli_tests.rs | 43 ++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 25f0d2d43..71dfeb101 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -310,6 +310,17 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } } +fn uses_edge_weights_flag(canonical: &str) -> bool { + matches!( + canonical, + "KthBestSpanningTree" + | "MaxCut" + | "MaximumMatching" + | "TravelingSalesman" + | "RuralPostman" + ) +} + fn help_flag_name(canonical: &str, field_name: &str) -> String { // Problem-specific overrides first match (canonical, field_name) { @@ -317,6 +328,10 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String { ("BoundedComponentSpanningForest", "max_weight") => return "bound".to_string(), _ => {} } + // Edge-weight problems use --edge-weights instead of --weights + if field_name == "weights" && uses_edge_weights_flag(canonical) { + return "edge-weights".to_string(); + } // General field-name overrides (previously in cli_flag_name) match field_name { "universe_size" => "universe".to_string(), @@ -334,6 +349,25 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String { } } +fn reject_vertex_weights_for_edge_weight_problem( + args: &CreateArgs, + canonical: &str, + graph_type: Option<&str>, +) -> Result<()> { + if args.weights.is_some() && uses_edge_weights_flag(canonical) { + bail!( + "{canonical} uses --edge-weights, not --weights.\n\n\ + Usage: pred create {} {}", + match graph_type { + Some(g) => format!("{canonical}/{g}"), + None => canonical.to_string(), + }, + example_for(canonical, graph_type) + ); + } + Ok(()) +} + fn help_flag_hint( canonical: &str, field_name: &str, @@ -783,6 +817,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // KthBestSpanningTree (weighted graph + k + bound) "KthBestSpanningTree" => { + reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; let (graph, _) = parse_graph(args).map_err(|e| { anyhow::anyhow!( "{e}\n\nUsage: pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3" @@ -810,6 +845,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // Graph problems with edge weights "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { + reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; let (graph, _) = parse_graph(args).map_err(|e| { anyhow::anyhow!( "{e}\n\nUsage: pred create {} --graph 0-1,1-2,2-3 [--edge-weights 1,1,1]", @@ -828,6 +864,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // RuralPostman "RuralPostman" => { + reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; let (graph, _) = parse_graph(args).map_err(|e| { anyhow::anyhow!( "{e}\n\nUsage: pred create RuralPostman --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --required-edges 0,2 --bound 6" diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index cd71652f3..07ece0ba5 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -2450,6 +2450,49 @@ fn test_create_kth_best_spanning_tree_rejects_zero_k() { ); } +#[test] +fn test_create_kth_best_spanning_tree_help_uses_edge_weights() { + let output = pred() + .args(["create", "KthBestSpanningTree"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("--edge-weights"), + "expected edge-weight help, got: {stderr}" + ); + assert!( + !stderr.contains("\n --weights"), + "vertex-weight flag should not be suggested, got: {stderr}" + ); +} + +#[test] +fn test_create_kth_best_spanning_tree_rejects_vertex_weights_flag() { + let output = pred() + .args([ + "create", + "KthBestSpanningTree", + "--graph", + "0-1,0-2,1-2", + "--weights", + "9,9,9", + "--k", + "1", + "--bound", + "3", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("--edge-weights"), + "expected guidance toward edge weights, got: {stderr}" + ); +} + #[test] fn test_create_length_bounded_disjoint_paths_rejects_equal_terminals() { let output = pred() From 641caa30bbd0e1396ac9157d540cf6e2cdbcee4d Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Tue, 17 Mar 2026 20:33:43 +0800 Subject: [PATCH 6/9] style: fix formatting in create.rs Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 71dfeb101..21d586efd 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -313,11 +313,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { fn uses_edge_weights_flag(canonical: &str) -> bool { matches!( canonical, - "KthBestSpanningTree" - | "MaxCut" - | "MaximumMatching" - | "TravelingSalesman" - | "RuralPostman" + "KthBestSpanningTree" | "MaxCut" | "MaximumMatching" | "TravelingSalesman" | "RuralPostman" ) } @@ -1704,7 +1700,6 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { emit_problem_output(&output, out) } - /// Reject non-unit weights when the resolved variant uses `weight=One`. fn reject_nonunit_weights_for_one_variant( canonical: &str, From 260fe932c00888d94888d337ac7c861b94b2c293 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Tue, 17 Mar 2026 22:04:45 +0800 Subject: [PATCH 7/9] fix: safe bound cast and remove dead code in KthBestSpanningTree Use i32::try_from instead of `as i32` for CLI --bound to prevent silent truncation. Remove unreachable defensive branches in evaluate_config to eliminate coverage gaps. Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 7 +++++-- src/models/graph/kth_best_spanning_tree.rs | 16 ++++------------ 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 21d586efd..21fd1b601 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -822,12 +822,15 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { let edge_weights = parse_edge_weights(args, graph.num_edges())?; let (k, _variant) = util::validate_k_param(&resolved_variant, args.k, None, "KthBestSpanningTree")?; - let bound = args.bound.ok_or_else(|| { + let bound_raw = args.bound.ok_or_else(|| { anyhow::anyhow!( "KthBestSpanningTree requires --bound\n\n\ Usage: pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3" ) - })? as i32; + })?; + let bound = i32::try_from(bound_raw).map_err(|_| { + anyhow::anyhow!("KthBestSpanningTree --bound value {bound_raw} does not fit in i32") + })?; ( ser(problemreductions::models::graph::KthBestSpanningTree::new( graph, diff --git a/src/models/graph/kth_best_spanning_tree.rs b/src/models/graph/kth_best_spanning_tree.rs index 80e001bf6..d0af7dc18 100644 --- a/src/models/graph/kth_best_spanning_tree.rs +++ b/src/models/graph/kth_best_spanning_tree.rs @@ -145,10 +145,9 @@ impl KthBestSpanningTree { return true; } - let start = match start { - Some(vertex) => vertex, - None => return false, - }; + // SAFETY: num_vertices > 1 and selected_count == num_vertices - 1 > 0, + // so at least one edge was selected and `start` is Some. + let start = start.expect("at least one selected edge"); let mut visited = vec![false; num_vertices]; let mut queue = VecDeque::new(); @@ -168,10 +167,7 @@ impl KthBestSpanningTree { } fn blocks_are_pairwise_distinct(&self, config: &[usize], block_size: usize) -> bool { - if block_size == 0 { - return self.k == 1; - } - + debug_assert!(block_size > 0, "block_size must be positive"); let blocks: Vec<&[usize]> = config.chunks_exact(block_size).collect(); for left in 0..blocks.len() { for right in (left + 1)..blocks.len() { @@ -195,10 +191,6 @@ impl KthBestSpanningTree { } let edges = self.graph.edges(); - let blocks = config.chunks_exact(block_size); - if !blocks.remainder().is_empty() { - return false; - } if !self.blocks_are_pairwise_distinct(config, block_size) { return false; From 346dcad0ef7daf9370d89c2a44c2e33e818e232a Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Tue, 17 Mar 2026 22:08:42 +0800 Subject: [PATCH 8/9] revert: restore original bound cast to match existing pattern Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/commands/create.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 21fd1b601..21d586efd 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -822,15 +822,12 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { let edge_weights = parse_edge_weights(args, graph.num_edges())?; let (k, _variant) = util::validate_k_param(&resolved_variant, args.k, None, "KthBestSpanningTree")?; - let bound_raw = args.bound.ok_or_else(|| { + let bound = args.bound.ok_or_else(|| { anyhow::anyhow!( "KthBestSpanningTree requires --bound\n\n\ Usage: pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3" ) - })?; - let bound = i32::try_from(bound_raw).map_err(|_| { - anyhow::anyhow!("KthBestSpanningTree --bound value {bound_raw} does not fit in i32") - })?; + })? as i32; ( ser(problemreductions::models::graph::KthBestSpanningTree::new( graph, From 36ac4f262b484f0a6e975a65430f1e442f15052f Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Wed, 18 Mar 2026 13:56:51 +0800 Subject: [PATCH 9/9] fix: use small canonical example for KthBestSpanningTree Replace the 24-variable example (5v, k=3, B=12) with a K4 instance (12 variables, k=2, B=4) that has exactly 2 satisfying configs. This enables exhaustive brute-force verification via regenerate-fixtures and eliminates the need for explicit_example with hard-coded solutions. Update paper example and tests accordingly. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 15 +++- src/example_db/fixtures/examples.json | 2 +- src/models/graph/kth_best_spanning_tree.rs | 31 ++----- .../models/graph/kth_best_spanning_tree.rs | 87 +++++++------------ 4 files changed, 50 insertions(+), 85 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index be7fd2d68..207d2106d 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -855,6 +855,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 #{ let x = load-model-example("KthBestSpanningTree") let edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1))) + let weights = x.instance.weights let m = edges.len() let sol = x.optimal.at(0).config let tree1 = sol.enumerate().filter(((i, v)) => i < m and v == 1).map(((i, _)) => edges.at(i)) @@ -868,21 +869,27 @@ is feasible: each set induces a connected subgraph, the component weights are $2 Variables: $k |E|$ binary values grouped into $k$ consecutive edge-selection blocks. Entry $x_(i, e) = 1$ means edge $e$ belongs to the $i$-th candidate tree. A configuration is satisfying exactly when each block selects a spanning tree, every selected tree has total weight at most $B$, and the $k$ blocks encode pairwise distinct edge sets. - *Example.* Consider the 5-vertex graph with weighted edges ${(0,1): 2, (0,2): 3, (1,2): 1, (1,3): 4, (2,3): 2, (2,4): 5, (3,4): 3, (0,4): 6}$. With $k = 3$ and $B = 12$, the spanning trees $T_1 = {(0,1), (1,2), (2,3), (3,4)}$, $T_2 = {(0,1), (1,2), (1,3), (3,4)}$, and $T_3 = {(0,2), (1,2), (2,3), (3,4)}$ have weights $8$, $10$, and $9$, respectively. They are all distinct and all satisfy the bound, so this instance is a YES-instance. + *Example.* Consider $K_4$ with edge weights $w = {(0,1): 1, (0,2): 1, (0,3): 2, (1,2): 2, (1,3): 2, (2,3): 3}$. With $k = 2$ and $B = 4$, exactly two of the $16$ spanning trees have total weight $lt.eq 4$: the star $T_1 = {(0,1), (0,2), (0,3)}$ with weight $4$ and $T_2 = {(0,1), (0,2), (1,3)}$ with weight $4$. Since two distinct bounded spanning trees exist, this is a YES-instance. #figure({ canvas(length: 1cm, { - let pos = ((0.0, 1.2), (1.1, 2.0), (2.3, 1.2), (1.8, 0.0), (0.3, 0.0)) - for (u, v) in edges { + import draw: * + let pos = ((0.0, 1.8), (2.4, 1.8), (2.4, 0.0), (0.0, 0.0)) + for (idx, (u, v)) in edges.enumerate() { let in-tree1 = tree1.any(e => (e.at(0) == u and e.at(1) == v) or (e.at(0) == v and e.at(1) == u)) g-edge(pos.at(u), pos.at(v), stroke: if in-tree1 { 2pt + blue } else { 1pt + gray }) + let mid-x = (pos.at(u).at(0) + pos.at(v).at(0)) / 2 + let mid-y = (pos.at(u).at(1) + pos.at(v).at(1)) / 2 + // Offset diagonal edge labels to avoid overlap at center + let (ox, oy) = if u == 0 and v == 2 { (0.3, 0) } else if u == 1 and v == 3 { (-0.3, 0) } else { (0, 0) } + content((mid-x + ox, mid-y + oy), text(7pt)[#weights.at(idx)], fill: white, frame: "rect", padding: .06, stroke: none) } for (idx, p) in pos.enumerate() { g-node(p, name: "v" + str(idx), fill: white, label: $v_#idx$) } }) }, - caption: [Kth Best Spanning Tree example graph. Blue edges show $T_1 = {(0,1), (1,2), (2,3), (3,4)}$, one of the three bounded spanning trees used to certify the YES-instance for $k = 3$ and $B = 12$.], + caption: [Kth Best Spanning Tree on $K_4$. Blue edges show $T_1 = {(0,1), (0,2), (0,3)}$, one of two spanning trees with weight $lt.eq 4$.], ) ] ] diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index eea339aef..d800e6c64 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -15,7 +15,7 @@ {"problem":"IsomorphicSpanningTree","variant":{},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"tree":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[0,1,2,3],"metric":true}],"optimal":[{"config":[0,1,2,3],"metric":true},{"config":[0,1,3,2],"metric":true},{"config":[0,2,1,3],"metric":true},{"config":[0,2,3,1],"metric":true},{"config":[0,3,1,2],"metric":true},{"config":[0,3,2,1],"metric":true},{"config":[1,0,2,3],"metric":true},{"config":[1,0,3,2],"metric":true},{"config":[1,2,0,3],"metric":true},{"config":[1,2,3,0],"metric":true},{"config":[1,3,0,2],"metric":true},{"config":[1,3,2,0],"metric":true},{"config":[2,0,1,3],"metric":true},{"config":[2,0,3,1],"metric":true},{"config":[2,1,0,3],"metric":true},{"config":[2,1,3,0],"metric":true},{"config":[2,3,0,1],"metric":true},{"config":[2,3,1,0],"metric":true},{"config":[3,0,1,2],"metric":true},{"config":[3,0,2,1],"metric":true},{"config":[3,1,0,2],"metric":true},{"config":[3,1,2,0],"metric":true},{"config":[3,2,0,1],"metric":true},{"config":[3,2,1,0],"metric":true}]}, {"problem":"KColoring","variant":{"graph":"SimpleGraph","k":"K3"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"num_colors":3},"samples":[{"config":[0,1,1,0,2],"metric":true}],"optimal":[{"config":[0,1,1,0,2],"metric":true},{"config":[0,1,1,2,0],"metric":true},{"config":[0,1,2,0,1],"metric":true},{"config":[0,2,1,0,2],"metric":true},{"config":[0,2,2,0,1],"metric":true},{"config":[0,2,2,1,0],"metric":true},{"config":[1,0,0,1,2],"metric":true},{"config":[1,0,0,2,1],"metric":true},{"config":[1,0,2,1,0],"metric":true},{"config":[1,2,0,1,2],"metric":true},{"config":[1,2,2,0,1],"metric":true},{"config":[1,2,2,1,0],"metric":true},{"config":[2,0,0,1,2],"metric":true},{"config":[2,0,0,2,1],"metric":true},{"config":[2,0,1,2,0],"metric":true},{"config":[2,1,0,2,1],"metric":true},{"config":[2,1,1,0,2],"metric":true},{"config":[2,1,1,2,0],"metric":true}]}, {"problem":"KSatisfiability","variant":{"k":"K3"},"instance":{"clauses":[{"literals":[1,2,3]},{"literals":[-1,-2,3]},{"literals":[1,-2,-3]}],"num_vars":3},"samples":[{"config":[1,0,1],"metric":true}],"optimal":[{"config":[0,0,1],"metric":true},{"config":[0,1,0],"metric":true},{"config":[1,0,0],"metric":true},{"config":[1,0,1],"metric":true},{"config":[1,1,1],"metric":true}]}, - {"problem":"KthBestSpanningTree","variant":{"weight":"i32"},"instance":{"bound":12,"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null],[0,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"k":3,"weights":[2,3,1,4,2,5,3,6]},"samples":[{"config":[1,0,1,0,1,0,1,0,1,0,1,1,0,0,1,0,0,1,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[1,0,1,0,1,0,1,0,1,0,1,1,0,0,1,0,0,1,1,0,1,0,1,0],"metric":true}]}, + {"problem":"KthBestSpanningTree","variant":{"weight":"i32"},"instance":{"bound":4,"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"k":2,"weights":[1,1,2,2,2,3]},"samples":[{"config":[1,1,1,0,0,0,1,1,0,0,1,0],"metric":true},{"config":[0,0,0,0,0,0,0,0,0,0,0,0],"metric":false}],"optimal":[{"config":[1,1,0,0,1,0,1,1,1,0,0,0],"metric":true},{"config":[1,1,1,0,0,0,1,1,0,0,1,0],"metric":true}]}, {"problem":"LengthBoundedDisjointPaths","variant":{"graph":"SimpleGraph"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,6,null],[0,2,null],[2,3,null],[3,6,null],[0,4,null],[4,5,null],[5,6,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null]}},"max_length":3,"num_paths_required":2,"sink":6,"source":0},"samples":[{"config":[1,1,0,0,0,0,1,1,0,1,1,0,0,1],"metric":true}],"optimal":[{"config":[1,0,0,0,1,1,1,1,0,1,1,0,0,1],"metric":true},{"config":[1,0,0,0,1,1,1,1,1,0,0,0,0,1],"metric":true},{"config":[1,0,1,1,0,0,1,1,0,0,0,1,1,1],"metric":true},{"config":[1,0,1,1,0,0,1,1,1,0,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,0,1,1,0,0,0,1,1,1],"metric":true},{"config":[1,1,0,0,0,0,1,1,0,1,1,0,0,1],"metric":true}]}, {"problem":"MaxCut","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,1,1,1,1,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,0,1,0],"metric":{"Valid":5}}],"optimal":[{"config":[0,1,1,0,0],"metric":{"Valid":5}},{"config":[0,1,1,0,1],"metric":{"Valid":5}},{"config":[1,0,0,1,0],"metric":{"Valid":5}},{"config":[1,0,0,1,1],"metric":{"Valid":5}}]}, {"problem":"MaximalIS","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[2,3,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"weights":[1,1,1,1,1]},"samples":[{"config":[0,1,0,1,0],"metric":{"Valid":2}},{"config":[1,0,1,0,1],"metric":{"Valid":3}}],"optimal":[{"config":[1,0,1,0,1],"metric":{"Valid":3}}]}, diff --git a/src/models/graph/kth_best_spanning_tree.rs b/src/models/graph/kth_best_spanning_tree.rs index d0af7dc18..aa155cc6d 100644 --- a/src/models/graph/kth_best_spanning_tree.rs +++ b/src/models/graph/kth_best_spanning_tree.rs @@ -232,29 +232,16 @@ pub(crate) fn canonical_model_example_specs() -> Vec KthBestSpanningTree { - let graph = SimpleGraph::new( - 5, - vec![ - (0, 1), - (0, 2), - (1, 2), - (1, 3), - (2, 3), - (2, 4), - (3, 4), - (0, 4), - ], - ); - let weights = vec![2, 3, 1, 4, 2, 5, 3, 6]; - KthBestSpanningTree::new(graph, weights, 3, 12) + let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); + KthBestSpanningTree::new(graph, vec![1, 1, 2, 2, 2, 3], 2, 4) } fn no_instance() -> KthBestSpanningTree { @@ -33,24 +24,11 @@ fn small_yes_instance() -> KthBestSpanningTree { KthBestSpanningTree::new(graph, weights, 2, 2) } +/// Star at 0: edges {01,02,03}, then {01,02,13}. fn yes_witness_config() -> Vec { vec![ - 1, 0, 1, 0, 1, 0, 1, 0, // {0,1}, {1,2}, {2,3}, {3,4} - 1, 0, 1, 1, 0, 0, 1, 0, // {0,1}, {1,2}, {1,3}, {3,4} - 0, 1, 1, 0, 1, 0, 1, 0, // {0,2}, {1,2}, {2,3}, {3,4} - ] -} - -fn duplicate_tree_config() -> Vec { - vec![ - 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, - ] -} - -fn overweight_tree_config() -> Vec { - vec![ - 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, - 0, // {0,1}, {0,2}, {1,3}, {2,4} => 14 + 1, 1, 1, 0, 0, 0, // block 1: edges 0,1,2 = {01,02,03} + 1, 1, 0, 0, 1, 0, // block 2: edges 0,1,4 = {01,02,13} ] } @@ -58,14 +36,14 @@ fn overweight_tree_config() -> Vec { fn test_kthbestspanningtree_creation() { let problem = yes_instance(); - assert_eq!(problem.dims(), vec![2; 24]); - assert_eq!(problem.graph().num_vertices(), 5); - assert_eq!(problem.graph().num_edges(), 8); - assert_eq!(problem.num_vertices(), 5); - assert_eq!(problem.num_edges(), 8); - assert_eq!(problem.k(), 3); - assert_eq!(problem.weights(), &[2, 3, 1, 4, 2, 5, 3, 6]); - assert_eq!(*problem.bound(), 12); + assert_eq!(problem.dims(), vec![2; 12]); + assert_eq!(problem.graph().num_vertices(), 4); + assert_eq!(problem.graph().num_edges(), 6); + assert_eq!(problem.num_vertices(), 4); + assert_eq!(problem.num_edges(), 6); + assert_eq!(problem.k(), 2); + assert_eq!(problem.weights(), &[1, 1, 2, 2, 2, 3]); + assert_eq!(*problem.bound(), 4); assert!(problem.is_weighted()); assert_eq!(KthBestSpanningTree::::NAME, "KthBestSpanningTree"); } @@ -80,19 +58,23 @@ fn test_kthbestspanningtree_evaluation_yes_instance() { #[test] fn test_kthbestspanningtree_evaluation_rejects_duplicate_trees() { let problem = yes_instance(); - assert!(!problem.evaluate(&duplicate_tree_config())); + // Same tree in both blocks: {01,02,03} twice + let dup = vec![1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0]; + assert!(!problem.evaluate(&dup)); } #[test] fn test_kthbestspanningtree_evaluation_rejects_overweight_tree() { let problem = yes_instance(); - assert!(!problem.evaluate(&overweight_tree_config())); + // {01,03,12} w=5 and {01,02,03} w=4: first tree exceeds B=4 + let config = vec![1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 0, 0]; + assert!(!problem.evaluate(&config)); } #[test] fn test_kthbestspanningtree_evaluation_rejects_wrong_length_config() { let problem = yes_instance(); - assert!(!problem.evaluate(&yes_witness_config()[..23])); + assert!(!problem.evaluate(&yes_witness_config()[..11])); } #[test] @@ -104,13 +86,14 @@ fn test_kthbestspanningtree_evaluation_rejects_nonbinary_value() { } #[test] -fn test_kthbestspanningtree_solver_yes_instance() { +fn test_kthbestspanningtree_solver_exhaustive() { let problem = yes_instance(); let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem); - assert!(solution.is_some()); - assert!(problem.evaluate(&solution.unwrap())); + // Exactly 2 spanning trees have weight ≤ 4, so exactly 2! = 2 satisfying configs. + let all = solver.find_all_satisfying(&problem); + assert_eq!(all.len(), 2); + assert!(all.iter().all(|config| problem.evaluate(config))); } #[test] @@ -146,18 +129,6 @@ fn test_kthbestspanningtree_serialization() { assert!(restored.evaluate(&yes_witness_config())); } -#[test] -fn test_kthbestspanningtree_paper_example() { - let problem = yes_instance(); - let witness = yes_witness_config(); - - assert!(problem.evaluate(&witness)); - - let solver = BruteForce::new(); - let solution = solver.find_satisfying(&problem).unwrap(); - assert!(problem.evaluate(&solution)); -} - #[test] fn test_kthbestspanningtree_single_vertex_accepts_single_empty_tree() { let problem = KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]), vec![], 1, 0);