Skip to content

Commit 5441368

Browse files
GiggleLiuisPANN
andauthored
Fix #440: [Model] GroupingBySwapping (#758)
* Add plan for #440: [Model] GroupingBySwapping * Implement #440: [Model] GroupingBySwapping * chore: remove plan file after implementation --------- Co-authored-by: Xiwei Pan <90967972+isPANN@users.noreply.github.com>
1 parent 3ee17e6 commit 5441368

10 files changed

Lines changed: 495 additions & 16 deletions

File tree

docs/paper/reductions.typ

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
"IntegralFlowWithMultipliers": [Integral Flow With Multipliers],
146146
"MinMaxMulticenter": [Min-Max Multicenter],
147147
"FlowShopScheduling": [Flow Shop Scheduling],
148+
"GroupingBySwapping": [Grouping by Swapping],
148149
"MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets],
149150
"MinimumDummyActivitiesPert": [Minimum Dummy Activities in PERT Networks],
150151
"MinimumSumMulticenter": [Minimum Sum Multicenter],
@@ -4381,6 +4382,67 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
43814382
]
43824383
}
43834384

4385+
#{
4386+
let x = load-model-example("GroupingBySwapping")
4387+
let source = x.instance.string
4388+
let alpha-size = x.instance.alphabet_size
4389+
let budget = x.instance.budget
4390+
let config = x.optimal_config
4391+
let alpha-map = range(alpha-size).map(i => str.from-unicode(97 + i))
4392+
let fmt-str(s) = s.map(c => alpha-map.at(c)).join("")
4393+
let source-str = fmt-str(source)
4394+
let step1 = (0, 1, 0, 2, 1, 2)
4395+
let step2 = (0, 0, 1, 2, 1, 2)
4396+
let step3 = (0, 0, 1, 1, 2, 2)
4397+
let step3-str = fmt-str(step3)
4398+
[
4399+
#problem-def("GroupingBySwapping")[
4400+
Given a finite alphabet $Sigma$, a string $x in Sigma^*$, and a positive integer $K$, determine whether there exists a sequence of at most $K$ adjacent symbol interchanges that transforms $x$ into a string $y in Sigma^*$ in which every symbol $a in Sigma$ appears in a single contiguous block. Equivalently, $y$ contains no subsequence $a b a$ with distinct $a, b in Sigma$.
4401+
][
4402+
Grouping by Swapping is the storage-and-retrieval problem SR21 in Garey and Johnson @garey1979. It asks whether a string can be locally reorganized, using only adjacent transpositions, until equal symbols coalesce into blocks. The implementation in this crate uses a fixed-length swap program with one slot per allowed operation, so the direct brute-force search explores $O(|x|^K)$ configurations.#footnote[This is the exact search bound induced by the fixed-length witness encoding implemented in the codebase; no sharper exact worst-case bound is claimed here.]
4403+
4404+
*Example.* Let $Sigma = {#alpha-map.join(", ")}$, $x = #source-str$, and $K = #budget$. The configuration $p = (#config.map(str).join(", "))$ performs adjacent swaps at positions $(2, 3)$, $(1, 2)$, and $(3, 4)$, then uses two trailing no-op slots. The resulting string is $y = #step3-str$, so every symbol now appears in one contiguous block and the verifier returns YES.
4405+
4406+
#pred-commands(
4407+
"pred create --example " + problem-spec(x) + " -o grouping-by-swapping.json",
4408+
"pred solve grouping-by-swapping.json --solver brute-force",
4409+
"pred evaluate grouping-by-swapping.json --config " + x.optimal_config.map(str).join(","),
4410+
)
4411+
4412+
#figure({
4413+
let blue = graph-colors.at(0)
4414+
let cell(ch, highlight: false) = {
4415+
let fill = if highlight { blue.transparentize(72%) } else { white }
4416+
box(width: 0.55cm, height: 0.55cm, fill: fill, stroke: 0.5pt + luma(120),
4417+
align(center + horizon, text(9pt, weight: "bold", ch)))
4418+
}
4419+
align(center, stack(dir: ttb, spacing: 0.45cm,
4420+
stack(dir: ltr, spacing: 0pt,
4421+
box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[$x: quad$])),
4422+
..source.map(c => cell(alpha-map.at(c))),
4423+
),
4424+
stack(dir: ltr, spacing: 0pt,
4425+
box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[swap$(2,3)$: quad])),
4426+
..step1.enumerate().map(((i, c)) => cell(alpha-map.at(c), highlight: i == 2 or i == 3)),
4427+
),
4428+
stack(dir: ltr, spacing: 0pt,
4429+
box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[swap$(1,2)$: quad])),
4430+
..step2.enumerate().map(((i, c)) => cell(alpha-map.at(c), highlight: i == 1 or i == 2)),
4431+
),
4432+
stack(dir: ltr, spacing: 0pt,
4433+
box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[swap$(3,4)$: quad])),
4434+
..step3.enumerate().map(((i, c)) => cell(alpha-map.at(c), highlight: i == 3 or i == 4)),
4435+
),
4436+
))
4437+
},
4438+
caption: [Grouping by Swapping on $x = #source-str$: three effective adjacent swaps turn the alternating string into $y = #step3-str$. The remaining two slots in $p = (#config.map(str).join(", "))$ are no-ops at position 5.],
4439+
) <fig:grouping-by-swapping>
4440+
4441+
The final row has exactly one block of $a$, one block of $b$, and one block of $c$, so it satisfies the grouping constraint within the allotted budget.
4442+
]
4443+
]
4444+
}
4445+
43844446
#{
43854447
let x = load-model-example("LongestCommonSubsequence")
43864448
let strings = x.instance.strings

problemreductions-cli/src/cli.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ Flags by problem type:
287287
AdditionalKey --num-attributes, --dependencies, --relation-attrs [--known-keys]
288288
ConsistencyOfDatabaseFrequencyTables --num-objects, --attribute-domains, --frequency-tables [--known-values]
289289
SubgraphIsomorphism --graph (host), --pattern (pattern)
290+
GroupingBySwapping --string, --bound [--alphabet-size]
290291
LCS --strings, --bound [--alphabet-size]
291292
FAS --arcs [--weights] [--num-vertices]
292293
FVS --arcs [--weights] [--num-vertices]
@@ -331,6 +332,7 @@ Examples:
331332
pred create GeneralizedHex --graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5
332333
pred create IntegralFlowWithMultipliers --arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2
333334
pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10
335+
pred create GroupingBySwapping --string \"0,1,2,0,1,2\" --bound 5 | pred solve - --solver brute-force
334336
pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | pred solve - --solver brute-force
335337
pred create MIS/KingsSubgraph --positions \"0,0;1,0;1,1;0,1\"
336338
pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5
@@ -551,7 +553,7 @@ pub struct CreateArgs {
551553
/// Required edge indices for RuralPostman (comma-separated, e.g., "0,2,4")
552554
#[arg(long)]
553555
pub required_edges: Option<String>,
554-
/// Bound parameter (lower bound for LongestCircuit; upper or length bound for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleCopyFileAllocation, MultipleChoiceBranching, OptimalLinearArrangement, RootedTreeArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection)
556+
/// Bound parameter (lower bound for LongestCircuit; upper or length bound for BoundedComponentSpanningForest, GroupingBySwapping, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleCopyFileAllocation, MultipleChoiceBranching, OptimalLinearArrangement, RootedTreeArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection)
555557
#[arg(long, allow_hyphen_values = true)]
556558
pub bound: Option<i64>,
557559
/// Upper bound on expected retrieval latency for ExpectedRetrievalCost
@@ -578,6 +580,9 @@ pub struct CreateArgs {
578580
/// Input strings for LCS (e.g., "ABAC;BACA" or "0,1,0;1,0,1") or SCS (e.g., "0,1,2;1,2,0")
579581
#[arg(long)]
580582
pub strings: Option<String>,
583+
/// Input string for GroupingBySwapping (comma-separated symbol indices, e.g., "0,1,2,0,1,2")
584+
#[arg(long)]
585+
pub string: Option<String>,
581586
/// Task costs for SequencingToMinimizeMaximumCumulativeCost (comma-separated, e.g., "2,-1,3,-2,1,-3")
582587
#[arg(long, allow_hyphen_values = true)]
583588
pub costs: Option<String>,
@@ -671,7 +676,7 @@ pub struct CreateArgs {
671676
/// Task availability rows for TimetableDesign (semicolon-separated 0/1 rows)
672677
#[arg(long)]
673678
pub task_avail: Option<String>,
674-
/// Alphabet size for LCS, SCS, StringToStringCorrection, or TwoDimensionalConsecutiveSets (optional; inferred from the input strings if omitted)
679+
/// Alphabet size for GroupingBySwapping, LCS, SCS, StringToStringCorrection, or TwoDimensionalConsecutiveSets (optional; inferred from the input strings if omitted)
675680
#[arg(long)]
676681
pub alphabet_size: Option<usize>,
677682

@@ -736,6 +741,7 @@ Examples:
736741
pred solve reduced.json # solve a reduction bundle
737742
pred solve reduced.json -o solution.json # save result to file
738743
pred create MIS --graph 0-1,1-2 | pred solve - # read from stdin when an ILP path exists
744+
pred create GroupingBySwapping --string \"0,1,2,0,1,2\" --bound 5 | pred solve - --solver brute-force
739745
pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | pred solve - --solver brute-force
740746
pred create TwoDimensionalConsecutiveSets --alphabet-size 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\" | pred solve - --solver brute-force
741747
pred solve problem.json --timeout 10 # abort after 10 seconds
@@ -751,8 +757,9 @@ Solve via explicit reduction:
751757
Input: a problem JSON from `pred create`, or a reduction bundle from `pred reduce`.
752758
When given a bundle, the target is solved and the solution is mapped back to the source.
753759
The ILP solver auto-reduces non-ILP problems before solving.
754-
Problems without an ILP reduction path, such as `LengthBoundedDisjointPaths`,
755-
`MinMaxMulticenter`, and `StringToStringCorrection`, currently need `--solver brute-force`.
760+
Problems without an ILP reduction path, such as `GroupingBySwapping`,
761+
`LengthBoundedDisjointPaths`, `MinMaxMulticenter`, and `StringToStringCorrection`,
762+
currently need `--solver brute-force`.
756763
757764
ILP backend (default: HiGHS). To use a different backend:
758765
cargo install problemreductions-cli --features coin-cbc

problemreductions-cli/src/commands/create.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use problemreductions::models::graph::{
2222
use problemreductions::models::misc::{
2323
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation,
2424
ConjunctiveBooleanQuery, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation,
25-
ExpectedRetrievalCost, FlowShopScheduling, FrequencyTable, KnownValue,
25+
ExpectedRetrievalCost, FlowShopScheduling, FrequencyTable, GroupingBySwapping, KnownValue,
2626
LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop,
2727
PartiallyOrderedKnapsack, QueryArg, RectilinearPictureCompression,
2828
ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines,
@@ -125,6 +125,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
125125
&& args.delay_budget.is_none()
126126
&& args.pattern.is_none()
127127
&& args.strings.is_none()
128+
&& args.string.is_none()
128129
&& args.costs.is_none()
129130
&& args.arc_costs.is_none()
130131
&& args.arcs.is_none()
@@ -680,6 +681,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
680681
"LongestCommonSubsequence" => {
681682
"--strings \"010110;100101;001011\" --bound 3 --alphabet-size 2"
682683
}
684+
"GroupingBySwapping" => "--string \"0,1,2,0,1,2\" --bound 5",
683685
"MinimumCardinalityKey" => {
684686
"--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --bound 2"
685687
}
@@ -809,6 +811,7 @@ fn help_flag_hint(
809811
("LongestCommonSubsequence", "strings") => {
810812
"raw strings: \"ABAC;BACA\" or symbol lists: \"0,1,0;1,0,1\""
811813
}
814+
("GroupingBySwapping", "string") => "symbol list: \"0,1,2,0,1,2\"",
812815
("ShortestCommonSupersequence", "strings") => "symbol lists: \"0,1,2;1,2,0\"",
813816
("MultipleChoiceBranching", "partition") => "semicolon-separated groups: \"0,1;2,3\"",
814817
("IntegralFlowHomologousArcs", "homologous_pairs") => {
@@ -2942,6 +2945,56 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
29422945
)
29432946
}
29442947

2948+
// GroupingBySwapping
2949+
"GroupingBySwapping" => {
2950+
let usage =
2951+
"Usage: pred create GroupingBySwapping --string \"0,1,2,0,1,2\" --bound 5 [--alphabet-size 3]";
2952+
let string_str = args.string.as_deref().ok_or_else(|| {
2953+
anyhow::anyhow!("GroupingBySwapping requires --string\n\n{usage}")
2954+
})?;
2955+
let bound = parse_nonnegative_usize_bound(
2956+
args.bound.ok_or_else(|| {
2957+
anyhow::anyhow!("GroupingBySwapping requires --bound\n\n{usage}")
2958+
})?,
2959+
"GroupingBySwapping",
2960+
usage,
2961+
)?;
2962+
2963+
let string = if string_str.trim().is_empty() {
2964+
Vec::new()
2965+
} else {
2966+
string_str
2967+
.split(',')
2968+
.map(|value| {
2969+
value
2970+
.trim()
2971+
.parse::<usize>()
2972+
.context("invalid symbol index")
2973+
})
2974+
.collect::<Result<Vec<_>>>()?
2975+
};
2976+
let inferred = string.iter().copied().max().map_or(0, |value| value + 1);
2977+
let alphabet_size = args.alphabet_size.unwrap_or(inferred);
2978+
anyhow::ensure!(
2979+
alphabet_size >= inferred,
2980+
"--alphabet-size {} is smaller than max symbol + 1 ({}) in the input string",
2981+
alphabet_size,
2982+
inferred
2983+
);
2984+
anyhow::ensure!(
2985+
alphabet_size > 0 || string.is_empty(),
2986+
"GroupingBySwapping requires a positive alphabet for non-empty strings.\n\n{usage}"
2987+
);
2988+
anyhow::ensure!(
2989+
!string.is_empty() || bound == 0,
2990+
"GroupingBySwapping requires --bound 0 when --string is empty.\n\n{usage}"
2991+
);
2992+
(
2993+
ser(GroupingBySwapping::new(alphabet_size, string, bound))?,
2994+
resolved_variant.clone(),
2995+
)
2996+
}
2997+
29452998
// ClosestVectorProblem
29462999
"ClosestVectorProblem" => {
29473000
let basis_str = args.basis.as_deref().ok_or_else(|| {
@@ -7245,6 +7298,7 @@ mod tests {
72457298
delay_budget: None,
72467299
pattern: None,
72477300
strings: None,
7301+
string: None,
72487302
arc_costs: None,
72497303
arcs: None,
72507304
values: None,

problemreductions-cli/src/problem_name.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ pub fn resolve_alias(input: &str) -> String {
1717
if input.eq_ignore_ascii_case("UndirectedFlowLowerBounds") {
1818
return "UndirectedFlowLowerBounds".to_string();
1919
}
20+
if input.eq_ignore_ascii_case("GroupingBySwapping") {
21+
return "GroupingBySwapping".to_string();
22+
}
2023
if let Some(pt) = problemreductions::registry::find_problem_type_by_alias(input) {
2124
return pt.canonical_name.to_string();
2225
}

problemreductions-cli/tests/cli_tests.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3548,6 +3548,69 @@ fn test_create_string_to_string_correction_rejects_negative_bound() {
35483548
assert!(stderr.contains("nonnegative --bound"), "stderr: {stderr}");
35493549
}
35503550

3551+
#[test]
3552+
fn test_create_grouping_by_swapping() {
3553+
let output = pred()
3554+
.args([
3555+
"create",
3556+
"GroupingBySwapping",
3557+
"--string",
3558+
"0,1,2,0,1,2",
3559+
"--bound",
3560+
"5",
3561+
])
3562+
.output()
3563+
.unwrap();
3564+
assert!(
3565+
output.status.success(),
3566+
"stderr: {}",
3567+
String::from_utf8_lossy(&output.stderr)
3568+
);
3569+
let stdout = String::from_utf8(output.stdout).unwrap();
3570+
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
3571+
assert_eq!(json["type"], "GroupingBySwapping");
3572+
assert_eq!(json["data"]["alphabet_size"], 3);
3573+
assert_eq!(
3574+
json["data"]["string"],
3575+
serde_json::json!([0, 1, 2, 0, 1, 2])
3576+
);
3577+
assert_eq!(json["data"]["budget"], 5);
3578+
}
3579+
3580+
#[test]
3581+
fn test_create_model_example_grouping_by_swapping() {
3582+
let output = pred()
3583+
.args(["create", "--example", "GroupingBySwapping"])
3584+
.output()
3585+
.unwrap();
3586+
assert!(
3587+
output.status.success(),
3588+
"stderr: {}",
3589+
String::from_utf8_lossy(&output.stderr)
3590+
);
3591+
let stdout = String::from_utf8(output.stdout).unwrap();
3592+
let json: serde_json::Value = serde_json::from_str(&stdout).unwrap();
3593+
assert_eq!(json["type"], "GroupingBySwapping");
3594+
assert_eq!(json["data"]["alphabet_size"], 3);
3595+
assert_eq!(
3596+
json["data"]["string"],
3597+
serde_json::json!([0, 1, 2, 0, 1, 2])
3598+
);
3599+
assert_eq!(json["data"]["budget"], 5);
3600+
}
3601+
3602+
#[test]
3603+
fn test_create_grouping_by_swapping_help_uses_cli_flags() {
3604+
let output = pred()
3605+
.args(["create", "GroupingBySwapping"])
3606+
.output()
3607+
.unwrap();
3608+
assert!(!output.status.success());
3609+
let stderr = String::from_utf8_lossy(&output.stderr);
3610+
assert!(stderr.contains("--string"), "stderr: {stderr}");
3611+
assert!(stderr.contains("--bound"), "stderr: {stderr}");
3612+
}
3613+
35513614
#[test]
35523615
fn test_create_spinglass() {
35533616
let output_file = std::env::temp_dir().join("pred_test_create_sg.json");

src/lib.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,14 @@ pub mod prelude {
7171
pub use crate::models::misc::{
7272
AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation,
7373
ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, ConsistencyOfDatabaseFrequencyTables,
74-
EnsembleComputation, ExpectedRetrievalCost, Factoring, FlowShopScheduling, Knapsack,
75-
LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop,
76-
Partition, QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling,
77-
SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost,
78-
SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness,
79-
SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals,
80-
ShortestCommonSupersequence, StackerCrane, StaffScheduling, StringToStringCorrection,
81-
SubsetSum, SumOfSquaresPartition, Term, TimetableDesign,
74+
EnsembleComputation, ExpectedRetrievalCost, Factoring, FlowShopScheduling,
75+
GroupingBySwapping, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing,
76+
MultiprocessorScheduling, PaintShop, Partition, QueryArg, RectilinearPictureCompression,
77+
ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines,
78+
SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime,
79+
SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines,
80+
SequencingWithinIntervals, ShortestCommonSupersequence, StackerCrane, StaffScheduling,
81+
StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term, TimetableDesign,
8282
};
8383
pub use crate::models::set::{
8484
ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking,

0 commit comments

Comments
 (0)