seq-len-balance 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,30 @@
1
+ # Generated by Cargo
2
+ # will have compiled files and executables
3
+ debug
4
+ target
5
+
6
+ # These are backup files generated by rustfmt
7
+ **/*.rs.bk
8
+
9
+ # MSVC Windows builds of rustc generate these, which store debugging information
10
+ *.pdb
11
+
12
+ # Generated by cargo mutants
13
+ # Contains mutation testing data
14
+ **/mutants.out*/
15
+
16
+ # RustRover
17
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
18
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
19
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
20
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
21
+ #.idea/
22
+
23
+
24
+ # Added by cargo
25
+
26
+ /target
27
+
28
+ .ipynb_checkpoints/
29
+
30
+ __pycache__
@@ -0,0 +1 @@
1
+ 3.11.3
@@ -0,0 +1,158 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "autocfg"
7
+ version = "1.5.0"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
10
+
11
+ [[package]]
12
+ name = "heck"
13
+ version = "0.5.0"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
16
+
17
+ [[package]]
18
+ name = "libc"
19
+ version = "0.2.182"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
22
+
23
+ [[package]]
24
+ name = "num-traits"
25
+ version = "0.2.19"
26
+ source = "registry+https://github.com/rust-lang/crates.io-index"
27
+ checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
28
+ dependencies = [
29
+ "autocfg",
30
+ ]
31
+
32
+ [[package]]
33
+ name = "once_cell"
34
+ version = "1.21.3"
35
+ source = "registry+https://github.com/rust-lang/crates.io-index"
36
+ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
37
+
38
+ [[package]]
39
+ name = "ordered-float"
40
+ version = "5.1.0"
41
+ source = "registry+https://github.com/rust-lang/crates.io-index"
42
+ checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d"
43
+ dependencies = [
44
+ "num-traits",
45
+ ]
46
+
47
+ [[package]]
48
+ name = "portable-atomic"
49
+ version = "1.13.1"
50
+ source = "registry+https://github.com/rust-lang/crates.io-index"
51
+ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
52
+
53
+ [[package]]
54
+ name = "proc-macro2"
55
+ version = "1.0.106"
56
+ source = "registry+https://github.com/rust-lang/crates.io-index"
57
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
58
+ dependencies = [
59
+ "unicode-ident",
60
+ ]
61
+
62
+ [[package]]
63
+ name = "pyo3"
64
+ version = "0.28.2"
65
+ source = "registry+https://github.com/rust-lang/crates.io-index"
66
+ checksum = "cf85e27e86080aafd5a22eae58a162e133a589551542b3e5cee4beb27e54f8e1"
67
+ dependencies = [
68
+ "libc",
69
+ "once_cell",
70
+ "portable-atomic",
71
+ "pyo3-build-config",
72
+ "pyo3-ffi",
73
+ "pyo3-macros",
74
+ ]
75
+
76
+ [[package]]
77
+ name = "pyo3-build-config"
78
+ version = "0.28.2"
79
+ source = "registry+https://github.com/rust-lang/crates.io-index"
80
+ checksum = "8bf94ee265674bf76c09fa430b0e99c26e319c945d96ca0d5a8215f31bf81cf7"
81
+ dependencies = [
82
+ "target-lexicon",
83
+ ]
84
+
85
+ [[package]]
86
+ name = "pyo3-ffi"
87
+ version = "0.28.2"
88
+ source = "registry+https://github.com/rust-lang/crates.io-index"
89
+ checksum = "491aa5fc66d8059dd44a75f4580a2962c1862a1c2945359db36f6c2818b748dc"
90
+ dependencies = [
91
+ "libc",
92
+ "pyo3-build-config",
93
+ ]
94
+
95
+ [[package]]
96
+ name = "pyo3-macros"
97
+ version = "0.28.2"
98
+ source = "registry+https://github.com/rust-lang/crates.io-index"
99
+ checksum = "f5d671734e9d7a43449f8480f8b38115df67bef8d21f76837fa75ee7aaa5e52e"
100
+ dependencies = [
101
+ "proc-macro2",
102
+ "pyo3-macros-backend",
103
+ "quote",
104
+ "syn",
105
+ ]
106
+
107
+ [[package]]
108
+ name = "pyo3-macros-backend"
109
+ version = "0.28.2"
110
+ source = "registry+https://github.com/rust-lang/crates.io-index"
111
+ checksum = "22faaa1ce6c430a1f71658760497291065e6450d7b5dc2bcf254d49f66ee700a"
112
+ dependencies = [
113
+ "heck",
114
+ "proc-macro2",
115
+ "pyo3-build-config",
116
+ "quote",
117
+ "syn",
118
+ ]
119
+
120
+ [[package]]
121
+ name = "quote"
122
+ version = "1.0.45"
123
+ source = "registry+https://github.com/rust-lang/crates.io-index"
124
+ checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
125
+ dependencies = [
126
+ "proc-macro2",
127
+ ]
128
+
129
+ [[package]]
130
+ name = "seq-len-balance-rs"
131
+ version = "0.1.0"
132
+ dependencies = [
133
+ "ordered-float",
134
+ "pyo3",
135
+ ]
136
+
137
+ [[package]]
138
+ name = "syn"
139
+ version = "2.0.117"
140
+ source = "registry+https://github.com/rust-lang/crates.io-index"
141
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
142
+ dependencies = [
143
+ "proc-macro2",
144
+ "quote",
145
+ "unicode-ident",
146
+ ]
147
+
148
+ [[package]]
149
+ name = "target-lexicon"
150
+ version = "0.13.5"
151
+ source = "registry+https://github.com/rust-lang/crates.io-index"
152
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
153
+
154
+ [[package]]
155
+ name = "unicode-ident"
156
+ version = "1.0.24"
157
+ source = "registry+https://github.com/rust-lang/crates.io-index"
158
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "seq-len-balance-rs"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ readme = "README.md"
6
+
7
+ [lib]
8
+ name = "seq_len_balance"
9
+ crate-type = ["cdylib", "rlib"]
10
+
11
+ [[bin]]
12
+ name = "seq-len-balance-rs"
13
+ path = "src/main.rs"
14
+
15
+ [dependencies]
16
+ pyo3 = { version = "0.28", features = ["extension-module"] }
17
+ ordered-float = "5"
@@ -0,0 +1,40 @@
1
+ # ---- Project metadata ----
2
+ PACKAGE_NAME := seq-len-balance-rs
3
+ PYTHON ?= python3
4
+ BUILD_DIR := target/wheels
5
+
6
+ # ---- Commands ----
7
+
8
+ .PHONY: help build develop publish clean
9
+
10
+ help:
11
+ @echo "Available commands:"
12
+ @echo " make build - Build release wheels (and sdist)"
13
+ @echo " make develop - Install locally in dev mode"
14
+ @echo " make publish - Publish to PyPI using maturin"
15
+ @echo " make clean - Remove build artifacts"
16
+
17
+ build:
18
+ @echo "🚀 Building release wheels..."
19
+ maturin build --release --sdist
20
+ @echo "✅ Wheels generated in $(BUILD_DIR)/"
21
+
22
+ develop:
23
+ @echo "💡 Installing in development mode..."
24
+ maturin develop
25
+
26
+ publish: build
27
+ @echo "📦 Publishing $(PACKAGE_NAME) to PyPI..."
28
+ @if [ -z "$$MATURIN_PASSWORD" ]; then \
29
+ echo "❌ Error: MATURIN_PASSWORD (your PyPI token) is not set."; \
30
+ echo " export MATURIN_USERNAME=__token__"; \
31
+ echo " export MATURIN_PASSWORD=pypi-xxxxxx"; \
32
+ exit 1; \
33
+ fi
34
+ maturin publish --skip-existing
35
+ @echo "✅ Published successfully!"
36
+
37
+ clean:
38
+ @echo "🧹 Cleaning build artifacts..."
39
+ cargo clean
40
+ rm -rf $(BUILD_DIR) dist build *.egg-info
@@ -0,0 +1,5 @@
1
+ Metadata-Version: 2.4
2
+ Name: seq-len-balance
3
+ Version: 0.1.0
4
+ Summary: Fast sequence-length balancing: KK partition, FFD, and BFD bin packing
5
+ Requires-Python: >=3.11
@@ -0,0 +1,48 @@
1
+ # seq-len-balance-rs
2
+
3
+ Fast sequence-length balancing: KK partition, FFD, and BFD bin packing (Rust + Python).
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ uv sync
9
+ pip install maturin
10
+ maturin develop
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```python
16
+ from seq_len_balance import kk, ffd, bfd
17
+
18
+ print(kk([8, 7, 6, 5, 4], 2)) # -> [[8, 5], [7, 4], [6]]
19
+ print(ffd([6, 3, 4, 5, 2, 7, 1], 10)) # -> [[7, 3], [6, 4], [5, 2, 1]]
20
+ print(bfd([6, 3, 4, 5, 2, 7, 1], 10)) # -> [[7, 2, 1], [6, 4], [5, 3]]
21
+ ```
22
+
23
+ ## Benchmark
24
+
25
+ ```bash
26
+ uv sync --group benchmark
27
+ uv run python test_kk.py
28
+ ```
29
+
30
+ ```
31
+ =================================================================
32
+ KK (greedy LPT) benchmark k=4 repeats=3
33
+ =================================================================
34
+ n= 10,000 Python: 2.4 ms Rust: 0.4 ms
35
+ n= 100,000 Python: 25.5 ms Rust: 2.9 ms
36
+ n= 500,000 Python: 131.4 ms Rust: 16.3 ms
37
+ n=1,000,000 Python: 294.7 ms Rust: 33.1 ms
38
+ n=2,000,000 Python: 663.9 ms Rust: 69.6 ms
39
+ n=4,000,000 Python: 1405.9 ms Rust: 144.9 ms
40
+ n=10,000,000 Python: 3577.8 ms Rust: 377.6 ms
41
+ ```
42
+
43
+ ## Rust in Jupyter (optional)
44
+
45
+ ```bash
46
+ cargo install evcxr_jupyter
47
+ evcxr_jupyter --install
48
+ ```
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["maturin>=1.0,<2.0"]
3
+ build-backend = "maturin"
4
+
5
+ [project]
6
+ name = "seq-len-balance"
7
+ version = "0.1.0"
8
+ requires-python = ">=3.11"
9
+ description = "Fast sequence-length balancing: KK partition, FFD, and BFD bin packing"
10
+ dependencies = []
11
+
12
+ [tool.maturin]
13
+ features = ["pyo3/extension-module"]
14
+
15
+ [dependency-groups]
16
+ benchmark = [
17
+ "matplotlib>=3.10.8",
18
+ "pytest>=9.0.2",
19
+ ]
20
+
21
+ [tool.uv]
22
+ dev-dependencies = [
23
+ "matplotlib>=3.10.8",
24
+ "maturin>=1.0,<2.0",
25
+ "numpy>=2.4.2",
26
+ ]
@@ -0,0 +1,173 @@
1
+ use std::fmt;
2
+ use std::ops::{Add, Sub};
3
+
4
+ /// Unified result of a bin packing or partitioning algorithm.
5
+ ///
6
+ /// - `capacity`: `Some(limit)` for bin packing (FFD/BFD), `None` for unconstrained partitioning (KK).
7
+ /// - `remaining[i]`: unused space in bin i; zero for partitioning results.
8
+ /// - `sums[i]`: total item weight assigned to bin/group i.
9
+ #[derive(Debug)]
10
+ pub struct BinPacking<T> {
11
+ pub bins: Vec<Vec<T>>,
12
+ pub sums: Vec<T>,
13
+ pub remaining: Vec<T>,
14
+ pub capacity: Option<T>,
15
+ }
16
+
17
+ impl<T> BinPacking<T>
18
+ where
19
+ T: Ord + Clone + Sub<Output = T>,
20
+ {
21
+ /// Number of bins / groups used.
22
+ pub fn num_bins(&self) -> usize {
23
+ self.bins.len()
24
+ }
25
+
26
+ /// Difference between the largest and smallest bin sum.
27
+ pub fn imbalance(&self) -> T {
28
+ let max = self.sums.iter().max().unwrap().clone();
29
+ let min = self.sums.iter().min().unwrap().clone();
30
+ max - min
31
+ }
32
+
33
+ /// Total wasted space across all bins (sum of `remaining`).
34
+ /// For partitioning results this is always zero.
35
+ pub fn waste(&self) -> T
36
+ where
37
+ T: Default + Add<Output = T>,
38
+ {
39
+ self.remaining
40
+ .iter()
41
+ .cloned()
42
+ .fold(T::default(), |acc, r| acc + r)
43
+ }
44
+ }
45
+
46
+ impl<T: fmt::Display> fmt::Display for BinPacking<T> {
47
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48
+ for (i, ((bin, sum), rem)) in self
49
+ .bins
50
+ .iter()
51
+ .zip(self.sums.iter())
52
+ .zip(self.remaining.iter())
53
+ .enumerate()
54
+ {
55
+ match &self.capacity {
56
+ Some(_) => write!(f, "Bin {i} (sum={sum}, remaining={rem}): [")?,
57
+ None => write!(f, "Bin {i} (sum={sum}): [")?,
58
+ }
59
+ for (j, item) in bin.iter().enumerate() {
60
+ if j > 0 {
61
+ write!(f, ", ")?;
62
+ }
63
+ write!(f, "{item}")?;
64
+ }
65
+ writeln!(f, "]")?;
66
+ }
67
+ Ok(())
68
+ }
69
+ }
70
+
71
+ /// First Fit Decreasing bin packing.
72
+ ///
73
+ /// Sorts items descending, then for each item tries bins in order (0, 1, 2, ...)
74
+ /// and places it in the **first** bin with enough remaining capacity.
75
+ /// Opens a new bin only when no existing bin fits.
76
+ /// Capacity defaults to the sum of all items when not provided.
77
+ pub fn first_fit_decreasing<T>(mut items: Vec<T>, capacity: Option<T>) -> BinPacking<T>
78
+ where
79
+ T: Ord + Add<Output = T> + Sub<Output = T> + Default + Clone,
80
+ {
81
+ let capacity = capacity
82
+ .unwrap_or_else(|| items.iter().cloned().fold(T::default(), |acc, x| acc + x));
83
+
84
+ items.sort_unstable_by(|a, b| b.cmp(a));
85
+
86
+ let mut bins: Vec<Vec<T>> = Vec::new();
87
+ let mut sums: Vec<T> = Vec::new();
88
+ let mut remaining: Vec<T> = Vec::new();
89
+
90
+ for item in items {
91
+ assert!(item <= capacity, "item exceeds bin capacity");
92
+
93
+ // First bin with enough room.
94
+ let target = bins
95
+ .iter()
96
+ .enumerate()
97
+ .find(|(i, _)| remaining[*i] >= item)
98
+ .map(|(i, _)| i);
99
+
100
+ match target {
101
+ Some(i) => {
102
+ remaining[i] = remaining[i].clone() - item.clone();
103
+ sums[i] = sums[i].clone() + item.clone();
104
+ bins[i].push(item);
105
+ }
106
+ None => {
107
+ remaining.push(capacity.clone() - item.clone());
108
+ sums.push(item.clone());
109
+ bins.push(vec![item]);
110
+ }
111
+ }
112
+ }
113
+
114
+ BinPacking {
115
+ bins,
116
+ sums,
117
+ remaining,
118
+ capacity: Some(capacity),
119
+ }
120
+ }
121
+
122
+ /// Best Fit Decreasing bin packing.
123
+ ///
124
+ /// Sorts items descending, then for each item finds the bin with the
125
+ /// **minimum remaining capacity** that still fits the item (tightest fit).
126
+ /// This leaves larger gaps available for larger future items.
127
+ /// Opens a new bin only when no existing bin fits.
128
+ /// Capacity defaults to the sum of all items when not provided.
129
+ pub fn best_fit_decreasing<T>(mut items: Vec<T>, capacity: Option<T>) -> BinPacking<T>
130
+ where
131
+ T: Ord + Add<Output = T> + Sub<Output = T> + Default + Clone,
132
+ {
133
+ let capacity = capacity
134
+ .unwrap_or_else(|| items.iter().cloned().fold(T::default(), |acc, x| acc + x));
135
+
136
+ items.sort_unstable_by(|a, b| b.cmp(a));
137
+
138
+ let mut bins: Vec<Vec<T>> = Vec::new();
139
+ let mut sums: Vec<T> = Vec::new();
140
+ let mut remaining: Vec<T> = Vec::new();
141
+
142
+ for item in items {
143
+ assert!(item <= capacity, "item exceeds bin capacity");
144
+
145
+ // Bin with the smallest remaining space that still fits (tightest fit).
146
+ let best_idx = remaining
147
+ .iter()
148
+ .enumerate()
149
+ .filter(|(_, r)| *r >= &item)
150
+ .min_by(|(_, a), (_, b)| a.cmp(b))
151
+ .map(|(i, _)| i);
152
+
153
+ match best_idx {
154
+ Some(i) => {
155
+ remaining[i] = remaining[i].clone() - item.clone();
156
+ sums[i] = sums[i].clone() + item.clone();
157
+ bins[i].push(item);
158
+ }
159
+ None => {
160
+ remaining.push(capacity.clone() - item.clone());
161
+ sums.push(item.clone());
162
+ bins.push(vec![item]);
163
+ }
164
+ }
165
+ }
166
+
167
+ BinPacking {
168
+ bins,
169
+ sums,
170
+ remaining,
171
+ capacity: Some(capacity),
172
+ }
173
+ }
@@ -0,0 +1,116 @@
1
+ #[derive(Debug, Default)]
2
+ pub struct MinHeap<T: Ord> {
3
+ data: Vec<T>,
4
+ }
5
+
6
+ impl<T: Ord> MinHeap<T> {
7
+ pub fn len(&self) -> usize {
8
+ self.data.len()
9
+ }
10
+
11
+ pub fn is_empty(&self) -> bool {
12
+ self.data.is_empty()
13
+ }
14
+ // used after inserting a new element
15
+ pub fn sift_up(&mut self, mut i: usize) {
16
+ while i > 0 {
17
+ let parent = (i - 1) / 2;
18
+
19
+ if self.data[parent] <= self.data[i] {
20
+ break;
21
+ }
22
+ self.data.swap(parent, i);
23
+ i = parent;
24
+ }
25
+ }
26
+ // after deleting an element
27
+ pub fn sift_down(&mut self, mut i: usize) {
28
+ let n = self.data.len();
29
+
30
+ loop {
31
+ // parent(i) = (i - 1) / 2
32
+ // left(i) = 2*i + 1
33
+ // right(i) = 2*i + 2
34
+ let left = 2 * i + 1;
35
+ let right = 2 * i + 2;
36
+
37
+ if left >= n {
38
+ break;
39
+ }
40
+ let mut smallest = left;
41
+ if right < n && self.data[right] < self.data[left] {
42
+ smallest = right;
43
+ }
44
+
45
+ if self.data[i] <= self.data[smallest] {
46
+ break;
47
+ }
48
+ self.data.swap(i, smallest);
49
+ i = smallest;
50
+ }
51
+ }
52
+
53
+ pub fn push(&mut self, val: T) {
54
+ self.data.push(val);
55
+ let i = self.data.len() - 1;
56
+ self.sift_up(i);
57
+ }
58
+
59
+ pub fn pop(&mut self) -> Option<T> {
60
+ if self.data.is_empty() {
61
+ return None;
62
+ }
63
+ let last = self.data.len() - 1;
64
+ self.data.swap(0, last);
65
+ let top = self.data.pop();
66
+ if !self.data.is_empty() {
67
+ self.sift_down(0);
68
+ }
69
+ top
70
+ }
71
+
72
+ pub fn peek(&self) -> Option<&T> {
73
+ self.data.first()
74
+ }
75
+
76
+ pub fn from_vec(data: Vec<T>) -> Self {
77
+ let mut h = Self { data };
78
+ let n = h.data.len();
79
+ for i in (0..n / 2).rev() {
80
+ h.sift_down(i);
81
+ }
82
+ h
83
+ }
84
+
85
+ pub fn with_capacity(cap: usize) -> Self {
86
+ Self {
87
+ data: Vec::with_capacity(cap),
88
+ }
89
+ }
90
+ }
91
+
92
+ impl<T: Ord + std::fmt::Display> MinHeap<T> {
93
+ pub fn print_tree(&self) {
94
+ if self.data.is_empty() {
95
+ println!("(empty)");
96
+ return;
97
+ }
98
+
99
+ let n = self.data.len();
100
+ let mut level_start = 0;
101
+ let mut level_size = 1;
102
+
103
+ while level_start < n {
104
+ let level_end = (level_start + level_size).min(n);
105
+ for i in level_start..level_end {
106
+ if i > level_start {
107
+ print!(" ");
108
+ }
109
+ print!("{}", self.data[i]);
110
+ }
111
+ println!();
112
+ level_start += level_size;
113
+ level_size *= 2;
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,56 @@
1
+ use crate::bin::BinPacking;
2
+ use std::cmp::Reverse;
3
+ use std::collections::BinaryHeap;
4
+ use std::ops::Add;
5
+
6
+ // [8,7,6,5,4]
7
+ // [0 | 0 | 8] -> sort ascending (first element)
8
+ // [7 | 0 | 0] -> sort descending element
9
+ // [7 | 0 | 8] -> sort -> [0 | 7 | 8]
10
+
11
+ // [0 | 7 | 8] -> sorted ascending
12
+ // [6 | 0 | 0] -> sorted descending
13
+ // [6 | 7 | 8] -> sort ascending -> [6 | 7 | 8]
14
+
15
+ // [6 | 7 | 8]
16
+ // [5 | 0 | 0]
17
+ // [(6,5) | 7 | 8] -> sort ascending -> [7 | 8 | 11]
18
+
19
+ // [7 | 8 | 11]
20
+ // [5 | 0 | 0]
21
+ // [(7,5) | 8 | (6,5)] -> [8 | 11 | 12]
22
+
23
+ // [8 | 11 | 12]
24
+ // [4 | 0 | 0]
25
+ // [(8,4) | 11 | 12] -> [(8,4) | (6,5) | (7,5) ]
26
+
27
+ pub fn kk_partition<T>(mut items: Vec<T>, k: usize) -> BinPacking<T>
28
+ where
29
+ T: Ord + Add<Output = T> + Default + Clone,
30
+ {
31
+ assert!(k > 0, "k must be at least 1");
32
+ // placing large items first improves balancing.
33
+ items.sort_unstable_by(|a, b| b.cmp(a));
34
+
35
+ let mut bins: Vec<Vec<T>> = vec![vec![]; k];
36
+ let mut sums: Vec<T> = vec![T::default(); k];
37
+ // BinaryHeap<Reverse<...>> gives min-heap behaviour.
38
+ let mut min_heap: BinaryHeap<Reverse<(T, usize)>> =
39
+ (0..k).map(|i| Reverse((T::default(), i))).collect();
40
+
41
+ for item in items {
42
+ let Reverse((min_sum, idx)) = min_heap.pop().unwrap();
43
+ let new_sum = min_sum + item.clone();
44
+ bins[idx].push(item);
45
+ sums[idx] = new_sum.clone();
46
+ min_heap.push(Reverse((new_sum, idx)));
47
+ }
48
+
49
+ let remaining = vec![T::default(); k];
50
+ BinPacking {
51
+ bins,
52
+ sums,
53
+ remaining,
54
+ capacity: None,
55
+ }
56
+ }