Skip to content

Commit a69b719

Browse files
GiggleLiuisPANN
andauthored
Fix #492: [Model] 3Partition (#759)
* Add plan for #492: [Model] 3Partition * Implement #492: [Model] 3Partition * chore: remove plan file after implementation * cargo fmt * Adapt ThreePartition to current API: Value=Or, declare_variants!, find_witness * Paper: derive 3-Partition example groups from optimal_config instead of hardcoded slices * Hoist u128 bound conversion out of validation loop --------- Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn>
1 parent 0980d5a commit a69b719

9 files changed

Lines changed: 516 additions & 3 deletions

File tree

docs/paper/reductions.typ

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
"ExactCoverBy3Sets": [Exact Cover by 3-Sets],
146146
"SubsetSum": [Subset Sum],
147147
"Partition": [Partition],
148+
"ThreePartition": [3-Partition],
148149
"PartialFeedbackEdgeSet": [Partial Feedback Edge Set],
149150
"MinimumFeedbackArcSet": [Minimum Feedback Arc Set],
150151
"MinimumFeedbackVertexSet": [Minimum Feedback Vertex Set],
@@ -4587,6 +4588,43 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
45874588
*Example.* Let $A = {5, 3, 8, 2, 7, 1}$ ($n = 6$) and $K = 3$ groups. The partition $A_1 = {8, 1}$, $A_2 = {5, 2}$, $A_3 = {3, 7}$ gives group sums $9, 7, 10$ and sum of squares $81 + 49 + 100 = 230$. The optimal partition has group sums ${9, 9, 8}$ yielding $81 + 81 + 64 = 226$.
45884589
]
45894590

4591+
#{
4592+
let x = load-model-example("ThreePartition")
4593+
let sizes = x.instance.sizes
4594+
let bound = x.instance.bound
4595+
let config = x.optimal_config
4596+
let m = int(sizes.len() / 3)
4597+
// Group elements by their assignment in optimal_config
4598+
let groups = range(m).map(g => {
4599+
let indices = range(sizes.len()).filter(i => config.at(i) == g)
4600+
indices.map(i => sizes.at(i))
4601+
})
4602+
[
4603+
#problem-def("ThreePartition")[
4604+
Given a set $A = {a_0, dots, a_(3m-1)}$ of $3m$ elements, a bound $B in ZZ^+$, and sizes $s(a) in ZZ^+$ such that $B/4 lt s(a) lt B/2$ for every $a in A$ and $sum_(a in A) s(a) = m B$, determine whether $A$ can be partitioned into $m$ disjoint triples $A_1, dots, A_m$ with $sum_(a in A_i) s(a) = B$ for every $i$.
4605+
][
4606+
3-Partition is Garey and Johnson's strongly NP-complete benchmark SP15 @garey1979. Unlike ordinary Partition, the strict size window forces every feasible block to contain exactly three elements, making the problem the canonical source for strong NP-completeness reductions to scheduling, packing, and layout models. The implementation in this repository uses one group-assignment variable per element, so the exported exact-search baseline is $O^*(3^n)$#footnote[This is the direct worst-case bound induced by the implementation's configuration space and matches the registered catalog expression `3^num_elements`; no sharper general exact bound was independently verified while preparing this entry.].
4607+
4608+
*Example.* Let $B = #bound$ and consider the #(sizes.len())-element instance with sizes $(#sizes.map(str).join(", "))$. The witness triples #groups.enumerate().map(((i, g)) => [$A_#(i+1) = {#g.map(str).join(", ")}$]).join([ and ]) both sum to $#bound$, so this instance is satisfiable.
4609+
4610+
#pred-commands(
4611+
"pred create --example ThreePartition -o three-partition.json",
4612+
"pred solve three-partition.json",
4613+
"pred evaluate three-partition.json --config " + config.map(str).join(","),
4614+
)
4615+
4616+
#align(center, table(
4617+
columns: 3,
4618+
align: center,
4619+
table.header([Triple], [Elements], [Sum]),
4620+
..groups.enumerate().map(((i, g)) => (
4621+
[$A_#(i+1)$], [$#(g.map(str).join(", "))$], [$#bound$],
4622+
)).flatten(),
4623+
))
4624+
]
4625+
]
4626+
}
4627+
45904628
#{
45914629
let x = load-model-example("SequencingWithReleaseTimesAndDeadlines")
45924630
let n = x.instance.lengths.len()

problemreductions-cli/src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ Flags by problem type:
247247
BinPacking --sizes, --capacity
248248
CapacityAssignment --capacities, --cost-matrix, --delay-matrix, --delay-budget
249249
SubsetSum --sizes, --target
250+
ThreePartition --sizes, --bound
250251
SumOfSquaresPartition --sizes, --num-groups
251252
ExpectedRetrievalCost --probabilities, --num-sectors
252253
PaintShop --sequence

problemreductions-cli/src/commands/create.rs

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use problemreductions::models::misc::{
2929
SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime,
3030
SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines,
3131
SequencingWithinIntervals, ShortestCommonSupersequence, StringToStringCorrection, SubsetSum,
32-
SumOfSquaresPartition, TimetableDesign,
32+
SumOfSquaresPartition, ThreePartition, TimetableDesign,
3333
};
3434
use problemreductions::models::BiconnectivityAugmentation;
3535
use problemreductions::prelude::*;
@@ -668,6 +668,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
668668
"--sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13"
669669
}
670670
"SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11",
671+
"ThreePartition" => "--sizes 4,5,6,4,6,5 --bound 15",
671672
"BoyceCoddNormalFormViolation" => {
672673
"--n 6 --sets \"0,1:2;2:3;3,4:5\" --target 0,1,2,3,4,5"
673674
}
@@ -2288,6 +2289,33 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
22882289
)
22892290
}
22902291

2292+
// ThreePartition
2293+
"ThreePartition" => {
2294+
let sizes_str = args.sizes.as_deref().ok_or_else(|| {
2295+
anyhow::anyhow!(
2296+
"ThreePartition requires --sizes and --bound\n\n\
2297+
Usage: pred create ThreePartition --sizes 4,5,6,4,6,5 --bound 15"
2298+
)
2299+
})?;
2300+
let bound = args.bound.ok_or_else(|| {
2301+
anyhow::anyhow!(
2302+
"ThreePartition requires --bound\n\n\
2303+
Usage: pred create ThreePartition --sizes 4,5,6,4,6,5 --bound 15"
2304+
)
2305+
})?;
2306+
let bound = u64::try_from(bound).map_err(|_| {
2307+
anyhow::anyhow!(
2308+
"ThreePartition requires a positive integer --bound\n\n\
2309+
Usage: pred create ThreePartition --sizes 4,5,6,4,6,5 --bound 15"
2310+
)
2311+
})?;
2312+
let sizes: Vec<u64> = util::parse_comma_list(sizes_str)?;
2313+
(
2314+
ser(ThreePartition::try_new(sizes, bound).map_err(anyhow::Error::msg)?)?,
2315+
resolved_variant.clone(),
2316+
)
2317+
}
2318+
22912319
// SumOfSquaresPartition
22922320
"SumOfSquaresPartition" => {
22932321
let sizes_str = args.sizes.as_deref().ok_or_else(|| {
@@ -6501,6 +6529,104 @@ mod tests {
65016529
assert!(example.contains("--requirement"));
65026530
}
65036531

6532+
#[test]
6533+
fn test_example_for_three_partition_mentions_sizes_and_bound() {
6534+
let example = example_for("ThreePartition", None);
6535+
assert!(example.contains("--sizes"));
6536+
assert!(example.contains("--bound"));
6537+
}
6538+
6539+
#[test]
6540+
fn test_create_three_partition_outputs_problem_json() {
6541+
let cli = Cli::try_parse_from([
6542+
"pred",
6543+
"create",
6544+
"ThreePartition",
6545+
"--sizes",
6546+
"4,5,6,4,6,5",
6547+
"--bound",
6548+
"15",
6549+
])
6550+
.expect("parse create command");
6551+
6552+
let args = match cli.command {
6553+
Commands::Create(args) => args,
6554+
_ => panic!("expected create command"),
6555+
};
6556+
6557+
let output_path = temp_output_path("three_partition_create");
6558+
let out = OutputConfig {
6559+
output: Some(output_path.clone()),
6560+
quiet: true,
6561+
json: false,
6562+
auto_json: false,
6563+
};
6564+
6565+
create(&args, &out).expect("create ThreePartition JSON");
6566+
6567+
let created: serde_json::Value =
6568+
serde_json::from_str(&fs::read_to_string(&output_path).unwrap()).unwrap();
6569+
fs::remove_file(output_path).ok();
6570+
6571+
assert_eq!(created["type"], "ThreePartition");
6572+
assert_eq!(
6573+
created["data"]["sizes"],
6574+
serde_json::json!([4, 5, 6, 4, 6, 5])
6575+
);
6576+
assert_eq!(created["data"]["bound"], 15);
6577+
}
6578+
6579+
#[test]
6580+
fn test_create_three_partition_requires_bound() {
6581+
let cli =
6582+
Cli::try_parse_from(["pred", "create", "ThreePartition", "--sizes", "4,5,6,4,6,5"])
6583+
.expect("parse create command");
6584+
6585+
let args = match cli.command {
6586+
Commands::Create(args) => args,
6587+
_ => panic!("expected create command"),
6588+
};
6589+
6590+
let out = OutputConfig {
6591+
output: None,
6592+
quiet: true,
6593+
json: false,
6594+
auto_json: false,
6595+
};
6596+
6597+
let err = create(&args, &out).unwrap_err().to_string();
6598+
assert!(err.contains("ThreePartition requires --bound"));
6599+
}
6600+
6601+
#[test]
6602+
fn test_create_three_partition_rejects_invalid_instance() {
6603+
let cli = Cli::try_parse_from([
6604+
"pred",
6605+
"create",
6606+
"ThreePartition",
6607+
"--sizes",
6608+
"4,5,6,4,6,5",
6609+
"--bound",
6610+
"14",
6611+
])
6612+
.expect("parse create command");
6613+
6614+
let args = match cli.command {
6615+
Commands::Create(args) => args,
6616+
_ => panic!("expected create command"),
6617+
};
6618+
6619+
let out = OutputConfig {
6620+
output: None,
6621+
quiet: true,
6622+
json: false,
6623+
auto_json: false,
6624+
};
6625+
6626+
let err = create(&args, &out).unwrap_err().to_string();
6627+
assert!(err.contains("must equal m * bound"));
6628+
}
6629+
65046630
#[test]
65056631
fn test_create_timetable_design_outputs_problem_json() {
65066632
let cli = Cli::try_parse_from([

problemreductions-cli/src/problem_name.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,8 @@ mod tests {
281281
assert_eq!(resolve_alias("MVC"), "MinimumVertexCover");
282282
assert_eq!(resolve_alias("SAT"), "Satisfiability");
283283
assert_eq!(resolve_alias("X3C"), "ExactCoverBy3Sets");
284+
assert_eq!(resolve_alias("3Partition"), "ThreePartition");
285+
assert_eq!(resolve_alias("3-partition"), "ThreePartition");
284286
// 3SAT is no longer a registered alias (removed to avoid confusion with KSatisfiability/KN)
285287
assert_eq!(resolve_alias("3SAT"), "3SAT"); // pass-through
286288
assert_eq!(resolve_alias("QUBO"), "QUBO");

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ pub mod prelude {
7878
SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime,
7979
SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines,
8080
SequencingWithinIntervals, ShortestCommonSupersequence, StackerCrane, StaffScheduling,
81-
StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term, TimetableDesign,
81+
StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term, ThreePartition,
82+
TimetableDesign,
8283
};
8384
pub use crate::models::set::{
8485
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,

src/models/misc/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ mod staff_scheduling;
6868
pub(crate) mod string_to_string_correction;
6969
mod subset_sum;
7070
pub(crate) mod sum_of_squares_partition;
71+
mod three_partition;
7172
mod timetable_design;
7273

7374
pub use additional_key::AdditionalKey;
@@ -106,6 +107,7 @@ pub use staff_scheduling::StaffScheduling;
106107
pub use string_to_string_correction::StringToStringCorrection;
107108
pub use subset_sum::SubsetSum;
108109
pub use sum_of_squares_partition::SumOfSquaresPartition;
110+
pub use three_partition::ThreePartition;
109111
pub use timetable_design::TimetableDesign;
110112

111113
#[cfg(feature = "example-db")]
@@ -146,5 +148,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
146148
specs.extend(bin_packing::canonical_model_example_specs());
147149
specs.extend(knapsack::canonical_model_example_specs());
148150
specs.extend(subset_sum::canonical_model_example_specs());
151+
specs.extend(three_partition::canonical_model_example_specs());
149152
specs
150153
}

0 commit comments

Comments
 (0)