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
65 changes: 65 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down Expand Up @@ -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.]
) <fig:comparative-containment>
]
]
}

#{
let x = load-model-example("SetBasis")
let coll = x.instance.collection
Expand Down
15 changes: 14 additions & 1 deletion problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<String>,
/// R-family sets for ComparativeContainment (semicolon-separated, e.g., "0,1;1,2")
#[arg(long)]
pub r_sets: Option<String>,
/// S-family sets for ComparativeContainment (semicolon-separated, e.g., "0,1;1,2")
#[arg(long)]
pub s_sets: Option<String>,
/// R-family weights for ComparativeContainment (comma-separated, e.g., "2,5")
#[arg(long)]
pub r_weights: Option<String>,
/// S-family weights for ComparativeContainment (comma-separated, e.g., "3,6")
#[arg(long)]
pub s_weights: Option<String>,
/// Partition groups for arc-index partitions (semicolon-separated, e.g., "0,1;2,3")
#[arg(long)]
pub partition: Option<String>,
/// Universe size for MinimumSetCovering
/// Universe size for set-system problems such as MinimumSetCovering and ComparativeContainment
#[arg(long)]
pub universe: Option<usize>,
/// Bipartite graph edges for BicliqueCover / BalancedCompleteBipartiteSubgraph (e.g., "0-0,0-1,1-2" for left-right pairs)
Expand Down
184 changes: 176 additions & 8 deletions problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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<u64>" => "comma-separated integers: 1,1,2",
"Vec<u64>" => "comma-separated integers: 1,2,3",
"Vec<W>" => "comma-separated: 1,2,3",
"Vec<usize>" => "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<Vec<usize>>" => "semicolon-separated sets: \"0,1;1,2;0,2\"",
"Vec<CNFClause>" => "semicolon-separated clauses: \"1,2;-1,3\"",
"Vec<Vec<W>>" => "semicolon-separated rows: \"1,0.5;0.5,2\"",
"Vec<Vec<usize>>" => "semicolon-separated groups: \"0,1;2,3\"",
"usize" | "W::Sum" => "integer",
"u64" => "integer",
"i64" => "integer",
Expand Down Expand Up @@ -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",
_ => "",
Expand Down Expand Up @@ -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),
}
}
Expand Down Expand Up @@ -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::<One>::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::<f64>::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(|| {
Expand Down Expand Up @@ -2092,10 +2174,12 @@ fn parse_clauses(args: &CreateArgs) -> Result<Vec<CNFClause>> {
/// Parse `--sets` as semicolon-separated sets of comma-separated usize.
/// E.g., "0,1;1,2;0,2"
fn parse_sets(args: &CreateArgs) -> Result<Vec<Vec<usize>>> {
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<Vec<Vec<usize>>> {
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| {
Expand All @@ -2111,6 +2195,23 @@ fn parse_sets(args: &CreateArgs) -> Result<Vec<Vec<usize>>> {
.collect()
}

fn validate_comparative_containment_sets(
family_name: &str,
flag: &str,
universe_size: usize,
sets: &[Vec<usize>],
) -> 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<Vec<Vec<usize>>> {
Expand Down Expand Up @@ -2175,18 +2276,81 @@ 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<Vec<i32>> {
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<Vec<i32>> {
match weights_str {
Some(w) => {
let weights: Vec<i32> = 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)
}
None => Ok(vec![1i32; num_sets]),
}
}

fn parse_named_set_weights_f64(
weights_str: Option<&str>,
num_sets: usize,
flag: &str,
) -> Result<Vec<f64>> {
match weights_str {
Some(w) => {
let weights: Vec<f64> = 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<Vec<Vec<bool>>> {
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading