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
24 changes: 24 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
"MinimumSetCovering": [Minimum Set Covering],
"ComparativeContainment": [Comparative Containment],
"SetBasis": [Set Basis],
"MinimumCardinalityKey": [Minimum Cardinality Key],
"SpinGlass": [Spin Glass],
"QUBO": [QUBO],
"ILP": [Integer Linear Programming],
Expand Down Expand Up @@ -1518,6 +1519,29 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS
]
}

#{
let x = load-model-example("MinimumCardinalityKey")
let n = x.instance.num_attributes
let deps = x.instance.dependencies
let m = deps.len()
let bound = x.instance.bound_k
let sample = x.samples.at(0)
let key-attrs = range(n).filter(i => sample.config.at(i) == 1)
let sat-count = x.optimal.len()
let fmt-set(s) = "${" + s.map(e => str(e)).join(", ") + "}$"
let fmt-fd(d) = fmt-set(d.at(0)) + " $arrow.r$ " + fmt-set(d.at(1))
[
#problem-def("MinimumCardinalityKey")[
Given a set $A$ of attribute names, a collection $F$ of functional dependencies (ordered pairs of subsets of $A$), and a positive integer $M$, does there exist a candidate key $K subset.eq A$ with $|K| <= M$, i.e., a minimal subset $K$ such that the closure of $K$ under $F^*$ equals $A$?
][
The Minimum Cardinality Key problem arises in relational database theory, where identifying the smallest candidate key determines the most efficient way to uniquely identify rows in a relation. It was shown NP-complete by Lucchesi and Osborn (1978) @lucchesi1978keys via transformation from Vertex Cover. The problem appears as SR26 in Garey & Johnson (A4) @garey1979. The closure $F^*$ is defined by Armstrong's axioms: reflexivity ($B subset.eq C$ implies $C arrow.r B$), transitivity, and union. The best known exact algorithm is brute-force enumeration of all subsets of $A$, giving $O^*(2^(|A|))$ time#footnote[Lucchesi and Osborn give an output-polynomial algorithm for enumerating all candidate keys, but the number of keys can be exponential.].

*Example.* Let $A = {0, 1, ..., #(n - 1)}$ ($|A| = #n$) with $M = #bound$ and functional dependencies $F = {#deps.enumerate().map(((i, d)) => fmt-fd(d)).join(", ")}$.
The candidate key $K = #fmt-set(key-attrs)$ has $|K| = #key-attrs.len() <= #bound$. Its closure: start with ${0, 1}$; apply ${0, 1} arrow.r {2}$ to get ${0, 1, 2}$; apply ${0, 2} arrow.r {3}$ to get ${0, 1, 2, 3}$; apply ${1, 3} arrow.r {4}$ to get ${0, 1, 2, 3, 4}$; apply ${2, 4} arrow.r {5}$ to get $A$. Neither ${0}$ nor ${1}$ alone determines $A$, so $K$ is minimal. There are #sat-count satisfying encodings in total.
]
]
}

== Optimization Problems

#{
Expand Down
11 changes: 11 additions & 0 deletions docs/paper/references.bib
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,17 @@ @article{lucchesi1978
doi = {10.1112/jlms/s2-17.3.369}
}

@article{lucchesi1978keys,
author = {Cl\'audio L. Lucchesi and Sylvia L. Osborn},
title = {Candidate Keys for Relations},
journal = {Journal of Computer and System Sciences},
volume = {17},
number = {2},
pages = {270--279},
year = {1978},
doi = {10.1016/0022-0000(78)90009-0}
}

@article{lenstra1976,
author = {Jan Karel Lenstra and Alexander H. G. Rinnooy Kan},
title = {On General Routing Problems},
Expand Down
10 changes: 9 additions & 1 deletion docs/src/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,7 @@ pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 -
pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json
pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json
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" -o x3c.json
pred create MinimumCardinalityKey --num-attributes 6 --dependencies "0,1>2;0,2>3;1,3>4;2,4>5" --k 2 -o mck.json
pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 --precedence-pairs "0>3,1>3,1>4,2>4" -o mts.json
pred create StringToStringCorrection --source-string "0,1,2,3,1,0" --target-string "0,1,3,2,1" --bound 2 | pred solve - --solver brute-force
pred create StrongConnectivityAugmentation --arcs "0>1,1>2,2>0,3>4,4>3,2>3,4>5,5>3" --candidate-arcs "3>0:5,3>1:3,3>2:4,4>0:6,4>1:2,4>2:7,5>0:4,5>1:3,5>2:1,0>3:8,0>4:3,0>5:2,1>3:6,1>4:4,1>5:5,2>4:3,2>5:7,1>0:2" --bound 1 -o sca.json
Expand Down Expand Up @@ -519,10 +520,17 @@ Source evaluation: Valid(2)

> **Note:** The ILP solver requires a reduction path from the target problem to ILP.
> Some problems do not currently have one. Examples include BoundedComponentSpanningForest,
> LengthBoundedDisjointPaths, QUBO, SpinGlass, MaxCut, CircuitSAT, and MultiprocessorScheduling.
> LengthBoundedDisjointPaths, MinimumCardinalityKey, QUBO, SpinGlass, MaxCut, CircuitSAT, and MultiprocessorScheduling.
> Use `pred solve <file> --solver brute-force` for these, or reduce to a problem that supports ILP first.
> For other problems, use `pred path <PROBLEM> ILP` to check whether an ILP reduction path exists.

For example, the canonical Minimum Cardinality Key instance can be created and solved with:

```bash
pred create MinimumCardinalityKey --num-attributes 6 --dependencies "0,1>2;0,2>3;1,3>4;2,4>5" --k 2 -o mck.json
pred solve mck.json --solver brute-force
```

## Shell Completions

Enable tab completion by adding one line to your shell config:
Expand Down
10 changes: 9 additions & 1 deletion problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,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
MinimumCardinalityKey --num-attributes, --dependencies, --k
BicliqueCover --left, --right, --biedges, --k
BalancedCompleteBipartiteSubgraph --left, --right, --biedges, --k
BiconnectivityAugmentation --graph, --potential-edges, --budget [--num-vertices]
Expand Down Expand Up @@ -286,7 +287,8 @@ Examples:
pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1
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 SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 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.
#[arg(value_parser = crate::problem_name::ProblemNameParser)]
Expand Down Expand Up @@ -498,6 +500,12 @@ pub struct CreateArgs {
/// Alphabet size for SCS or StringToStringCorrection (optional; inferred from max symbol + 1 if omitted)
#[arg(long)]
pub alphabet_size: Option<usize>,
/// Functional dependencies for MinimumCardinalityKey (semicolon-separated "lhs>rhs" pairs, e.g., "0,1>2;0,2>3")
#[arg(long)]
pub dependencies: Option<String>,
/// Number of attributes for MinimumCardinalityKey
#[arg(long)]
pub num_attributes: Option<usize>,
/// Source string for StringToStringCorrection (comma-separated symbol indices, e.g., "0,1,2,3")
#[arg(long)]
pub source_string: Option<String>,
Expand Down
69 changes: 69 additions & 0 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
&& args.requirements.is_none()
&& args.num_workers.is_none()
&& args.alphabet_size.is_none()
&& args.dependencies.is_none()
&& args.num_attributes.is_none()
&& args.source_string.is_none()
&& args.target_string.is_none()
&& args.capacities.is_none()
Expand Down Expand Up @@ -239,6 +241,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
Some("UnitDiskGraph") => "float positions: \"0.0,0.0;1.0,0.0\"",
_ => "edge list: 0-1,1-2,2-3",
},
"Vec<(Vec<usize>, Vec<usize>)>" => "semicolon-separated dependencies: \"0,1>2;0,2>3\"",
"Vec<u64>" => "comma-separated integers: 4,5,3,2,6",
"Vec<W>" => "comma-separated: 1,2,3",
"Vec<usize>" => "comma-separated indices: 0,2,4",
Expand Down Expand Up @@ -329,6 +332,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",
"MinimumCardinalityKey" => {
"--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2"
}
"ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4",
"StringToStringCorrection" => {
"--source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2"
Expand All @@ -342,6 +348,7 @@ 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(),
("MinimumCardinalityKey", "bound_k") => return "k".to_string(),
("StaffScheduling", "shifts_per_schedule") => return "k".to_string(),
_ => {}
}
Expand Down Expand Up @@ -1223,6 +1230,33 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// MinimumCardinalityKey
"MinimumCardinalityKey" => {
let num_attributes = args.num_attributes.ok_or_else(|| {
anyhow::anyhow!(
"MinimumCardinalityKey requires --num-attributes, --dependencies, and --k\n\n\
Usage: pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2"
)
})?;
let k = args.k.ok_or_else(|| {
anyhow::anyhow!("MinimumCardinalityKey requires --k (bound on key cardinality)")
})?;
let deps_str = args.dependencies.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"MinimumCardinalityKey requires --dependencies (e.g., \"0,1>2;0,2>3\")"
)
})?;
let dependencies = parse_dependencies(deps_str)?;
(
ser(problemreductions::models::set::MinimumCardinalityKey::new(
num_attributes,
dependencies,
k,
))?,
resolved_variant.clone(),
)
}

// BicliqueCover
"BicliqueCover" => {
let usage = "pred create BicliqueCover --left 2 --right 2 --biedges 0-0,0-1,1-1 --k 2";
Expand Down Expand Up @@ -2328,6 +2362,39 @@ fn parse_named_sets(sets_str: Option<&str>, flag: &str) -> Result<Vec<Vec<usize>
.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>)>> {
fn parse_dependency_side(side: &str) -> Result<Vec<usize>> {
if side.trim().is_empty() {
return Ok(vec![]);
}
side.split(',')
.map(|s| {
s.trim()
.parse::<usize>()
.map_err(|e| anyhow::anyhow!("Invalid attribute index: {}", e))
})
.collect()
}

input
.split(';')
.map(|dep| {
let parts: Vec<&str> = dep.trim().split('>').collect();
if parts.len() != 2 {
bail!(
"Invalid dependency format: expected 'lhs>rhs', got '{}'",
dep.trim()
);
}
let lhs = parse_dependency_side(parts[0])?;
let rhs = parse_dependency_side(parts[1])?;
Ok((lhs, rhs))
})
.collect()
}

fn validate_comparative_containment_sets(
family_name: &str,
flag: &str,
Expand Down Expand Up @@ -3240,6 +3307,8 @@ mod tests {
deadline: None,
num_processors: None,
alphabet_size: None,
dependencies: None,
num_attributes: None,
source_string: None,
target_string: None,
schedules: None,
Expand Down
67 changes: 67 additions & 0 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,73 @@ fn test_create_set_basis_rejects_out_of_range_elements() {
assert!(!stderr.contains("panicked at"), "stderr: {stderr}");
}

#[test]
fn test_create_minimum_cardinality_key_problem_help_uses_supported_flags() {
let output = pred()
.args(["create", "MinimumCardinalityKey"])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("--num-attributes"), "stderr: {stderr}");
assert!(stderr.contains("--dependencies"), "stderr: {stderr}");
assert!(stderr.contains("--k"), "stderr: {stderr}");
assert!(
stderr.contains("semicolon-separated dependencies"),
"stderr: {stderr}"
);
assert!(!stderr.contains("--bound-k"), "stderr: {stderr}");
}

#[test]
fn test_create_minimum_cardinality_key_allows_empty_lhs_dependency() {
let output = pred()
.args([
"create",
"MinimumCardinalityKey",
"--num-attributes",
"1",
"--dependencies",
">0",
"--k",
"1",
])
.output()
.unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);

let stdout = String::from_utf8(output.stdout).unwrap();
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert_eq!(json["type"], "MinimumCardinalityKey");
assert_eq!(json["data"]["num_attributes"], 1);
assert_eq!(json["data"]["bound_k"], 1);
assert_eq!(json["data"]["dependencies"][0][0], serde_json::json!([]));
assert_eq!(json["data"]["dependencies"][0][1], serde_json::json!([0]));
}

#[test]
fn test_create_minimum_cardinality_key_missing_num_attributes_message() {
let output = pred()
.args([
"create",
"MinimumCardinalityKey",
"--dependencies",
"0>0",
"--k",
"1",
])
.output()
.unwrap();
assert!(!output.status.success());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("MinimumCardinalityKey requires --num-attributes"));
assert!(!stderr.contains("--num-vertices"), "stderr: {stderr}");
}

#[test]
fn test_create_then_evaluate() {
// Create a problem
Expand Down
1 change: 1 addition & 0 deletions src/example_db/fixtures/examples.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
{"problem":"MaximumIndependentSet","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,0,null],[5,7,null],[7,9,null],[9,6,null],[6,8,null],[8,5,null],[0,5,null],[1,6,null],[2,7,null],[3,8,null],[4,9,null]],"node_holes":[],"nodes":[null,null,null,null,null,null,null,null,null,null]}},"weights":[5,1,1,1,1,3,1,1,1,3]},"samples":[{"config":[1,0,1,0,0,0,0,0,1,1],"metric":{"Valid":10}}],"optimal":[{"config":[1,0,1,0,0,0,0,0,1,1],"metric":{"Valid":10}}]},
{"problem":"MaximumMatching","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,1,1,1,1,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,0,0,1,0],"metric":{"Valid":2}}],"optimal":[{"config":[0,0,1,0,1,0],"metric":{"Valid":2}},{"config":[0,1,0,0,0,1],"metric":{"Valid":2}},{"config":[0,1,1,0,0,0],"metric":{"Valid":2}},{"config":[1,0,0,0,0,1],"metric":{"Valid":2}},{"config":[1,0,0,0,1,0],"metric":{"Valid":2}},{"config":[1,0,0,1,0,0],"metric":{"Valid":2}}]},
{"problem":"MaximumSetPacking","variant":{"weight":"i32"},"instance":{"sets":[[0,1],[1,2],[2,3],[3,4]],"weights":[1,1,1,1]},"samples":[{"config":[1,0,1,0],"metric":{"Valid":2}}],"optimal":[{"config":[0,1,0,1],"metric":{"Valid":2}},{"config":[1,0,0,1],"metric":{"Valid":2}},{"config":[1,0,1,0],"metric":{"Valid":2}}]},
{"problem":"MinimumCardinalityKey","variant":{},"instance":{"bound_k":2,"dependencies":[[[0,1],[2]],[[0,2],[3]],[[1,3],[4]],[[2,4],[5]]],"num_attributes":6},"samples":[{"config":[1,1,0,0,0,0],"metric":true}],"optimal":[{"config":[1,1,0,0,0,0],"metric":true}]},
{"problem":"MinimumDominatingSet","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"weights":[1,1,1,1,1]},"samples":[{"config":[0,0,1,1,0],"metric":{"Valid":2}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":2}},{"config":[0,1,0,0,1],"metric":{"Valid":2}},{"config":[0,1,0,1,0],"metric":{"Valid":2}},{"config":[0,1,1,0,0],"metric":{"Valid":2}},{"config":[1,0,0,0,1],"metric":{"Valid":2}},{"config":[1,0,0,1,0],"metric":{"Valid":2}},{"config":[1,0,1,0,0],"metric":{"Valid":2}}]},
{"problem":"MinimumFeedbackVertexSet","variant":{"weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"directed","edges":[[0,1,null],[1,2,null],[2,0,null],[0,3,null],[3,4,null],[4,1,null],[4,2,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"weights":[1,1,1,1,1]},"samples":[{"config":[1,0,0,0,0],"metric":{"Valid":1}}],"optimal":[{"config":[0,0,1,0,0],"metric":{"Valid":1}},{"config":[1,0,0,0,0],"metric":{"Valid":1}}]},
{"problem":"MinimumMultiwayCut","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[2,3,1,2,4,5],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[2,3,null],[3,4,null],[0,4,null],[1,3,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"terminals":[0,2,4]},"samples":[{"config":[1,0,0,1,1,0],"metric":{"Valid":8}}],"optimal":[{"config":[1,0,0,1,1,0],"metric":{"Valid":8}}]},
Expand Down
3 changes: 2 additions & 1 deletion src/models/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,6 @@ pub use misc::{
ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum,
};
pub use set::{
ComparativeContainment, ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis,
ComparativeContainment, ExactCoverBy3Sets, MaximumSetPacking, MinimumCardinalityKey,
MinimumSetCovering, SetBasis,
};
Loading
Loading