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
4 changes: 1 addition & 3 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -8147,7 +8147,6 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V|
let sets = x.instance.sets
let k = x.instance.k
let bound = x.instance.bound
let config = x.optimal_config
let m = sets.len()
// Count qualifying tuples by enumerating the Cartesian product
let total = sets.fold(1, (acc, s) => acc * s.len())
Expand All @@ -8157,12 +8156,11 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V|
][
The $K$th Largest $m$-Tuple problem is MP10 in Garey and Johnson's appendix @garey1979. It is _not known to be in NP_, because a "yes" certificate may need to exhibit $K$ qualifying tuples and $K$ can be exponentially large. The problem is PP-complete under polynomial-time Turing reductions @haase2016, though the special case $m = 2$, $K = 1$ is NP-complete via reduction from Subset Sum. In the general case, the only known exact approach is brute-force enumeration of all $product_(i=1)^m |X_i|$ tuples, so the registered catalog complexity is `total_tuples * num_sets`#footnote[No algorithm improving on brute-force is known for the general $K$th Largest $m$-Tuple problem.].

*Example.* Let $m = #m$, $B = #bound$, and $K = #k$ with sets #sets.enumerate().map(((i, s)) => [$X_#(i+1) = {#s.map(str).join(", ")}$]).join([, ]). The Cartesian product has $#total$ tuples. For instance, the tuple $(#config.enumerate().map(((i, c)) => str(sets.at(i).at(c))).join(", "))$ has sum $#config.enumerate().map(((i, c)) => sets.at(i).at(c)).sum() >= #bound$, contributing 1 to the count. In total, #k of the #total tuples satisfy the bound, so the answer is _yes_ (count $= K$).
*Example.* Let $m = #m$, $B = #bound$, and $K = #k$ with sets #sets.enumerate().map(((i, s)) => [$X_#(i+1) = {#s.map(str).join(", ")}$]).join([, ]). The Cartesian product has $#total$ tuples. Exactly #k tuples have sum at least #bound, so the answer is _yes_ (count $= K$). The evaluator enumerates the Cartesian product internally and stops once it has found $K$ qualifying tuples.

#pred-commands(
"pred create --example KthLargestMTuple -o kth-largest-m-tuple.json",
"pred solve kth-largest-m-tuple.json --solver brute-force",
"pred evaluate kth-largest-m-tuple.json --config " + config.map(str).join(","),
)
]
]
Expand Down
50 changes: 50 additions & 0 deletions problemreductions-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2624,6 +2624,56 @@ fn test_create_model_example_multiple_choice_branching_round_trips_into_solve()
std::fs::remove_file(&path).ok();
}

#[test]
fn test_kth_largest_m_tuple_solve_uses_k_threshold() {
let solve = |k: u64| {
let create = pred()
.args([
"create",
"KthLargestMTuple",
"--sets",
"2,5,8;3,6;1,4,7",
"--k",
&k.to_string(),
"--bound",
"12",
])
.output()
.unwrap();
assert!(
create.status.success(),
"stderr: {}",
String::from_utf8_lossy(&create.stderr)
);

let path = std::env::temp_dir().join(format!(
"pred_test_kth_largest_m_tuple_{}_{}.json",
std::process::id(),
k
));
std::fs::write(&path, create.stdout).unwrap();

let output = pred()
.args(["solve", path.to_str().unwrap(), "--solver", "brute-force"])
.output()
.unwrap();
std::fs::remove_file(path).unwrap();
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap()
};

let at_threshold = solve(14);
let above_threshold = solve(15);

assert_eq!(at_threshold["evaluation"], "Or(true)");
assert_eq!(above_threshold["evaluation"], "Or(false)");
assert_ne!(at_threshold["evaluation"], above_threshold["evaluation"]);
}

#[test]
fn test_create_acyclic_partition() {
let output = pred()
Expand Down
93 changes: 55 additions & 38 deletions src/models/misc/kth_largest_m_tuple.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
//! Kth Largest m-Tuple problem implementation.
//!
//! Given m sets of positive integers and thresholds K and B, count how many
//! distinct m-tuples (one element per set) have total size at least B.
//! The answer is YES iff the count is at least K. Garey & Johnson MP10.
//! Given m sets of positive integers and thresholds K and B, determine whether
//! at least K distinct m-tuples (one element per set) have total size at least B.
//! Garey & Johnson MP10.

use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry};
use crate::traits::Problem;
use crate::types::Sum;
use crate::types::Or;
use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize};

Expand Down Expand Up @@ -36,15 +36,14 @@ inventory::submit! {
/// The Kth Largest m-Tuple problem.
///
/// Given sets `X_1, ..., X_m` of positive integers, a threshold `K`, and a
/// bound `B`, count how many distinct m-tuples `(x_1, ..., x_m)` in
/// `X_1 x ... x X_m` satisfy `sum(x_i) >= B`. The answer is YES iff the
/// count is at least `K`.
/// bound `B`, determine whether at least `K` distinct m-tuples
/// `(x_1, ..., x_m)` in `X_1 x ... x X_m` satisfy `sum(x_i) >= B`.
///
/// # Representation
///
/// Variable `i` selects an element from set `X_i`, ranging over `{0, ..., |X_i|-1}`.
/// `evaluate` returns `Sum(1)` if the tuple sum >= B, else `Sum(0)`.
/// The aggregate over all configurations gives the total count of qualifying tuples.
/// The empty configuration triggers enumeration of the Cartesian product.
/// `evaluate` returns `Or(true)` as soon as `K` qualifying tuples have been
/// found and `Or(false)` if the complete product contains fewer than `K`.
///
/// # Example
///
Expand All @@ -58,9 +57,9 @@ inventory::submit! {
/// 12,
/// );
/// let solver = BruteForce::new();
/// let value = solver.solve(&problem);
/// // 14 of the 18 tuples have sum >= 12
/// assert_eq!(value, problemreductions::types::Sum(14));
/// let answer = solver.solve(&problem);
/// // 14 of the 18 tuples have sum >= 12, so count >= K.
/// assert_eq!(answer, problemreductions::types::Or(true));
/// ```
#[derive(Debug, Clone, Serialize)]
pub struct KthLargestMTuple {
Expand Down Expand Up @@ -126,7 +125,42 @@ impl KthLargestMTuple {

/// Returns the total number of m-tuples (product of set sizes).
pub fn total_tuples(&self) -> usize {
self.sets.iter().map(|s| s.len()).product()
self.sets
.iter()
.try_fold(1usize, |total, set| total.checked_mul(set.len()))
.expect("KthLargestMTuple total tuple count exceeds usize")
}

fn has_at_least_k_qualifying_tuples(&self) -> bool {
let mut choices = vec![0; self.sets.len()];
let mut qualifying = 0;

loop {
let mut remaining_bound = self.bound;
for (set, &choice) in self.sets.iter().zip(&choices) {
remaining_bound = remaining_bound.saturating_sub(set[choice]);
}
if remaining_bound == 0 {
qualifying += 1;
if qualifying == self.k {
return true;
}
}

let mut advanced = false;
for set_index in (0..choices.len()).rev() {
choices[set_index] += 1;
if choices[set_index] == self.sets[set_index].len() {
choices[set_index] = 0;
} else {
advanced = true;
break;
}
}
if !advanced {
return false;
}
}
}
}

Expand All @@ -149,35 +183,18 @@ impl<'de> Deserialize<'de> for KthLargestMTuple {

impl Problem for KthLargestMTuple {
const NAME: &'static str = "KthLargestMTuple";
type Value = Sum<u64>;
type Value = Or;

fn variant() -> Vec<(&'static str, &'static str)> {
crate::variant_params![]
}

fn dims(&self) -> Vec<usize> {
self.sets.iter().map(|s| s.len()).collect()
vec![]
}

fn evaluate(&self, config: &[usize]) -> Sum<u64> {
if config.len() != self.num_sets() {
return Sum(0);
}
for (i, &choice) in config.iter().enumerate() {
if choice >= self.sets[i].len() {
return Sum(0);
}
}
let total: u64 = config
.iter()
.enumerate()
.map(|(i, &choice)| self.sets[i][choice])
.sum();
if total >= self.bound {
Sum(1)
} else {
Sum(0)
}
fn evaluate(&self, config: &[usize]) -> Or {
Or(config.is_empty() && self.has_at_least_k_qualifying_tuples())
}
}

Expand All @@ -190,16 +207,16 @@ crate::declare_variants! {
#[cfg(feature = "example-db")]
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
// m=3, X_1={2,5,8}, X_2={3,6}, X_3={1,4,7}, B=12, K=14.
// 14 of 18 tuples have sum >= 12. The config [2,1,2] picks (8,6,7) with sum=21 >= 12.
// 14 of 18 tuples have sum >= 12, so the answer is YES at K=14.
vec![crate::example_db::specs::ModelExampleSpec {
id: "kth_largest_m_tuple",
instance: Box::new(KthLargestMTuple::new(
vec![vec![2, 5, 8], vec![3, 6], vec![1, 4, 7]],
14,
12,
)),
optimal_config: vec![2, 1, 2],
optimal_value: serde_json::json!(1),
optimal_config: vec![],
optimal_value: serde_json::json!(true),
}]
}

Expand Down
99 changes: 39 additions & 60 deletions src/unit_tests/models/misc/kth_largest_m_tuple.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use crate::models::misc::KthLargestMTuple;
use crate::solvers::{BruteForce, Solver};
use crate::traits::Problem;
use crate::types::Sum;
use crate::types::Or;

fn example_problem() -> KthLargestMTuple {
// m=3, X_1={2,5,8}, X_2={3,6}, X_3={1,4,7}, B=12, K=14
KthLargestMTuple::new(vec![vec![2, 5, 8], vec![3, 6], vec![1, 4, 7]], 14, 12)
fn example_problem(k: u64) -> KthLargestMTuple {
// m=3, X_1={2,5,8}, X_2={3,6}, X_3={1,4,7}, B=12
KthLargestMTuple::new(vec![vec![2, 5, 8], vec![3, 6], vec![1, 4, 7]], k, 12)
}

#[test]
fn test_kth_largest_m_tuple_creation() {
let p = example_problem();
let p = example_problem(14);
assert_eq!(p.sets().len(), 3);
assert_eq!(p.sets()[0], vec![2, 5, 8]);
assert_eq!(p.sets()[1], vec![3, 6]);
Expand All @@ -19,64 +19,31 @@ fn test_kth_largest_m_tuple_creation() {
assert_eq!(p.bound(), 12);
assert_eq!(p.num_sets(), 3);
assert_eq!(p.total_tuples(), 18);
assert_eq!(p.dims(), vec![3, 2, 3]);
assert_eq!(p.num_variables(), 3);
assert_eq!(p.dims(), Vec::<usize>::new());
assert_eq!(p.num_variables(), 0);
assert_eq!(<KthLargestMTuple as Problem>::NAME, "KthLargestMTuple");
assert_eq!(<KthLargestMTuple as Problem>::variant(), vec![]);
}

#[test]
fn test_kth_largest_m_tuple_evaluate_qualifying_tuple() {
let p = example_problem();
// (8,6,7) = sum 21 >= 12 -> Sum(1)
assert_eq!(p.evaluate(&[2, 1, 2]), Sum(1));
// (5,6,4) = sum 15 >= 12 -> Sum(1)
assert_eq!(p.evaluate(&[1, 1, 1]), Sum(1));
}
fn test_kth_largest_m_tuple_threshold_decision() {
let p = example_problem(14);
assert_eq!(BruteForce::new().solve(&p), Or(true));

#[test]
fn test_kth_largest_m_tuple_evaluate_non_qualifying_tuple() {
let p = example_problem();
// (2,3,1) = sum 6 < 12 -> Sum(0)
assert_eq!(p.evaluate(&[0, 0, 0]), Sum(0));
// (2,3,4) = sum 9 < 12 -> Sum(0)
assert_eq!(p.evaluate(&[0, 0, 1]), Sum(0));
let above_threshold = example_problem(15);
assert_eq!(BruteForce::new().solve(&above_threshold), Or(false));
}

#[test]
fn test_kth_largest_m_tuple_evaluate_invalid_configs() {
let p = example_problem();
// Wrong length
assert_eq!(p.evaluate(&[0, 0]), Sum(0));
assert_eq!(p.evaluate(&[0, 0, 0, 0]), Sum(0));
// Out of range
assert_eq!(p.evaluate(&[3, 0, 0]), Sum(0));
assert_eq!(p.evaluate(&[0, 2, 0]), Sum(0));
assert_eq!(p.evaluate(&[0, 0, 3]), Sum(0));
}

#[test]
fn test_kth_largest_m_tuple_solver() {
let p = example_problem();
let solver = BruteForce::new();
let value = solver.solve(&p);
// 14 of 18 tuples qualify (sum >= 12)
assert_eq!(value, Sum(14));
}

#[test]
fn test_kth_largest_m_tuple_boundary_example() {
// K=14 and count=14, so the answer is YES (count >= K)
let p = example_problem();
let solver = BruteForce::new();
let count = solver.solve(&p);
assert_eq!(count, Sum(14));
assert!(count.0 >= p.k());
let p = example_problem(14);
assert_eq!(p.evaluate(&[0]), Or(false));
assert_eq!(p.evaluate(&[2, 1, 2]), Or(false));
}

#[test]
fn test_kth_largest_m_tuple_serialization_round_trip() {
let p = example_problem();
let p = example_problem(14);
let json = serde_json::to_value(&p).unwrap();
assert_eq!(
json,
Expand Down Expand Up @@ -135,24 +102,17 @@ fn test_kth_largest_m_tuple_zero_size_panics() {
fn test_kth_largest_m_tuple_paper_example() {
// Issue example: m=3, X_1={2,5,8}, X_2={3,6}, X_3={1,4,7}, B=12, K=14
// 14 of 18 tuples have sum >= 12 -> YES (boundary case: count == K)
let p = example_problem();
let p = example_problem(14);
let solver = BruteForce::new();
let count = solver.solve(&p);
assert_eq!(count, Sum(14));

// Verify a specific qualifying tuple: (8,6,7), sum=21
assert_eq!(p.evaluate(&[2, 1, 2]), Sum(1));

// Verify a specific non-qualifying tuple: (2,3,1), sum=6
assert_eq!(p.evaluate(&[0, 0, 0]), Sum(0));
assert_eq!(solver.solve(&p), Or(true));
}

#[test]
fn test_kth_largest_m_tuple_all_qualify() {
// Two sets each with one large element, B=1 -> all tuples qualify
let p = KthLargestMTuple::new(vec![vec![5], vec![10]], 1, 1);
let solver = BruteForce::new();
assert_eq!(solver.solve(&p), Sum(1));
assert_eq!(solver.solve(&p), Or(true));
assert_eq!(p.total_tuples(), 1);
}

Expand All @@ -161,5 +121,24 @@ fn test_kth_largest_m_tuple_none_qualify() {
// B is larger than any possible sum
let p = KthLargestMTuple::new(vec![vec![1, 2], vec![1, 2]], 1, 100);
let solver = BruteForce::new();
assert_eq!(solver.solve(&p), Sum(0));
assert_eq!(solver.solve(&p), Or(false));
}

#[test]
fn test_kth_largest_m_tuple_sum_beyond_u64_max_qualifies() {
let p = KthLargestMTuple::new(vec![vec![u64::MAX], vec![1]], 1, u64::MAX);
assert_eq!(BruteForce::new().solve(&p), Or(true));
}

#[test]
fn test_kth_largest_m_tuple_many_singleton_sets_do_not_use_call_stack() {
let p = KthLargestMTuple::new(vec![vec![1]; 10_000], 1, 10_000);
assert_eq!(BruteForce::new().solve(&p), Or(true));
}

#[test]
#[should_panic(expected = "total tuple count exceeds usize")]
fn test_kth_largest_m_tuple_total_tuples_overflow_panics() {
let p = KthLargestMTuple::new(vec![vec![1, 2]; usize::BITS as usize], 1, 1);
p.total_tuples();
}