Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
"StrongConnectivityAugmentation": [Strong Connectivity Augmentation],
"SubgraphIsomorphism": [Subgraph Isomorphism],
"PartitionIntoTriangles": [Partition Into Triangles],
"PrimeAttributeName": [Prime Attribute Name],
"FlowShopScheduling": [Flow Shop Scheduling],
"StaffScheduling": [Staff Scheduling],
"MultiprocessorScheduling": [Multiprocessor Scheduling],
Expand Down Expand Up @@ -1669,6 +1670,72 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS
]
}

#{
let x = load-model-example("PrimeAttributeName")
let n = x.instance.num_attributes
let deps = x.instance.dependencies
let q = x.instance.query_attribute
let sample = x.samples.at(0)
let key = sample.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i)
let num-sat = x.optimal.len()
// Format a set as {e0, e1, ...} (0-indexed) — for use in text mode
let fmt-set(s) = "${" + s.map(e => str(e)).join(", ") + "}$"
// Format a set for use inside math mode (no $ delimiters)
let fmt-set-math(s) = "{" + s.map(e => str(e)).join(", ") + "}"
[
#problem-def("PrimeAttributeName")[
Given a set $A = {0, 1, ..., #(n - 1)}$ of attribute names, a collection $F$ of functional dependencies on $A$, and a specified attribute $x in A$, determine whether $x$ is a _prime attribute_ for $chevron.l A, F chevron.r$ --- i.e., whether there exists a candidate key $K$ for $chevron.l A, F chevron.r$ such that $x in K$.

A _candidate key_ is a minimal subset $K subset.eq A$ whose closure $K^+_F = A$, where the closure $K^+_F$ is the set of all attributes functionally determined by $K$ under $F$.
][
Classical NP-complete problem from relational database theory (Lucchesi and Osborn, 1978; Garey & Johnson SR28). Prime attributes are central to database normalization: Second Normal Form (2NF) requires that no non-prime attribute is partially dependent on any candidate key, and Third Normal Form (3NF) requires that for every non-trivial functional dependency $X arrow Y$, either $X$ is a superkey or $Y$ consists only of prime attributes. The brute-force approach enumerates all $2^n$ subsets of $A$ containing $x$, checking each for the key property; no algorithm significantly improving on this is known for the general problem.

*Example.* Let $A = {0, 1, ..., #(n - 1)}$ ($n = #n$), query attribute $x = #q$, and $F = {#deps.enumerate().map(((i, d)) => $#fmt-set-math(d.at(0)) arrow #fmt-set-math(d.at(1))$).join(", ")}$. The subset $K = #fmt-set-math(key)$ is a candidate key containing $x = #q$: its closure is $K^+_F = A$ (since $#fmt-set-math(key.sorted()) arrow #fmt-set-math(deps.at(1).at(1))$ by the second FD, yielding all of $A$), and removing either element breaks the superkey property (${#(key.at(0))} arrow.r.not A$ and ${#(key.at(1))} arrow.r.not A$), so $K$ is minimal. Thus attribute #q is prime. There are #num-sat candidate keys containing attribute #q in total.

#figure(
canvas(length: 1cm, {
import draw: *
// Attribute nodes in two rows
let positions = (
(0, 1.2), // 0: top-left
(1.5, 1.2), // 1: top-center
(3.0, 1.2), // 2: top-right
(0, 0), // 3: bottom-left (query)
(1.5, 0), // 4: bottom-center
(3.0, 0), // 5: bottom-right
)
// Draw attribute nodes
for (k, pos) in positions.enumerate() {
let is-key = key.contains(k)
let is-query = k == q
g-node(pos, name: "a" + str(k), radius: 0.25,
fill: if is-key { graph-colors.at(0) } else if is-query { graph-colors.at(1) } else { white },
label: if is-key or is-query { text(fill: white)[$#k$] } else { [$#k$] })
}
// Draw functional dependencies as grouped arrows
// FD 1: {0,1} -> {2,3,4,5}
let fd-y-offsets = (0.55, -0.55, -1.15)
for (fi, (lhs, rhs)) in deps.enumerate() {
let ly = if fi == 0 { 2.0 } else if fi == 1 { -0.8 } else { 2.5 }
// Compute LHS and RHS centers
let lx = lhs.map(a => positions.at(a).at(0)).sum() / lhs.len()
let rx = rhs.map(a => positions.at(a).at(0)).sum() / rhs.len()
let mid-x = (lx + rx) / 2
// Draw arrow from LHS region to RHS region
let arrow-y = ly
on-layer(1, {
content((mid-x, arrow-y),
text(7pt)[FD#(fi + 1): $#fmt-set-math(lhs) arrow #fmt-set-math(rhs)$],
fill: white, frame: "rect", padding: 0.06, stroke: none)
})
}
}),
caption: [Prime Attribute Name instance with $n = #n$ attributes. Candidate key $K = #fmt-set-math(key)$ is highlighted in blue; query attribute $x = #q$ is a member of $K$. The three functional dependencies determine the closure of every subset.],
) <fig:prime-attribute-name>
]
]
}

#{
let x = load-model-example("MinimumCardinalityKey")
let n = x.instance.num_attributes
Expand Down
8 changes: 8 additions & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ Flags by problem type:
ComparativeContainment --universe, --r-sets, --s-sets [--r-weights] [--s-weights]
X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each)
SetBasis --universe, --sets, --k
PrimeAttributeName --universe, --deps, --query
MinimumCardinalityKey --num-attributes, --dependencies, --k
BicliqueCover --left, --right, --biedges, --k
BalancedCompleteBipartiteSubgraph --left, --right, --biedges, --k
Expand Down Expand Up @@ -292,6 +293,7 @@ Examples:
pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1
pred create X3C --universe 9 --sets \"0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8\"
pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3
pred create PrimeAttributeName --universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3
pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2")]
pub struct CreateArgs {
/// Problem type (e.g., MIS, QUBO, SAT). Omit when using --example.
Expand Down Expand Up @@ -507,6 +509,12 @@ pub struct CreateArgs {
/// Alphabet size for LCS, SCS, or StringToStringCorrection (optional; inferred from the input strings if omitted)
#[arg(long)]
pub alphabet_size: Option<usize>,
/// Functional dependencies (semicolon-separated, each dep is lhs>rhs with comma-separated indices, e.g., "0,1>2,3;2,3>0,1")
#[arg(long)]
pub deps: Option<String>,
/// Query attribute index for PrimeAttributeName
#[arg(long)]
pub query: Option<usize>,
/// Number of groups for SumOfSquaresPartition
#[arg(long)]
pub num_groups: Option<usize>,
Expand Down
107 changes: 107 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.sink_2.is_none()
&& args.requirement_1.is_none()
&& args.requirement_2.is_none()
&& args.deps.is_none()
&& args.query.is_none()
}

fn emit_problem_output(output: &ProblemJsonOutput, out: &OutputConfig) -> Result<()> {
Expand Down Expand Up @@ -339,6 +341,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"--universe 4 --r-sets \"0,1,2,3;0,1\" --s-sets \"0,1,2,3;2,3\" --r-weights 2,5 --s-weights 3,6"
}
"SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3",
"PrimeAttributeName" => {
"--universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3"
}
"LongestCommonSubsequence" => {
"--strings \"010110;100101;001011\" --bound 3 --alphabet-size 2"
}
Expand All @@ -365,6 +370,9 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String {
match (canonical, field_name) {
("BoundedComponentSpanningForest", "max_components") => return "k".to_string(),
("BoundedComponentSpanningForest", "max_weight") => return "bound".to_string(),
("PrimeAttributeName", "num_attributes") => return "universe".to_string(),
("PrimeAttributeName", "dependencies") => return "deps".to_string(),
("PrimeAttributeName", "query_attribute") => return "query".to_string(),
("MinimumCardinalityKey", "bound_k") => return "k".to_string(),
("StaffScheduling", "shifts_per_schedule") => return "k".to_string(),
_ => {}
Expand Down Expand Up @@ -417,6 +425,9 @@ fn help_flag_hint(
) -> &'static str {
match (canonical, field_name) {
("BoundedComponentSpanningForest", "max_weight") => "integer",
("PrimeAttributeName", "dependencies") => {
"semicolon-separated dependencies: \"0,1>2,3;2,3>0,1\""
}
("LongestCommonSubsequence", "strings") => {
"raw strings: \"ABAC;BACA\" or symbol lists: \"0,1,0;1,0,1\""
}
Expand Down Expand Up @@ -2056,6 +2067,52 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// PrimeAttributeName
"PrimeAttributeName" => {
let universe = args.universe.ok_or_else(|| {
anyhow::anyhow!(
"PrimeAttributeName requires --universe, --deps, and --query\n\n\
Usage: pred create PrimeAttributeName --universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3"
)
})?;
let deps_str = args.deps.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"PrimeAttributeName requires --deps\n\n\
Usage: pred create PrimeAttributeName --universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3"
)
})?;
let query = args.query.ok_or_else(|| {
anyhow::anyhow!(
"PrimeAttributeName requires --query\n\n\
Usage: pred create PrimeAttributeName --universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3"
)
})?;
let dependencies = parse_deps(deps_str)?;
for (i, (lhs, rhs)) in dependencies.iter().enumerate() {
for &attr in lhs.iter().chain(rhs.iter()) {
if attr >= universe {
bail!(
"Dependency {} references attribute {} outside universe of size {}",
i,
attr,
universe
);
}
}
}
if query >= universe {
bail!(
"Query attribute {} is outside universe of size {}",
query,
universe
);
}
(
ser(PrimeAttributeName::new(universe, dependencies, query))?,
resolved_variant.clone(),
)
}

// SequencingWithReleaseTimesAndDeadlines
"SequencingWithReleaseTimesAndDeadlines" => {
let lengths_str = args.lengths.as_deref().ok_or_else(|| {
Expand Down Expand Up @@ -2633,6 +2690,33 @@ fn parse_named_sets(sets_str: Option<&str>, flag: &str) -> Result<Vec<Vec<usize>
.collect()
}

/// Parse a dependency string as semicolon-separated `lhs>rhs` pairs.
/// E.g., "0,1>2,3;2,3>0,1"
fn parse_deps(s: &str) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
s.split(';')
.map(|dep| {
let parts: Vec<&str> = dep.split('>').collect();
if parts.len() != 2 {
bail!("Invalid dependency format '{}': expected 'lhs>rhs'", dep);
}
let lhs = parse_index_list(parts[0])?;
let rhs = parse_index_list(parts[1])?;
Ok((lhs, rhs))
})
.collect()
}

/// Parse a comma-separated list of usize indices.
fn parse_index_list(s: &str) -> Result<Vec<usize>> {
s.split(',')
.map(|x| {
x.trim()
.parse::<usize>()
.map_err(|e| anyhow::anyhow!("Invalid index '{}': {}", x.trim(), e))
})
.collect()
}

/// Parse `--dependencies` as semicolon-separated "lhs>rhs" pairs.
/// E.g., "0,1>2;0,2>3;1,3>4;2,4>5" means {0,1}->{2}, {0,2}->{3}, etc.
fn parse_dependencies(input: &str) -> Result<Vec<(Vec<usize>, Vec<usize>)>> {
Expand Down Expand Up @@ -3432,6 +3516,27 @@ mod tests {
);
}

#[test]
fn test_problem_help_uses_prime_attribute_name_cli_overrides() {
assert_eq!(
problem_help_flag_name("PrimeAttributeName", "num_attributes", "usize", false),
"universe"
);
assert_eq!(
problem_help_flag_name(
"PrimeAttributeName",
"dependencies",
"Vec<(Vec<usize>, Vec<usize>)>",
false,
),
"deps"
);
assert_eq!(
problem_help_flag_name("PrimeAttributeName", "query_attribute", "usize", false),
"query"
);
}

#[test]
fn test_problem_help_uses_problem_specific_lcs_strings_hint() {
assert_eq!(
Expand Down Expand Up @@ -3643,6 +3748,8 @@ mod tests {
deadline: None,
num_processors: None,
alphabet_size: None,
deps: None,
query: None,
dependencies: None,
num_attributes: None,
source_string: None,
Expand Down
34 changes: 34 additions & 0 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2860,6 +2860,40 @@ fn test_create_set_basis_no_flags_uses_actual_cli_flag_names() {
);
}

#[test]
fn test_create_prime_attribute_name_no_flags_uses_actual_cli_flag_names() {
let output = pred()
.args(["create", "PrimeAttributeName"])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("--universe"),
"expected '--universe' in help output, got: {stderr}"
);
assert!(
stderr.contains("--deps"),
"expected '--deps' in help output, got: {stderr}"
);
assert!(
stderr.contains("--query"),
"expected '--query' in help output, got: {stderr}"
);
assert!(
!stderr.contains("--num-attributes"),
"help should not advertise schema field names: {stderr}"
);
assert!(
!stderr.contains("--dependencies"),
"help should not advertise schema field names: {stderr}"
);
assert!(
!stderr.contains("--query-attribute"),
"help should not advertise schema field names: {stderr}"
);
}

#[test]
fn test_create_lcs_with_raw_strings_infers_alphabet() {
let output = pred()
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub mod prelude {
};
pub use crate::models::set::{
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
MinimumCardinalityKey, MinimumSetCovering, SetBasis,
MinimumCardinalityKey, MinimumSetCovering, PrimeAttributeName, SetBasis,
};

// Core traits
Expand Down
2 changes: 1 addition & 1 deletion src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ pub use misc::{
};
pub use set::{
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,
MinimumCardinalityKey, MinimumSetCovering, SetBasis,
MinimumCardinalityKey, MinimumSetCovering, PrimeAttributeName, SetBasis,
};
4 changes: 4 additions & 0 deletions src/models/set/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
//! - [`ComparativeContainment`]: Compare containment-weight sums for two set families
//! - [`MaximumSetPacking`]: Maximum weight set packing
//! - [`MinimumSetCovering`]: Minimum weight set cover
//! - [`PrimeAttributeName`]: Determine if an attribute belongs to any candidate key

pub(crate) mod comparative_containment;
pub(crate) mod consecutive_sets;
pub(crate) mod exact_cover_by_3_sets;
pub(crate) mod maximum_set_packing;
pub(crate) mod minimum_cardinality_key;
pub(crate) mod minimum_set_covering;
pub(crate) mod prime_attribute_name;
pub(crate) mod set_basis;

pub use comparative_containment::ComparativeContainment;
Expand All @@ -21,6 +23,7 @@ pub use exact_cover_by_3_sets::ExactCoverBy3Sets;
pub use maximum_set_packing::MaximumSetPacking;
pub use minimum_cardinality_key::MinimumCardinalityKey;
pub use minimum_set_covering::MinimumSetCovering;
pub use prime_attribute_name::PrimeAttributeName;
pub use set_basis::SetBasis;

#[cfg(feature = "example-db")]
Expand All @@ -32,6 +35,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(maximum_set_packing::canonical_model_example_specs());
specs.extend(minimum_set_covering::canonical_model_example_specs());
specs.extend(minimum_cardinality_key::canonical_model_example_specs());
specs.extend(prime_attribute_name::canonical_model_example_specs());
specs.extend(set_basis::canonical_model_example_specs());
specs
}
Loading
Loading