diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 1c0e5f785..69a3141b7 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -78,6 +78,7 @@ "MaximumClique": [Maximum Clique], "MaximumSetPacking": [Maximum Set Packing], "MinimumSetCovering": [Minimum Set Covering], + "ComparativeContainment": [Comparative Containment], "SetBasis": [Set Basis], "SpinGlass": [Spin Glass], "QUBO": [QUBO], @@ -1413,6 +1414,70 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("ComparativeContainment") + let n = x.instance.universe_size + let R = x.instance.r_sets + let S = x.instance.s_sets + let r-weights = x.instance.r_weights + let s-weights = x.instance.s_weights + let sample = x.samples.at(0) + let selected = sample.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i) + let satisfiers = x.optimal.map(sol => sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => i)) + let contains-selected(family-set) = selected.all(i => family-set.contains(i)) + let r-active = range(R.len()).filter(i => contains-selected(R.at(i))) + let s-active = range(S.len()).filter(i => contains-selected(S.at(i))) + let r-total = r-active.map(i => r-weights.at(i)).sum(default: 0) + let s-total = s-active.map(i => s-weights.at(i)).sum(default: 0) + let fmt-set(items) = if items.len() == 0 { + $emptyset$ + } else { + "${" + items.map(e => str(e + 1)).join(", ") + "}$" + } + let left-elems = ( + (-3.1, 0.4), + (-2.4, -0.4), + (-1.6, 0.4), + (-0.9, -0.4), + ) + let right-elems = ( + (0.9, 0.4), + (1.6, -0.4), + (2.4, 0.4), + (3.1, -0.4), + ) + [ + #problem-def("ComparativeContainment")[ + Given a finite universe $X$, two set families $cal(R) = {R_1, dots, R_k}$ and $cal(S) = {S_1, dots, S_l}$ over $X$, and positive integer weights $w_R(R_i)$ and $w_S(S_j)$, does there exist a subset $Y subset.eq X$ such that $sum_(Y subset.eq R_i) w_R(R_i) >= sum_(Y subset.eq S_j) w_S(S_j)$? + ][ + Comparative Containment is the set-system comparison problem SP10 in Garey & Johnson @garey1979. Unlike covering and packing problems, feasibility depends on how the chosen subset $Y$ is nested inside two competing set families: the $cal(R)$ family rewards containment while the $cal(S)$ family penalizes it. The problem remains NP-complete in the unit-weight special case and provides a clean weighted-set comparison primitive for future reduction entries in this catalog. + + A direct exact algorithm enumerates all $2^n$ subsets $Y subset.eq X$ for $n = |X|$ and checks which members of $cal(R)$ and $cal(S)$ contain each candidate. This yields an $O^*(2^n)$ exact algorithm, with the polynomial factor coming from scanning the $k + l$ sets for each subset#footnote[No specialized exact algorithm improving on brute-force enumeration is recorded in the standard references used for this catalog entry.]. + + *Example.* Let $X = {1, 2, dots, #n}$, $cal(R) = {#range(R.len()).map(i => $R_#(i + 1)$).join(", ")}$ with #R.enumerate().map(((i, family-set)) => [$R_#(i + 1) = #fmt-set(family-set)$ with $w_R(R_#(i + 1)) = #(r-weights.at(i))$]).join(", "), and $cal(S) = {#range(S.len()).map(i => $S_#(i + 1)$).join(", ")}$ with #S.enumerate().map(((i, family-set)) => [$S_#(i + 1) = #fmt-set(family-set)$ with $w_S(S_#(i + 1)) = #(s-weights.at(i))$]).join(", "). The subset $Y = #fmt-set(selected)$ is satisfying because #r-active.map(i => $R_#(i + 1)$).join(", ") contribute $#r-total$ on the left while #s-active.map(i => $S_#(i + 1)$).join(", ") contribute only $#s-total$ on the right, so $#r-total >= #s-total$. In fact, the satisfying subsets are #satisfiers.map(fmt-set).join(", "), so this instance has exactly #satisfiers.len() satisfying solutions. + + #figure( + canvas(length: 1cm, { + import draw: * + content((-2.0, 1.5), text(8pt)[$cal(R)$]) + content((2.0, 1.5), text(8pt)[$cal(S)$]) + sregion((left-elems.at(0), left-elems.at(1), left-elems.at(2), left-elems.at(3)), pad: 0.5, label: [$R_1$], ..if r-active.contains(0) { sregion-selected } else { sregion-dimmed }) + sregion((left-elems.at(0), left-elems.at(1)), pad: 0.35, label: [$R_2$], ..if r-active.contains(1) { sregion-selected } else { sregion-dimmed }) + sregion((right-elems.at(0), right-elems.at(1), right-elems.at(2), right-elems.at(3)), pad: 0.5, label: [$S_1$], ..if s-active.contains(0) { sregion-selected } else { sregion-dimmed }) + sregion((right-elems.at(2), right-elems.at(3)), pad: 0.35, label: [$S_2$], ..if s-active.contains(1) { sregion-selected } else { sregion-dimmed }) + for (k, pos) in left-elems.enumerate() { + selem(pos, label: [#(k + 1)], fill: if selected.contains(k) { graph-colors.at(0) } else { black }) + } + for (k, pos) in right-elems.enumerate() { + selem(pos, label: [#(k + 1)], fill: if selected.contains(k) { graph-colors.at(0) } else { black }) + } + }), + caption: [Comparative containment for $Y = #fmt-set(selected)$: both $R_1$ and $R_2$ contain $Y$, while only $S_1$ does, so the $cal(R)$ side dominates the $cal(S)$ side.] + ) + ] + ] +} + #{ let x = load-model-example("SetBasis") let coll = x.instance.collection diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 1a9037e1d..2ed085d11 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -234,6 +234,7 @@ Flags by problem type: PaintShop --sequence MaximumSetPacking --sets [--weights] MinimumSetCovering --universe, --sets [--weights] + ComparativeContainment --universe, --r-sets, --s-sets [--r-weights] [--s-weights] X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each) SetBasis --universe, --sets, --k BicliqueCover --left, --right, --biedges, --k @@ -389,10 +390,22 @@ pub struct CreateArgs { /// Sets for SetPacking/SetCovering (semicolon-separated, e.g., "0,1;1,2;0,2") #[arg(long)] pub sets: Option, + /// R-family sets for ComparativeContainment (semicolon-separated, e.g., "0,1;1,2") + #[arg(long)] + pub r_sets: Option, + /// S-family sets for ComparativeContainment (semicolon-separated, e.g., "0,1;1,2") + #[arg(long)] + pub s_sets: Option, + /// R-family weights for ComparativeContainment (comma-separated, e.g., "2,5") + #[arg(long)] + pub r_weights: Option, + /// S-family weights for ComparativeContainment (comma-separated, e.g., "3,6") + #[arg(long)] + pub s_weights: Option, /// Partition groups for arc-index partitions (semicolon-separated, e.g., "0,1;2,3") #[arg(long)] pub partition: Option, - /// Universe size for MinimumSetCovering + /// Universe size for set-system problems such as MinimumSetCovering and ComparativeContainment #[arg(long)] pub universe: Option, /// Bipartite graph edges for BicliqueCover / BalancedCompleteBipartiteSubgraph (e.g., "0-0,0-1,1-2" for left-right pairs) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 973005907..09d98d7df 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -59,6 +59,10 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.capacity.is_none() && args.sequence.is_none() && args.sets.is_none() + && args.r_sets.is_none() + && args.s_sets.is_none() + && args.r_weights.is_none() + && args.s_weights.is_none() && args.partition.is_none() && args.universe.is_none() && args.biedges.is_none() @@ -222,15 +226,15 @@ 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" => "comma-separated integers: 1,1,2", + "Vec" => "comma-separated integers: 1,2,3", "Vec" => "comma-separated: 1,2,3", "Vec" => "comma-separated indices: 0,2,4", "Vec<(usize, usize, W)>" | "Vec<(usize,usize,W)>" => { "comma-separated weighted edges: 0-2:3,1-3:5" } + "Vec>" => "semicolon-separated sets: \"0,1;1,2;0,2\"", "Vec" => "semicolon-separated clauses: \"1,2;-1,3\"", "Vec>" => "semicolon-separated rows: \"1,0.5;0.5,2\"", - "Vec>" => "semicolon-separated groups: \"0,1;2,3\"", "usize" | "W::Sum" => "integer", "u64" => "integer", "i64" => "integer", @@ -303,6 +307,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", + "ComparativeContainment" => { + "--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", "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", _ => "", @@ -341,6 +348,7 @@ fn help_flag_hint( ) -> &'static str { match (canonical, field_name) { ("BoundedComponentSpanningForest", "max_weight") => "integer", + ("MultipleChoiceBranching", "partition") => "semicolon-separated groups: \"0,1;2,3\"", _ => type_format_hint(type_name, graph_type), } } @@ -1037,6 +1045,80 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // ComparativeContainment + "ComparativeContainment" => { + let universe = args.universe.ok_or_else(|| { + anyhow::anyhow!( + "ComparativeContainment requires --universe, --r-sets, and --s-sets\n\n\ + Usage: pred create ComparativeContainment --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]" + ) + })?; + let r_sets = parse_named_sets(args.r_sets.as_deref(), "--r-sets")?; + let s_sets = parse_named_sets(args.s_sets.as_deref(), "--s-sets")?; + validate_comparative_containment_sets("R", "--r-sets", universe, &r_sets)?; + validate_comparative_containment_sets("S", "--s-sets", universe, &s_sets)?; + let data = match resolved_variant.get("weight").map(|value| value.as_str()) { + Some("One") => { + let r_weights = parse_named_set_weights( + args.r_weights.as_deref(), + r_sets.len(), + "--r-weights", + )?; + let s_weights = parse_named_set_weights( + args.s_weights.as_deref(), + s_sets.len(), + "--s-weights", + )?; + if r_weights.iter().any(|&w| w != 1) || s_weights.iter().any(|&w| w != 1) { + bail!( + "Non-unit weights are not supported for ComparativeContainment/One.\n\n\ + Use `pred create ComparativeContainment/i32 ... --r-weights ... --s-weights ...` for weighted instances." + ); + } + ser(ComparativeContainment::::new(universe, r_sets, s_sets))? + } + Some("f64") => { + let r_weights = parse_named_set_weights_f64( + args.r_weights.as_deref(), + r_sets.len(), + "--r-weights", + )?; + validate_comparative_containment_f64_weights("R", "--r-weights", &r_weights)?; + let s_weights = parse_named_set_weights_f64( + args.s_weights.as_deref(), + s_sets.len(), + "--s-weights", + )?; + validate_comparative_containment_f64_weights("S", "--s-weights", &s_weights)?; + ser(ComparativeContainment::::with_weights( + universe, r_sets, s_sets, r_weights, s_weights, + ))? + } + Some("i32") | None => { + let r_weights = parse_named_set_weights( + args.r_weights.as_deref(), + r_sets.len(), + "--r-weights", + )?; + validate_comparative_containment_i32_weights("R", "--r-weights", &r_weights)?; + let s_weights = parse_named_set_weights( + args.s_weights.as_deref(), + s_sets.len(), + "--s-weights", + )?; + validate_comparative_containment_i32_weights("S", "--s-weights", &s_weights)?; + ser(ComparativeContainment::with_weights( + universe, r_sets, s_sets, r_weights, s_weights, + ))? + } + Some(other) => bail!( + "Unsupported ComparativeContainment weight variant: {}", + other + ), + }; + (data, resolved_variant.clone()) + } + // ExactCoverBy3Sets "ExactCoverBy3Sets" => { let universe = args.universe.ok_or_else(|| { @@ -2092,10 +2174,12 @@ fn parse_clauses(args: &CreateArgs) -> Result> { /// Parse `--sets` as semicolon-separated sets of comma-separated usize. /// E.g., "0,1;1,2;0,2" fn parse_sets(args: &CreateArgs) -> Result>> { - let sets_str = args - .sets - .as_deref() - .ok_or_else(|| anyhow::anyhow!("This problem requires --sets (e.g., \"0,1;1,2;0,2\")"))?; + parse_named_sets(args.sets.as_deref(), "--sets") +} + +fn parse_named_sets(sets_str: Option<&str>, flag: &str) -> Result>> { + let sets_str = sets_str + .ok_or_else(|| anyhow::anyhow!("This problem requires {flag} (e.g., \"0,1;1,2;0,2\")"))?; sets_str .split(';') .map(|set| { @@ -2111,6 +2195,23 @@ fn parse_sets(args: &CreateArgs) -> Result>> { .collect() } +fn validate_comparative_containment_sets( + family_name: &str, + flag: &str, + universe_size: usize, + sets: &[Vec], +) -> Result<()> { + for (set_index, set) in sets.iter().enumerate() { + for &element in set { + anyhow::ensure!( + element < universe_size, + "{family_name} set {set_index} from {flag} contains element {element} outside universe of size {universe_size}" + ); + } + } + Ok(()) +} + /// Parse `--partition` as semicolon-separated groups of comma-separated arc indices. /// E.g., "0,1;2,3;4,7;5,6" fn parse_partition_groups(args: &CreateArgs, num_arcs: usize) -> Result>> { @@ -2175,11 +2276,24 @@ fn parse_multiple_choice_branching_threshold(args: &CreateArgs, usage: &str) -> /// Parse `--weights` for set-based problems (i32), defaulting to all 1s. fn parse_set_weights(args: &CreateArgs, num_sets: usize) -> Result> { - match &args.weights { + parse_named_set_weights(args.weights.as_deref(), num_sets, "--weights") +} + +fn parse_named_set_weights( + weights_str: Option<&str>, + num_sets: usize, + flag: &str, +) -> Result> { + match weights_str { Some(w) => { let weights: Vec = util::parse_comma_list(w)?; if weights.len() != num_sets { - bail!("Expected {} weights but got {}", num_sets, weights.len()); + bail!( + "Expected {} values for {} but got {}", + num_sets, + flag, + weights.len() + ); } Ok(weights) } @@ -2187,6 +2301,56 @@ fn parse_set_weights(args: &CreateArgs, num_sets: usize) -> Result> { } } +fn parse_named_set_weights_f64( + weights_str: Option<&str>, + num_sets: usize, + flag: &str, +) -> Result> { + match weights_str { + Some(w) => { + let weights: Vec = util::parse_comma_list(w)?; + if weights.len() != num_sets { + bail!( + "Expected {} values for {} but got {}", + num_sets, + flag, + weights.len() + ); + } + Ok(weights) + } + None => Ok(vec![1.0f64; num_sets]), + } +} + +fn validate_comparative_containment_i32_weights( + family_name: &str, + flag: &str, + weights: &[i32], +) -> Result<()> { + for (index, weight) in weights.iter().enumerate() { + anyhow::ensure!( + *weight > 0, + "{family_name} weights from {flag} must be positive; found {weight} at index {index}" + ); + } + Ok(()) +} + +fn validate_comparative_containment_f64_weights( + family_name: &str, + flag: &str, + weights: &[f64], +) -> Result<()> { + for (index, weight) in weights.iter().enumerate() { + anyhow::ensure!( + weight.is_finite() && *weight > 0.0, + "{family_name} weights from {flag} must be finite and positive; found {weight} at index {index}" + ); + } + Ok(()) +} + /// Parse `--matrix` as semicolon-separated rows of comma-separated bool values (0/1). /// E.g., "1,0;0,1;1,1" fn parse_bool_matrix(args: &CreateArgs) -> Result>> { @@ -2728,6 +2892,10 @@ mod tests { capacity: None, sequence: None, sets: None, + r_sets: None, + s_sets: None, + r_weights: None, + s_weights: None, partition: None, universe: None, biedges: None, diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index bd96bfeed..803009433 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -1028,6 +1028,101 @@ fn test_create_x3c_rejects_duplicate_subset_elements() { ); } +#[test] +fn test_create_comparative_containment() { + let output_file = std::env::temp_dir().join("pred_test_create_comparative_containment.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "ComparativeContainment", + "--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", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(output_file.exists()); + + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "ComparativeContainment"); + assert_eq!(json["variant"]["weight"], "i32"); + assert_eq!(json["data"]["universe_size"], 4); + assert_eq!( + json["data"]["r_sets"], + serde_json::json!([[0, 1, 2, 3], [0, 1]]) + ); + assert_eq!( + json["data"]["s_sets"], + serde_json::json!([[0, 1, 2, 3], [2, 3]]) + ); + assert_eq!(json["data"]["r_weights"], serde_json::json!([2, 5])); + assert_eq!(json["data"]["s_weights"], serde_json::json!([3, 6])); + + std::fs::remove_file(&output_file).ok(); +} + +#[test] +fn test_create_comparative_containment_rejects_out_of_range_elements_without_panicking() { + let output = pred() + .args([ + "create", + "ComparativeContainment", + "--universe", + "4", + "--r-sets", + "0,1,4", + "--s-sets", + "0,1", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("outside universe of size 4"), + "stderr: {stderr}" + ); + assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); +} + +#[test] +fn test_create_comparative_containment_rejects_nonpositive_weights_without_panicking() { + let output = pred() + .args([ + "create", + "ComparativeContainment", + "--universe", + "4", + "--r-sets", + "0,1", + "--s-sets", + "0,1", + "--r-weights", + "0", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("positive"), "stderr: {stderr}"); + assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); +} + #[test] fn test_create_set_basis() { let output_file = std::env::temp_dir().join("pred_test_create_set_basis.json"); @@ -1062,6 +1157,94 @@ fn test_create_set_basis() { std::fs::remove_file(&output_file).ok(); } +#[test] +fn test_create_comparative_containment_f64() { + let output_file = + std::env::temp_dir().join("pred_test_create_comparative_containment_f64.json"); + let output = pred() + .args([ + "-o", + output_file.to_str().unwrap(), + "create", + "ComparativeContainment/f64", + "--universe", + "4", + "--r-sets", + "0,1,2,3;0,1", + "--s-sets", + "0,1,2,3;2,3", + "--r-weights", + "2.5,5.0", + "--s-weights", + "3.5,6.0", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(output_file.exists()); + + let content = std::fs::read_to_string(&output_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "ComparativeContainment"); + assert_eq!(json["variant"]["weight"], "f64"); + assert_eq!(json["data"]["r_weights"], serde_json::json!([2.5, 5.0])); + assert_eq!(json["data"]["s_weights"], serde_json::json!([3.5, 6.0])); + + std::fs::remove_file(&output_file).ok(); +} + +#[test] +fn test_create_comparative_containment_one_rejects_nonunit_weights() { + let output = pred() + .args([ + "create", + "ComparativeContainment/One", + "--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", + ]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "stdout: {}", + String::from_utf8_lossy(&output.stdout) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("Non-unit weights are not supported for ComparativeContainment/One"), + "stderr: {stderr}" + ); +} + +#[test] +fn test_create_comparative_containment_no_flags_shows_help() { + let output = pred() + .args(["create", "ComparativeContainment"]) + .output() + .unwrap(); + assert!( + !output.status.success(), + "should exit non-zero when showing help without data flags" + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("--universe"), "stderr: {stderr}"); + assert!(stderr.contains("--r-sets"), "stderr: {stderr}"); + assert!(stderr.contains("--s-sets"), "stderr: {stderr}"); + assert!(!stderr.contains("--universe-size"), "stderr: {stderr}"); +} + #[test] fn test_create_set_basis_requires_k() { let output = pred() @@ -2385,6 +2568,10 @@ fn test_create_multiple_choice_branching_help_uses_bound_flag() { !stderr.contains("--threshold"), "help output should not advertise '--threshold', got: {stderr}" ); + assert!( + stderr.contains("semicolon-separated groups"), + "expected '--partition' help to describe groups, got: {stderr}" + ); } #[test] diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index d8dd0fb6e..3d8f22710 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -7,6 +7,7 @@ {"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":"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":"ComparativeContainment","variant":{"weight":"i32"},"instance":{"r_sets":[[0,1,2,3],[0,1]],"r_weights":[2,5],"s_sets":[[0,1,2,3],[2,3]],"s_weights":[3,6],"universe_size":4},"samples":[{"config":[1,0,0,0],"metric":true}],"optimal":[{"config":[0,1,0,0],"metric":true},{"config":[1,0,0,0],"metric":true},{"config":[1,1,0,0],"metric":true}]}, {"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}]}, {"problem":"ExactCoverBy3Sets","variant":{},"instance":{"subsets":[[0,1,2],[0,2,4],[3,4,5],[3,5,7],[6,7,8],[1,4,6],[2,5,8]],"universe_size":9},"samples":[{"config":[1,0,1,0,1,0,0],"metric":true}],"optimal":[{"config":[1,0,1,0,1,0,0],"metric":true}]}, {"problem":"Factoring","variant":{},"instance":{"m":2,"n":3,"target":15},"samples":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}],"optimal":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}]}, diff --git a/src/lib.rs b/src/lib.rs index 68b747161..90dfc42a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -63,7 +63,7 @@ pub mod prelude { ShortestCommonSupersequence, SubsetSum, }; pub use crate::models::set::{ - ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, + ComparativeContainment, ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, }; // Core traits diff --git a/src/models/mod.rs b/src/models/mod.rs index 96f409c8a..73ca52680 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -26,4 +26,6 @@ pub use misc::{ MinimumTardinessSequencing, PaintShop, SequencingWithinIntervals, ShortestCommonSupersequence, SubsetSum, }; -pub use set::{ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis}; +pub use set::{ + ComparativeContainment, ExactCoverBy3Sets, MaximumSetPacking, MinimumSetCovering, SetBasis, +}; diff --git a/src/models/set/comparative_containment.rs b/src/models/set/comparative_containment.rs new file mode 100644 index 000000000..89f11113e --- /dev/null +++ b/src/models/set/comparative_containment.rs @@ -0,0 +1,253 @@ +//! Comparative Containment problem implementation. +//! +//! Given two weighted families of sets over a common universe, determine +//! whether there exists a subset of the universe whose containment weight +//! in the first family is at least its containment weight in the second. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; +use crate::traits::{Problem, SatisfactionProblem}; +use crate::types::{One, WeightElement}; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "ComparativeContainment", + display_name: "Comparative Containment", + aliases: &[], + dimensions: &[VariantDimension::new("weight", "i32", &["One", "i32", "f64"])], + module_path: module_path!(), + description: "Compare containment-weight sums for two set families over a shared universe", + fields: &[ + FieldInfo { name: "universe_size", type_name: "usize", description: "Size of the universe X" }, + FieldInfo { name: "r_sets", type_name: "Vec>", description: "First set family R over X" }, + FieldInfo { name: "s_sets", type_name: "Vec>", description: "Second set family S over X" }, + FieldInfo { name: "r_weights", type_name: "Vec", description: "Positive weights for sets in R" }, + FieldInfo { name: "s_weights", type_name: "Vec", description: "Positive weights for sets in S" }, + ], + } +} + +/// Comparative Containment. +/// +/// Given a universe `X`, two set families `R` and `S`, and positive weights +/// on those sets, determine whether there exists a subset `Y ⊆ X` such that +/// the total weight of `R`-sets containing `Y` is at least the total weight +/// of `S`-sets containing `Y`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ComparativeContainment { + universe_size: usize, + r_sets: Vec>, + s_sets: Vec>, + r_weights: Vec, + s_weights: Vec, +} + +impl ComparativeContainment { + /// Create a new instance with unit weights. + pub fn new(universe_size: usize, r_sets: Vec>, s_sets: Vec>) -> Self + where + W: From, + { + let r_weights = vec![W::from(1); r_sets.len()]; + let s_weights = vec![W::from(1); s_sets.len()]; + Self::with_weights(universe_size, r_sets, s_sets, r_weights, s_weights) + } + + /// Create a new instance with explicit weights. + pub fn with_weights( + universe_size: usize, + r_sets: Vec>, + s_sets: Vec>, + r_weights: Vec, + s_weights: Vec, + ) -> Self { + assert_eq!( + r_sets.len(), + r_weights.len(), + "number of R sets and R weights must match" + ); + assert_eq!( + s_sets.len(), + s_weights.len(), + "number of S sets and S weights must match" + ); + validate_set_family("R", universe_size, &r_sets); + validate_set_family("S", universe_size, &s_sets); + validate_weight_family("R", &r_weights); + validate_weight_family("S", &s_weights); + Self { + universe_size, + r_sets, + s_sets, + r_weights, + s_weights, + } + } + + /// Get the size of the universe. + pub fn universe_size(&self) -> usize { + self.universe_size + } + + /// Get the number of sets in the R family. + pub fn num_r_sets(&self) -> usize { + self.r_sets.len() + } + + /// Get the number of sets in the S family. + pub fn num_s_sets(&self) -> usize { + self.s_sets.len() + } + + /// Get the R family. + pub fn r_sets(&self) -> &[Vec] { + &self.r_sets + } + + /// Get the S family. + pub fn s_sets(&self) -> &[Vec] { + &self.s_sets + } + + /// Get the R-family weights. + pub fn r_weights(&self) -> &[W] { + &self.r_weights + } + + /// Get the S-family weights. + pub fn s_weights(&self) -> &[W] { + &self.s_weights + } + + /// Check whether the subset selected by `config` is contained in `set`. + pub fn contains_selected_subset(&self, config: &[usize], set: &[usize]) -> bool { + self.valid_config(config) && contains_selected_subset_unchecked(config, set) + } + + fn valid_config(&self, config: &[usize]) -> bool { + config.len() == self.universe_size && config.iter().all(|&value| value <= 1) + } +} + +impl ComparativeContainment +where + W: WeightElement, +{ + /// Total R-family weight for sets containing the selected subset. + pub fn r_weight_sum(&self, config: &[usize]) -> Option { + self.sum_containing_weights(config, &self.r_sets, &self.r_weights) + } + + /// Total S-family weight for sets containing the selected subset. + pub fn s_weight_sum(&self, config: &[usize]) -> Option { + self.sum_containing_weights(config, &self.s_sets, &self.s_weights) + } + + /// Check if a configuration is a satisfying solution. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + match (self.r_weight_sum(config), self.s_weight_sum(config)) { + (Some(r_total), Some(s_total)) => r_total >= s_total, + _ => false, + } + } + + fn sum_containing_weights( + &self, + config: &[usize], + sets: &[Vec], + weights: &[W], + ) -> Option { + if !self.valid_config(config) { + return None; + } + + let mut total = W::Sum::zero(); + for (set, weight) in sets.iter().zip(weights.iter()) { + if contains_selected_subset_unchecked(config, set) { + total += weight.to_sum(); + } + } + Some(total) + } +} + +impl Problem for ComparativeContainment +where + W: WeightElement + crate::variant::VariantParam, +{ + const NAME: &'static str = "ComparativeContainment"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![2; self.universe_size] + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.is_valid_solution(config) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![W] + } +} + +impl SatisfactionProblem for ComparativeContainment where + W: WeightElement + crate::variant::VariantParam +{ +} + +crate::declare_variants! { + sat ComparativeContainment => "2^universe_size", + default sat ComparativeContainment => "2^universe_size", + sat ComparativeContainment => "2^universe_size", +} + +fn validate_set_family(label: &str, universe_size: usize, sets: &[Vec]) { + for (set_index, set) in sets.iter().enumerate() { + for &element in set { + assert!( + element < universe_size, + "{label} set {set_index} contains element {element} outside universe of size {universe_size}" + ); + } + } +} + +fn validate_weight_family(label: &str, weights: &[W]) { + for (index, weight) in weights.iter().enumerate() { + let sum = weight.to_sum(); + assert!( + sum.partial_cmp(&W::Sum::zero()) == Some(std::cmp::Ordering::Greater), + "{label} weights must be finite and positive; weight at index {index} is not" + ); + } +} + +fn contains_selected_subset_unchecked(config: &[usize], set: &[usize]) -> bool { + config + .iter() + .enumerate() + .all(|(element, &selected)| selected == 0 || set.contains(&element)) +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "comparative_containment_i32", + build: || { + let problem = ComparativeContainment::with_weights( + 4, + vec![vec![0, 1, 2, 3], vec![0, 1]], + vec![vec![0, 1, 2, 3], vec![2, 3]], + vec![2, 5], + vec![3, 6], + ); + crate::example_db::specs::satisfaction_example(problem, vec![vec![1, 0, 0, 0]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/set/comparative_containment.rs"] +mod tests; diff --git a/src/models/set/mod.rs b/src/models/set/mod.rs index fb8ee7cd8..9b9117b03 100644 --- a/src/models/set/mod.rs +++ b/src/models/set/mod.rs @@ -4,12 +4,15 @@ //! - [`MinimumSetCovering`]: Minimum weight set cover //! - [`MaximumSetPacking`]: Maximum weight set packing //! - [`ExactCoverBy3Sets`]: Exact cover by 3-element subsets (X3C) +//! - [`ComparativeContainment`]: Compare containment-weight sums for two set families +pub(crate) mod comparative_containment; pub(crate) mod exact_cover_by_3_sets; pub(crate) mod maximum_set_packing; pub(crate) mod minimum_set_covering; pub(crate) mod set_basis; +pub use comparative_containment::ComparativeContainment; pub use exact_cover_by_3_sets::ExactCoverBy3Sets; pub use maximum_set_packing::MaximumSetPacking; pub use minimum_set_covering::MinimumSetCovering; @@ -18,6 +21,7 @@ pub use set_basis::SetBasis; #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { let mut specs = Vec::new(); + specs.extend(comparative_containment::canonical_model_example_specs()); specs.extend(exact_cover_by_3_sets::canonical_model_example_specs()); specs.extend(maximum_set_packing::canonical_model_example_specs()); specs.extend(minimum_set_covering::canonical_model_example_specs()); diff --git a/src/unit_tests/models/set/comparative_containment.rs b/src/unit_tests/models/set/comparative_containment.rs new file mode 100644 index 000000000..e7e05d001 --- /dev/null +++ b/src/unit_tests/models/set/comparative_containment.rs @@ -0,0 +1,168 @@ +use super::*; +use crate::solvers::BruteForce; +use crate::traits::Problem; +use crate::types::One; + +fn yes_instance() -> ComparativeContainment { + ComparativeContainment::with_weights( + 4, + vec![vec![0, 1, 2, 3], vec![0, 1]], + vec![vec![0, 1, 2, 3], vec![2, 3]], + vec![2, 5], + vec![3, 6], + ) +} + +fn no_instance() -> ComparativeContainment { + ComparativeContainment::with_weights( + 2, + vec![vec![0], vec![1]], + vec![vec![0, 1]], + vec![1, 1], + vec![3], + ) +} + +#[test] +fn test_comparative_containment_creation() { + let problem = yes_instance(); + assert_eq!(problem.universe_size(), 4); + assert_eq!(problem.num_r_sets(), 2); + assert_eq!(problem.num_s_sets(), 2); + assert_eq!(problem.num_variables(), 4); + assert_eq!(problem.dims(), vec![2, 2, 2, 2]); +} + +#[test] +fn test_comparative_containment_unit_weights() { + let problem = + ComparativeContainment::::new(3, vec![vec![0, 1], vec![1, 2]], vec![vec![0]]); + assert_eq!(problem.r_weights(), &[One, One]); + assert_eq!(problem.s_weights(), &[One]); +} + +#[test] +fn test_comparative_containment_evaluation_yes_and_no_examples() { + let yes = yes_instance(); + assert!(yes.evaluate(&[1, 0, 0, 0])); + assert!(!yes.evaluate(&[0, 0, 1, 0])); + assert!(!yes.evaluate(&[0, 0, 0, 0])); + + let no = no_instance(); + assert!(!no.evaluate(&[0, 0])); + assert!(!no.evaluate(&[1, 0])); + assert!(!no.evaluate(&[0, 1])); + assert!(!no.evaluate(&[1, 1])); +} + +#[test] +fn test_comparative_containment_rejects_invalid_configs() { + let problem = yes_instance(); + assert!(!problem.evaluate(&[1, 0, 0])); + assert!(!problem.evaluate(&[1, 0, 0, 2])); +} + +#[test] +fn test_comparative_containment_contains_selected_subset_requires_valid_config() { + let problem = yes_instance(); + assert!(problem.contains_selected_subset(&[1, 0, 0, 0], &[0, 1, 2, 3])); + assert!(!problem.contains_selected_subset(&[0, 0, 1, 0], &[0, 1])); + assert!(!problem.contains_selected_subset(&[1, 0, 0], &[0, 1, 2, 3])); + assert!(!problem.contains_selected_subset(&[1, 0, 0, 2], &[0, 1, 2, 3])); +} + +#[test] +fn test_comparative_containment_solver() { + let solver = BruteForce::new(); + + let yes_solutions = solver.find_all_satisfying(&yes_instance()); + assert!(yes_solutions.contains(&vec![1, 0, 0, 0])); + assert!(!yes_solutions.is_empty()); + + let no_solutions = solver.find_all_satisfying(&no_instance()); + assert!(no_solutions.is_empty()); +} + +#[test] +fn test_comparative_containment_serialization() { + let problem = yes_instance(); + let json = serde_json::to_string(&problem).unwrap(); + let restored: ComparativeContainment = serde_json::from_str(&json).unwrap(); + assert_eq!(restored.universe_size(), problem.universe_size()); + assert_eq!(restored.r_sets(), problem.r_sets()); + assert_eq!(restored.s_sets(), problem.s_sets()); + assert_eq!(restored.r_weights(), problem.r_weights()); + assert_eq!(restored.s_weights(), problem.s_weights()); +} + +#[test] +fn test_comparative_containment_paper_example() { + let problem = yes_instance(); + let config = vec![1, 0, 0, 0]; + assert!(problem.evaluate(&config)); + + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert_eq!(solutions.len(), 3); + assert!(solutions.contains(&config)); +} + +#[test] +fn test_comparative_containment_weight_sums() { + let problem = yes_instance(); + // Y = {0}: R1={0,1,2,3} contains {0} (w=2), R2={0,1} contains {0} (w=5) → 7 + assert_eq!(problem.r_weight_sum(&[1, 0, 0, 0]), Some(7)); + // Y = {0}: S1={0,1,2,3} contains {0} (w=3), S2={2,3} does not → 3 + assert_eq!(problem.s_weight_sum(&[1, 0, 0, 0]), Some(3)); + // Invalid config returns None + assert_eq!(problem.r_weight_sum(&[1, 0, 0]), None); + assert_eq!(problem.s_weight_sum(&[1, 0, 0, 2]), None); +} + +#[test] +#[should_panic(expected = "number of R sets and R weights must match")] +fn test_comparative_containment_rejects_mismatched_r_weights() { + ComparativeContainment::with_weights(2, vec![vec![0]], vec![vec![0]], vec![1, 2], vec![1]); +} + +#[test] +#[should_panic(expected = "number of S sets and S weights must match")] +fn test_comparative_containment_rejects_mismatched_s_weights() { + ComparativeContainment::with_weights(2, vec![vec![0]], vec![vec![0]], vec![1], vec![1, 2]); +} + +#[test] +#[should_panic(expected = "R weights must be finite and positive")] +fn test_comparative_containment_rejects_nonpositive_i32_weights() { + ComparativeContainment::with_weights(2, vec![vec![0]], vec![vec![0]], vec![0], vec![1]); +} + +#[test] +#[should_panic(expected = "S weights must be finite and positive")] +fn test_comparative_containment_rejects_nonpositive_i32_s_weights() { + ComparativeContainment::with_weights(2, vec![vec![0]], vec![vec![0]], vec![1], vec![0]); +} + +#[test] +#[should_panic(expected = "R weights must be finite and positive")] +fn test_comparative_containment_rejects_non_finite_f64_weights() { + ComparativeContainment::with_weights( + 2, + vec![vec![0]], + vec![vec![0]], + vec![f64::NAN], + vec![1.0], + ); +} + +#[test] +#[should_panic(expected = "S weights must be finite and positive")] +fn test_comparative_containment_rejects_nonpositive_f64_weights() { + ComparativeContainment::with_weights(2, vec![vec![0]], vec![vec![0]], vec![1.0], vec![0.0]); +} + +#[test] +#[should_panic(expected = "contains element")] +fn test_comparative_containment_rejects_out_of_range_elements() { + ComparativeContainment::::new(2, vec![vec![0, 2]], vec![vec![0]]); +}