diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index a49177a39..4bc3f9be7 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -103,6 +103,7 @@ "BalancedCompleteBipartiteSubgraph": [Balanced Complete Bipartite Subgraph], "BoundedComponentSpanningForest": [Bounded Component Spanning Forest], "BinPacking": [Bin Packing], + "BoyceCoddNormalFormViolation": [Boyce-Codd Normal Form Violation], "ClosestVectorProblem": [Closest Vector Problem], "ConsecutiveSets": [Consecutive Sets], "MinimumMultiwayCut": [Minimum Multiway Cut], @@ -2884,6 +2885,14 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], *Example.* Let $n = 6$ tasks, $m = 3$ processors, $r = 1$ resource with $B_1 = 20$, and deadline $D = 2$. Resource requirements: $R_1(t_1) = 6$, $R_1(t_2) = 7$, $R_1(t_3) = 7$, $R_1(t_4) = 6$, $R_1(t_5) = 8$, $R_1(t_6) = 6$. Schedule: slot 0 $arrow.l {t_1, t_2, t_3}$ (3 tasks, resource $= 20$), slot 1 $arrow.l {t_4, t_5, t_6}$ (3 tasks, resource $= 20$). Both constraints satisfied; answer: YES. ] +#problem-def("BoyceCoddNormalFormViolation")[ + *Instance:* A set $A$ of attribute names, a collection $F$ of functional dependencies on $A$, and a subset $A' subset.eq A$. + + *Question:* Is there a subset $X subset.eq A'$ and two attributes $y, z in A' backslash X$ such that $y in X^+$ but $z in.not X^+$, where $X^+$ is the closure of $X$ under $F$? +][ + A relation satisfies _Boyce-Codd Normal Form_ (BCNF) if every non-trivial functional dependency $X arrow.r Y$ has $X$ as a superkey --- that is, $X^+$ = $A'$. This classical NP-complete problem from database theory asks whether the given attribute subset $A'$ violates BCNF. The NP-completeness was established by Beeri and Bernstein (1979) via reduction from Hitting Set. It appears as problem SR29 in Garey and Johnson's compendium (category A4: Storage and Retrieval). +] + #problem-def("SumOfSquaresPartition")[ Given a finite set $A = {a_0, dots, a_(n-1)}$ with sizes $s(a_i) in ZZ^+$, a positive integer $K lt.eq |A|$ (number of groups), and a positive integer $J$ (bound), determine whether $A$ can be partitioned into $K$ disjoint sets $A_1, dots, A_K$ such that $sum_(i=1)^K (sum_(a in A_i) s(a))^2 lt.eq J$. ][ diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index e8d89a26b..6d5532620 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -14,9 +14,10 @@ use problemreductions::models::graph::{ MultipleChoiceBranching, SteinerTree, StrongConnectivityAugmentation, }; use problemreductions::models::misc::{ - BinPacking, CbqRelation, ConjunctiveBooleanQuery, FlowShopScheduling, LongestCommonSubsequence, - MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, - QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, + BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, + FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, + MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, QueryArg, + RectilinearPictureCompression, ResourceConstrainedScheduling, SequencingToMinimizeMaximumCumulativeCost, SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, @@ -154,6 +155,20 @@ fn format_problem_ref(problem: &ProblemRef) -> String { format!("{}/{}", problem.name, values) } +fn ensure_attribute_indices_in_range( + indices: &[usize], + num_attributes: usize, + context: &str, +) -> Result<()> { + for &attr in indices { + anyhow::ensure!( + attr < num_attributes, + "{context} contains attribute index {attr}, which is out of range for --n {num_attributes}" + ); + } + Ok(()) +} + fn resolve_example_problem_ref( input: &str, rgraph: &problemreductions::rules::ReductionGraph, @@ -402,6 +417,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "--matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --k 2" } "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", + "BoyceCoddNormalFormViolation" => { + "--n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" + } "SumOfSquaresPartition" => "--sizes 5,3,8,2,7,1 --num-groups 3 --bound 240", "ComparativeContainment" => { "--universe 4 --r-sets \"0,1,2,3;0,1\" --s-sets \"0,1,2,3;2,3\" --r-weights 2,5 --s-weights 3,6" @@ -1301,6 +1319,58 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { create_vertex_weight_problem(args, canonical, graph_type, &resolved_variant)? } + // BoyceCoddNormalFormViolation + "BoyceCoddNormalFormViolation" => { + let n = args.n.ok_or_else(|| { + anyhow::anyhow!( + "BoyceCoddNormalFormViolation requires --n, --sets, and --target\n\n\ + Usage: pred create BoyceCoddNormalFormViolation --n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" + ) + })?; + let sets_str = args.sets.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "BoyceCoddNormalFormViolation requires --sets (functional deps as lhs:rhs;...)\n\n\ + Usage: pred create BoyceCoddNormalFormViolation --n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" + ) + })?; + let target_str = args.target.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "BoyceCoddNormalFormViolation requires --target (comma-separated attribute indices)\n\n\ + Usage: pred create BoyceCoddNormalFormViolation --n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5" + ) + })?; + let fds: Vec<(Vec, Vec)> = sets_str + .split(';') + .map(|fd_str| { + let parts: Vec<&str> = fd_str.split(':').collect(); + anyhow::ensure!( + parts.len() == 2, + "Each FD must be lhs:rhs, got '{}'", + fd_str + ); + let lhs: Vec = util::parse_comma_list(parts[0])?; + let rhs: Vec = util::parse_comma_list(parts[1])?; + ensure_attribute_indices_in_range( + &lhs, + n, + &format!("Functional dependency '{fd_str}' lhs"), + )?; + ensure_attribute_indices_in_range( + &rhs, + n, + &format!("Functional dependency '{fd_str}' rhs"), + )?; + Ok((lhs, rhs)) + }) + .collect::>()?; + let target: Vec = util::parse_comma_list(target_str)?; + ensure_attribute_indices_in_range(&target, n, "Target subset")?; + ( + ser(BoyceCoddNormalFormViolation::new(n, fds, target))?, + resolved_variant.clone(), + ) + } + // BinPacking "BinPacking" => { let sizes_str = args.sizes.as_deref().ok_or_else(|| { @@ -4007,6 +4077,7 @@ mod tests { use super::help_flag_name; use super::parse_bool_rows; use super::*; + use super::{ensure_attribute_indices_in_range, problem_help_flag_name}; use crate::cli::{Cli, Commands}; use crate::output::OutputConfig; @@ -4039,6 +4110,16 @@ mod tests { ); } + #[test] + fn test_ensure_attribute_indices_in_range_rejects_out_of_range_index() { + let err = ensure_attribute_indices_in_range(&[0, 4], 3, "Functional dependency '0:4' rhs") + .unwrap_err(); + assert!( + err.to_string().contains("out of range"), + "unexpected error: {err}" + ); + } + #[test] fn test_problem_help_uses_prime_attribute_name_cli_overrides() { assert_eq!( diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index dcc6e1e11..76573496a 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -4988,6 +4988,88 @@ fn test_create_factoring_missing_bits() { ); } +#[test] +fn test_create_bcnf_rejects_out_of_range_attribute_indices() { + let output = pred() + .args([ + "create", + "BoyceCoddNormalFormViolation", + "--n", + "3", + "--sets", + "0:4", + "--target", + "0,1,2", + ]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "expected invalid indices to be rejected" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + !stderr.contains("panicked at"), + "CLI should return a user-facing error, got: {stderr}" + ); + assert!( + stderr.contains("out of range"), + "expected out-of-range error, got: {stderr}" + ); +} + +#[test] +fn test_create_bcnf_rejects_out_of_range_lhs_attribute_indices() { + let output = pred() + .args([ + "create", + "BoyceCoddNormalFormViolation", + "--n", + "3", + "--sets", + "4:0", + "--target", + "0,1,2", + ]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "expected invalid lhs indices to be rejected" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("lhs contains attribute index 4"), + "expected lhs-specific out-of-range error, got: {stderr}" + ); +} + +#[test] +fn test_create_bcnf_rejects_out_of_range_target_attribute_indices() { + let output = pred() + .args([ + "create", + "BoyceCoddNormalFormViolation", + "--n", + "3", + "--sets", + "0:1", + "--target", + "0,1,4", + ]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "expected invalid target indices to be rejected" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("Target subset contains attribute index 4"), + "expected target-specific out-of-range error, got: {stderr}" + ); +} + #[test] fn test_create_sequencing_to_minimize_maximum_cumulative_cost() { let output = pred() diff --git a/src/lib.rs b/src/lib.rs index 304388298..6b6f055ab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,13 +60,13 @@ pub mod prelude { UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ - BinPacking, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, Factoring, - FlowShopScheduling, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, - MultiprocessorScheduling, PaintShop, Partition, QueryArg, RectilinearPictureCompression, - ResourceConstrainedScheduling, SequencingToMinimizeMaximumCumulativeCost, - SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, - ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum, - SumOfSquaresPartition, Term, + BinPacking, BoyceCoddNormalFormViolation, CbqRelation, ConjunctiveBooleanQuery, + ConjunctiveQueryFoldability, Factoring, FlowShopScheduling, Knapsack, + LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, + Partition, QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, + SequencingToMinimizeMaximumCumulativeCost, SequencingWithReleaseTimesAndDeadlines, + SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling, + StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term, }; pub use crate::models::set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, diff --git a/src/models/misc/boyce_codd_normal_form_violation.rs b/src/models/misc/boyce_codd_normal_form_violation.rs new file mode 100644 index 000000000..2348e816a --- /dev/null +++ b/src/models/misc/boyce_codd_normal_form_violation.rs @@ -0,0 +1,243 @@ +//! Boyce-Codd Normal Form Violation problem implementation. +//! +//! Given a set of attributes `A`, a collection of functional dependencies over `A`, +//! and a target subset `A' ⊆ A`, determine whether there exists a non-trivial subset +//! `X ⊆ A'` such that the closure of `X` under the functional dependencies contains +//! some but not all attributes of `A' \ X` — i.e., a witness to a BCNF violation. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +inventory::submit! { + ProblemSchemaEntry { + name: "BoyceCoddNormalFormViolation", + display_name: "Boyce-Codd Normal Form Violation", + aliases: &["BCNFViolation", "BCNF"], + dimensions: &[], + module_path: module_path!(), + description: "Test whether a subset of attributes violates Boyce-Codd normal form", + fields: &[ + FieldInfo { name: "num_attributes", type_name: "usize", description: "Total number of attributes in A" }, + FieldInfo { name: "functional_deps", type_name: "Vec<(Vec, Vec)>", description: "Functional dependencies (lhs_attributes, rhs_attributes)" }, + FieldInfo { name: "target_subset", type_name: "Vec", description: "Subset A' of attributes to test for BCNF violation" }, + ], + } +} + +/// The Boyce-Codd Normal Form Violation decision problem. +/// +/// Given a set of attributes `A = {0, ..., num_attributes - 1}`, a collection of +/// functional dependencies `F` over `A`, and a target subset `A' ⊆ A`, determine +/// whether there exists a subset `X ⊆ A'` such that the closure `X⁺` under `F` +/// contains some element of `A' \ X` but not all — witnessing a BCNF violation. +/// +/// # Representation +/// +/// A configuration is a binary vector of length `|A'|`, where bit `i = 1` means +/// attribute `target_subset[i]` is included in the candidate set `X`. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::BoyceCoddNormalFormViolation; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 6 attributes, FDs: {0,1}→{2}, {2}→{3}, {3,4}→{5} +/// let problem = BoyceCoddNormalFormViolation::new( +/// 6, +/// vec![ +/// (vec![0, 1], vec![2]), +/// (vec![2], vec![3]), +/// (vec![3, 4], vec![5]), +/// ], +/// vec![0, 1, 2, 3, 4, 5], +/// ); +/// let solver = BruteForce::new(); +/// // X = {2}: closure = {2, 3}, y=3 ∈ closure, z=0 ∉ closure → BCNF violation +/// assert!(problem.evaluate(&[0, 0, 1, 0, 0, 0])); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BoyceCoddNormalFormViolation { + /// Total number of attributes (elements are `0..num_attributes`). + num_attributes: usize, + /// Functional dependencies as (lhs_attributes, rhs_attributes) pairs. + functional_deps: Vec<(Vec, Vec)>, + /// Target subset `A'` of attributes to test for BCNF violation. + target_subset: Vec, +} + +impl BoyceCoddNormalFormViolation { + /// Create a new Boyce-Codd Normal Form Violation instance. + /// + /// # Panics + /// + /// Panics if any attribute index in `functional_deps` or `target_subset` is + /// out of range (≥ `num_attributes`), if `target_subset` is empty, or if any + /// functional dependency has an empty LHS. + /// + /// The constructor also normalizes the instance by sorting and deduplicating + /// every functional dependency LHS/RHS and the `target_subset`. As a result, + /// the configuration bit positions correspond to the normalized + /// `target_subset()` order rather than the caller's original input order. + pub fn new( + num_attributes: usize, + functional_deps: Vec<(Vec, Vec)>, + target_subset: Vec, + ) -> Self { + assert!(!target_subset.is_empty(), "target_subset must be non-empty"); + + let mut functional_deps = functional_deps; + for (fd_index, (lhs, rhs)) in functional_deps.iter_mut().enumerate() { + assert!( + !lhs.is_empty(), + "Functional dependency {} has an empty LHS", + fd_index + ); + lhs.sort_unstable(); + lhs.dedup(); + rhs.sort_unstable(); + rhs.dedup(); + for &attr in lhs.iter().chain(rhs.iter()) { + assert!( + attr < num_attributes, + "Functional dependency {} contains attribute {} which is out of range (num_attributes = {})", + fd_index, + attr, + num_attributes + ); + } + } + + let mut target_subset = target_subset; + target_subset.sort_unstable(); + target_subset.dedup(); + for &attr in &target_subset { + assert!( + attr < num_attributes, + "target_subset contains attribute {} which is out of range (num_attributes = {})", + attr, + num_attributes + ); + } + + Self { + num_attributes, + functional_deps, + target_subset, + } + } + + /// Return the total number of attributes. + pub fn num_attributes(&self) -> usize { + self.num_attributes + } + + /// Return the number of functional dependencies. + pub fn num_functional_deps(&self) -> usize { + self.functional_deps.len() + } + + /// Return the number of attributes in the target subset. + pub fn num_target_attributes(&self) -> usize { + self.target_subset.len() + } + + /// Return the functional dependencies. + pub fn functional_deps(&self) -> &[(Vec, Vec)] { + &self.functional_deps + } + + /// Return the target subset `A'`. + pub fn target_subset(&self) -> &[usize] { + &self.target_subset + } + + /// Compute the closure of a set of attributes under a collection of functional dependencies. + fn compute_closure(x: &HashSet, fds: &[(Vec, Vec)]) -> HashSet { + let mut closure = x.clone(); + let mut changed = true; + while changed { + changed = false; + for (lhs, rhs) in fds { + if lhs.iter().all(|a| closure.contains(a)) { + for &a in rhs { + if closure.insert(a) { + changed = true; + } + } + } + } + } + closure + } +} + +impl Problem for BoyceCoddNormalFormViolation { + const NAME: &'static str = "BoyceCoddNormalFormViolation"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![2; self.target_subset.len()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.target_subset.len() || config.iter().any(|&v| v > 1) { + return false; + } + let x: HashSet = config + .iter() + .enumerate() + .filter(|(_, &v)| v == 1) + .map(|(i, _)| self.target_subset[i]) + .collect(); + let closure = Self::compute_closure(&x, &self.functional_deps); + // Check: ∃ y, z ∈ A' \ X s.t. y ∈ closure ∧ z ∉ closure + let mut has_in_closure = false; + let mut has_not_in_closure = false; + for &a in &self.target_subset { + if !x.contains(&a) { + if closure.contains(&a) { + has_in_closure = true; + } else { + has_not_in_closure = true; + } + } + } + has_in_closure && has_not_in_closure + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for BoyceCoddNormalFormViolation {} + +crate::declare_variants! { + default sat BoyceCoddNormalFormViolation => "2^num_target_attributes * num_target_attributes^2 * num_functional_deps", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "boyce_codd_normal_form_violation", + instance: Box::new(BoyceCoddNormalFormViolation::new( + 6, + vec![ + (vec![0, 1], vec![2]), + (vec![2], vec![3]), + (vec![3, 4], vec![5]), + ], + vec![0, 1, 2, 3, 4, 5], + )), + // X={2}: closure={2,3}, y=3 in closure, z=0 not in closure -> violation + optimal_config: vec![0, 0, 1, 0, 0, 0], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/boyce_codd_normal_form_violation.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 0fdc4c03d..31521e4e6 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -2,6 +2,7 @@ //! //! Problems with unique input structures that don't fit other categories: //! - [`BinPacking`]: Bin Packing (minimize bins) +//! - [`BoyceCoddNormalFormViolation`]: Boyce-Codd Normal Form Violation (BCNF) //! - [`ConjunctiveBooleanQuery`]: Evaluate a conjunctive Boolean query over relations //! - [`ConjunctiveQueryFoldability`]: Conjunctive Query Foldability //! - [`Factoring`]: Integer factorization @@ -25,6 +26,7 @@ //! - [`SumOfSquaresPartition`]: Partition integers into K groups minimizing sum of squared group sums mod bin_packing; +mod boyce_codd_normal_form_violation; pub(crate) mod conjunctive_boolean_query; pub(crate) mod conjunctive_query_foldability; pub(crate) mod factoring; @@ -49,6 +51,7 @@ mod subset_sum; pub(crate) mod sum_of_squares_partition; pub use bin_packing::BinPacking; +pub use boyce_codd_normal_form_violation::BoyceCoddNormalFormViolation; pub use conjunctive_boolean_query::{ConjunctiveBooleanQuery, QueryArg, Relation as CbqRelation}; pub use conjunctive_query_foldability::{ConjunctiveQueryFoldability, Term}; pub use factoring::Factoring; @@ -75,6 +78,7 @@ pub use sum_of_squares_partition::SumOfSquaresPartition; #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { let mut specs = Vec::new(); + specs.extend(boyce_codd_normal_form_violation::canonical_model_example_specs()); specs.extend(conjunctive_boolean_query::canonical_model_example_specs()); specs.extend(conjunctive_query_foldability::canonical_model_example_specs()); specs.extend(factoring::canonical_model_example_specs()); diff --git a/src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs b/src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs new file mode 100644 index 000000000..c41ee0f94 --- /dev/null +++ b/src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs @@ -0,0 +1,203 @@ +use super::*; +use crate::solvers::BruteForce; +use crate::traits::Problem; + +/// Build the canonical example: 6 attributes, 3 FDs, full target subset. +fn canonical_problem() -> BoyceCoddNormalFormViolation { + BoyceCoddNormalFormViolation::new( + 6, + vec![ + (vec![0, 1], vec![2]), + (vec![2], vec![3]), + (vec![3, 4], vec![5]), + ], + vec![0, 1, 2, 3, 4, 5], + ) +} + +#[test] +fn test_bcnf_creation() { + let problem = canonical_problem(); + assert_eq!(problem.num_attributes(), 6); + assert_eq!(problem.num_functional_deps(), 3); + assert_eq!(problem.num_target_attributes(), 6); + assert_eq!(problem.num_variables(), 6); + assert_eq!(problem.dims(), vec![2; 6]); + assert_eq!(problem.target_subset(), &[0, 1, 2, 3, 4, 5]); + assert_eq!(problem.functional_deps().len(), 3); +} + +#[test] +fn test_bcnf_evaluate_violation() { + let problem = canonical_problem(); + // X = {2}: closure = {2, 3}. In A' \ X = {0,1,3,4,5}: 3 ∈ closure, 0 ∉ closure → violation. + assert!(problem.evaluate(&[0, 0, 1, 0, 0, 0])); +} + +#[test] +fn test_bcnf_evaluate_no_violation_empty_x() { + let problem = canonical_problem(); + // X = {} (all zeros): A' \ X = all attributes, closure of {} = {}. + // Nothing in closure → no violation. + assert!(!problem.evaluate(&[0, 0, 0, 0, 0, 0])); +} + +#[test] +fn test_bcnf_evaluate_no_violation_x_covers_all() { + let problem = canonical_problem(); + // X = all attributes: A' \ X = {} → no attributes to test → no violation. + assert!(!problem.evaluate(&[1, 1, 1, 1, 1, 1])); +} + +#[test] +fn test_bcnf_evaluate_invalid_config_length() { + let problem = canonical_problem(); + assert!(!problem.evaluate(&[0, 0, 1, 0, 0])); // too short + assert!(!problem.evaluate(&[0, 0, 1, 0, 0, 0, 0])); // too long +} + +#[test] +fn test_bcnf_evaluate_invalid_config_values() { + let problem = canonical_problem(); + assert!(!problem.evaluate(&[0, 0, 2, 0, 0, 0])); // value > 1 +} + +#[test] +fn test_bcnf_solver_finds_violation() { + let problem = canonical_problem(); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + // All returned solutions must evaluate to true. + for sol in &solutions { + assert!(problem.evaluate(sol)); + } + // The canonical witness must be among them. + assert!(solutions.contains(&vec![0, 0, 1, 0, 0, 0])); +} + +#[test] +fn test_bcnf_no_violation_when_fds_trivial() { + // Only trivial FD: {0} → {0}. No non-trivial closure possible. + let problem = BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![0])], vec![0, 1, 2]); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(solutions.is_empty()); +} + +#[test] +fn test_bcnf_partial_target_subset() { + // Only test a subset of attributes. + // FD: {0} → {1}; target = {0, 1}. + // X = {0}: closure = {0, 1}. A' \ X = {1}. 1 ∈ closure but nothing is outside → no violation. + let problem = BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![1])], vec![0, 1]); + assert!(!problem.evaluate(&[1, 0])); // X={0}: all of A'\X = {1} ⊆ closure → no violation + assert!(!problem.evaluate(&[0, 0])); // X={}: closure={}, nothing in closure → no violation +} + +#[test] +fn test_bcnf_violation_with_three_attrs_in_target() { + // Attrs 0,1,2. FD: {0} → {1}. Target = {0, 1, 2}. + // X = {0}: closure = {0, 1}. A' \ X = {1, 2}. 1 ∈ closure, 2 ∉ closure → BCNF violation. + let problem = BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![1])], vec![0, 1, 2]); + assert!(problem.evaluate(&[1, 0, 0])); // X = {0} + assert!(!problem.evaluate(&[0, 1, 0])); // X = {1}: A'\X = {0,2}, closure of {1} = {1}, 0∉closure, 2∉closure → no violation +} + +#[test] +fn test_bcnf_serialization() { + let problem = canonical_problem(); + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: BoyceCoddNormalFormViolation = serde_json::from_str(&json).unwrap(); + + assert_eq!(deserialized.num_attributes(), problem.num_attributes()); + assert_eq!( + deserialized.num_functional_deps(), + problem.num_functional_deps() + ); + assert_eq!(deserialized.target_subset(), problem.target_subset()); + assert_eq!(deserialized.functional_deps(), problem.functional_deps()); +} + +#[test] +#[should_panic(expected = "target_subset must be non-empty")] +fn test_bcnf_rejects_empty_target_subset() { + BoyceCoddNormalFormViolation::new(3, vec![], vec![]); +} + +#[test] +#[should_panic(expected = "empty LHS")] +fn test_bcnf_rejects_empty_lhs_fd() { + BoyceCoddNormalFormViolation::new(3, vec![(vec![], vec![1])], vec![0, 1]); +} + +#[test] +#[should_panic(expected = "out of range")] +fn test_bcnf_rejects_out_of_range_fd_attr() { + BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![5])], vec![0, 1]); +} + +#[test] +#[should_panic(expected = "out of range")] +fn test_bcnf_rejects_out_of_range_target_attr() { + BoyceCoddNormalFormViolation::new(3, vec![], vec![0, 5]); +} + +#[test] +fn test_bcnf_deduplicates_fd_attrs() { + // LHS with duplicates should be deduped without panic. + let problem = + BoyceCoddNormalFormViolation::new(3, vec![(vec![0, 0], vec![1, 1])], vec![0, 1, 2]); + assert_eq!(problem.functional_deps()[0].0, vec![0]); + assert_eq!(problem.functional_deps()[0].1, vec![1]); +} + +#[test] +fn test_bcnf_deduplicates_target_subset() { + let problem = BoyceCoddNormalFormViolation::new(3, vec![(vec![0], vec![1])], vec![0, 1, 0, 2]); + assert_eq!(problem.target_subset(), &[0, 1, 2]); + assert_eq!(problem.num_target_attributes(), 3); +} + +#[test] +fn test_bcnf_fds_outside_target_subset() { + // FDs reference attributes outside A'. X={0} triggers {0}→{3}→{4} but 3,4 ∉ A'. + // A' \ X = {1, 2}: neither 1 nor 2 is in closure → no violation. + let problem = BoyceCoddNormalFormViolation::new( + 5, + vec![(vec![0], vec![3]), (vec![3], vec![4])], + vec![0, 1, 2], + ); + assert!(!problem.evaluate(&[1, 0, 0])); // X={0}: closure reaches {0,3,4} but A'\X={1,2} untouched +} + +#[test] +fn test_bcnf_cyclic_keys_no_violation() { + // Issue example: 4 attributes, cyclic keys — every non-trivial subset is a superkey. + // FDs: {0,1}→{2,3}, {2,3}→{0,1}, {0,2}→{1,3}, {1,3}→{0,2}. + // All 2-element subsets containing a key pair have full closure → no BCNF violation. + let problem = BoyceCoddNormalFormViolation::new( + 4, + vec![ + (vec![0, 1], vec![2, 3]), + (vec![2, 3], vec![0, 1]), + (vec![0, 2], vec![1, 3]), + (vec![1, 3], vec![0, 2]), + ], + vec![0, 1, 2, 3], + ); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!( + solutions.is_empty(), + "Cyclic-key instance should have no BCNF violation" + ); +} + +#[test] +fn test_bcnf_multi_step_transitive_closure() { + // X={0,1}: {0,1}→{2} then {2}→{3} (two-step chain). + // A' \ X = {2,3,4,5}. closure = {0,1,2,3}. 2∈closure, 4∉closure → violation. + let problem = canonical_problem(); + assert!(problem.evaluate(&[1, 1, 0, 0, 0, 0])); +} diff --git a/tests/suites/integration.rs b/tests/suites/integration.rs index d7a256a4a..14e3d477c 100644 --- a/tests/suites/integration.rs +++ b/tests/suites/integration.rs @@ -218,6 +218,18 @@ mod all_problems_solvable { } } + #[test] + fn test_bcnf_violation_available_from_prelude() { + let problem = problemreductions::prelude::BoyceCoddNormalFormViolation::new( + 3, + vec![(vec![0], vec![1])], + vec![0, 1, 2], + ); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(solutions.contains(&vec![1, 0, 0])); + } + #[test] fn test_paintshop_solvable() { let problem = PaintShop::new(vec!["a", "b", "a", "b"]);