From d6563dddf41ed91046de8dd7210776f28c9c8c18 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 7 Aug 2026 19:24:20 +0800 Subject: [PATCH] Fix KthLargestMTuple threshold decision --- docs/paper/reductions.typ | 4 +- problemreductions-cli/tests/cli_tests.rs | 50 ++++++++++ src/models/misc/kth_largest_m_tuple.rs | 93 ++++++++++------- .../models/misc/kth_largest_m_tuple.rs | 99 ++++++++----------- 4 files changed, 145 insertions(+), 101 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index bdf289f4c..4acee27a7 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -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()) @@ -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(","), ) ] ] diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 8ed0a2387..ff038fcaa 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -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::(&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() diff --git a/src/models/misc/kth_largest_m_tuple.rs b/src/models/misc/kth_largest_m_tuple.rs index 6b600a98e..79b8078b1 100644 --- a/src/models/misc/kth_largest_m_tuple.rs +++ b/src/models/misc/kth_largest_m_tuple.rs @@ -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}; @@ -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 /// @@ -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 { @@ -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; + } + } } } @@ -149,35 +183,18 @@ impl<'de> Deserialize<'de> for KthLargestMTuple { impl Problem for KthLargestMTuple { const NAME: &'static str = "KthLargestMTuple"; - type Value = Sum; + type Value = Or; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] } fn dims(&self) -> Vec { - self.sets.iter().map(|s| s.len()).collect() + vec![] } - fn evaluate(&self, config: &[usize]) -> Sum { - 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()) } } @@ -190,7 +207,7 @@ crate::declare_variants! { #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { // 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( @@ -198,8 +215,8 @@ pub(crate) fn canonical_model_example_specs() -> Vec 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]); @@ -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::::new()); + assert_eq!(p.num_variables(), 0); assert_eq!(::NAME, "KthLargestMTuple"); assert_eq!(::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, @@ -135,16 +102,9 @@ 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] @@ -152,7 +112,7 @@ 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); } @@ -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(); }