From ced1053a0ab4ebf93700393e24d7825cf88ecbbb Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 03:13:11 +0800 Subject: [PATCH 1/5] Add plan for #447: [Model] BoyceCoddNormalFormViolation --- ...-03-17-boyce-codd-normal-form-violation.md | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 docs/plans/2026-03-17-boyce-codd-normal-form-violation.md diff --git a/docs/plans/2026-03-17-boyce-codd-normal-form-violation.md b/docs/plans/2026-03-17-boyce-codd-normal-form-violation.md new file mode 100644 index 000000000..c2651e8ff --- /dev/null +++ b/docs/plans/2026-03-17-boyce-codd-normal-form-violation.md @@ -0,0 +1,207 @@ +# Plan: Add BoyceCoddNormalFormViolation Model + +**Issue:** #447 +**Type:** [Model] — Satisfaction problem +**Category:** `misc/` (unique input structure: functional dependencies on attributes) +**Reference:** Beeri & Bernstein, 1979; Garey & Johnson A4 SR29 + +## Problem Summary + +Given a set A of attributes, a collection F of functional dependencies on A, and a subset A' ⊆ A, determine whether A' violates Boyce-Codd Normal Form (BCNF). A violation exists when there is a subset X ⊆ A' and two attributes y, z ∈ A' \ X such that y ∈ X⁺ (the closure of X under F) but z ∉ X⁺ — meaning X determines y but not z, so X is not a superkey. + +This is a satisfaction problem: the binary variables encode which attributes are in X, and `evaluate()` returns true iff that X witnesses a BCNF violation. + +## Batch 1: Implementation (Steps 1–5.5) + +### Step 1: Create model file `src/models/misc/boyce_codd_normal_form_violation.rs` + +**Inventory registration:** +```rust +inventory::submit! { + ProblemSchemaEntry { + name: "BoyceCoddNormalFormViolation", + display_name: "Boyce-Codd Normal Form Violation", + aliases: &["BCNFViolation", "BCNF"], + dimensions: &[], + module_path: "models::misc::boyce_codd_normal_form_violation", + description: "Test whether a subset of attributes violates Boyce-Codd normal form", + fields: &["num_attributes", "functional_deps", "target_subset"], + } +} +``` + +**Struct:** +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BoyceCoddNormalFormViolation { + num_attributes: usize, + functional_deps: Vec<(Vec, Vec)>, + target_subset: Vec, +} +``` + +**Constructor `new(num_attributes, functional_deps, target_subset)`:** +- Validate: all attribute indices in functional_deps and target_subset are < num_attributes +- Validate: target_subset is non-empty, sorted, deduplicated +- Validate: each FD has non-empty LHS +- Sort and dedup each FD's LHS and RHS +- Sort and dedup target_subset + +**Getter methods:** +- `num_attributes(&self) -> usize` — returns `self.num_attributes` +- `num_functional_deps(&self) -> usize` — returns `self.functional_deps.len()` +- `num_target_attributes(&self) -> usize` — returns `self.target_subset.len()` +- `functional_deps(&self) -> &[(Vec, Vec)]` +- `target_subset(&self) -> &[usize]` + +**Helper: `compute_closure(x: &HashSet, fds: &[(Vec, Vec)]) -> HashSet`** +- Start with closure = x.clone() +- Repeat until no change: for each FD (lhs, rhs), if lhs ⊆ closure, add all rhs to closure +- Return closure + +**Problem trait:** +```rust +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 { + // X = {target_subset[i] : config[i] == 1} + // Compute X⁺ under F + // Check: ∃ y,z ∈ A' \ X s.t. y ∈ X⁺ ∧ z ∉ X⁺ + 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); + let outside_x: Vec = self.target_subset.iter() + .filter(|a| !x.contains(a)) + .copied() + .collect(); + let has_in_closure = outside_x.iter().any(|a| closure.contains(a)); + let has_not_in_closure = outside_x.iter().any(|a| !closure.contains(a)); + has_in_closure && has_not_in_closure + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for BoyceCoddNormalFormViolation {} +``` + +### Step 1.5: Register variant complexity + +```rust +crate::declare_variants! { + default sat BoyceCoddNormalFormViolation => "2^num_target_attributes * num_target_attributes^2 * num_functional_deps", +} +``` + +Getter methods: `num_target_attributes()`, `num_functional_deps()` — validated at compile time. + +### Step 2: Register in `src/models/misc/mod.rs` + +- Add `mod boyce_codd_normal_form_violation;` +- Add `pub use boyce_codd_normal_form_violation::BoyceCoddNormalFormViolation;` +- Add `specs.extend(boyce_codd_normal_form_violation::canonical_model_example_specs());` to `canonical_model_example_specs()` + +### Step 3: CLI create support in `problemreductions-cli/src/commands/create.rs` + +Add BoyceCoddNormalFormViolation to the import list from `problemreductions::models::misc::`. + +Add to the `usage_hint` match: +``` +"BoyceCoddNormalFormViolation" => "--n 6 --sets '0,1:2;2:3;3,4:5' --target '0,1,2,3,4,5'" +``` + +Add to the `create_from_args` match: +- Parse `--n` as num_attributes +- Parse `--sets` as functional dependencies (format: `lhs:rhs;lhs:rhs` where each side is comma-separated indices) +- Parse `--target` as target_subset (comma-separated indices) +- Construct `BoyceCoddNormalFormViolation::new(n, fds, target)` + +Since functional dependencies have a unique input format (pairs of attribute sets), use the `--sets` flag for FDs (semicolon-separated, colon between LHS:RHS) and `--target` for target_subset. Use `--n` for num_attributes. + +### Step 4: Canonical example in model file + +```rust +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "boyce_codd_normal_form_violation", + build: || { + // YES instance: 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], + ); + // X={2} witnesses violation: closure={2,3}, y=3∈closure, z=0∉closure + crate::example_db::specs::satisfaction_example( + problem, + vec![vec![0, 0, 1, 0, 0, 0]], // config for X={2} + ) + }, + }] +} +``` + +### Step 5: Unit tests in `src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs` + +Link from model file: +```rust +#[cfg(test)] +#[path = "../../unit_tests/models/misc/boyce_codd_normal_form_violation.rs"] +mod tests; +``` + +Test functions (minimum 3): + +1. **`test_bcnf_violation_creation`** — construct instances, verify getters +2. **`test_bcnf_violation_evaluate_yes`** — YES instance from issue: X={2} with closure {2,3}, verify evaluate returns true +3. **`test_bcnf_violation_evaluate_no`** — NO instance from issue (cyclic keys): verify all configs return false (or at least specific ones) +4. **`test_bcnf_violation_evaluate_superkey`** — X is a superkey (closure = A'), evaluate returns false +5. **`test_bcnf_violation_evaluate_trivial`** — X has trivial closure (X⁺ = X), evaluate returns false +6. **`test_bcnf_violation_solver`** — use BruteForce::find_satisfying on YES instance, verify it finds a solution; on NO instance, verify None +7. **`test_bcnf_violation_serialization`** — round-trip JSON serialization + +Also ensure the unit_tests/models/misc/mod.rs includes the new test module. + +### Step 5.5: trait_consistency test + +Add `BoyceCoddNormalFormViolation` to the existing trait consistency tests if a pattern file exists, or verify it passes the `example_db` tests via `make test`. + +## Batch 2: Paper Entry (Step 6) + +### Step 6: Add to `docs/paper/reductions.typ` + +**Prerequisites:** Batch 1 must be complete. Run `make paper` to regenerate exports first. + +1. Add to `display-name` dictionary: +```typst +"BoyceCoddNormalFormViolation": [Boyce-Codd Normal Form Violation], +``` + +2. Add `problem-def` entry (place near other misc/database-theory problems): +```typst +#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 -> Y$ has $X$ as a superkey — that is, $X^+ = A'$. + This problem asks whether the given attribute subset $A'$ violates BCNF, which is NP-complete by reduction from Hitting Set @BeeriB1979. +] +``` + +3. Run `make paper` to verify compilation. From f10747b2c578ab9a98a3ea50f6883d2fd3596806 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 03:48:28 +0800 Subject: [PATCH 2/5] Implement #447: [Model] BoyceCoddNormalFormViolation Add the Boyce-Codd Normal Form Violation satisfaction problem model (Garey & Johnson A4 SR29). Given attributes, functional dependencies, and a target subset, determines whether a BCNF violation exists. - Model: src/models/misc/boyce_codd_normal_form_violation.rs - Tests: 20 unit tests covering evaluation, solver, serialization, edge cases - CLI: create support with --n, --sets (lhs:rhs format), --target - Paper: problem-def entry in docs/paper/reductions.typ - Example-db: canonical example with violation witness X={2} --- docs/paper/reductions.typ | 9 + problemreductions-cli/src/commands/create.rs | 49 +++- src/example_db/fixtures/examples.json | 1 + .../misc/boyce_codd_normal_form_violation.rs | 239 ++++++++++++++++++ src/models/misc/mod.rs | 4 + .../misc/boyce_codd_normal_form_violation.rs | 203 +++++++++++++++ 6 files changed, 503 insertions(+), 2 deletions(-) create mode 100644 src/models/misc/boyce_codd_normal_form_violation.rs create mode 100644 src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 01372f7cb..8d3fe70ee 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -94,6 +94,7 @@ "BicliqueCover": [Biclique Cover], "BoundedComponentSpanningForest": [Bounded Component Spanning Forest], "BinPacking": [Bin Packing], + "BoyceCoddNormalFormViolation": [Boyce-Codd Normal Form Violation], "ClosestVectorProblem": [Closest Vector Problem], "OptimalLinearArrangement": [Optimal Linear Arrangement], "RuralPostman": [Rural Postman], @@ -1871,6 +1872,14 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ) ] +#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' without 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("Knapsack")[ Given $n$ items with weights $w_0, dots, w_(n-1) in NN$ and values $v_0, dots, v_(n-1) in NN$, and a capacity $C in NN$, find $S subset.eq {0, dots, n - 1}$ maximizing $sum_(i in S) v_i$ subject to $sum_(i in S) w_i lt.eq C$. ][ diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 03220f4f6..af63fe223 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -13,8 +13,9 @@ use problemreductions::models::graph::{ SteinerTree, }; use problemreductions::models::misc::{ - BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, - PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum, + BinPacking, BoyceCoddNormalFormViolation, FlowShopScheduling, LongestCommonSubsequence, + MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, + SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -294,6 +295,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } "SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1", "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" + } "SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3", "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", _ => "", @@ -901,6 +905,47 @@ 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])?; + Ok((lhs, rhs)) + }) + .collect::>()?; + let target: Vec = util::parse_comma_list(target_str)?; + ( + ser(BoyceCoddNormalFormViolation::new(n, fds, target))?, + resolved_variant.clone(), + ) + } + // BinPacking "BinPacking" => { let sizes_str = args.sizes.as_deref().ok_or_else(|| { diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 6e52883b2..78fe4fa82 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -3,6 +3,7 @@ {"problem":"BMF","variant":{},"instance":{"k":2,"m":3,"matrix":[[true,true,false],[true,true,true],[false,true,true]],"n":3},"samples":[{"config":[1,0,1,1,0,1,1,1,0,0,1,1],"metric":{"Valid":0}}],"optimal":[{"config":[0,1,1,1,1,0,0,1,1,1,1,0],"metric":{"Valid":0}},{"config":[1,0,1,1,0,1,1,1,0,0,1,1],"metric":{"Valid":0}}]}, {"problem":"BicliqueCover","variant":{},"instance":{"graph":{"edges":[[0,0],[0,1],[1,1],[1,2]],"left_size":2,"right_size":3},"k":2},"samples":[{"config":[1,0,0,1,1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[0,1,0,1,0,1,0,1,0,1],"metric":{"Valid":5}},{"config":[1,0,1,0,1,0,1,0,1,0],"metric":{"Valid":5}}]}, {"problem":"BoundedComponentSpanningForest","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[2,3,null],[3,4,null],[4,5,null],[5,6,null],[6,7,null],[0,7,null],[1,5,null],[2,6,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null,null]}},"max_components":3,"max_weight":6,"weights":[2,3,1,2,3,1,2,1]},"samples":[{"config":[0,0,1,1,1,2,2,0],"metric":true}],"optimal":[{"config":[0,0,0,1,1,1,2,2],"metric":true},{"config":[0,0,0,1,1,2,2,2],"metric":true},{"config":[0,0,0,2,2,1,1,1],"metric":true},{"config":[0,0,0,2,2,2,1,1],"metric":true},{"config":[0,0,1,1,1,0,2,2],"metric":true},{"config":[0,0,1,1,1,2,2,0],"metric":true},{"config":[0,0,1,1,1,2,2,2],"metric":true},{"config":[0,0,1,1,2,0,1,1],"metric":true},{"config":[0,0,1,1,2,1,1,0],"metric":true},{"config":[0,0,1,1,2,2,1,0],"metric":true},{"config":[0,0,1,1,2,2,1,1],"metric":true},{"config":[0,0,1,1,2,2,2,0],"metric":true},{"config":[0,0,1,2,2,0,1,1],"metric":true},{"config":[0,0,1,2,2,1,1,0],"metric":true},{"config":[0,0,1,2,2,1,1,1],"metric":true},{"config":[0,0,1,2,2,2,1,0],"metric":true},{"config":[0,0,1,2,2,2,1,1],"metric":true},{"config":[0,0,2,1,1,0,2,2],"metric":true},{"config":[0,0,2,1,1,1,2,0],"metric":true},{"config":[0,0,2,1,1,1,2,2],"metric":true},{"config":[0,0,2,1,1,2,2,0],"metric":true},{"config":[0,0,2,1,1,2,2,2],"metric":true},{"config":[0,0,2,2,1,0,2,2],"metric":true},{"config":[0,0,2,2,1,1,1,0],"metric":true},{"config":[0,0,2,2,1,1,2,0],"metric":true},{"config":[0,0,2,2,1,1,2,2],"metric":true},{"config":[0,0,2,2,1,2,2,0],"metric":true},{"config":[0,0,2,2,2,0,1,1],"metric":true},{"config":[0,0,2,2,2,1,1,0],"metric":true},{"config":[0,0,2,2,2,1,1,1],"metric":true},{"config":[0,1,0,2,2,1,0,0],"metric":true},{"config":[0,1,0,2,2,2,0,0],"metric":true},{"config":[0,1,1,1,2,0,0,0],"metric":true},{"config":[0,1,1,1,2,2,0,0],"metric":true},{"config":[0,1,1,1,2,2,2,0],"metric":true},{"config":[0,1,1,2,2,0,0,0],"metric":true},{"config":[0,1,1,2,2,1,0,0],"metric":true},{"config":[0,1,1,2,2,2,0,0],"metric":true},{"config":[0,1,1,2,2,2,1,0],"metric":true},{"config":[0,1,2,2,2,0,0,0],"metric":true},{"config":[0,1,2,2,2,1,0,0],"metric":true},{"config":[0,1,2,2,2,1,1,0],"metric":true},{"config":[0,2,0,1,1,1,0,0],"metric":true},{"config":[0,2,0,1,1,2,0,0],"metric":true},{"config":[0,2,1,1,1,0,0,0],"metric":true},{"config":[0,2,1,1,1,2,0,0],"metric":true},{"config":[0,2,1,1,1,2,2,0],"metric":true},{"config":[0,2,2,1,1,0,0,0],"metric":true},{"config":[0,2,2,1,1,1,0,0],"metric":true},{"config":[0,2,2,1,1,1,2,0],"metric":true},{"config":[0,2,2,1,1,2,0,0],"metric":true},{"config":[0,2,2,2,1,0,0,0],"metric":true},{"config":[0,2,2,2,1,1,0,0],"metric":true},{"config":[0,2,2,2,1,1,1,0],"metric":true},{"config":[1,0,0,0,2,1,1,1],"metric":true},{"config":[1,0,0,0,2,2,1,1],"metric":true},{"config":[1,0,0,0,2,2,2,1],"metric":true},{"config":[1,0,0,2,2,0,1,1],"metric":true},{"config":[1,0,0,2,2,1,1,1],"metric":true},{"config":[1,0,0,2,2,2,0,1],"metric":true},{"config":[1,0,0,2,2,2,1,1],"metric":true},{"config":[1,0,1,2,2,0,1,1],"metric":true},{"config":[1,0,1,2,2,2,1,1],"metric":true},{"config":[1,0,2,2,2,0,0,1],"metric":true},{"config":[1,0,2,2,2,0,1,1],"metric":true},{"config":[1,0,2,2,2,1,1,1],"metric":true},{"config":[1,1,0,0,0,1,2,2],"metric":true},{"config":[1,1,0,0,0,2,2,1],"metric":true},{"config":[1,1,0,0,0,2,2,2],"metric":true},{"config":[1,1,0,0,2,0,0,1],"metric":true},{"config":[1,1,0,0,2,1,0,0],"metric":true},{"config":[1,1,0,0,2,2,0,0],"metric":true},{"config":[1,1,0,0,2,2,0,1],"metric":true},{"config":[1,1,0,0,2,2,2,1],"metric":true},{"config":[1,1,0,2,2,0,0,0],"metric":true},{"config":[1,1,0,2,2,0,0,1],"metric":true},{"config":[1,1,0,2,2,1,0,0],"metric":true},{"config":[1,1,0,2,2,2,0,0],"metric":true},{"config":[1,1,0,2,2,2,0,1],"metric":true},{"config":[1,1,1,0,0,0,2,2],"metric":true},{"config":[1,1,1,0,0,2,2,2],"metric":true},{"config":[1,1,1,2,2,0,0,0],"metric":true},{"config":[1,1,1,2,2,2,0,0],"metric":true},{"config":[1,1,2,0,0,0,2,1],"metric":true},{"config":[1,1,2,0,0,0,2,2],"metric":true},{"config":[1,1,2,0,0,1,2,2],"metric":true},{"config":[1,1,2,0,0,2,2,1],"metric":true},{"config":[1,1,2,0,0,2,2,2],"metric":true},{"config":[1,1,2,2,0,0,0,1],"metric":true},{"config":[1,1,2,2,0,0,2,1],"metric":true},{"config":[1,1,2,2,0,0,2,2],"metric":true},{"config":[1,1,2,2,0,1,2,2],"metric":true},{"config":[1,1,2,2,0,2,2,1],"metric":true},{"config":[1,1,2,2,2,0,0,0],"metric":true},{"config":[1,1,2,2,2,0,0,1],"metric":true},{"config":[1,1,2,2,2,1,0,0],"metric":true},{"config":[1,2,0,0,0,1,1,1],"metric":true},{"config":[1,2,0,0,0,2,1,1],"metric":true},{"config":[1,2,0,0,0,2,2,1],"metric":true},{"config":[1,2,1,0,0,0,1,1],"metric":true},{"config":[1,2,1,0,0,2,1,1],"metric":true},{"config":[1,2,2,0,0,0,1,1],"metric":true},{"config":[1,2,2,0,0,0,2,1],"metric":true},{"config":[1,2,2,0,0,1,1,1],"metric":true},{"config":[1,2,2,0,0,2,1,1],"metric":true},{"config":[1,2,2,2,0,0,0,1],"metric":true},{"config":[1,2,2,2,0,0,1,1],"metric":true},{"config":[1,2,2,2,0,1,1,1],"metric":true},{"config":[2,0,0,0,1,1,1,2],"metric":true},{"config":[2,0,0,0,1,1,2,2],"metric":true},{"config":[2,0,0,0,1,2,2,2],"metric":true},{"config":[2,0,0,1,1,0,2,2],"metric":true},{"config":[2,0,0,1,1,1,0,2],"metric":true},{"config":[2,0,0,1,1,1,2,2],"metric":true},{"config":[2,0,0,1,1,2,2,2],"metric":true},{"config":[2,0,1,1,1,0,0,2],"metric":true},{"config":[2,0,1,1,1,0,2,2],"metric":true},{"config":[2,0,1,1,1,2,2,2],"metric":true},{"config":[2,0,2,1,1,0,2,2],"metric":true},{"config":[2,0,2,1,1,1,2,2],"metric":true},{"config":[2,1,0,0,0,1,1,2],"metric":true},{"config":[2,1,0,0,0,1,2,2],"metric":true},{"config":[2,1,0,0,0,2,2,2],"metric":true},{"config":[2,1,1,0,0,0,1,2],"metric":true},{"config":[2,1,1,0,0,0,2,2],"metric":true},{"config":[2,1,1,0,0,1,2,2],"metric":true},{"config":[2,1,1,0,0,2,2,2],"metric":true},{"config":[2,1,1,1,0,0,0,2],"metric":true},{"config":[2,1,1,1,0,0,2,2],"metric":true},{"config":[2,1,1,1,0,2,2,2],"metric":true},{"config":[2,1,2,0,0,0,2,2],"metric":true},{"config":[2,1,2,0,0,1,2,2],"metric":true},{"config":[2,2,0,0,0,1,1,1],"metric":true},{"config":[2,2,0,0,0,1,1,2],"metric":true},{"config":[2,2,0,0,0,2,1,1],"metric":true},{"config":[2,2,0,0,1,0,0,2],"metric":true},{"config":[2,2,0,0,1,1,0,0],"metric":true},{"config":[2,2,0,0,1,1,0,2],"metric":true},{"config":[2,2,0,0,1,1,1,2],"metric":true},{"config":[2,2,0,0,1,2,0,0],"metric":true},{"config":[2,2,0,1,1,0,0,0],"metric":true},{"config":[2,2,0,1,1,0,0,2],"metric":true},{"config":[2,2,0,1,1,1,0,0],"metric":true},{"config":[2,2,0,1,1,1,0,2],"metric":true},{"config":[2,2,0,1,1,2,0,0],"metric":true},{"config":[2,2,1,0,0,0,1,1],"metric":true},{"config":[2,2,1,0,0,0,1,2],"metric":true},{"config":[2,2,1,0,0,1,1,1],"metric":true},{"config":[2,2,1,0,0,1,1,2],"metric":true},{"config":[2,2,1,0,0,2,1,1],"metric":true},{"config":[2,2,1,1,0,0,0,2],"metric":true},{"config":[2,2,1,1,0,0,1,1],"metric":true},{"config":[2,2,1,1,0,0,1,2],"metric":true},{"config":[2,2,1,1,0,1,1,2],"metric":true},{"config":[2,2,1,1,0,2,1,1],"metric":true},{"config":[2,2,1,1,1,0,0,0],"metric":true},{"config":[2,2,1,1,1,0,0,2],"metric":true},{"config":[2,2,1,1,1,2,0,0],"metric":true},{"config":[2,2,2,0,0,0,1,1],"metric":true},{"config":[2,2,2,0,0,1,1,1],"metric":true},{"config":[2,2,2,1,1,0,0,0],"metric":true},{"config":[2,2,2,1,1,1,0,0],"metric":true}]}, + {"problem":"BoyceCoddNormalFormViolation","variant":{},"instance":{"functional_deps":[[[0,1],[2]],[[2],[3]],[[3,4],[5]]],"num_attributes":6,"target_subset":[0,1,2,3,4,5]},"samples":[{"config":[0,0,1,0,0,0],"metric":true}],"optimal":[{"config":[0,0,0,1,1,0],"metric":true},{"config":[0,0,1,0,0,0],"metric":true},{"config":[0,0,1,0,0,1],"metric":true},{"config":[0,0,1,0,1,0],"metric":true},{"config":[0,0,1,0,1,1],"metric":true},{"config":[0,0,1,1,1,0],"metric":true},{"config":[0,1,0,1,1,0],"metric":true},{"config":[0,1,1,0,0,0],"metric":true},{"config":[0,1,1,0,0,1],"metric":true},{"config":[0,1,1,0,1,0],"metric":true},{"config":[0,1,1,0,1,1],"metric":true},{"config":[0,1,1,1,1,0],"metric":true},{"config":[1,0,0,1,1,0],"metric":true},{"config":[1,0,1,0,0,0],"metric":true},{"config":[1,0,1,0,0,1],"metric":true},{"config":[1,0,1,0,1,0],"metric":true},{"config":[1,0,1,0,1,1],"metric":true},{"config":[1,0,1,1,1,0],"metric":true},{"config":[1,1,0,0,0,0],"metric":true},{"config":[1,1,0,0,0,1],"metric":true},{"config":[1,1,0,1,0,0],"metric":true},{"config":[1,1,0,1,0,1],"metric":true},{"config":[1,1,1,0,0,0],"metric":true},{"config":[1,1,1,0,0,1],"metric":true}]}, {"problem":"CircuitSAT","variant":{},"instance":{"circuit":{"assignments":[{"expr":{"op":{"And":[{"op":{"Var":"x1"}},{"op":{"Var":"x2"}}]}},"outputs":["a"]},{"expr":{"op":{"Or":[{"op":{"Var":"x1"}},{"op":{"Var":"x2"}}]}},"outputs":["b"]},{"expr":{"op":{"Xor":[{"op":{"Var":"a"}},{"op":{"Var":"b"}}]}},"outputs":["c"]}]},"variables":["a","b","c","x1","x2"]},"samples":[{"config":[0,1,1,0,1],"metric":true},{"config":[0,1,1,1,0],"metric":true}],"optimal":[{"config":[0,0,0,0,0],"metric":true},{"config":[0,1,1,0,1],"metric":true},{"config":[0,1,1,1,0],"metric":true},{"config":[1,1,0,1,1],"metric":true}]}, {"problem":"ClosestVectorProblem","variant":{"weight":"i32"},"instance":{"basis":[[2,0],[1,2]],"bounds":[{"lower":-2,"upper":4},{"lower":-2,"upper":4}],"target":[2.8,1.5]},"samples":[{"config":[3,3],"metric":{"Valid":0.5385164807134505}}],"optimal":[{"config":[3,3],"metric":{"Valid":0.5385164807134505}}]}, {"problem":"DirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,1,1,1,1,1,1],"graph":{"inner":{"edge_property":"directed","edges":[[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,4,null],[2,5,null],[3,4,null],[3,5,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":4,"sink_2":5,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true}],"optimal":[{"config":[0,0,0,1,0,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,0,1,1,0,1,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,0,0,1,0,0,1],"metric":true},{"config":[0,0,0,1,0,0,1,0,1,1,1,0,1,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,0,0,1,0,1,1,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,0,0,1,1,0],"metric":true},{"config":[0,0,1,0,1,0,0,0,1,1,0,1,0,1,1,1],"metric":true},{"config":[0,0,1,1,0,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,1,0,1,1,0,1,1,0,0,1,0,0,1],"metric":true},{"config":[0,0,1,1,1,0,0,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,1,1,0,0,1,1,1,0,0,0,1,1,0],"metric":true},{"config":[0,0,1,1,1,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[0,0,1,1,1,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,0,1,1,1,0,1,0,1,1,0,0,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,0,0,1,1,1,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,0,1,1,0,0,1],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,0,0,0,0,1,0,1,0,1,1,1,1,0,1],"metric":true},{"config":[0,1,0,1,0,0,1,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[0,1,0,1,0,0,1,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,0,1,0,0,1,1,1,0,1,0,1,1,0,0],"metric":true},{"config":[0,1,1,0,0,1,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,1,0,0,1,1,0,1,0,0,1,1,0,0,1],"metric":true},{"config":[0,1,1,0,1,0,0,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,1,0,1,0,0,1,1,0,0,1,0,1,1,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,0,0,0,0,1,0,0],"metric":true},{"config":[0,1,1,0,1,0,1,0,1,0,0,1,0,1,0,1],"metric":true},{"config":[0,1,1,1,1,0,1,1,1,0,0,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,0,1,1,0,1,1,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,0,0,1,1,0],"metric":true},{"config":[1,0,0,0,1,0,0,0,0,1,1,1,0,1,1,1],"metric":true},{"config":[1,0,0,1,0,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,1,0,1,1,0,0,1,1,0,1,0,0,1],"metric":true},{"config":[1,0,0,1,1,0,0,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,1,1,0,0,1,0,1,1,0,0,1,1,0],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,0,1,1,0,1,0,0,1,1,0,0,1,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,0,1,0,1,1,0,0,0,1,0,1,0,0,1,1],"metric":true},{"config":[1,0,1,1,1,1,1,0,0,1,0,0,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,1,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,1,0,0,0,1,1,0,0,0,1,1,1,0,0,1],"metric":true},{"config":[1,1,0,0,1,0,0,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,0,0,1,0,0,1,0,0,1,1,0,1,1,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,0,1,0,0,0,1],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,0,0,1,0,1,0,0,0,1,1,0,1,0,1],"metric":true},{"config":[1,1,0,1,1,0,1,1,0,0,1,0,0,1,0,0],"metric":true},{"config":[1,1,1,0,1,1,1,0,0,0,0,1,0,0,0,1],"metric":true}]}, 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..9aaddc6c9 --- /dev/null +++ b/src/models/misc/boyce_codd_normal_form_violation.rs @@ -0,0 +1,239 @@ +//! 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. + 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", + build: || { + 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], + ); + // X={2}: closure={2,3}, y=3 in closure, z=0 not in closure -> violation + crate::example_db::specs::satisfaction_example(problem, vec![vec![0, 0, 1, 0, 0, 0]]) + }, + }] +} + +#[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 c4b125274..593d8daa5 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) //! - [`Factoring`]: Integer factorization //! - [`FlowShopScheduling`]: Flow Shop Scheduling (meet deadline on m processors) //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) @@ -13,6 +14,7 @@ //! - [`SubsetSum`]: Find a subset summing to exactly a target value mod bin_packing; +mod boyce_codd_normal_form_violation; pub(crate) mod factoring; mod flow_shop_scheduling; mod knapsack; @@ -24,6 +26,7 @@ pub(crate) mod shortest_common_supersequence; mod subset_sum; pub use bin_packing::BinPacking; +pub use boyce_codd_normal_form_violation::BoyceCoddNormalFormViolation; pub use factoring::Factoring; pub use flow_shop_scheduling::FlowShopScheduling; pub use knapsack::Knapsack; @@ -37,6 +40,7 @@ pub use subset_sum::SubsetSum; #[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(factoring::canonical_model_example_specs()); specs.extend(paintshop::canonical_model_example_specs()); specs.extend(sequencing_within_intervals::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])); +} From 41b8d23b68dca8dadb3c42b1325f391fc30c50ae Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 03:48:48 +0800 Subject: [PATCH 3/5] chore: remove plan file after implementation --- ...-03-17-boyce-codd-normal-form-violation.md | 207 ------------------ 1 file changed, 207 deletions(-) delete mode 100644 docs/plans/2026-03-17-boyce-codd-normal-form-violation.md diff --git a/docs/plans/2026-03-17-boyce-codd-normal-form-violation.md b/docs/plans/2026-03-17-boyce-codd-normal-form-violation.md deleted file mode 100644 index c2651e8ff..000000000 --- a/docs/plans/2026-03-17-boyce-codd-normal-form-violation.md +++ /dev/null @@ -1,207 +0,0 @@ -# Plan: Add BoyceCoddNormalFormViolation Model - -**Issue:** #447 -**Type:** [Model] — Satisfaction problem -**Category:** `misc/` (unique input structure: functional dependencies on attributes) -**Reference:** Beeri & Bernstein, 1979; Garey & Johnson A4 SR29 - -## Problem Summary - -Given a set A of attributes, a collection F of functional dependencies on A, and a subset A' ⊆ A, determine whether A' violates Boyce-Codd Normal Form (BCNF). A violation exists when there is a subset X ⊆ A' and two attributes y, z ∈ A' \ X such that y ∈ X⁺ (the closure of X under F) but z ∉ X⁺ — meaning X determines y but not z, so X is not a superkey. - -This is a satisfaction problem: the binary variables encode which attributes are in X, and `evaluate()` returns true iff that X witnesses a BCNF violation. - -## Batch 1: Implementation (Steps 1–5.5) - -### Step 1: Create model file `src/models/misc/boyce_codd_normal_form_violation.rs` - -**Inventory registration:** -```rust -inventory::submit! { - ProblemSchemaEntry { - name: "BoyceCoddNormalFormViolation", - display_name: "Boyce-Codd Normal Form Violation", - aliases: &["BCNFViolation", "BCNF"], - dimensions: &[], - module_path: "models::misc::boyce_codd_normal_form_violation", - description: "Test whether a subset of attributes violates Boyce-Codd normal form", - fields: &["num_attributes", "functional_deps", "target_subset"], - } -} -``` - -**Struct:** -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BoyceCoddNormalFormViolation { - num_attributes: usize, - functional_deps: Vec<(Vec, Vec)>, - target_subset: Vec, -} -``` - -**Constructor `new(num_attributes, functional_deps, target_subset)`:** -- Validate: all attribute indices in functional_deps and target_subset are < num_attributes -- Validate: target_subset is non-empty, sorted, deduplicated -- Validate: each FD has non-empty LHS -- Sort and dedup each FD's LHS and RHS -- Sort and dedup target_subset - -**Getter methods:** -- `num_attributes(&self) -> usize` — returns `self.num_attributes` -- `num_functional_deps(&self) -> usize` — returns `self.functional_deps.len()` -- `num_target_attributes(&self) -> usize` — returns `self.target_subset.len()` -- `functional_deps(&self) -> &[(Vec, Vec)]` -- `target_subset(&self) -> &[usize]` - -**Helper: `compute_closure(x: &HashSet, fds: &[(Vec, Vec)]) -> HashSet`** -- Start with closure = x.clone() -- Repeat until no change: for each FD (lhs, rhs), if lhs ⊆ closure, add all rhs to closure -- Return closure - -**Problem trait:** -```rust -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 { - // X = {target_subset[i] : config[i] == 1} - // Compute X⁺ under F - // Check: ∃ y,z ∈ A' \ X s.t. y ∈ X⁺ ∧ z ∉ X⁺ - 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); - let outside_x: Vec = self.target_subset.iter() - .filter(|a| !x.contains(a)) - .copied() - .collect(); - let has_in_closure = outside_x.iter().any(|a| closure.contains(a)); - let has_not_in_closure = outside_x.iter().any(|a| !closure.contains(a)); - has_in_closure && has_not_in_closure - } - - fn variant() -> Vec<(&'static str, &'static str)> { - crate::variant_params![] - } -} - -impl SatisfactionProblem for BoyceCoddNormalFormViolation {} -``` - -### Step 1.5: Register variant complexity - -```rust -crate::declare_variants! { - default sat BoyceCoddNormalFormViolation => "2^num_target_attributes * num_target_attributes^2 * num_functional_deps", -} -``` - -Getter methods: `num_target_attributes()`, `num_functional_deps()` — validated at compile time. - -### Step 2: Register in `src/models/misc/mod.rs` - -- Add `mod boyce_codd_normal_form_violation;` -- Add `pub use boyce_codd_normal_form_violation::BoyceCoddNormalFormViolation;` -- Add `specs.extend(boyce_codd_normal_form_violation::canonical_model_example_specs());` to `canonical_model_example_specs()` - -### Step 3: CLI create support in `problemreductions-cli/src/commands/create.rs` - -Add BoyceCoddNormalFormViolation to the import list from `problemreductions::models::misc::`. - -Add to the `usage_hint` match: -``` -"BoyceCoddNormalFormViolation" => "--n 6 --sets '0,1:2;2:3;3,4:5' --target '0,1,2,3,4,5'" -``` - -Add to the `create_from_args` match: -- Parse `--n` as num_attributes -- Parse `--sets` as functional dependencies (format: `lhs:rhs;lhs:rhs` where each side is comma-separated indices) -- Parse `--target` as target_subset (comma-separated indices) -- Construct `BoyceCoddNormalFormViolation::new(n, fds, target)` - -Since functional dependencies have a unique input format (pairs of attribute sets), use the `--sets` flag for FDs (semicolon-separated, colon between LHS:RHS) and `--target` for target_subset. Use `--n` for num_attributes. - -### Step 4: Canonical example in model file - -```rust -#[cfg(feature = "example-db")] -pub(crate) fn canonical_model_example_specs() -> Vec { - vec![crate::example_db::specs::ModelExampleSpec { - id: "boyce_codd_normal_form_violation", - build: || { - // YES instance: 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], - ); - // X={2} witnesses violation: closure={2,3}, y=3∈closure, z=0∉closure - crate::example_db::specs::satisfaction_example( - problem, - vec![vec![0, 0, 1, 0, 0, 0]], // config for X={2} - ) - }, - }] -} -``` - -### Step 5: Unit tests in `src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs` - -Link from model file: -```rust -#[cfg(test)] -#[path = "../../unit_tests/models/misc/boyce_codd_normal_form_violation.rs"] -mod tests; -``` - -Test functions (minimum 3): - -1. **`test_bcnf_violation_creation`** — construct instances, verify getters -2. **`test_bcnf_violation_evaluate_yes`** — YES instance from issue: X={2} with closure {2,3}, verify evaluate returns true -3. **`test_bcnf_violation_evaluate_no`** — NO instance from issue (cyclic keys): verify all configs return false (or at least specific ones) -4. **`test_bcnf_violation_evaluate_superkey`** — X is a superkey (closure = A'), evaluate returns false -5. **`test_bcnf_violation_evaluate_trivial`** — X has trivial closure (X⁺ = X), evaluate returns false -6. **`test_bcnf_violation_solver`** — use BruteForce::find_satisfying on YES instance, verify it finds a solution; on NO instance, verify None -7. **`test_bcnf_violation_serialization`** — round-trip JSON serialization - -Also ensure the unit_tests/models/misc/mod.rs includes the new test module. - -### Step 5.5: trait_consistency test - -Add `BoyceCoddNormalFormViolation` to the existing trait consistency tests if a pattern file exists, or verify it passes the `example_db` tests via `make test`. - -## Batch 2: Paper Entry (Step 6) - -### Step 6: Add to `docs/paper/reductions.typ` - -**Prerequisites:** Batch 1 must be complete. Run `make paper` to regenerate exports first. - -1. Add to `display-name` dictionary: -```typst -"BoyceCoddNormalFormViolation": [Boyce-Codd Normal Form Violation], -``` - -2. Add `problem-def` entry (place near other misc/database-theory problems): -```typst -#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 -> Y$ has $X$ as a superkey — that is, $X^+ = A'$. - This problem asks whether the given attribute subset $A'$ violates BCNF, which is NP-complete by reduction from Hitting Set @BeeriB1979. -] -``` - -3. Run `make paper` to verify compilation. From ead280f5b63bbcc817619295e5c2579f25ffffa3 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 06:48:17 +0800 Subject: [PATCH 4/5] fix: address PR #685 review comments --- docs/paper/reductions.typ | 2 +- problemreductions-cli/src/commands/create.rs | 37 ++++++++- problemreductions-cli/tests/cli_tests.rs | 82 +++++++++++++++++++ src/lib.rs | 4 +- .../misc/boyce_codd_normal_form_violation.rs | 5 ++ tests/suites/integration.rs | 12 +++ 6 files changed, 138 insertions(+), 4 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 8d3fe70ee..f8888d0fb 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -1875,7 +1875,7 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS #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' without X$ such that $y in X^+$ but $z in.not X^+$, where $X^+$ is the closure of $X$ under $F$? + *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). ] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index af63fe223..ffda918da 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -120,6 +120,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, @@ -936,10 +950,21 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ); 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(), @@ -2457,7 +2482,7 @@ fn create_random( #[cfg(test)] mod tests { - use super::problem_help_flag_name; + use super::{ensure_attribute_indices_in_range, problem_help_flag_name}; #[test] fn test_problem_help_uses_bound_for_length_bounded_disjoint_paths() { @@ -2479,4 +2504,14 @@ mod tests { "num-paths-required" ); } + + #[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}" + ); + } } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 9c35fd001..781143f55 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -4068,6 +4068,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}" + ); +} + // ---- Timeout tests (H3) ---- #[test] diff --git a/src/lib.rs b/src/lib.rs index 42bc6c927..bf2ef89bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,8 +57,8 @@ pub mod prelude { UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ - BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, + BinPacking, BoyceCoddNormalFormViolation, Factoring, FlowShopScheduling, Knapsack, + LongestCommonSubsequence, MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{ diff --git a/src/models/misc/boyce_codd_normal_form_violation.rs b/src/models/misc/boyce_codd_normal_form_violation.rs index 9aaddc6c9..bd7bf61d5 100644 --- a/src/models/misc/boyce_codd_normal_form_violation.rs +++ b/src/models/misc/boyce_codd_normal_form_violation.rs @@ -76,6 +76,11 @@ impl BoyceCoddNormalFormViolation { /// 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)>, diff --git a/tests/suites/integration.rs b/tests/suites/integration.rs index 49e43f6fa..fe53890ca 100644 --- a/tests/suites/integration.rs +++ b/tests/suites/integration.rs @@ -205,6 +205,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"]); From 004bfc04308020e2ed4858ae936b90d83ed9e6ae Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 01:01:00 +0800 Subject: [PATCH 5/5] fix: update BCNF canonical example spec to new ModelExampleSpec API Co-Authored-By: Claude Opus 4.6 (1M context) --- .../misc/boyce_codd_normal_form_violation.rs | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/models/misc/boyce_codd_normal_form_violation.rs b/src/models/misc/boyce_codd_normal_form_violation.rs index bd7bf61d5..2348e816a 100644 --- a/src/models/misc/boyce_codd_normal_form_violation.rs +++ b/src/models/misc/boyce_codd_normal_form_violation.rs @@ -223,19 +223,18 @@ crate::declare_variants! { pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "boyce_codd_normal_form_violation", - build: || { - 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], - ); - // X={2}: closure={2,3}, y=3 in closure, z=0 not in closure -> violation - crate::example_db::specs::satisfaction_example(problem, vec![vec![0, 0, 1, 0, 0, 0]]) - }, + 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), }] }