diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 01372f7cb..4a8420ada 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -62,6 +62,7 @@ // Problem display names for theorem headers #let display-name = ( + "AdditionalKey": [Additional Key], "MaximumIndependentSet": [Maximum Independent Set], "MinimumVertexCover": [Minimum Vertex Cover], "MaxCut": [Max-Cut], @@ -2326,6 +2327,28 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ) ] +#problem-def("AdditionalKey")[ + Given a set $A$ of attribute names, a collection $F$ of functional dependencies on $A$, + a subset $R subset.eq A$, and a set $K$ of candidate keys for the relational scheme $chevron.l R, F chevron.r$, + determine whether there exists a subset $R' subset.eq R$ such that $R' in.not K$, + the closure $R'^+$ under $F$ equals $R$, and no proper subset of $R'$ also has this property. +][ + A classical NP-complete problem from relational database theory @beeri1979. + Enumerating all candidate keys is necessary to verify Boyce-Codd Normal Form (BCNF), + and the NP-completeness of Additional Key implies that BCNF testing is intractable in general. + The best known exact algorithm is brute-force enumeration of all $2^(|R|)$ subsets, + checking each for the key property via closure computation under Armstrong's axioms. + #footnote[No algorithm improving on brute-force is known for the Additional Key problem.] + + *Example.* Consider attribute set $A = {0, 1, 2, 3, 4, 5}$ with functional dependencies + $F = {{0,1} -> {2,3}, {2,3} -> {4,5}, {4,5} -> {0,1}, {0,2} -> {3}, {3,5} -> {1}}$, + relation $R = A$, and known keys $K = {{0,1}, {2,3}, {4,5}}$. + The subset ${0,2}$ is an additional key: starting from ${0,2}$, we apply ${0,2} -> {3}$ + to get ${0,2,3}$, then ${2,3} -> {4,5}$ to get ${0,2,3,4,5}$, then ${4,5} -> {0,1}$ + to reach $R^+ = A$. The set ${0,2}$ is minimal (neither ${0}$ nor ${2}$ alone determines $A$) + and ${0,2} in.not K$, so the answer is YES. +] + // Completeness check: warn about problem types in JSON but missing from paper #{ let json-models = { diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 6482b48a2..c20920188 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -104,6 +104,17 @@ @article{lucas2014 year = {2014} } +@article{beeri1979, + author = {Catriel Beeri and Philip A. Bernstein}, + title = {Computational Problems Related to the Design of Normal Form Relational Schemas}, + journal = {ACM Transactions on Database Systems}, + volume = {4}, + number = {1}, + pages = {30--59}, + year = {1979}, + doi = {10.1145/320064.320066} +} + @article{barahona1982, author = {Francisco Barahona}, title = {On the computational complexity of Ising spin glass models}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index e71a9e426..c583412e7 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -243,6 +243,7 @@ Flags by problem type: OptimalLinearArrangement --graph, --bound RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound MultipleChoiceBranching --arcs [--weights] --partition --bound [--num-vertices] + AdditionalKey --num-attributes, --dependencies, --relation-attrs, --known-keys SubgraphIsomorphism --graph (host), --pattern (pattern) LCS --strings FAS --arcs [--weights] [--num-vertices] @@ -456,6 +457,18 @@ pub struct CreateArgs { /// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted) #[arg(long)] pub alphabet_size: Option, + /// Number of attributes for AdditionalKey + #[arg(long)] + pub num_attributes: Option, + /// Functional dependencies for AdditionalKey (e.g., "0,1:2,3;2,3:4,5") + #[arg(long)] + pub dependencies: Option, + /// Relation scheme attributes for AdditionalKey (comma-separated, e.g., "0,1,2,3,4,5") + #[arg(long)] + pub relation_attrs: Option, + /// Known candidate keys for AdditionalKey (e.g., "0,1;2,3") + #[arg(long)] + pub known_keys: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 03220f4f6..ceb68eb22 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, + AdditionalKey, BinPacking, FlowShopScheduling, LongestCommonSubsequence, + MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, + SubsetSum, }; use problemreductions::prelude::*; use problemreductions::registry::collect_schemas; @@ -90,6 +91,10 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.sink_2.is_none() && args.requirement_1.is_none() && args.requirement_2.is_none() + && args.num_attributes.is_none() + && args.dependencies.is_none() + && args.relation_attrs.is_none() + && args.known_keys.is_none() } fn emit_problem_output(output: &ProblemJsonOutput, out: &OutputConfig) -> Result<()> { @@ -292,6 +297,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "MultipleChoiceBranching" => { "--arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10" } + "AdditionalKey" => "--num-attributes 6 --dependencies \"0,1:2,3;2,3:4,5;4,5:0,1\" --relation-attrs 0,1,2,3,4,5 --known-keys \"0,1;2,3;4,5\"", "SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1", "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", "SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3", @@ -932,6 +938,51 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { } } + // AdditionalKey + "AdditionalKey" => { + let usage = "Usage: pred create AdditionalKey --num-attributes 6 --dependencies \"0,1:2,3;2,3:4,5\" --relation-attrs \"0,1,2,3,4,5\" --known-keys \"0,1;2,3\""; + let num_attributes = args.num_attributes.ok_or_else(|| { + anyhow::anyhow!("AdditionalKey requires --num-attributes\n\n{usage}") + })?; + let deps_str = args.dependencies.as_deref().ok_or_else(|| { + anyhow::anyhow!("AdditionalKey requires --dependencies\n\n{usage}") + })?; + let ra_str = args.relation_attrs.as_deref().ok_or_else(|| { + anyhow::anyhow!("AdditionalKey requires --relation-attrs\n\n{usage}") + })?; + let dependencies: Vec<(Vec, Vec)> = deps_str + .split(';') + .map(|dep| { + let parts: Vec<&str> = dep.trim().split(':').collect(); + anyhow::ensure!( + parts.len() == 2, + "Invalid dependency format '{}', expected 'lhs:rhs' (e.g., '0,1:2,3')", + dep.trim() + ); + let lhs: Vec = util::parse_comma_list(parts[0].trim())?; + let rhs: Vec = util::parse_comma_list(parts[1].trim())?; + Ok((lhs, rhs)) + }) + .collect::>>()?; + let relation_attrs: Vec = util::parse_comma_list(ra_str)?; + let known_keys: Vec> = match args.known_keys.as_deref() { + Some(s) if !s.is_empty() => s + .split(';') + .map(|k| util::parse_comma_list(k.trim())) + .collect::>>()?, + _ => vec![], + }; + ( + ser(AdditionalKey::new( + num_attributes, + dependencies, + relation_attrs, + known_keys, + ))?, + resolved_variant.clone(), + ) + } + // SubsetSum "SubsetSum" => { 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..fe36a1947 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -1,5 +1,6 @@ { "models": [ + {"problem":"AdditionalKey","variant":{},"instance":{"dependencies":[[[0,1],[2,3]],[[2,3],[4,5]],[[4,5],[0,1]],[[0,2],[3]],[[3,5],[1]]],"known_keys":[[0,1],[2,3],[4,5]],"num_attributes":6,"relation_attrs":[0,1,2,3,4,5]},"samples":[{"config":[1,0,1,0,0,0],"metric":true}],"optimal":[{"config":[1,0,0,1,0,1],"metric":true},{"config":[1,0,1,0,0,0],"metric":true}]}, {"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}]}, diff --git a/src/lib.rs b/src/lib.rs index 42bc6c927..1167e9b85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -57,9 +57,9 @@ pub mod prelude { UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ - BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, - MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, - ShortestCommonSupersequence, SubsetSum, + AdditionalKey, BinPacking, Factoring, FlowShopScheduling, Knapsack, + LongestCommonSubsequence, MinimumTardinessSequencing, PaintShop, + SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{ ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, diff --git a/src/models/misc/additional_key.rs b/src/models/misc/additional_key.rs new file mode 100644 index 000000000..248825173 --- /dev/null +++ b/src/models/misc/additional_key.rs @@ -0,0 +1,287 @@ +//! Additional Key problem implementation. +//! +//! Given a relational schema (R, F) and a set K of known candidate keys, +//! determine whether there exists a candidate key of R (under the functional +//! dependencies F) that is not in K. A candidate key is a minimal set of +//! attributes whose closure under F covers all of R. +//! +//! The problem is NP-complete (Garey & Johnson, SR7). + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "AdditionalKey", + display_name: "Additional Key", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Determine whether a relational schema has a candidate key not in a given set", + fields: &[ + FieldInfo { name: "num_attributes", type_name: "usize", description: "Number of attributes in A" }, + FieldInfo { name: "dependencies", type_name: "Vec<(Vec, Vec)>", description: "Functional dependencies F; each (lhs, rhs)" }, + FieldInfo { name: "relation_attrs", type_name: "Vec", description: "Relation scheme attributes R ⊆ A" }, + FieldInfo { name: "known_keys", type_name: "Vec>", description: "Known candidate keys K" }, + ], + } +} + +/// The Additional Key problem. +/// +/// Given a set `A` of attributes, a set of functional dependencies `F` over `A`, +/// a relation scheme `R ⊆ A`, and a set `K` of known candidate keys of `R` +/// under `F`, determine whether `R` has a candidate key not in `K`. +/// +/// A **candidate key** is a minimal subset `X ⊆ R` such that the closure of `X` +/// under `F` contains all attributes of `R`. +/// +/// # Representation +/// +/// Each attribute in `R` has a binary variable: `x_i = 1` if the attribute is +/// selected, `0` otherwise. A configuration is satisfying iff the selected +/// attributes form a candidate key of `R` that is not in `K`. +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::misc::AdditionalKey; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// let problem = AdditionalKey::new( +/// 3, +/// vec![(vec![0], vec![1, 2])], +/// vec![0, 1, 2], +/// vec![], +/// ); +/// let solver = BruteForce::new(); +/// let solution = solver.find_satisfying(&problem); +/// assert!(solution.is_some()); +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AdditionalKey { + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, + relation_attrs: Vec, + known_keys: Vec>, +} + +impl AdditionalKey { + /// Create a new AdditionalKey instance. + /// + /// # Panics + /// + /// Panics if any attribute index is >= `num_attributes`, or if + /// `relation_attrs` contains duplicates. + pub fn new( + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, + relation_attrs: Vec, + known_keys: Vec>, + ) -> Self { + // Validate all attribute indices + for &a in &relation_attrs { + assert!( + a < num_attributes, + "relation_attrs element {a} >= num_attributes {num_attributes}" + ); + } + // Validate relation_attrs uniqueness + let mut sorted_ra = relation_attrs.clone(); + sorted_ra.sort_unstable(); + sorted_ra.dedup(); + assert_eq!( + sorted_ra.len(), + relation_attrs.len(), + "relation_attrs contains duplicates" + ); + for (lhs, rhs) in &dependencies { + for &a in lhs { + assert!( + a < num_attributes, + "dependency lhs attribute {a} >= num_attributes {num_attributes}" + ); + } + for &a in rhs { + assert!( + a < num_attributes, + "dependency rhs attribute {a} >= num_attributes {num_attributes}" + ); + } + } + for key in &known_keys { + for &a in key { + assert!( + a < num_attributes, + "known_keys attribute {a} >= num_attributes {num_attributes}" + ); + } + } + // Sort known_keys entries internally for consistent comparison + let known_keys: Vec> = known_keys + .into_iter() + .map(|mut k| { + k.sort_unstable(); + k + }) + .collect(); + Self { + num_attributes, + dependencies, + relation_attrs, + known_keys, + } + } + + /// Returns the number of attributes in the universal set A. + pub fn num_attributes(&self) -> usize { + self.num_attributes + } + + /// Returns the number of functional dependencies. + pub fn num_dependencies(&self) -> usize { + self.dependencies.len() + } + + /// Returns the number of attributes in the relation scheme R. + pub fn num_relation_attrs(&self) -> usize { + self.relation_attrs.len() + } + + /// Returns the number of known candidate keys. + pub fn num_known_keys(&self) -> usize { + self.known_keys.len() + } + + /// Returns the functional dependencies. + pub fn dependencies(&self) -> &[(Vec, Vec)] { + &self.dependencies + } + + /// Returns the relation scheme attributes. + pub fn relation_attrs(&self) -> &[usize] { + &self.relation_attrs + } + + /// Returns the known candidate keys. + pub fn known_keys(&self) -> &[Vec] { + &self.known_keys + } + + /// Compute the closure of a set of attributes under the functional dependencies. + fn compute_closure(&self, attrs: &[bool]) -> Vec { + let mut closure = attrs.to_vec(); + let mut changed = true; + while changed { + changed = false; + for (lhs, rhs) in &self.dependencies { + if lhs.iter().all(|&a| closure[a]) { + for &a in rhs { + if !closure[a] { + closure[a] = true; + changed = true; + } + } + } + } + } + closure + } +} + +impl Problem for AdditionalKey { + const NAME: &'static str = "AdditionalKey"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2; self.relation_attrs.len()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + // Check config length + if config.len() != self.relation_attrs.len() { + return false; + } + // Check all values are 0 or 1 + if config.iter().any(|&v| v >= 2) { + return false; + } + + // Build selected attribute set + let selected: Vec = config + .iter() + .enumerate() + .filter(|(_, &v)| v == 1) + .map(|(i, _)| self.relation_attrs[i]) + .collect(); + + // Empty selection is not a key + if selected.is_empty() { + return false; + } + + // Compute closure of selected attributes + let mut attr_set = vec![false; self.num_attributes]; + for &a in &selected { + attr_set[a] = true; + } + let closure = self.compute_closure(&attr_set); + + // Check closure covers all relation_attrs + if !self.relation_attrs.iter().all(|&a| closure[a]) { + return false; + } + + // Check minimality: removing any single selected attribute should break coverage + for &a in &selected { + let mut reduced = attr_set.clone(); + reduced[a] = false; + let reduced_closure = self.compute_closure(&reduced); + if self.relation_attrs.iter().all(|&ra| reduced_closure[ra]) { + return false; // Not minimal + } + } + + // Build sorted selected vec and check it's not in known_keys + let mut sorted_selected = selected; + sorted_selected.sort_unstable(); + !self.known_keys.contains(&sorted_selected) + } +} + +impl SatisfactionProblem for AdditionalKey {} + +crate::declare_variants! { + default sat AdditionalKey => "2^num_relation_attrs * num_dependencies * num_attributes", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "additional_key", + build: || { + let problem = AdditionalKey::new( + 6, + vec![ + (vec![0, 1], vec![2, 3]), + (vec![2, 3], vec![4, 5]), + (vec![4, 5], vec![0, 1]), + (vec![0, 2], vec![3]), + (vec![3, 5], vec![1]), + ], + vec![0, 1, 2, 3, 4, 5], + vec![vec![0, 1], vec![2, 3], vec![4, 5]], + ); + crate::example_db::specs::satisfaction_example(problem, vec![vec![1, 0, 1, 0, 0, 0]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/additional_key.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index c4b125274..9052484c2 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -1,6 +1,7 @@ //! Miscellaneous problems. //! //! Problems with unique input structures that don't fit other categories: +//! - [`AdditionalKey`]: Determine whether a relational schema has an additional candidate key //! - [`BinPacking`]: Bin Packing (minimize bins) //! - [`Factoring`]: Integer factorization //! - [`FlowShopScheduling`]: Flow Shop Scheduling (meet deadline on m processors) @@ -12,6 +13,7 @@ //! - [`ShortestCommonSupersequence`]: Find a common supersequence of bounded length //! - [`SubsetSum`]: Find a subset summing to exactly a target value +pub(crate) mod additional_key; mod bin_packing; pub(crate) mod factoring; mod flow_shop_scheduling; @@ -23,6 +25,7 @@ mod sequencing_within_intervals; pub(crate) mod shortest_common_supersequence; mod subset_sum; +pub use additional_key::AdditionalKey; pub use bin_packing::BinPacking; pub use factoring::Factoring; pub use flow_shop_scheduling::FlowShopScheduling; @@ -42,5 +45,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec AdditionalKey { + AdditionalKey::new( + 6, + vec![ + (vec![0, 1], vec![2, 3]), + (vec![2, 3], vec![4, 5]), + (vec![4, 5], vec![0, 1]), + (vec![0, 2], vec![3]), + (vec![3, 5], vec![1]), + ], + vec![0, 1, 2, 3, 4, 5], + vec![vec![0, 1], vec![2, 3], vec![4, 5]], + ) +} + +/// Instance 2: 3 attributes, single FD {0}->{1,2}, known key [{0}]. +fn instance2() -> AdditionalKey { + AdditionalKey::new(3, vec![(vec![0], vec![1, 2])], vec![0, 1, 2], vec![vec![0]]) +} + +#[test] +fn test_additional_key_creation() { + let problem = instance1(); + assert_eq!(problem.num_attributes(), 6); + assert_eq!(problem.num_dependencies(), 5); + assert_eq!(problem.num_relation_attrs(), 6); + assert_eq!(problem.num_known_keys(), 3); + assert_eq!(problem.dims(), vec![2, 2, 2, 2, 2, 2]); + assert_eq!(::NAME, "AdditionalKey"); + assert_eq!(::variant(), vec![]); +} + +#[test] +fn test_additional_key_evaluate_satisfying() { + let problem = instance1(); + // Config [1,0,1,0,0,0] selects attrs {0,2}. + // Closure of {0,2}: {0,2} -> apply (0,2)->3 => {0,2,3} -> apply (2,3)->(4,5) => {0,2,3,4,5} + // -> apply (4,5)->(0,1) => {0,1,2,3,4,5}. Covers all. + // Minimality: remove 0 => {2}, closure of {2} = {2} => does not cover all. OK. + // remove 2 => {0}, closure of {0} = {0} => does not cover all. OK. + // {0,2} sorted is [0,2], not in known_keys [{0,1},{2,3},{4,5}]. + assert!(problem.evaluate(&[1, 0, 1, 0, 0, 0])); +} + +#[test] +fn test_additional_key_evaluate_known_key() { + let problem = instance1(); + // Config [1,1,0,0,0,0] selects attrs {0,1} which IS in known_keys. + assert!(!problem.evaluate(&[1, 1, 0, 0, 0, 0])); +} + +#[test] +fn test_additional_key_evaluate_not_a_key() { + let problem = instance1(); + // Config [0,0,0,0,0,1] selects {5}. Closure of {5} = {5}. + // Does not cover all attrs. + assert!(!problem.evaluate(&[0, 0, 0, 0, 0, 1])); +} + +#[test] +fn test_additional_key_evaluate_non_minimal() { + let problem = instance1(); + // Config [1,1,1,0,0,0] selects {0,1,2}. + // {0,1} alone determines all attrs (known key), so {0,1,2} is NOT minimal. + assert!(!problem.evaluate(&[1, 1, 1, 0, 0, 0])); +} + +#[test] +fn test_additional_key_no_additional_key() { + let problem = instance2(); + // Only candidate key is {0}, which is already known. + let solver = BruteForce::new(); + let solution = solver.find_satisfying(&problem); + assert!(solution.is_none()); +} + +#[test] +fn test_additional_key_wrong_config_length() { + let problem = instance1(); + assert!(!problem.evaluate(&[1, 0])); + assert!(!problem.evaluate(&[1, 0, 0, 0, 0, 0, 0])); +} + +#[test] +fn test_additional_key_invalid_variable_value() { + let problem = instance1(); + assert!(!problem.evaluate(&[2, 0, 0, 0, 0, 0])); +} + +#[test] +fn test_additional_key_brute_force() { + let problem = instance1(); + let solver = BruteForce::new(); + let solution = solver + .find_satisfying(&problem) + .expect("should find a solution"); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_additional_key_brute_force_all() { + let problem = instance1(); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_additional_key_serialization() { + let problem = instance1(); + let json = serde_json::to_value(&problem).unwrap(); + let restored: AdditionalKey = serde_json::from_value(json.clone()).unwrap(); + assert_eq!(restored.num_attributes(), problem.num_attributes()); + assert_eq!(restored.num_dependencies(), problem.num_dependencies()); + assert_eq!(restored.num_relation_attrs(), problem.num_relation_attrs()); + assert_eq!(restored.num_known_keys(), problem.num_known_keys()); + // Verify round-trip produces same evaluation + assert_eq!( + problem.evaluate(&[1, 0, 1, 0, 0, 0]), + restored.evaluate(&[1, 0, 1, 0, 0, 0]) + ); +} + +#[test] +fn test_additional_key_paper_example() { + let problem = instance1(); + // Verify {0,2} is a valid additional key + assert!(problem.evaluate(&[1, 0, 1, 0, 0, 0])); + // Count all satisfying solutions + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_additional_key_empty_selection() { + let problem = instance1(); + // All zeros = no attributes selected = not a key + assert!(!problem.evaluate(&[0, 0, 0, 0, 0, 0])); +}