Skip to content

Commit 54e54c2

Browse files
GiggleLiuzazabapclaude
authored
Fix #400: [Model] SetBasis (#661)
* Add plan for #400: [Model] SetBasis * Implement #400: [Model] SetBasis * Add CLI coverage for #400 SetBasis * chore: remove plan file after implementation * fix: address SetBasis review feedback - harden SetBasis evaluation against malformed serialized input - validate SetBasis CLI subsets before construction - align problem-specific create help with actual CLI flags * fix: add coverage tests and constructor edge cases for SetBasis Add targeted tests for uncovered code paths: - is_subset returning false (basis set not subset of target) - is_valid_solution wrapper method - k=0 with empty/non-empty collection (edge cases) - Empty collection with k > 0 (trivially satisfiable) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zazabap <sweynan@icloud.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9a2b2b7 commit 54e54c2

14 files changed

Lines changed: 636 additions & 26 deletions

File tree

docs/paper/reductions.typ

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"MaximumClique": [Maximum Clique],
7676
"MaximumSetPacking": [Maximum Set Packing],
7777
"MinimumSetCovering": [Minimum Set Covering],
78+
"SetBasis": [Set Basis],
7879
"SpinGlass": [Spin Glass],
7980
"QUBO": [QUBO],
8081
"ILP": [Integer Linear Programming],
@@ -1107,6 +1108,43 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS
11071108
]
11081109
}
11091110

1111+
#{
1112+
let x = load-model-example("SetBasis")
1113+
let coll = x.instance.collection
1114+
let m = coll.len()
1115+
let U-size = x.instance.universe_size
1116+
let k = x.instance.k
1117+
let sample = x.samples.at(0)
1118+
let sat-count = x.optimal.len()
1119+
let basis = range(k).map(i =>
1120+
range(U-size).filter(j => sample.config.at(i * U-size + j) == 1)
1121+
)
1122+
let fmt-set(s) = "${" + s.map(e => str(e + 1)).join(", ") + "}$"
1123+
[
1124+
#problem-def("SetBasis")[
1125+
Given finite set $S$, collection $cal(C)$ of subsets of $S$, and integer $k$, does there exist a family $cal(B) = {B_1, ..., B_k}$ with each $B_i subset.eq S$ such that for every $C in cal(C)$ there exists $cal(B)_C subset.eq cal(B)$ with $union.big_(B in cal(B)_C) B = C$?
1126+
][
1127+
The Set Basis problem was shown NP-complete by Stockmeyer @stockmeyer1975setbasis and appears as SP7 in Garey & Johnson @garey1979. It asks for an exact union-based description of a family of sets, unlike Set Cover which only requires covering the underlying universe. Applications include data compression, database schema design, and Boolean function minimization. The library's decision encoding uses $k |S|$ membership bits, so brute-force over those bits gives an $O^*(2^(k |S|))$ exact algorithm#footnote[This is the direct search bound induced by the encoding implemented here; we are not aware of a faster general exact worst-case algorithm for this representation.].
1128+
1129+
*Example.* Let $S = {1, 2, 3, 4}$, $k = #k$, and $cal(C) = {#range(m).map(i => $C_#(i + 1)$).join(", ")}$ with #coll.enumerate().map(((i, s)) => $C_#(i + 1) = #fmt-set(s)$).join(", "). The sample basis from the issue is $cal(B) = {#range(k).map(i => $B_#(i + 1)$).join(", ")}$ with #basis.enumerate().map(((i, s)) => $B_#(i + 1) = #fmt-set(s)$).join(", "). Then $C_1 = B_1 union B_2$, $C_2 = B_2 union B_3$, $C_3 = B_1 union B_3$, and $C_4 = B_1 union B_2 union B_3$. There are #sat-count satisfying encodings in total: the singleton basis can be permuted in $3! = 6$ ways, and the three pair sets $C_1, C_2, C_3$ also form a basis with another six row permutations.
1130+
1131+
#figure(
1132+
canvas(length: 1cm, {
1133+
let elems = ((-0.9, 0.2), (0.0, -0.5), (0.9, 0.2), (1.8, -0.5))
1134+
for i in range(k) {
1135+
let positions = basis.at(i).map(e => elems.at(e))
1136+
sregion(positions, pad: 0.28, label: [$B_#(i + 1)$], ..sregion-selected)
1137+
}
1138+
for (idx, pos) in elems.enumerate() {
1139+
selem(pos, label: [#(idx + 1)], fill: if idx < 3 { black } else { luma(160) })
1140+
}
1141+
}),
1142+
caption: [Set Basis example: the singleton basis $cal(B) = {#range(k).map(i => $B_#(i + 1)$).join(", ")}$ reconstructs every target set in $cal(C)$; element $4$ is unused by the target family.],
1143+
) <fig:set-basis>
1144+
]
1145+
]
1146+
}
1147+
11101148
== Optimization Problems
11111149

11121150
#{

docs/paper/references.bib

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,15 @@ @article{arora2009
562562
doi = {10.1145/1502793.1502794}
563563
}
564564

565+
@techreport{stockmeyer1975setbasis,
566+
author = {Larry J. Stockmeyer},
567+
title = {The Set Basis Problem Is NP-Complete},
568+
institution = {IBM Thomas J. Watson Research Center},
569+
number = {RC 5431},
570+
address = {Yorktown Heights, New York},
571+
year = {1975}
572+
}
573+
565574
@article{cygan2014,
566575
author = {Marek Cygan and Daniel Lokshtanov and Marcin Pilipczuk and Micha{\l} Pilipczuk and Saket Saurabh},
567576
title = {Minimum Bisection Is Fixed Parameter Tractable},

docs/src/reductions/problem_schemas.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,27 @@
594594
}
595595
]
596596
},
597+
{
598+
"name": "SetBasis",
599+
"description": "Determine whether a collection of sets admits a basis of size k under union",
600+
"fields": [
601+
{
602+
"name": "universe_size",
603+
"type_name": "usize",
604+
"description": "Size of the ground set S"
605+
},
606+
{
607+
"name": "collection",
608+
"type_name": "Vec<Vec<usize>>",
609+
"description": "Collection C of target subsets of S"
610+
},
611+
{
612+
"name": "k",
613+
"type_name": "usize",
614+
"description": "Required number of basis sets"
615+
}
616+
]
617+
},
597618
{
598619
"name": "ShortestCommonSupersequence",
599620
"description": "Find a common supersequence of bounded length for a set of strings",

docs/src/reductions/reduction_graph.json

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,13 @@
464464
"doc_path": "models/formula/struct.Satisfiability.html",
465465
"complexity": "2^num_variables"
466466
},
467+
{
468+
"name": "SetBasis",
469+
"variant": {},
470+
"category": "set",
471+
"doc_path": "models/set/struct.SetBasis.html",
472+
"complexity": "2^(basis_size * universe_size)"
473+
},
467474
{
468475
"name": "ShortestCommonSupersequence",
469476
"variant": {},
@@ -569,7 +576,7 @@
569576
},
570577
{
571578
"source": 4,
572-
"target": 54,
579+
"target": 55,
573580
"overhead": [
574581
{
575582
"field": "num_spins",
@@ -733,7 +740,7 @@
733740
},
734741
{
735742
"source": 21,
736-
"target": 58,
743+
"target": 59,
737744
"overhead": [
738745
{
739746
"field": "num_elements",
@@ -789,7 +796,7 @@
789796
},
790797
{
791798
"source": 25,
792-
"target": 54,
799+
"target": 55,
793800
"overhead": [
794801
{
795802
"field": "num_spins",
@@ -1235,7 +1242,7 @@
12351242
},
12361243
{
12371244
"source": 49,
1238-
"target": 53,
1245+
"target": 54,
12391246
"overhead": [
12401247
{
12411248
"field": "num_spins",
@@ -1320,7 +1327,7 @@
13201327
"doc_path": "rules/sat_minimumdominatingset/index.html"
13211328
},
13221329
{
1323-
"source": 53,
1330+
"source": 54,
13241331
"target": 49,
13251332
"overhead": [
13261333
{
@@ -1331,7 +1338,7 @@
13311338
"doc_path": "rules/spinglass_qubo/index.html"
13321339
},
13331340
{
1334-
"source": 54,
1341+
"source": 55,
13351342
"target": 25,
13361343
"overhead": [
13371344
{
@@ -1346,8 +1353,8 @@
13461353
"doc_path": "rules/spinglass_maxcut/index.html"
13471354
},
13481355
{
1349-
"source": 54,
1350-
"target": 53,
1356+
"source": 55,
1357+
"target": 54,
13511358
"overhead": [
13521359
{
13531360
"field": "num_spins",
@@ -1361,7 +1368,7 @@
13611368
"doc_path": "rules/spinglass_casts/index.html"
13621369
},
13631370
{
1364-
"source": 59,
1371+
"source": 60,
13651372
"target": 12,
13661373
"overhead": [
13671374
{
@@ -1376,7 +1383,7 @@
13761383
"doc_path": "rules/travelingsalesman_ilp/index.html"
13771384
},
13781385
{
1379-
"source": 59,
1386+
"source": 60,
13801387
"target": 49,
13811388
"overhead": [
13821389
{

problemreductions-cli/src/cli.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ Flags by problem type:
231231
MaximumSetPacking --sets [--weights]
232232
MinimumSetCovering --universe, --sets [--weights]
233233
X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each)
234+
SetBasis --universe, --sets, --k
234235
BicliqueCover --left, --right, --biedges, --k
235236
BMF --matrix (0/1), --rank
236237
SteinerTree --graph, --edge-weights, --terminals
@@ -264,7 +265,8 @@ Examples:
264265
pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5
265266
pred create MIS --random --num-vertices 10 --edge-prob 0.3
266267
pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1
267-
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\"")]
268+
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\"
269+
pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3")]
268270
pub struct CreateArgs {
269271
/// Problem type (e.g., MIS, QUBO, SAT). Omit when using --example.
270272
#[arg(value_parser = crate::problem_name::ProblemNameParser)]

problemreductions-cli/src/commands/create.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,21 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
213213
}
214214
}
215215

216+
fn cli_flag_name(field_name: &str) -> String {
217+
match field_name {
218+
"universe_size" => "universe".to_string(),
219+
"collection" | "subsets" => "sets".to_string(),
220+
"left_size" => "left".to_string(),
221+
"right_size" => "right".to_string(),
222+
"edges" => "biedges".to_string(),
223+
"vertex_weights" => "weights".to_string(),
224+
"edge_lengths" => "edge-weights".to_string(),
225+
"num_tasks" => "n".to_string(),
226+
"precedences" => "precedence-pairs".to_string(),
227+
_ => field_name.replace('_', "-"),
228+
}
229+
}
230+
216231
fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
217232
match canonical {
218233
"MaximumIndependentSet"
@@ -248,6 +263,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
248263
}
249264
"SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1",
250265
"SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11",
266+
"SetBasis" => "--universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3",
251267
"ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4",
252268
_ => "",
253269
}
@@ -280,7 +296,7 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> {
280296
let hint = type_format_hint(&field.type_name, graph_type);
281297
eprintln!(
282298
" --{:<16} {} ({})",
283-
field.name.replace('_', "-"),
299+
cli_flag_name(&field.name),
284300
field.description,
285301
hint
286302
);
@@ -718,6 +734,41 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
718734
)
719735
}
720736

737+
// SetBasis
738+
"SetBasis" => {
739+
let universe = args.universe.ok_or_else(|| {
740+
anyhow::anyhow!(
741+
"SetBasis requires --universe, --sets, and --k\n\n\
742+
Usage: pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3"
743+
)
744+
})?;
745+
let k = args.k.ok_or_else(|| {
746+
anyhow::anyhow!(
747+
"SetBasis requires --k\n\n\
748+
Usage: pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3"
749+
)
750+
})?;
751+
let sets = parse_sets(args)?;
752+
for (i, set) in sets.iter().enumerate() {
753+
for &element in set {
754+
if element >= universe {
755+
bail!(
756+
"Set {} contains element {} which is outside universe of size {}",
757+
i,
758+
element,
759+
universe
760+
);
761+
}
762+
}
763+
}
764+
(
765+
ser(problemreductions::models::set::SetBasis::new(
766+
universe, sets, k,
767+
))?,
768+
resolved_variant.clone(),
769+
)
770+
}
771+
721772
// BicliqueCover
722773
"BicliqueCover" => {
723774
let left = args.left.ok_or_else(|| {

0 commit comments

Comments
 (0)