Skip to content

Commit 31ed79e

Browse files
zazabapclaudeGiggleLiu
authored
Fix #244: Add IsomorphicSpanningTree model (#612)
* Add plan for #244: [Model] IsomorphicSpanningTree Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add IsomorphicSpanningTree satisfaction problem model (#244) Implements the Isomorphic Spanning Tree decision problem: given a graph G and a tree T with |V(G)| = |V(T)|, determine if G contains a spanning tree isomorphic to T. NP-complete (Garey & Johnson ND8). - Model with SatisfactionProblem (Metric = bool), permutation-based config - Constructor validates tree connectivity and edge count - CLI support with --graph and --tree flags - 11 unit tests covering evaluation, solver, serialization, edge cases Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove plan file after implementation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix #412: Add ShortestCommonSupersequence model (#627) * Add plan for #412: ShortestCommonSupersequence model * Implement #412: Add ShortestCommonSupersequence model * style: apply rustfmt formatting * Address review: fix docs, add --alphabet-size flag, add edge case tests * chore: remove plan file after implementation * Address Copilot review comments - Align docs/schema wording: bound is exact config length, equivalent to |w| ≤ B via padding - Add alphabet_size > 0 validation in constructor - Handle empty string segments in --strings CLI parsing - Add --alphabet-size to CLI help text - Regenerate problem_schemas.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix agentic test issues: add SCS to prelude, fix CLI duplicate fields - Add ShortestCommonSupersequence to prelude re-exports (was missing unlike all other misc models) - Fix duplicate CLI struct fields (strings, bound) by sharing between LCS/SCS and RuralPostman/SCS with i64 type and casts at usage sites Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: trigger CI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: apply rustfmt formatting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com> * Fix #507: Add FlowShopScheduling model (#629) * Add plan for #507: FlowShopScheduling model * Implement #507: Add FlowShopScheduling model Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: remove plan file after implementation * fix: address Copilot review comments on FlowShopScheduling - Switch dims() from vec![n;n] to Lehmer code encoding [n,n-1,...,1] - Update evaluate() to decode Lehmer code into permutations - Fix complexity from "3^num_jobs" to "num_jobs^num_jobs" - Add validation to compute_makespan() for out-of-bounds indices - Add --num-processors CLI flag instead of overloading --m - Validate task_lengths row lengths in CLI with bail! instead of panic - Fix test comment about deadline value Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add Lehmer code encoding explanation to FlowShopScheduling Add concrete example of how Lehmer code configs map to job orderings in the struct doc comment, improving discoverability for new users. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: remove unused enumerate in Lehmer code decoding Fixes clippy::unused_enumerate_index warning. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add missing paper section, trait_consistency entry, and correct complexity - Add FlowShopScheduling problem-def and display-name to docs/paper/reductions.typ - Add FlowShopScheduling to trait_consistency test - Fix complexity from "num_jobs^num_jobs" to "factorial(num_jobs)" (n! is the exact search space) - Relax check_problem_trait assertion from >= 2 to >= 1 (Lehmer code encoding produces trailing dim of 1) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add Gantt chart visualization and use issue's 5-job example in paper Replace the simple 2-machine example with the issue's canonical 3-machine, 5-job example. Add a CeTZ Gantt chart showing the feasible schedule with job order (j4, j1, j5, j3, j2) achieving makespan 23 within deadline 25. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com> * Add paper section, trait_consistency entry, and formatting fixes for IsomorphicSpanningTree - Add display-name and problem-def entry in docs/paper/reductions.typ with formal definition, background (Garey & Johnson ND8), and CeTZ figure - Add bibliography entry for Papadimitriou & Yannakakis 1982 - Add check_problem_trait for IsomorphicSpanningTree in trait_consistency.rs - Apply cargo fmt formatting fixes from merge Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: GiggleLiu <cacate0129@gmail.com>
1 parent 837bc1e commit 31ed79e

16 files changed

Lines changed: 528 additions & 21 deletions

File tree

docs/paper/reductions.typ

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"MaxCut": [Max-Cut],
3232
"GraphPartitioning": [Graph Partitioning],
3333
"HamiltonianPath": [Hamiltonian Path],
34+
"IsomorphicSpanningTree": [Isomorphic Spanning Tree],
3435
"KColoring": [$k$-Coloring],
3536
"MinimumDominatingSet": [Minimum Dominating Set],
3637
"MaximumMatching": [Maximum Matching],
@@ -451,6 +452,55 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co
451452

452453
Variables: $n = |V|$ values forming a permutation. Position $i$ holds the vertex visited at step $i$. A configuration is satisfying when it forms a valid permutation of all vertices and consecutive vertices are adjacent in $G$.
453454
]
455+
#problem-def("IsomorphicSpanningTree")[
456+
Given a graph $G = (V, E)$ and a tree $T = (V_T, E_T)$ with $|V| = |V_T|$, determine whether $G$ contains a spanning tree isomorphic to $T$: does there exist a bijection $pi: V_T -> V$ such that for every edge ${u, v} in E_T$, ${pi(u), pi(v)} in E$?
457+
][
458+
A classical NP-complete problem listed as ND8 in Garey & Johnson @garey1979. The Isomorphic Spanning Tree problem strictly generalizes Hamiltonian Path: a graph $G$ has a Hamiltonian path if and only if $G$ contains a spanning tree isomorphic to the path $P_n$. The problem remains NP-complete even when $T$ is restricted to trees of bounded degree @papadimitriou1982.
459+
460+
Brute-force enumeration of all bijections $pi: V_T -> V$ and checking each against the edge set of $G$ runs in $O(n! dot n)$ time. No substantially faster exact algorithm is known for general instances.
461+
462+
Variables: $n = |V|$ values forming a permutation. Position $i$ holds the graph vertex that tree vertex $i$ maps to under $pi$. A configuration is satisfying when it forms a valid permutation and every tree edge maps to a graph edge.
463+
464+
*Example.* Consider $G = K_4$ (the complete graph on 4 vertices) and $T$ the star $S_3$ with center $0$ and leaves ${1, 2, 3}$. Since $K_4$ contains all possible edges, any bijection $pi$ maps the star's edges to edges of $G$. For instance, the identity mapping $pi(i) = i$ gives the spanning tree ${(0,1), (0,2), (0,3)} subset.eq E(K_4)$.
465+
466+
#figure({
467+
let blue = graph-colors.at(0)
468+
let gray = luma(200)
469+
canvas(length: 1cm, {
470+
import draw: *
471+
// G = K4 on the left
472+
let gv = ((0, 0), (1.5, 0), (1.5, 1.5), (0, 1.5))
473+
let ge = ((0,1),(0,2),(0,3),(1,2),(1,3),(2,3))
474+
let tree-edges = ((0,1),(0,2),(0,3))
475+
for (u, v) in ge {
476+
let is-tree = tree-edges.any(e => (e.at(0) == u and e.at(1) == v) or (e.at(0) == v and e.at(1) == u))
477+
g-edge(gv.at(u), gv.at(v), stroke: if is-tree { 2pt + blue } else { 1pt + gray })
478+
}
479+
for (k, pos) in gv.enumerate() {
480+
let is-center = k == 0
481+
g-node(pos, name: "g" + str(k),
482+
fill: if is-center { blue } else { white },
483+
label: if is-center { text(fill: white)[$v_#k$] } else { [$v_#k$] })
484+
}
485+
// Arrow
486+
content((2.5, 0.75), text(10pt)[$arrow.l.double$])
487+
// T = star S3 on the right
488+
let tv = ((3.5, 0.75), (5.0, 0), (5.0, 0.75), (5.0, 1.5))
489+
let te = ((0,1),(0,2),(0,3))
490+
for (u, v) in te {
491+
g-edge(tv.at(u), tv.at(v), stroke: 2pt + blue)
492+
}
493+
for (k, pos) in tv.enumerate() {
494+
let is-center = k == 0
495+
g-node(pos, name: "t" + str(k),
496+
fill: if is-center { blue } else { white },
497+
label: if is-center { text(fill: white)[$u_#k$] } else { [$u_#k$] })
498+
}
499+
})
500+
},
501+
caption: [Isomorphic Spanning Tree: the graph $G = K_4$ (left) contains a spanning tree isomorphic to the star $S_3$ (right, blue edges). The identity mapping $pi(u_i) = v_i$ embeds all three star edges into $G$. Center vertex $v_0$ shown in blue.],
502+
) <fig:isomorphic-spanning-tree>
503+
]
454504
#problem-def("KColoring")[
455505
Given $G = (V, E)$ and $k$ colors, find $c: V -> {1, ..., k}$ minimizing $|{(u, v) in E : c(u) = c(v)}|$.
456506
][

docs/paper/references.bib

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,3 +532,14 @@ @article{lucchesi1978
532532
year = {1978},
533533
doi = {10.1112/jlms/s2-17.3.369}
534534
}
535+
536+
@article{papadimitriou1982,
537+
author = {Christos H. Papadimitriou and Mihalis Yannakakis},
538+
title = {The Complexity of Restricted Spanning Tree Problems},
539+
journal = {Journal of the ACM},
540+
volume = {29},
541+
number = {2},
542+
pages = {285--309},
543+
year = {1982},
544+
doi = {10.1145/322307.322309}
545+
}

docs/src/reductions/problem_schemas.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,22 @@
158158
}
159159
]
160160
},
161+
{
162+
"name": "IsomorphicSpanningTree",
163+
"description": "Does graph G contain a spanning tree isomorphic to tree T?",
164+
"fields": [
165+
{
166+
"name": "graph",
167+
"type_name": "SimpleGraph",
168+
"description": "The host graph G"
169+
},
170+
{
171+
"name": "tree",
172+
"type_name": "SimpleGraph",
173+
"description": "The target tree T (must be a tree with |V(T)| = |V(G)|)"
174+
}
175+
]
176+
},
161177
{
162178
"name": "KColoring",
163179
"description": "Find valid k-coloring of a graph",

problemreductions-cli/src/cli.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ Flags by problem type:
210210
KColoring --graph, --k
211211
PartitionIntoTriangles --graph
212212
GraphPartitioning --graph
213+
IsomorphicSpanningTree --graph, --tree
213214
Factoring --target, --m, --n
214215
BinPacking --sizes, --capacity
215216
SubsetSum --sizes, --target
@@ -337,6 +338,9 @@ pub struct CreateArgs {
337338
/// Variable bounds for CVP as "lower,upper" (e.g., "-10,10") [default: -10,10]
338339
#[arg(long, allow_hyphen_values = true)]
339340
pub bounds: Option<String>,
341+
/// Tree edge list for IsomorphicSpanningTree (e.g., 0-1,1-2,2-3)
342+
#[arg(long)]
343+
pub tree: Option<String>,
340344
/// Required edge indices for RuralPostman (comma-separated, e.g., "0,2,4")
341345
#[arg(long)]
342346
pub required_edges: Option<String>,

problemreductions-cli/src/commands/create.rs

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
5050
&& args.basis.is_none()
5151
&& args.target_vec.is_none()
5252
&& args.bounds.is_none()
53+
&& args.tree.is_none()
5354
&& args.required_edges.is_none()
5455
&& args.bound.is_none()
5556
&& args.pattern.is_none()
@@ -93,6 +94,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
9394
},
9495
"GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3",
9596
"HamiltonianPath" => "--graph 0-1,1-2,2-3",
97+
"IsomorphicSpanningTree" => "--graph 0-1,1-2,0-2 --tree 0-1,1-2",
9698
"MaxCut" | "MaximumMatching" | "TravelingSalesman" => {
9799
"--graph 0-1,1-2,2-3 --edge-weights 1,1,1"
98100
}
@@ -101,7 +103,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
101103
"QUBO" => "--matrix \"1,0.5;0.5,2\"",
102104
"SpinGlass" => "--graph 0-1,1-2 --couplings 1,1",
103105
"KColoring" => "--graph 0-1,1-2,2-0 --k 3",
104-
"MinimumSumMulticenter" => "--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2",
106+
"MinimumSumMulticenter" => {
107+
"--graph 0-1,1-2,2-3 --weights 1,1,1,1 --edge-weights 1,1,1 --k 2"
108+
}
105109
"PartitionIntoTriangles" => "--graph 0-1,1-2,0-2",
106110
"Factoring" => "--target 15 --m 4 --n 4",
107111
"MinimumFeedbackArcSet" => "--arcs \"0>1,1>2,2>0\"",
@@ -239,13 +243,47 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
239243

240244
// Hamiltonian path (graph only, no weights)
241245
"HamiltonianPath" => {
246+
let (graph, _) = parse_graph(args).map_err(|e| {
247+
anyhow::anyhow!("{e}\n\nUsage: pred create HamiltonianPath --graph 0-1,1-2,2-3")
248+
})?;
249+
(ser(HamiltonianPath::new(graph))?, resolved_variant.clone())
250+
}
251+
252+
// IsomorphicSpanningTree (graph + tree)
253+
"IsomorphicSpanningTree" => {
242254
let (graph, _) = parse_graph(args).map_err(|e| {
243255
anyhow::anyhow!(
244-
"{e}\n\nUsage: pred create HamiltonianPath --graph 0-1,1-2,2-3"
256+
"{e}\n\nUsage: pred create IsomorphicSpanningTree --graph 0-1,1-2,0-2 --tree 0-1,1-2"
245257
)
246258
})?;
259+
let tree_str = args.tree.as_deref().ok_or_else(|| {
260+
anyhow::anyhow!(
261+
"IsomorphicSpanningTree requires --tree\n\n\
262+
Usage: pred create IsomorphicSpanningTree --graph 0-1,1-2,0-2 --tree 0-1,1-2"
263+
)
264+
})?;
265+
let tree_edges: Vec<(usize, usize)> = tree_str
266+
.split(',')
267+
.map(|pair| {
268+
let parts: Vec<&str> = pair.trim().split('-').collect();
269+
if parts.len() != 2 {
270+
bail!("Invalid tree edge '{}': expected format u-v", pair.trim());
271+
}
272+
let u: usize = parts[0].parse()?;
273+
let v: usize = parts[1].parse()?;
274+
Ok((u, v))
275+
})
276+
.collect::<Result<Vec<_>>>()?;
277+
let tree_num_vertices = tree_edges
278+
.iter()
279+
.flat_map(|(u, v)| [*u, *v])
280+
.max()
281+
.map(|m| m + 1)
282+
.unwrap_or(0)
283+
.max(graph.num_vertices());
284+
let tree = SimpleGraph::new(tree_num_vertices, tree_edges);
247285
(
248-
ser(HamiltonianPath::new(graph))?,
286+
ser(problemreductions::models::graph::IsomorphicSpanningTree::new(graph, tree))?,
249287
resolved_variant.clone(),
250288
)
251289
}
@@ -609,7 +647,11 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
609647
}
610648
}
611649
(
612-
ser(FlowShopScheduling::new(num_processors, task_lengths, deadline))?,
650+
ser(FlowShopScheduling::new(
651+
num_processors,
652+
task_lengths,
653+
deadline,
654+
))?,
613655
resolved_variant.clone(),
614656
)
615657
}

problemreductions-cli/src/dispatch.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,9 @@ pub fn load_problem(
216216
"MinimumSumMulticenter" => deser_opt::<MinimumSumMulticenter<SimpleGraph, i32>>(data),
217217
"GraphPartitioning" => deser_opt::<GraphPartitioning<SimpleGraph>>(data),
218218
"HamiltonianPath" => deser_sat::<HamiltonianPath<SimpleGraph>>(data),
219+
"IsomorphicSpanningTree" => {
220+
deser_sat::<problemreductions::models::graph::IsomorphicSpanningTree>(data)
221+
}
219222
"MaxCut" => deser_opt::<MaxCut<SimpleGraph, i32>>(data),
220223
"MaximalIS" => deser_opt::<MaximalIS<SimpleGraph, i32>>(data),
221224
"TravelingSalesman" => deser_opt::<TravelingSalesman<SimpleGraph, i32>>(data),
@@ -285,6 +288,9 @@ pub fn serialize_any_problem(
285288
"MinimumSumMulticenter" => try_ser::<MinimumSumMulticenter<SimpleGraph, i32>>(any),
286289
"GraphPartitioning" => try_ser::<GraphPartitioning<SimpleGraph>>(any),
287290
"HamiltonianPath" => try_ser::<HamiltonianPath<SimpleGraph>>(any),
291+
"IsomorphicSpanningTree" => {
292+
try_ser::<problemreductions::models::graph::IsomorphicSpanningTree>(any)
293+
}
288294
"MaxCut" => try_ser::<MaxCut<SimpleGraph, i32>>(any),
289295
"MaximalIS" => try_ser::<MaximalIS<SimpleGraph, i32>>(any),
290296
"TravelingSalesman" => try_ser::<TravelingSalesman<SimpleGraph, i32>>(any),

problemreductions-cli/src/mcp/tools.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ use crate::util;
22
use problemreductions::models::algebraic::QUBO;
33
use problemreductions::models::formula::{CNFClause, Satisfiability};
44
use problemreductions::models::graph::{
5-
MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumSumMulticenter,
6-
MinimumDominatingSet, MinimumVertexCover, SpinGlass, TravelingSalesman,
5+
MaxCut, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet,
6+
MinimumSumMulticenter, MinimumVertexCover, SpinGlass, TravelingSalesman,
77
};
88
use problemreductions::models::misc::Factoring;
99
use problemreductions::registry::collect_schemas;
@@ -528,7 +528,12 @@ impl McpServer {
528528
})?;
529529
let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]);
530530
(
531-
ser(MinimumSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?,
531+
ser(MinimumSumMulticenter::new(
532+
graph,
533+
vertex_weights,
534+
edge_lengths,
535+
k,
536+
))?,
532537
variant,
533538
)
534539
}
@@ -672,7 +677,12 @@ impl McpServer {
672677
.unwrap_or(1.max(num_vertices / 3));
673678
let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]);
674679
(
675-
ser(MinimumSumMulticenter::new(graph, vertex_weights, edge_lengths, k))?,
680+
ser(MinimumSumMulticenter::new(
681+
graph,
682+
vertex_weights,
683+
edge_lengths,
684+
k,
685+
))?,
676686
variant,
677687
)
678688
}

problemreductions-cli/src/problem_name.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ pub fn resolve_alias(input: &str) -> String {
3939
"ksat" | "ksatisfiability" => "KSatisfiability".to_string(),
4040
"qubo" => "QUBO".to_string(),
4141
"graphpartitioning" => "GraphPartitioning".to_string(),
42+
"isomorphicspanningtree" => "IsomorphicSpanningTree".to_string(),
4243
"maxcut" => "MaxCut".to_string(),
4344
"spinglass" => "SpinGlass".to_string(),
4445
"ilp" => "ILP".to_string(),

src/lib.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,14 @@ pub mod prelude {
4141
pub use crate::models::algebraic::{BMF, QUBO};
4242
pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability};
4343
pub use crate::models::graph::{
44-
BicliqueCover, GraphPartitioning, HamiltonianPath, SpinGlass, SubgraphIsomorphism,
44+
BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, SpinGlass,
45+
SubgraphIsomorphism,
4546
};
4647
pub use crate::models::graph::{
4748
KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching,
4849
MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet,
49-
MinimumSumMulticenter, MinimumVertexCover, PartitionIntoTriangles,
50-
RuralPostman, TravelingSalesman,
50+
MinimumSumMulticenter, MinimumVertexCover, PartitionIntoTriangles, RuralPostman,
51+
TravelingSalesman,
5152
};
5253
pub use crate::models::misc::{
5354
BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, PaintShop,

0 commit comments

Comments
 (0)