Skip to content

Commit 95a7ddb

Browse files
authored
Fix KthLargestMTuple threshold decision (#1122)
1 parent 61521b4 commit 95a7ddb

4 files changed

Lines changed: 145 additions & 101 deletions

File tree

docs/paper/reductions.typ

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8147,7 +8147,6 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V|
81478147
let sets = x.instance.sets
81488148
let k = x.instance.k
81498149
let bound = x.instance.bound
8150-
let config = x.optimal_config
81518150
let m = sets.len()
81528151
// Count qualifying tuples by enumerating the Cartesian product
81538152
let total = sets.fold(1, (acc, s) => acc * s.len())
@@ -8157,12 +8156,11 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V|
81578156
][
81588157
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.].
81598158

8160-
*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$).
8159+
*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.
81618160

81628161
#pred-commands(
81638162
"pred create --example KthLargestMTuple -o kth-largest-m-tuple.json",
81648163
"pred solve kth-largest-m-tuple.json --solver brute-force",
8165-
"pred evaluate kth-largest-m-tuple.json --config " + config.map(str).join(","),
81668164
)
81678165
]
81688166
]

problemreductions-cli/tests/cli_tests.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2624,6 +2624,56 @@ fn test_create_model_example_multiple_choice_branching_round_trips_into_solve()
26242624
std::fs::remove_file(&path).ok();
26252625
}
26262626

2627+
#[test]
2628+
fn test_kth_largest_m_tuple_solve_uses_k_threshold() {
2629+
let solve = |k: u64| {
2630+
let create = pred()
2631+
.args([
2632+
"create",
2633+
"KthLargestMTuple",
2634+
"--sets",
2635+
"2,5,8;3,6;1,4,7",
2636+
"--k",
2637+
&k.to_string(),
2638+
"--bound",
2639+
"12",
2640+
])
2641+
.output()
2642+
.unwrap();
2643+
assert!(
2644+
create.status.success(),
2645+
"stderr: {}",
2646+
String::from_utf8_lossy(&create.stderr)
2647+
);
2648+
2649+
let path = std::env::temp_dir().join(format!(
2650+
"pred_test_kth_largest_m_tuple_{}_{}.json",
2651+
std::process::id(),
2652+
k
2653+
));
2654+
std::fs::write(&path, create.stdout).unwrap();
2655+
2656+
let output = pred()
2657+
.args(["solve", path.to_str().unwrap(), "--solver", "brute-force"])
2658+
.output()
2659+
.unwrap();
2660+
std::fs::remove_file(path).unwrap();
2661+
assert!(
2662+
output.status.success(),
2663+
"stderr: {}",
2664+
String::from_utf8_lossy(&output.stderr)
2665+
);
2666+
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap()
2667+
};
2668+
2669+
let at_threshold = solve(14);
2670+
let above_threshold = solve(15);
2671+
2672+
assert_eq!(at_threshold["evaluation"], "Or(true)");
2673+
assert_eq!(above_threshold["evaluation"], "Or(false)");
2674+
assert_ne!(at_threshold["evaluation"], above_threshold["evaluation"]);
2675+
}
2676+
26272677
#[test]
26282678
fn test_create_acyclic_partition() {
26292679
let output = pred()

src/models/misc/kth_largest_m_tuple.rs

Lines changed: 55 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
//! Kth Largest m-Tuple problem implementation.
22
//!
3-
//! Given m sets of positive integers and thresholds K and B, count how many
4-
//! distinct m-tuples (one element per set) have total size at least B.
5-
//! The answer is YES iff the count is at least K. Garey & Johnson MP10.
3+
//! Given m sets of positive integers and thresholds K and B, determine whether
4+
//! at least K distinct m-tuples (one element per set) have total size at least B.
5+
//! Garey & Johnson MP10.
66
77
use crate::registry::{FieldInfo, ProblemSchemaEntry, ProblemSizeFieldEntry};
88
use crate::traits::Problem;
9-
use crate::types::Sum;
9+
use crate::types::Or;
1010
use serde::de::Error as _;
1111
use serde::{Deserialize, Deserializer, Serialize};
1212

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

127126
/// Returns the total number of m-tuples (product of set sizes).
128127
pub fn total_tuples(&self) -> usize {
129-
self.sets.iter().map(|s| s.len()).product()
128+
self.sets
129+
.iter()
130+
.try_fold(1usize, |total, set| total.checked_mul(set.len()))
131+
.expect("KthLargestMTuple total tuple count exceeds usize")
132+
}
133+
134+
fn has_at_least_k_qualifying_tuples(&self) -> bool {
135+
let mut choices = vec![0; self.sets.len()];
136+
let mut qualifying = 0;
137+
138+
loop {
139+
let mut remaining_bound = self.bound;
140+
for (set, &choice) in self.sets.iter().zip(&choices) {
141+
remaining_bound = remaining_bound.saturating_sub(set[choice]);
142+
}
143+
if remaining_bound == 0 {
144+
qualifying += 1;
145+
if qualifying == self.k {
146+
return true;
147+
}
148+
}
149+
150+
let mut advanced = false;
151+
for set_index in (0..choices.len()).rev() {
152+
choices[set_index] += 1;
153+
if choices[set_index] == self.sets[set_index].len() {
154+
choices[set_index] = 0;
155+
} else {
156+
advanced = true;
157+
break;
158+
}
159+
}
160+
if !advanced {
161+
return false;
162+
}
163+
}
130164
}
131165
}
132166

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

150184
impl Problem for KthLargestMTuple {
151185
const NAME: &'static str = "KthLargestMTuple";
152-
type Value = Sum<u64>;
186+
type Value = Or;
153187

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

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

162-
fn evaluate(&self, config: &[usize]) -> Sum<u64> {
163-
if config.len() != self.num_sets() {
164-
return Sum(0);
165-
}
166-
for (i, &choice) in config.iter().enumerate() {
167-
if choice >= self.sets[i].len() {
168-
return Sum(0);
169-
}
170-
}
171-
let total: u64 = config
172-
.iter()
173-
.enumerate()
174-
.map(|(i, &choice)| self.sets[i][choice])
175-
.sum();
176-
if total >= self.bound {
177-
Sum(1)
178-
} else {
179-
Sum(0)
180-
}
196+
fn evaluate(&self, config: &[usize]) -> Or {
197+
Or(config.is_empty() && self.has_at_least_k_qualifying_tuples())
181198
}
182199
}
183200

@@ -190,16 +207,16 @@ crate::declare_variants! {
190207
#[cfg(feature = "example-db")]
191208
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
192209
// m=3, X_1={2,5,8}, X_2={3,6}, X_3={1,4,7}, B=12, K=14.
193-
// 14 of 18 tuples have sum >= 12. The config [2,1,2] picks (8,6,7) with sum=21 >= 12.
210+
// 14 of 18 tuples have sum >= 12, so the answer is YES at K=14.
194211
vec![crate::example_db::specs::ModelExampleSpec {
195212
id: "kth_largest_m_tuple",
196213
instance: Box::new(KthLargestMTuple::new(
197214
vec![vec![2, 5, 8], vec![3, 6], vec![1, 4, 7]],
198215
14,
199216
12,
200217
)),
201-
optimal_config: vec![2, 1, 2],
202-
optimal_value: serde_json::json!(1),
218+
optimal_config: vec![],
219+
optimal_value: serde_json::json!(true),
203220
}]
204221
}
205222

Lines changed: 39 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
use crate::models::misc::KthLargestMTuple;
22
use crate::solvers::{BruteForce, Solver};
33
use crate::traits::Problem;
4-
use crate::types::Sum;
4+
use crate::types::Or;
55

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

1111
#[test]
1212
fn test_kth_largest_m_tuple_creation() {
13-
let p = example_problem();
13+
let p = example_problem(14);
1414
assert_eq!(p.sets().len(), 3);
1515
assert_eq!(p.sets()[0], vec![2, 5, 8]);
1616
assert_eq!(p.sets()[1], vec![3, 6]);
@@ -19,64 +19,31 @@ fn test_kth_largest_m_tuple_creation() {
1919
assert_eq!(p.bound(), 12);
2020
assert_eq!(p.num_sets(), 3);
2121
assert_eq!(p.total_tuples(), 18);
22-
assert_eq!(p.dims(), vec![3, 2, 3]);
23-
assert_eq!(p.num_variables(), 3);
22+
assert_eq!(p.dims(), Vec::<usize>::new());
23+
assert_eq!(p.num_variables(), 0);
2424
assert_eq!(<KthLargestMTuple as Problem>::NAME, "KthLargestMTuple");
2525
assert_eq!(<KthLargestMTuple as Problem>::variant(), vec![]);
2626
}
2727

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

37-
#[test]
38-
fn test_kth_largest_m_tuple_evaluate_non_qualifying_tuple() {
39-
let p = example_problem();
40-
// (2,3,1) = sum 6 < 12 -> Sum(0)
41-
assert_eq!(p.evaluate(&[0, 0, 0]), Sum(0));
42-
// (2,3,4) = sum 9 < 12 -> Sum(0)
43-
assert_eq!(p.evaluate(&[0, 0, 1]), Sum(0));
33+
let above_threshold = example_problem(15);
34+
assert_eq!(BruteForce::new().solve(&above_threshold), Or(false));
4435
}
4536

4637
#[test]
4738
fn test_kth_largest_m_tuple_evaluate_invalid_configs() {
48-
let p = example_problem();
49-
// Wrong length
50-
assert_eq!(p.evaluate(&[0, 0]), Sum(0));
51-
assert_eq!(p.evaluate(&[0, 0, 0, 0]), Sum(0));
52-
// Out of range
53-
assert_eq!(p.evaluate(&[3, 0, 0]), Sum(0));
54-
assert_eq!(p.evaluate(&[0, 2, 0]), Sum(0));
55-
assert_eq!(p.evaluate(&[0, 0, 3]), Sum(0));
56-
}
57-
58-
#[test]
59-
fn test_kth_largest_m_tuple_solver() {
60-
let p = example_problem();
61-
let solver = BruteForce::new();
62-
let value = solver.solve(&p);
63-
// 14 of 18 tuples qualify (sum >= 12)
64-
assert_eq!(value, Sum(14));
65-
}
66-
67-
#[test]
68-
fn test_kth_largest_m_tuple_boundary_example() {
69-
// K=14 and count=14, so the answer is YES (count >= K)
70-
let p = example_problem();
71-
let solver = BruteForce::new();
72-
let count = solver.solve(&p);
73-
assert_eq!(count, Sum(14));
74-
assert!(count.0 >= p.k());
39+
let p = example_problem(14);
40+
assert_eq!(p.evaluate(&[0]), Or(false));
41+
assert_eq!(p.evaluate(&[2, 1, 2]), Or(false));
7542
}
7643

7744
#[test]
7845
fn test_kth_largest_m_tuple_serialization_round_trip() {
79-
let p = example_problem();
46+
let p = example_problem(14);
8047
let json = serde_json::to_value(&p).unwrap();
8148
assert_eq!(
8249
json,
@@ -135,24 +102,17 @@ fn test_kth_largest_m_tuple_zero_size_panics() {
135102
fn test_kth_largest_m_tuple_paper_example() {
136103
// Issue example: m=3, X_1={2,5,8}, X_2={3,6}, X_3={1,4,7}, B=12, K=14
137104
// 14 of 18 tuples have sum >= 12 -> YES (boundary case: count == K)
138-
let p = example_problem();
105+
let p = example_problem(14);
139106
let solver = BruteForce::new();
140-
let count = solver.solve(&p);
141-
assert_eq!(count, Sum(14));
142-
143-
// Verify a specific qualifying tuple: (8,6,7), sum=21
144-
assert_eq!(p.evaluate(&[2, 1, 2]), Sum(1));
145-
146-
// Verify a specific non-qualifying tuple: (2,3,1), sum=6
147-
assert_eq!(p.evaluate(&[0, 0, 0]), Sum(0));
107+
assert_eq!(solver.solve(&p), Or(true));
148108
}
149109

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

@@ -161,5 +121,24 @@ fn test_kth_largest_m_tuple_none_qualify() {
161121
// B is larger than any possible sum
162122
let p = KthLargestMTuple::new(vec![vec![1, 2], vec![1, 2]], 1, 100);
163123
let solver = BruteForce::new();
164-
assert_eq!(solver.solve(&p), Sum(0));
124+
assert_eq!(solver.solve(&p), Or(false));
125+
}
126+
127+
#[test]
128+
fn test_kth_largest_m_tuple_sum_beyond_u64_max_qualifies() {
129+
let p = KthLargestMTuple::new(vec![vec![u64::MAX], vec![1]], 1, u64::MAX);
130+
assert_eq!(BruteForce::new().solve(&p), Or(true));
131+
}
132+
133+
#[test]
134+
fn test_kth_largest_m_tuple_many_singleton_sets_do_not_use_call_stack() {
135+
let p = KthLargestMTuple::new(vec![vec![1]; 10_000], 1, 10_000);
136+
assert_eq!(BruteForce::new().solve(&p), Or(true));
137+
}
138+
139+
#[test]
140+
#[should_panic(expected = "total tuple count exceeds usize")]
141+
fn test_kth_largest_m_tuple_total_tuples_overflow_panics() {
142+
let p = KthLargestMTuple::new(vec![vec![1, 2]; usize::BITS as usize], 1, 1);
143+
p.total_tuples();
165144
}

0 commit comments

Comments
 (0)