From 5ac18b97e598411ef378c5385b9dabc0c7b4381d Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 06:05:53 +0800 Subject: [PATCH 1/4] Add plan for #407: [Model] RootedTreeArrangement --- .../2026-03-22-rooted-tree-arrangement.md | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 docs/plans/2026-03-22-rooted-tree-arrangement.md diff --git a/docs/plans/2026-03-22-rooted-tree-arrangement.md b/docs/plans/2026-03-22-rooted-tree-arrangement.md new file mode 100644 index 000000000..bbfa0d285 --- /dev/null +++ b/docs/plans/2026-03-22-rooted-tree-arrangement.md @@ -0,0 +1,254 @@ +# RootedTreeArrangement Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add `RootedTreeArrangement` as a graph-based satisfaction problem with the parent-array plus permutation encoding described in issue `#407`, including CLI/example-db support, tests, and a paper entry. + +**Architecture:** Implement the model alongside [`OptimalLinearArrangement`](/Users/jinguomini/rcode/problem-reductions/.worktrees/issue-407/src/models/graph/optimal_linear_arrangement.rs) as a generic graph satisfaction problem over `G: Graph`. The core evaluator will parse a `2n`-variable configuration into a rooted-tree parent array and a bijection `f`, validate the rooted tree and mapping, then check the ancestor/descendant edge constraint and the total stretch bound `K`. + +**Tech Stack:** Rust workspace, registry macros (`inventory::submit!`, `declare_variants!`), `pred` CLI, example-db exports, Typst paper, `cargo`/`make` verification. + +--- + +## Issue-Specific Constraints + +- Use the issue comment corrections as source of truth: + - fixed-length encoding is `dims() = vec![n; 2 * n]` + - expose `num_vertices()` and `num_edges()` + - keep Example 2 consistent; use it as the canonical worked example +- Associated open rule issue exists: `#424 [Rule] Rooted Tree Arrangement to Rooted Tree Storage Assignment`, so this model is not an orphan. +- Reuse existing bibliography key `gareyJohnsonStockmeyer1976`; add missing keys for Gavril (1977) and Adolphson-Hu (1973) if they are not already in `docs/paper/references.bib`. + +## Batch 1: add-model Steps 1-5.5 + +### Task 1: Add failing model tests first + +**Files:** +- Create: `src/unit_tests/models/graph/rooted_tree_arrangement.rs` +- Reference: `src/unit_tests/models/graph/optimal_linear_arrangement.rs` +- Reference: `src/models/graph/optimal_linear_arrangement.rs` + +**Step 1: Write the failing test file** + +Add targeted tests for: +- basic getters and `dims()` on a 5-vertex issue example +- a valid YES witness using the chain tree plus identity mapping from issue Example 2 +- rejection of invalid parent arrays (multiple roots / directed cycle) +- rejection of invalid bijections (duplicate or out-of-range image) +- rejection when an edge endpoint pair is not ancestor-comparable +- rejection when the total tree distance exceeds `bound` +- serde round-trip and brute-force sanity on a tiny instance + +Use the issue encoding directly in the tests: +- parent array occupies `config[..n]` +- bijection occupies `config[n..]` + +**Step 2: Run the targeted test to verify RED** + +Run: `cargo test rooted_tree_arrangement --lib` + +Expected: FAIL because `RootedTreeArrangement` does not exist yet. + +**Step 3: Commit the red test scaffold** + +```bash +git add src/unit_tests/models/graph/rooted_tree_arrangement.rs +git commit -m "test: add RootedTreeArrangement coverage" +``` + +### Task 2: Implement the model core + +**Files:** +- Create: `src/models/graph/rooted_tree_arrangement.rs` +- Modify: `src/models/graph/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` + +**Step 1: Implement the new graph model** + +Follow repo-local `add-model` Steps 1-3 in this file: +- add `ProblemSchemaEntry` with `graph` dimension only +- define `RootedTreeArrangement` with fields `graph: G` and `bound: usize` +- implement getters `graph()`, `bound()`, `num_vertices()`, `num_edges()` +- set `dims()` to `vec![n; 2 * n]` +- implement helpers to: + - validate exactly one root in the parent array + - reject self-parenting outside the root + - reject cycles / disconnected parent forests + - compute depth of every tree node + - validate the bijection as a permutation of `0..n` + - test whether two mapped vertices are ancestor-comparable + - sum tree distances for graph edges +- expose `is_valid_solution()` and a stretch helper that returns `Option` +- implement `Problem` and `SatisfactionProblem` +- declare the default variant as `sat RootedTreeArrangement => "2^num_vertices"` +- add `canonical_model_example_specs()` using the cleaned-up Example 2 instance from the issue +- link the new unit test file at the bottom of the module + +**Step 2: Register exports** + +Update: +- `src/models/graph/mod.rs` +- `src/models/mod.rs` +- `src/lib.rs` + +Add the module declaration, public re-export, prelude export, and graph example-db registration chain entry. + +**Step 3: Run the targeted tests to verify GREEN** + +Run: +- `cargo test rooted_tree_arrangement --lib` + +Expected: PASS for the new model tests. + +**Step 4: Refactor lightly if needed** + +Keep helpers local to the model file unless duplication appears. + +**Step 5: Commit** + +```bash +git add src/models/graph/rooted_tree_arrangement.rs src/models/graph/mod.rs src/models/mod.rs src/lib.rs +git commit -m "feat: add RootedTreeArrangement model" +``` + +### Task 3: Add CLI discovery and creation support + +**Files:** +- Modify: `problemreductions-cli/src/problem_name.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/src/cli.rs` + +**Step 1: Extend CLI discovery** + +In `problem_name.rs`, ensure the catalog alias path is sufficient; only add a manual alias if the registry alias is absent and the abbreviation is literature-backed. + +**Step 2: Extend create command support** + +In `create.rs`: +- add example/help strings for `RootedTreeArrangement` +- add the direct `pred create RootedTreeArrangement --graph ... --bound ...` match arm +- add the random graph generator arm mirroring `OptimalLinearArrangement`, defaulting `bound` to a safe satisfiable upper bound if omitted + +**Step 3: Update CLI help text** + +In `cli.rs`: +- add `RootedTreeArrangement` to the “Flags by problem type” table +- extend the `--bound` doc comment if the problem name list is maintained manually there + +**Step 4: Verify** + +Run: +- `cargo test -p problemreductions-cli create:: -- --nocapture` + +If the CLI test filter is too broad or absent, run a targeted `cargo test -p problemreductions-cli rooted_tree_arrangement`. + +**Step 5: Commit** + +```bash +git add problemreductions-cli/src/problem_name.rs problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs +git commit -m "feat: add RootedTreeArrangement CLI support" +``` + +### Task 4: Wire the canonical example and example-db coverage + +**Files:** +- Modify: `src/models/graph/rooted_tree_arrangement.rs` +- Reference: `src/example_db/model_builders.rs` +- Reference: `src/unit_tests/example_db.rs` + +**Step 1: Confirm example-db wiring** + +Ensure the `canonical_model_example_specs()` entry added in Task 2 is reachable through `src/models/graph/mod.rs` so `src/example_db/model_builders.rs` picks it up automatically. + +**Step 2: Add or extend tests if needed** + +If the model tests do not already exercise the canonical example config, add one focused assertion that the example-db config evaluates to `true`. + +**Step 3: Verify** + +Run: +- `cargo test example_db` +- `cargo test rooted_tree_arrangement --features example-db` + +**Step 4: Commit** + +```bash +git add src/models/graph/rooted_tree_arrangement.rs src/models/graph/mod.rs +git commit -m "test: cover RootedTreeArrangement example-db wiring" +``` + +## Batch 2: add-model Step 6 + +### Task 5: Document the model in the paper + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `docs/paper/references.bib` + +**Step 1: Add bibliography entries if missing** + +Add citation keys for: +- Gavril (1977), the GT45 NP-completeness reference +- Adolphson and Hu (1973), the tree linear arrangement algorithm reference + +Reuse existing `gareyJohnsonStockmeyer1976` for the `OptimalLinearArrangement` connection. + +**Step 2: Add display-name and `problem-def`** + +In `reductions.typ`: +- add `"RootedTreeArrangement": [Rooted Tree Arrangement],` to the display-name dictionary +- add a `problem-def("RootedTreeArrangement")` section near `OptimalLinearArrangement` +- explain the decision formulation, the rooted-tree witness, and the ancestor-path constraint +- use the canonical Example 2 instance from the model example export +- include a `pred-commands(...)` block using `pred create --example RootedTreeArrangement` + +**Step 3: Build the paper** + +Run: +- `make paper` + +Expected: PASS with no Typst errors. + +**Step 4: Commit** + +```bash +git add docs/paper/reductions.typ docs/paper/references.bib +git commit -m "docs: document RootedTreeArrangement" +``` + +## Final Verification: add-model Step 7 + +### Task 6: Run focused and repo-level verification + +**Files:** +- No code changes expected + +**Step 1: Focused verification** + +Run: +- `cargo test rooted_tree_arrangement` +- `cargo test optimal_linear_arrangement` +- `cargo test example_db --features example-db` + +**Step 2: Project verification** + +Run: +- `make fmt` +- `make check` + +If `make check` is too slow or exposes unrelated failures, capture the exact failing command and stop rather than masking it. + +**Step 3: Review git state** + +Run: +- `git status --short` + +Expected: clean working tree except for intentionally ignored/generated files. + +**Step 4: Commit any remaining implementation work** + +```bash +git add -A +git commit -m "Implement #407: add RootedTreeArrangement" +``` From e12808c61d702e4a2d625f4d3b99147cbdacf6b2 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 06:19:39 +0800 Subject: [PATCH 2/4] Implement #407: [Model] RootedTreeArrangement --- docs/paper/reductions.typ | 25 ++ docs/paper/references.bib | 19 ++ problemreductions-cli/src/cli.rs | 3 +- problemreductions-cli/src/commands/create.rs | 42 +++- problemreductions-cli/tests/cli_tests.rs | 28 +++ src/lib.rs | 4 +- src/models/graph/mod.rs | 4 + src/models/graph/rooted_tree_arrangement.rs | 228 ++++++++++++++++++ src/models/mod.rs | 6 +- .../models/graph/rooted_tree_arrangement.rs | 107 ++++++++ 10 files changed, 457 insertions(+), 9 deletions(-) create mode 100644 src/models/graph/rooted_tree_arrangement.rs create mode 100644 src/unit_tests/models/graph/rooted_tree_arrangement.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 9167a0c15..fb8ec8dce 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -118,6 +118,7 @@ "ConsecutiveSets": [Consecutive Sets], "MinimumMultiwayCut": [Minimum Multiway Cut], "OptimalLinearArrangement": [Optimal Linear Arrangement], + "RootedTreeArrangement": [Rooted Tree Arrangement], "RuralPostman": [Rural Postman], "MixedChinesePostman": [Mixed Chinese Postman], "StackerCrane": [Stacker Crane], @@ -1744,6 +1745,30 @@ is feasible: each set induces a connected subgraph, the component weights are $2 ] ] } +#{ + let x = load-model-example("RootedTreeArrangement") + let nv = graph-num-vertices(x.instance) + let ne = graph-num-edges(x.instance) + let edges = x.instance.graph.edges.map(e => (e.at(0), e.at(1))) + let K = x.instance.bound + [ + #problem-def("RootedTreeArrangement")[ + Given an undirected graph $G = (V, E)$ and a non-negative integer $K$, is there a rooted tree $T = (U, F)$ with $|U| = |V|$ and a bijection $f: V -> U$ such that every edge $\{u, v\} in E$ maps to two nodes lying on a common root-to-leaf path in $T$, and $sum_(\{u, v\} in E) d_T(f(u), f(v)) <= K$? + ][ + Rooted Tree Arrangement is GT45 in Garey and Johnson @garey1979. It generalizes Optimal Linear Arrangement by allowing the host layout to be any rooted tree rather than a single path. Garey and Johnson cite Gavril's NP-completeness proof via reduction from Optimal Linear Arrangement @gavril1977. + + The connection to Optimal Linear Arrangement is immediate: if the rooted tree is restricted to a chain, the stretch objective becomes the linear-arrangement objective. This explains why the two problems live in the same arrangement family. For tree-oriented ordering problems, Adolphson and Hu give a polynomial-time algorithm for optimal linear ordering on trees @adolphsonHu1973, showing that the difficulty here comes from simultaneously choosing both the rooted-tree topology and the vertex-to-node bijection. + + *Example.* Consider the graph with $n = #nv$ vertices, $|E| = #ne$ edges, and edge set ${#edges.map(((u, v)) => $(v_#u, v_#v)$).join(", ")}$. With bound $K = #K$, the chain tree encoded by parent array $(0, 0, 1, 2)$ and identity mapping $(0, 1, 2, 3)$ is a valid witness: every listed edge lies on the unique root-to-leaf chain, and the total stretch is $1 + 2 + 1 + 1 = 5 <= #K$. Therefore this canonical instance is a YES instance. + + #pred-commands( + "pred create --example RootedTreeArrangement -o rooted-tree-arrangement.json", + "pred solve rooted-tree-arrangement.json", + "pred evaluate rooted-tree-arrangement.json --config " + x.optimal_config.map(str).join(","), + ) + ] + ] +} #{ let x = load-model-example("KClique") let nv = graph-num-vertices(x.instance) diff --git a/docs/paper/references.bib b/docs/paper/references.bib index f24f1a092..96743882e 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -244,6 +244,14 @@ @article{gareyJohnsonStockmeyer1976 year = {1976} } +@inproceedings{gavril1977, + author = {F. Gavril}, + title = {Some {NP}-Complete Problems on Graphs}, + booktitle = {Proceedings of the 11th Conference on Information Sciences and Systems}, + pages = {91--95}, + year = {1977} +} + @article{evenItaiShamir1976, author = {Shimon Even and Alon Itai and Adi Shamir}, title = {On the Complexity of Timetable and Multicommodity Flow Problems}, @@ -1141,6 +1149,17 @@ @article{hu1961 doi = {10.1287/opre.9.6.841} } +@article{adolphsonHu1973, + author = {Donald Adolphson and Te Chiang Hu}, + title = {Optimal Linear Ordering}, + journal = {SIAM Journal on Applied Mathematics}, + volume = {25}, + number = {3}, + pages = {403--423}, + year = {1973}, + doi = {10.1137/0125040} +} + @inproceedings{kolaitis1998, author = {Phokion G. Kolaitis and Moshe Y. Vardi}, title = {Conjunctive-Query Containment and Constraint Satisfaction}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 02df552d7..a5b0c8c55 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -265,6 +265,7 @@ Flags by problem type: MultiprocessorScheduling --lengths, --num-processors, --deadline SequencingWithinIntervals --release-times, --deadlines, --lengths OptimalLinearArrangement --graph, --bound + RootedTreeArrangement --graph, --bound MinMaxMulticenter (pCenter) --graph, --weights, --edge-weights, --k, --bound MixedChinesePostman (MCPP) --graph, --arcs, --edge-weights, --arc-costs, --bound [--num-vertices] RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound @@ -501,7 +502,7 @@ pub struct CreateArgs { /// Required edge indices for RuralPostman (comma-separated, e.g., "0,2,4") #[arg(long)] pub required_edges: Option, - /// Bound parameter (lower bound for LongestCircuit; upper or length bound for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleCopyFileAllocation, MultipleChoiceBranching, OptimalLinearArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection) + /// Bound parameter (lower bound for LongestCircuit; upper or length bound for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleCopyFileAllocation, MultipleChoiceBranching, OptimalLinearArrangement, RootedTreeArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection) #[arg(long, allow_hyphen_values = true)] pub bound: Option, /// Upper bound on total path length diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index d03de0341..38dd27414 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -14,8 +14,8 @@ use problemreductions::models::formula::Quantifier; use problemreductions::models::graph::{ GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, LengthBoundedDisjointPaths, LongestCircuit, MinimumCutIntoBoundedSets, MinimumMultiwayCut, - MixedChinesePostman, MultipleChoiceBranching, SteinerTree, SteinerTreeInGraphs, - StrongConnectivityAugmentation, + MixedChinesePostman, MultipleChoiceBranching, RootedTreeArrangement, SteinerTree, + SteinerTreeInGraphs, StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, @@ -581,6 +581,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "--arcs \"0>1,0>2,1>3,1>4,2>4,2>5,3>5,4>5\" --weights 2,3,2,1,3,1 --arc-costs 1,1,1,1,1,1,1,1 --weight-bound 5 --cost-bound 5" } "OptimalLinearArrangement" => "--graph 0-1,1-2,2-3 --bound 5", + "RootedTreeArrangement" => "--graph 0-1,0-2,1-2,2-3,3-4 --bound 7", "DirectedTwoCommodityIntegralFlow" => { "--arcs \"0>2,0>3,1>2,1>3,2>4,2>5,3>4,3>5\" --capacities 1,1,1,1,1,1,1,1 --source-1 0 --sink-1 4 --source-2 1 --sink-2 5 --requirement-1 1 --requirement-2 1" } @@ -2922,6 +2923,23 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // RootedTreeArrangement — graph + bound + "RootedTreeArrangement" => { + let usage = + "Usage: pred create RootedTreeArrangement --graph 0-1,0-2,1-2,2-3,3-4 --bound 7"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let bound_raw = args.bound.ok_or_else(|| { + anyhow::anyhow!( + "RootedTreeArrangement requires --bound (upper bound K on total tree stretch)\n\n{usage}" + ) + })?; + let bound = parse_nonnegative_usize_bound(bound_raw, "RootedTreeArrangement", usage)?; + ( + ser(RootedTreeArrangement::new(graph, bound))?, + resolved_variant.clone(), + ) + } + // FlowShopScheduling "FlowShopScheduling" => { let task_str = args.task_lengths.as_deref().ok_or_else(|| { @@ -5369,12 +5387,30 @@ fn create_random( (ser(OptimalLinearArrangement::new(graph, bound))?, variant) } + // RootedTreeArrangement — graph + bound + "RootedTreeArrangement" => { + let edge_prob = args.edge_prob.unwrap_or(0.5); + if !(0.0..=1.0).contains(&edge_prob) { + bail!("--edge-prob must be between 0.0 and 1.0"); + } + let graph = util::create_random_graph(num_vertices, edge_prob, args.seed); + let n = graph.num_vertices(); + let usage = "Usage: pred create RootedTreeArrangement --random --num-vertices 5 [--edge-prob 0.5] [--seed 42] [--bound 10]"; + let bound = args + .bound + .map(|b| parse_nonnegative_usize_bound(b, "RootedTreeArrangement", usage)) + .transpose()? + .unwrap_or((n.saturating_sub(1)) * graph.num_edges()); + let variant = variant_map(&[("graph", "SimpleGraph")]); + (ser(RootedTreeArrangement::new(graph, bound))?, variant) + } + _ => bail!( "Random generation is not supported for {canonical}. \ Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \ MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, KClique, TravelingSalesman, \ BottleneckTravelingSalesman, SteinerTreeInGraphs, HamiltonianCircuit, SteinerTree, \ - OptimalLinearArrangement, HamiltonianPath, LongestCircuit, GeneralizedHex)" + OptimalLinearArrangement, RootedTreeArrangement, HamiltonianPath, LongestCircuit, GeneralizedHex)" ), }; diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 5c38df249..e6194858f 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -3009,6 +3009,34 @@ fn test_create_ola_rejects_negative_bound() { assert!(stderr.contains("nonnegative --bound"), "stderr: {stderr}"); } +#[test] +fn test_create_rooted_tree_arrangement() { + let output_file = std::env::temp_dir().join("pred_test_create_rooted_tree_arrangement.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "RootedTreeArrangement", + "--graph", + "0-1,0-2,1-2,2-3,3-4", + "--bound", + "7", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "RootedTreeArrangement"); + assert_eq!(json["data"]["bound"], 7); + std::fs::remove_file(&output_file).ok(); +} + #[test] fn test_create_scs_rejects_negative_bound() { let output = pred() diff --git a/src/lib.rs b/src/lib.rs index f9e84dca0..9d3bcdd08 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,8 +61,8 @@ pub mod prelude { MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, - RuralPostman, ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman, - UndirectedTwoCommodityIntegralFlow, + RootedTreeArrangement, RuralPostman, ShortestWeightConstrainedPath, SteinerTreeInGraphs, + TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 06627fb70..fd68ca9c8 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -32,6 +32,7 @@ //! - [`BottleneckTravelingSalesman`]: Hamiltonian cycle minimizing the maximum selected edge weight //! - [`MultipleCopyFileAllocation`]: File-copy placement under storage and access costs //! - [`OptimalLinearArrangement`]: Optimal linear arrangement (total edge length at most K) +//! - [`RootedTreeArrangement`]: Rooted-tree embedding with bounded total edge stretch //! - [`MinimumFeedbackArcSet`]: Minimum feedback arc set on directed graphs //! - [`MinMaxMulticenter`]: Min-max multicenter (vertex p-center, satisfaction) //! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median) @@ -81,6 +82,7 @@ pub(crate) mod multiple_copy_file_allocation; pub(crate) mod optimal_linear_arrangement; pub(crate) mod partition_into_paths_of_length_2; pub(crate) mod partition_into_triangles; +pub(crate) mod rooted_tree_arrangement; pub(crate) mod rural_postman; pub(crate) mod shortest_weight_constrained_path; pub(crate) mod spin_glass; @@ -127,6 +129,7 @@ pub use multiple_copy_file_allocation::MultipleCopyFileAllocation; pub use optimal_linear_arrangement::OptimalLinearArrangement; pub use partition_into_paths_of_length_2::PartitionIntoPathsOfLength2; pub use partition_into_triangles::PartitionIntoTriangles; +pub use rooted_tree_arrangement::RootedTreeArrangement; pub use rural_postman::RuralPostman; pub use shortest_weight_constrained_path::ShortestWeightConstrainedPath; pub use spin_glass::SpinGlass; @@ -174,6 +177,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec { + graph: G, + bound: usize, +} + +#[derive(Debug, Clone)] +struct TreeInfo { + depth: Vec, +} + +impl RootedTreeArrangement { + pub fn new(graph: G, bound: usize) -> Self { + Self { graph, bound } + } + + pub fn graph(&self) -> &G { + &self.graph + } + + pub fn bound(&self) -> usize { + self.bound + } + + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + matches!(self.total_edge_stretch(config), Some(stretch) if stretch <= self.bound) + } + + pub fn total_edge_stretch(&self, config: &[usize]) -> Option { + let n = self.graph.num_vertices(); + if n == 0 { + return config.is_empty().then_some(0); + } + + let (parent, mapping) = self.split_config(config)?; + let tree = analyze_parent_array(parent)?; + if !is_valid_permutation(mapping) { + return None; + } + + let mut total = 0usize; + for (u, v) in self.graph.edges() { + let tree_u = mapping[u]; + let tree_v = mapping[v]; + if !are_ancestor_comparable(parent, tree_u, tree_v) { + return None; + } + total += tree.depth[tree_u].abs_diff(tree.depth[tree_v]); + } + + Some(total) + } + + fn split_config<'a>(&self, config: &'a [usize]) -> Option<(&'a [usize], &'a [usize])> { + let n = self.graph.num_vertices(); + (config.len() == 2 * n).then(|| config.split_at(n)) + } +} + +impl Problem for RootedTreeArrangement +where + G: Graph + VariantParam, +{ + const NAME: &'static str = "RootedTreeArrangement"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![G] + } + + fn dims(&self) -> Vec { + let n = self.graph.num_vertices(); + vec![n; 2 * n] + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.is_valid_solution(config) + } +} + +impl SatisfactionProblem for RootedTreeArrangement {} + +fn analyze_parent_array(parent: &[usize]) -> Option { + let n = parent.len(); + if n == 0 { + return Some(TreeInfo { depth: vec![] }); + } + + if parent.iter().any(|&p| p >= n) { + return None; + } + + let roots = parent + .iter() + .enumerate() + .filter_map(|(node, &p)| (node == p).then_some(node)) + .collect::>(); + if roots.len() != 1 { + return None; + } + let root = roots[0]; + + let mut state = vec![0u8; n]; + let mut depth = vec![0usize; n]; + + fn visit( + node: usize, + root: usize, + parent: &[usize], + state: &mut [u8], + depth: &mut [usize], + ) -> Option { + match state[node] { + 1 => return None, + 2 => return Some(depth[node]), + _ => {} + } + + state[node] = 1; + let d = if node == root { + 0 + } else { + let next = parent[node]; + if next == node { + return None; + } + visit(next, root, parent, state, depth)? + 1 + }; + depth[node] = d; + state[node] = 2; + Some(d) + } + + for node in 0..n { + visit(node, root, parent, &mut state, &mut depth)?; + } + + Some(TreeInfo { depth }) +} + +fn is_valid_permutation(mapping: &[usize]) -> bool { + let n = mapping.len(); + let mut seen = vec![false; n]; + for &image in mapping { + if image >= n || seen[image] { + return false; + } + seen[image] = true; + } + true +} + +fn is_ancestor(parent: &[usize], ancestor: usize, descendant: usize) -> bool { + let mut current = descendant; + loop { + if current == ancestor { + return true; + } + let next = parent[current]; + if next == current { + return false; + } + current = next; + } +} + +fn are_ancestor_comparable(parent: &[usize], u: usize, v: usize) -> bool { + is_ancestor(parent, u, v) || is_ancestor(parent, v, u) +} + +crate::declare_variants! { + default sat RootedTreeArrangement => "2^num_vertices", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "rooted_tree_arrangement_simplegraph", + instance: Box::new(RootedTreeArrangement::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)]), + 5, + )), + optimal_config: vec![0, 0, 1, 2, 0, 1, 2, 3], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/rooted_tree_arrangement.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index d15d4f5d6..cea23de1b 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -26,9 +26,9 @@ pub use graph::{ MinimumCutIntoBoundedSets, MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MixedChinesePostman, MultipleChoiceBranching, MultipleCopyFileAllocation, - OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, RuralPostman, - ShortestWeightConstrainedPath, SpinGlass, SteinerTree, SteinerTreeInGraphs, - StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, + OptimalLinearArrangement, PartitionIntoPathsOfLength2, PartitionIntoTriangles, + RootedTreeArrangement, RuralPostman, ShortestWeightConstrainedPath, SpinGlass, SteinerTree, + SteinerTreeInGraphs, StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; pub use misc::PartiallyOrderedKnapsack; diff --git a/src/unit_tests/models/graph/rooted_tree_arrangement.rs b/src/unit_tests/models/graph/rooted_tree_arrangement.rs new file mode 100644 index 000000000..2e162c649 --- /dev/null +++ b/src/unit_tests/models/graph/rooted_tree_arrangement.rs @@ -0,0 +1,107 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::topology::SimpleGraph; +use crate::traits::Problem; + +fn issue_example() -> RootedTreeArrangement { + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 2), (2, 3), (3, 4)]); + RootedTreeArrangement::new(graph, 7) +} + +fn issue_chain_witness() -> Vec { + vec![0, 0, 1, 2, 3, 0, 1, 2, 3, 4] +} + +#[test] +fn test_rootedtreearrangement_basic_yes_example() { + let problem = issue_example(); + let config = issue_chain_witness(); + + assert_eq!(problem.num_vertices(), 5); + assert_eq!(problem.num_edges(), 5); + assert_eq!(problem.bound(), 7); + assert_eq!(problem.dims(), vec![5; 10]); + assert!(problem.evaluate(&config)); + assert_eq!(problem.total_edge_stretch(&config), Some(6)); +} + +#[test] +fn test_rootedtreearrangement_rejects_invalid_parent_arrays() { + let problem = issue_example(); + + // Two roots: node 0 and node 1 are both self-parented. + let multiple_roots = vec![0, 1, 1, 2, 3, 0, 1, 2, 3, 4]; + assert!(!problem.evaluate(&multiple_roots)); + assert_eq!(problem.total_edge_stretch(&multiple_roots), None); + + // Directed cycle between nodes 1 and 2. + let cycle = vec![0, 2, 1, 2, 3, 0, 1, 2, 3, 4]; + assert!(!problem.evaluate(&cycle)); + assert_eq!(problem.total_edge_stretch(&cycle), None); +} + +#[test] +fn test_rootedtreearrangement_rejects_invalid_bijections() { + let problem = issue_example(); + + let duplicate_image = vec![0, 0, 1, 2, 3, 0, 0, 2, 3, 4]; + assert!(!problem.evaluate(&duplicate_image)); + assert_eq!(problem.total_edge_stretch(&duplicate_image), None); + + let out_of_range = vec![0, 0, 1, 2, 3, 0, 1, 2, 3, 5]; + assert!(!problem.evaluate(&out_of_range)); + assert_eq!(problem.total_edge_stretch(&out_of_range), None); + + let wrong_length = vec![0, 0, 1, 2, 3, 0, 1, 2, 3]; + assert!(!problem.evaluate(&wrong_length)); + assert_eq!(problem.total_edge_stretch(&wrong_length), None); +} + +#[test] +fn test_rootedtreearrangement_rejects_noncomparable_edges() { + let graph = SimpleGraph::new(5, vec![(0, 1), (0, 2), (1, 2), (2, 3), (3, 4)]); + let problem = RootedTreeArrangement::new(graph, 99); + + // Tree: 0 is root, 1 and 2 are siblings, 3 and 4 descend from 2. + // The graph edge {1,2} is invalid because mapped nodes 1 and 2 are not ancestor-comparable. + let branching_tree = vec![0, 0, 0, 2, 3, 0, 1, 2, 3, 4]; + assert!(!problem.evaluate(&branching_tree)); + assert_eq!(problem.total_edge_stretch(&branching_tree), None); +} + +#[test] +fn test_rootedtreearrangement_enforces_bound() { + let problem = issue_example(); + + // Same chain tree as the YES witness, but the mapping stretches edge {2,3} too far. + let over_bound = vec![0, 0, 1, 2, 3, 2, 1, 0, 3, 4]; + assert!(!problem.evaluate(&over_bound)); + assert_eq!(problem.total_edge_stretch(&over_bound), Some(8)); +} + +#[test] +fn test_rootedtreearrangement_solver_and_serialization() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let problem = RootedTreeArrangement::new(graph, 2); + + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("expected satisfying solution"); + assert!(problem.evaluate(&solution)); + + let json = serde_json::to_string(&problem).unwrap(); + let restored: RootedTreeArrangement = serde_json::from_str(&json).unwrap(); + assert_eq!(restored.num_vertices(), 3); + assert_eq!(restored.num_edges(), 2); + assert_eq!(restored.bound(), 2); + assert_eq!(restored.evaluate(&solution), problem.evaluate(&solution)); +} + +#[test] +fn test_rootedtreearrangement_problem_name() { + assert_eq!( + as Problem>::NAME, + "RootedTreeArrangement" + ); +} From 1a854c39a588f95a3e75e2b4c1a981aead5d9d0e Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 06:19:44 +0800 Subject: [PATCH 3/4] chore: remove plan file after implementation --- .../2026-03-22-rooted-tree-arrangement.md | 254 ------------------ 1 file changed, 254 deletions(-) delete mode 100644 docs/plans/2026-03-22-rooted-tree-arrangement.md diff --git a/docs/plans/2026-03-22-rooted-tree-arrangement.md b/docs/plans/2026-03-22-rooted-tree-arrangement.md deleted file mode 100644 index bbfa0d285..000000000 --- a/docs/plans/2026-03-22-rooted-tree-arrangement.md +++ /dev/null @@ -1,254 +0,0 @@ -# RootedTreeArrangement Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add `RootedTreeArrangement` as a graph-based satisfaction problem with the parent-array plus permutation encoding described in issue `#407`, including CLI/example-db support, tests, and a paper entry. - -**Architecture:** Implement the model alongside [`OptimalLinearArrangement`](/Users/jinguomini/rcode/problem-reductions/.worktrees/issue-407/src/models/graph/optimal_linear_arrangement.rs) as a generic graph satisfaction problem over `G: Graph`. The core evaluator will parse a `2n`-variable configuration into a rooted-tree parent array and a bijection `f`, validate the rooted tree and mapping, then check the ancestor/descendant edge constraint and the total stretch bound `K`. - -**Tech Stack:** Rust workspace, registry macros (`inventory::submit!`, `declare_variants!`), `pred` CLI, example-db exports, Typst paper, `cargo`/`make` verification. - ---- - -## Issue-Specific Constraints - -- Use the issue comment corrections as source of truth: - - fixed-length encoding is `dims() = vec![n; 2 * n]` - - expose `num_vertices()` and `num_edges()` - - keep Example 2 consistent; use it as the canonical worked example -- Associated open rule issue exists: `#424 [Rule] Rooted Tree Arrangement to Rooted Tree Storage Assignment`, so this model is not an orphan. -- Reuse existing bibliography key `gareyJohnsonStockmeyer1976`; add missing keys for Gavril (1977) and Adolphson-Hu (1973) if they are not already in `docs/paper/references.bib`. - -## Batch 1: add-model Steps 1-5.5 - -### Task 1: Add failing model tests first - -**Files:** -- Create: `src/unit_tests/models/graph/rooted_tree_arrangement.rs` -- Reference: `src/unit_tests/models/graph/optimal_linear_arrangement.rs` -- Reference: `src/models/graph/optimal_linear_arrangement.rs` - -**Step 1: Write the failing test file** - -Add targeted tests for: -- basic getters and `dims()` on a 5-vertex issue example -- a valid YES witness using the chain tree plus identity mapping from issue Example 2 -- rejection of invalid parent arrays (multiple roots / directed cycle) -- rejection of invalid bijections (duplicate or out-of-range image) -- rejection when an edge endpoint pair is not ancestor-comparable -- rejection when the total tree distance exceeds `bound` -- serde round-trip and brute-force sanity on a tiny instance - -Use the issue encoding directly in the tests: -- parent array occupies `config[..n]` -- bijection occupies `config[n..]` - -**Step 2: Run the targeted test to verify RED** - -Run: `cargo test rooted_tree_arrangement --lib` - -Expected: FAIL because `RootedTreeArrangement` does not exist yet. - -**Step 3: Commit the red test scaffold** - -```bash -git add src/unit_tests/models/graph/rooted_tree_arrangement.rs -git commit -m "test: add RootedTreeArrangement coverage" -``` - -### Task 2: Implement the model core - -**Files:** -- Create: `src/models/graph/rooted_tree_arrangement.rs` -- Modify: `src/models/graph/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` - -**Step 1: Implement the new graph model** - -Follow repo-local `add-model` Steps 1-3 in this file: -- add `ProblemSchemaEntry` with `graph` dimension only -- define `RootedTreeArrangement` with fields `graph: G` and `bound: usize` -- implement getters `graph()`, `bound()`, `num_vertices()`, `num_edges()` -- set `dims()` to `vec![n; 2 * n]` -- implement helpers to: - - validate exactly one root in the parent array - - reject self-parenting outside the root - - reject cycles / disconnected parent forests - - compute depth of every tree node - - validate the bijection as a permutation of `0..n` - - test whether two mapped vertices are ancestor-comparable - - sum tree distances for graph edges -- expose `is_valid_solution()` and a stretch helper that returns `Option` -- implement `Problem` and `SatisfactionProblem` -- declare the default variant as `sat RootedTreeArrangement => "2^num_vertices"` -- add `canonical_model_example_specs()` using the cleaned-up Example 2 instance from the issue -- link the new unit test file at the bottom of the module - -**Step 2: Register exports** - -Update: -- `src/models/graph/mod.rs` -- `src/models/mod.rs` -- `src/lib.rs` - -Add the module declaration, public re-export, prelude export, and graph example-db registration chain entry. - -**Step 3: Run the targeted tests to verify GREEN** - -Run: -- `cargo test rooted_tree_arrangement --lib` - -Expected: PASS for the new model tests. - -**Step 4: Refactor lightly if needed** - -Keep helpers local to the model file unless duplication appears. - -**Step 5: Commit** - -```bash -git add src/models/graph/rooted_tree_arrangement.rs src/models/graph/mod.rs src/models/mod.rs src/lib.rs -git commit -m "feat: add RootedTreeArrangement model" -``` - -### Task 3: Add CLI discovery and creation support - -**Files:** -- Modify: `problemreductions-cli/src/problem_name.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/src/cli.rs` - -**Step 1: Extend CLI discovery** - -In `problem_name.rs`, ensure the catalog alias path is sufficient; only add a manual alias if the registry alias is absent and the abbreviation is literature-backed. - -**Step 2: Extend create command support** - -In `create.rs`: -- add example/help strings for `RootedTreeArrangement` -- add the direct `pred create RootedTreeArrangement --graph ... --bound ...` match arm -- add the random graph generator arm mirroring `OptimalLinearArrangement`, defaulting `bound` to a safe satisfiable upper bound if omitted - -**Step 3: Update CLI help text** - -In `cli.rs`: -- add `RootedTreeArrangement` to the “Flags by problem type” table -- extend the `--bound` doc comment if the problem name list is maintained manually there - -**Step 4: Verify** - -Run: -- `cargo test -p problemreductions-cli create:: -- --nocapture` - -If the CLI test filter is too broad or absent, run a targeted `cargo test -p problemreductions-cli rooted_tree_arrangement`. - -**Step 5: Commit** - -```bash -git add problemreductions-cli/src/problem_name.rs problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs -git commit -m "feat: add RootedTreeArrangement CLI support" -``` - -### Task 4: Wire the canonical example and example-db coverage - -**Files:** -- Modify: `src/models/graph/rooted_tree_arrangement.rs` -- Reference: `src/example_db/model_builders.rs` -- Reference: `src/unit_tests/example_db.rs` - -**Step 1: Confirm example-db wiring** - -Ensure the `canonical_model_example_specs()` entry added in Task 2 is reachable through `src/models/graph/mod.rs` so `src/example_db/model_builders.rs` picks it up automatically. - -**Step 2: Add or extend tests if needed** - -If the model tests do not already exercise the canonical example config, add one focused assertion that the example-db config evaluates to `true`. - -**Step 3: Verify** - -Run: -- `cargo test example_db` -- `cargo test rooted_tree_arrangement --features example-db` - -**Step 4: Commit** - -```bash -git add src/models/graph/rooted_tree_arrangement.rs src/models/graph/mod.rs -git commit -m "test: cover RootedTreeArrangement example-db wiring" -``` - -## Batch 2: add-model Step 6 - -### Task 5: Document the model in the paper - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `docs/paper/references.bib` - -**Step 1: Add bibliography entries if missing** - -Add citation keys for: -- Gavril (1977), the GT45 NP-completeness reference -- Adolphson and Hu (1973), the tree linear arrangement algorithm reference - -Reuse existing `gareyJohnsonStockmeyer1976` for the `OptimalLinearArrangement` connection. - -**Step 2: Add display-name and `problem-def`** - -In `reductions.typ`: -- add `"RootedTreeArrangement": [Rooted Tree Arrangement],` to the display-name dictionary -- add a `problem-def("RootedTreeArrangement")` section near `OptimalLinearArrangement` -- explain the decision formulation, the rooted-tree witness, and the ancestor-path constraint -- use the canonical Example 2 instance from the model example export -- include a `pred-commands(...)` block using `pred create --example RootedTreeArrangement` - -**Step 3: Build the paper** - -Run: -- `make paper` - -Expected: PASS with no Typst errors. - -**Step 4: Commit** - -```bash -git add docs/paper/reductions.typ docs/paper/references.bib -git commit -m "docs: document RootedTreeArrangement" -``` - -## Final Verification: add-model Step 7 - -### Task 6: Run focused and repo-level verification - -**Files:** -- No code changes expected - -**Step 1: Focused verification** - -Run: -- `cargo test rooted_tree_arrangement` -- `cargo test optimal_linear_arrangement` -- `cargo test example_db --features example-db` - -**Step 2: Project verification** - -Run: -- `make fmt` -- `make check` - -If `make check` is too slow or exposes unrelated failures, capture the exact failing command and stop rather than masking it. - -**Step 3: Review git state** - -Run: -- `git status --short` - -Expected: clean working tree except for intentionally ignored/generated files. - -**Step 4: Commit any remaining implementation work** - -```bash -git add -A -git commit -m "Implement #407: add RootedTreeArrangement" -``` From e249df398fc8b0cf5676a362e108fb7c5cf92930 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 22 Mar 2026 20:16:40 +0800 Subject: [PATCH 4/4] Fix paper solve command to include --solver brute-force RootedTreeArrangement has no ILP reduction path, so the default pred solve fails. Add --solver brute-force as required. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ea4c2eb58..60f37fb54 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -2045,7 +2045,7 @@ is feasible: each set induces a connected subgraph, the component weights are $2 #pred-commands( "pred create --example RootedTreeArrangement -o rooted-tree-arrangement.json", - "pred solve rooted-tree-arrangement.json", + "pred solve rooted-tree-arrangement.json --solver brute-force", "pred evaluate rooted-tree-arrangement.json --config " + x.optimal_config.map(str).join(","), ) ]