diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 0bb2b68d5..60f37fb54 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -123,6 +123,7 @@ "DisjointConnectingPaths": [Disjoint Connecting Paths], "MinimumMultiwayCut": [Minimum Multiway Cut], "OptimalLinearArrangement": [Optimal Linear Arrangement], + "RootedTreeArrangement": [Rooted Tree Arrangement], "RuralPostman": [Rural Postman], "MixedChinesePostman": [Mixed Chinese Postman], "StackerCrane": [Stacker Crane], @@ -2026,6 +2027,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 --solver brute-force", + "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 004689c35..307837a77 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -277,6 +277,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}, @@ -1206,6 +1214,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 44fa075dc..6306791b9 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -272,6 +272,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 @@ -534,7 +535,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 b631b2a84..1db378ef9 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -15,8 +15,8 @@ use problemreductions::models::graph::{ DisjointConnectingPaths, GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IntegralFlowBundles, LengthBoundedDisjointPaths, LongestCircuit, LongestPath, MinimumCutIntoBoundedSets, MinimumDummyActivitiesPert, MinimumMultiwayCut, MixedChinesePostman, - MultipleChoiceBranching, PathConstrainedNetworkFlow, SteinerTree, SteinerTreeInGraphs, - StrongConnectivityAugmentation, + MultipleChoiceBranching, PathConstrainedNetworkFlow, RootedTreeArrangement, SteinerTree, + SteinerTreeInGraphs, StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, @@ -614,6 +614,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" } @@ -3185,6 +3186,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(|| { @@ -5963,12 +5981,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 13e9cc358..b4ce70b5c 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -3412,6 +3412,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 e2d3daa4a..c26d77caa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ pub mod prelude { MinimumDummyActivitiesPert, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, PartitionIntoPathsOfLength2, - PartitionIntoTriangles, PathConstrainedNetworkFlow, RuralPostman, + PartitionIntoTriangles, PathConstrainedNetworkFlow, RootedTreeArrangement, RuralPostman, ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman, UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow, }; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 45a012730..69b4c01d4 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -34,6 +34,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) @@ -96,6 +97,7 @@ pub(crate) mod optimal_linear_arrangement; pub(crate) mod partition_into_paths_of_length_2; pub(crate) mod partition_into_triangles; pub(crate) mod path_constrained_network_flow; +pub(crate) mod rooted_tree_arrangement; pub(crate) mod rural_postman; pub(crate) mod shortest_weight_constrained_path; pub(crate) mod spin_glass; @@ -150,6 +152,7 @@ pub use optimal_linear_arrangement::OptimalLinearArrangement; pub use partition_into_paths_of_length_2::PartitionIntoPathsOfLength2; pub use partition_into_triangles::PartitionIntoTriangles; pub use path_constrained_network_flow::PathConstrainedNetworkFlow; +pub use rooted_tree_arrangement::RootedTreeArrangement; pub use rural_postman::RuralPostman; pub use shortest_weight_constrained_path::ShortestWeightConstrainedPath; pub use spin_glass::SpinGlass; @@ -203,6 +206,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 7a71c3916..2a2f4cda1 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -28,9 +28,9 @@ pub use graph::{ MinimumDummyActivitiesPert, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumMultiwayCut, MinimumSumMulticenter, MinimumVertexCover, MixedChinesePostman, MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, - PartitionIntoPathsOfLength2, PartitionIntoTriangles, PathConstrainedNetworkFlow, RuralPostman, - ShortestWeightConstrainedPath, SpinGlass, SteinerTree, SteinerTreeInGraphs, - StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, + PartitionIntoPathsOfLength2, PartitionIntoTriangles, PathConstrainedNetworkFlow, + RootedTreeArrangement, RuralPostman, ShortestWeightConstrainedPath, SpinGlass, SteinerTree, + SteinerTreeInGraphs, StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, UndirectedFlowLowerBounds, 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" + ); +}