Repository navigation
Expand file tree
/
Copy pathinteger.rs
More file actions
2694 lines (2547 loc) · 101 KB
/
Copy pathinteger.rs
File metadata and controls
2694 lines (2547 loc) · 101 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//! Integer programming, dynamic programming, and combinatorial search.
//!
//! Adding "and the answer must be a whole number" to a linear program changes
//! its character completely. The feasible region stops being a convex
//! polyhedron and becomes a scatter of lattice points inside one, so the
//! guarantee that made linear programming easy -- that an optimum sits at a
//! vertex, reachable by local moves -- is gone. What remains is the
//! relaxation: drop the integrality, solve the linear program, and use its
//! value as a bound on what any integer solution could achieve. Branch and
//! bound is that observation applied recursively, and the bound is the only
//! reason it terminates before enumerating everything.
//!
//! Most problems here have that flavour. A few do not, and those are the
//! dynamic programming classics: when a problem decomposes into overlapping
//! subproblems whose optimal solutions compose, the exponential search
//! collapses to a table and the answer is exact in polynomial time. Knapsack,
//! edit distance and the rest are here because the boundary between the two
//! situations is worth being able to see -- the 0/1 knapsack is
//! NP-hard and yet has a pseudo-polynomial table, which is not a
//! contradiction but a statement about what "polynomial" is measured against.
//!
//! Where an exact method is impractical the module gives a greedy one with
//! its proven ratio: first-fit-decreasing bin packing within `11/9` of
//! optimal, greedy set cover within `H_n`, longest-processing-time
//! scheduling within `4/3 - 1/(3m)`. Those ratios are worst-case guarantees
//! rather than typical behaviour, and the tests check the guarantee holds
//! against an exact answer on small instances rather than checking the greedy
//! answer is merely plausible.
// The bitmask searches below isolate the lowest set bit with the standard
// `x & -x` idiom. Clippy suggests `isolate_lowest_one` instead, which is a
// better name for the same operation -- but it is still an unstable library
// feature, and the toolchain Kani pins is far enough behind the one clippy
// runs on that taking the suggestion breaks the model-checking job outright.
// The idiom is stable on every toolchain and compiles to the same instruction.
#![allow(clippy::manual_isolate_lowest_one)]
use crate::error::GeomError;
use crate::exact::bigint::BigInt;
use crate::optimization::lp::{simplex, Cmp, LpProblem, LpResult};
/// Values within this of an integer are treated as integral.
const INTEGRALITY_TOL: f64 = 1e-7;
// ---------------------------------------------------------------------------
// Branch and bound over the linear relaxation
// ---------------------------------------------------------------------------
/// Solves a mixed-integer linear program by branch and bound.
///
/// Solves the linear relaxation; if the named variables all came out integral
/// the answer is optimal, and otherwise one fractional variable is chosen and
/// the problem split into the branch where it is rounded down and the branch
/// where it is rounded up. The relaxation's value bounds every integer
/// solution below it, so a branch whose relaxation is already worse than the
/// best integer solution found can be discarded whole -- which is the entire
/// content of the method, and the reason it beats enumeration.
///
/// `node_limit` caps the search. Returns `None` if the problem is infeasible
/// over the integers, or if the limit is reached before any integer solution
/// is found.
///
/// # Errors
/// Returns an error if a named variable is out of range, or the underlying
/// linear program is malformed.
pub fn branch_and_bound(
p: &LpProblem,
integer_vars: &[usize],
node_limit: usize,
) -> Result<Option<(Vec<f64>, f64)>, GeomError> {
p.validate()?;
if integer_vars.iter().any(|&j| j >= p.n()) {
return Err(GeomError::InvalidArgument("branch_and_bound: variable index out of range"));
}
// A maximisation improves upward and a minimisation downward; carrying the
// sign lets one comparison serve both.
let better = |a: f64, b: f64| if p.maximize { a > b } else { a < b };
let mut best: Option<(Vec<f64>, f64)> = None;
let mut stack = vec![p.clone()];
let mut nodes = 0usize;
while let Some(node) = stack.pop() {
nodes += 1;
if nodes > node_limit {
break;
}
let LpResult::Optimal { x, objective, .. } = simplex(&node)? else {
// Infeasible or unbounded: nothing below this node to find.
continue;
};
// Bound: this branch cannot beat what is already in hand.
if let Some((_, incumbent)) = &best {
if !better(objective, *incumbent) {
continue;
}
}
let fractional = integer_vars
.iter()
.copied()
.find(|&j| (x[j] - x[j].round()).abs() > INTEGRALITY_TOL);
let Some(j) = fractional else {
// Every named variable is integral, so this is a candidate.
let rounded: Vec<f64> = x
.iter()
.enumerate()
.map(|(k, &v)| if integer_vars.contains(&k) { v.round() } else { v })
.collect();
let value = node.objective_at(&rounded);
if best.as_ref().is_none_or(|(_, incumbent)| better(value, *incumbent)) {
best = Some((rounded, value));
}
continue;
};
// Branch: the fractional value lies strictly between two integers, so
// no integer solution is lost by excluding the gap between them.
let floor = x[j].floor();
for (bound, side) in [(floor, Cmp::Le), (floor + 1.0, Cmp::Ge)] {
let mut child = node.clone();
let (lo, hi) = child.bounds[j];
match side {
Cmp::Le => {
if bound < lo - INTEGRALITY_TOL {
continue;
}
child.bounds[j] = (lo, hi.min(bound));
}
Cmp::Ge => {
if bound > hi + INTEGRALITY_TOL {
continue;
}
child.bounds[j] = (lo.max(bound), hi);
}
Cmp::Eq => unreachable!("branching uses only inequalities"),
}
if child.bounds[j].0 <= child.bounds[j].1 + INTEGRALITY_TOL {
stack.push(child);
}
}
}
Ok(best)
}
/// Adds Chvatal-Gomory rounding cuts to a linear program.
///
/// A cut is only worth the name if it is valid: satisfied by every integer
/// point of the feasible region, while removing part of the fractional
/// relaxation. The rounding cut earns that as follows. Scale a `<=` row by
/// some `lambda > 0`, so `lambda a . x <= lambda b` still holds. Rounding each
/// coefficient down can only lower the left-hand side when `x >= 0`, so
/// `floor(lambda a) . x <= lambda b`. But the left-hand side is now an integer
/// combination of integers, hence an integer, so it is bounded by the floor of
/// the right:
///
/// ```text
/// sum_j floor(lambda a_j) x_j <= floor(lambda b)
/// ```
///
/// Every non-negative integer point survives that, and a fractional one need
/// not. Multipliers are tried at the reciprocals of the row's own
/// coefficients and at a few small fractions, and a cut is kept only when the
/// current relaxation optimum actually violates it.
///
/// # Errors
/// Returns an error unless every variable is integer and non-negative, which
/// is what the rounding argument requires, or if the relaxation has no
/// optimum.
pub fn gomory_cuts(
p: &LpProblem,
integer_vars: &[usize],
max_cuts: usize,
) -> Result<LpProblem, GeomError> {
p.validate()?;
if integer_vars.len() != p.n() || (0..p.n()).any(|j| !integer_vars.contains(&j)) {
return Err(GeomError::InvalidArgument(
"gomory_cuts requires every variable to be an integer variable",
));
}
if p.bounds.iter().any(|&(lo, _)| lo < 0.0) {
return Err(GeomError::InvalidArgument(
"gomory_cuts requires non-negative variables; the rounding step needs it",
));
}
let mut out = p.clone();
for _ in 0..max_cuts {
let LpResult::Optimal { x, .. } = simplex(&out)? else {
break;
};
if x.iter().all(|v| (v - v.round()).abs() <= INTEGRALITY_TOL) {
// Nothing fractional left to cut off.
break;
}
let mut best: Option<(Vec<f64>, f64, f64)> = None;
for i in 0..out.m() {
// Orient the row as `<=`; an equality serves in both directions.
let orientations: &[f64] = match out.constraint_types[i] {
Cmp::Le => &[1.0],
Cmp::Ge => &[-1.0],
Cmp::Eq => &[1.0, -1.0],
};
for &sign in orientations {
let row: Vec<f64> = (0..out.n()).map(|j| sign * out.a.get(i, j)).collect();
let rhs = sign * out.b[i];
let mut multipliers: Vec<f64> = vec![0.5, 1.0 / 3.0, 2.0 / 3.0, 0.25];
for &v in &row {
if v.abs() > 1e-9 {
multipliers.push(1.0 / v.abs());
}
}
for lambda in multipliers {
if !(lambda > 0.0) || !lambda.is_finite() {
continue;
}
let cut: Vec<f64> = row.iter().map(|&v| (lambda * v).floor()).collect();
let bound = (lambda * rhs).floor();
if cut.iter().all(|&v| v == 0.0) {
continue;
}
let lhs: f64 = cut.iter().zip(&x).map(|(a, b)| a * b).sum();
let violation = lhs - bound;
if violation > 1e-6
&& best.as_ref().is_none_or(|(_, _, v)| violation > *v)
{
best = Some((cut, bound, violation));
}
}
}
}
let Some((cut, bound, _)) = best else { break };
// Append the cut as a new row.
let (m, n) = (out.m(), out.n());
let mut a = crate::linalg::matrix::Matrix::zeros(m + 1, n);
for i in 0..m {
for j in 0..n {
a.set(i, j, out.a.get(i, j));
}
}
for (j, &v) in cut.iter().enumerate() {
a.set(m, j, v);
}
out.a = a;
out.b.push(bound);
out.constraint_types.push(Cmp::Le);
}
Ok(out)
}
// ---------------------------------------------------------------------------
// The knapsack family
// ---------------------------------------------------------------------------
/// The 0/1 knapsack by dynamic programming: each item taken at most once.
///
/// Returns the best value and which items to take. The table is
/// `O(n * capacity)`, which is polynomial in the *value* of the capacity but
/// exponential in the number of digits it takes to write it down -- the
/// problem is NP-hard, and the table is pseudo-polynomial rather than a
/// contradiction of that.
///
/// # Panics
/// Panics unless the value and weight lists have the same length.
#[must_use]
pub fn knapsack_01(values: &[u64], weights: &[u64], capacity: u64) -> (u64, Vec<bool>) {
assert!(values.len() == weights.len(), "knapsack_01 needs one weight per value");
let n = values.len();
let cap = capacity as usize;
let mut table = vec![vec![0u64; cap + 1]; n + 1];
for i in 1..=n {
let w = weights[i - 1] as usize;
for c in 0..=cap {
table[i][c] = table[i - 1][c];
if w <= c {
let with = table[i - 1][c - w] + values[i - 1];
if with > table[i][c] {
table[i][c] = with;
}
}
}
}
// Walk back through the table: an item was taken exactly where the value
// differs from the row above.
let mut chosen = vec![false; n];
let mut c = cap;
for i in (1..=n).rev() {
if table[i][c] != table[i - 1][c] {
chosen[i - 1] = true;
c -= weights[i - 1] as usize;
}
}
(table[n][cap], chosen)
}
/// The unbounded knapsack: each item available without limit.
///
/// Returns the best value and how many of each item to take. A one-dimensional
/// table suffices, because an item may be reused within the same pass.
///
/// # Panics
/// Panics unless the lists match in length and every weight is positive.
#[must_use]
pub fn knapsack_unbounded(values: &[u64], weights: &[u64], capacity: u64) -> (u64, Vec<u64>) {
assert!(values.len() == weights.len(), "knapsack_unbounded needs one weight per value");
assert!(weights.iter().all(|&w| w > 0), "knapsack_unbounded requires positive weights");
let cap = capacity as usize;
let mut best = vec![0u64; cap + 1];
let mut taken = vec![usize::MAX; cap + 1];
for c in 1..=cap {
for (i, (&v, &w)) in values.iter().zip(weights).enumerate() {
let w = w as usize;
if w <= c && best[c - w] + v > best[c] {
best[c] = best[c - w] + v;
taken[c] = i;
}
}
}
let mut counts = vec![0u64; values.len()];
let mut c = cap;
while c > 0 && taken[c] != usize::MAX {
let i = taken[c];
counts[i] += 1;
c -= weights[i] as usize;
}
(best[cap], counts)
}
/// The bounded knapsack: each item available up to its own limit.
///
/// Expanded by binary splitting -- an item with a limit of `k` becomes items
/// of multiplicity `1, 2, 4, ...` summing to `k` -- so any count up to the
/// limit is expressible and the 0/1 solver applies. That costs
/// `O(log k)` copies rather than the `k` a naive expansion would need.
///
/// # Panics
/// Panics unless all three lists match in length.
#[must_use]
pub fn knapsack_bounded(
values: &[u64],
weights: &[u64],
limits: &[u64],
capacity: u64,
) -> (u64, Vec<u64>) {
assert!(
values.len() == weights.len() && values.len() == limits.len(),
"knapsack_bounded needs one weight and limit per value"
);
let mut expanded_values = Vec::new();
let mut expanded_weights = Vec::new();
let mut origin = Vec::new();
let mut multiplicity = Vec::new();
for (i, ((&v, &w), &limit)) in values.iter().zip(weights).zip(limits).enumerate() {
let mut remaining = limit;
let mut piece = 1u64;
while remaining > 0 {
let take = piece.min(remaining);
expanded_values.push(v * take);
expanded_weights.push(w * take);
origin.push(i);
multiplicity.push(take);
remaining -= take;
piece *= 2;
}
}
let (best, chosen) = knapsack_01(&expanded_values, &expanded_weights, capacity);
let mut counts = vec![0u64; values.len()];
for (k, &taken) in chosen.iter().enumerate() {
if taken {
counts[origin[k]] += multiplicity[k];
}
}
(best, counts)
}
/// The multiple knapsack: several bins, each item into at most one.
///
/// Solved greedily by value density with a first-fit placement, which is not
/// exact -- the problem is NP-hard even with two bins -- so the result is a
/// lower bound on the optimum. Returns the total value and the bin each item
/// went into, `None` for an item left out.
///
/// # Panics
/// Panics unless the lists match in length.
#[must_use]
pub fn knapsack_multiple(
values: &[u64],
weights: &[u64],
capacities: &[u64],
) -> (u64, Vec<Option<usize>>) {
assert!(values.len() == weights.len(), "knapsack_multiple needs one weight per value");
let n = values.len();
let mut order: Vec<usize> = (0..n).collect();
// Densest first: the classic greedy order for a knapsack.
order.sort_by(|&a, &b| {
let da = values[a] as f64 / weights[a].max(1) as f64;
let db = values[b] as f64 / weights[b].max(1) as f64;
db.partial_cmp(&da).unwrap_or(std::cmp::Ordering::Equal)
});
let mut remaining: Vec<u64> = capacities.to_vec();
let mut placement = vec![None; n];
let mut total = 0u64;
for &i in &order {
if let Some(bin) = remaining.iter().position(|&r| r >= weights[i]) {
remaining[bin] -= weights[i];
placement[i] = Some(bin);
total += values[i];
}
}
(total, placement)
}
/// The 0/1 knapsack by branch and bound over the fractional relaxation.
///
/// The relaxation of a knapsack is solved by taking items in density order
/// and splitting the last one, which gives a bound in linear time once the
/// items are sorted. Nodes whose bound cannot beat the incumbent are pruned.
///
/// Exact, and must agree with [`knapsack_01`] on every instance -- one walks a
/// table and the other a search tree, so their agreement is a real check on
/// both.
///
/// # Panics
/// Panics unless the lists match in length.
#[must_use]
pub fn knapsack_branch_bound(values: &[u64], weights: &[u64], capacity: u64) -> (u64, Vec<bool>) {
assert!(values.len() == weights.len(), "knapsack_branch_bound needs one weight per value");
let n = values.len();
if n == 0 {
return (0, Vec::new());
}
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
let da = values[a] as f64 / weights[a].max(1) as f64;
let db = values[b] as f64 / weights[b].max(1) as f64;
db.partial_cmp(&da).unwrap_or(std::cmp::Ordering::Equal)
});
// The fractional optimum from position `k` onward, given `room` left.
let bound = |k: usize, room: u64, value: u64| -> f64 {
let mut left = room;
let mut total = value as f64;
for &i in &order[k..] {
if weights[i] <= left {
left -= weights[i];
total += values[i] as f64;
} else {
// The relaxation may split this one item, and nothing after
// it can add more than that fraction is worth.
total += values[i] as f64 * left as f64 / weights[i].max(1) as f64;
break;
}
}
total
};
let mut best_value = 0u64;
let mut best_take = vec![false; n];
let mut take = vec![false; n];
// An explicit stack of (depth, remaining capacity, value so far, whether
// the parent's decision has been undone) rather than recursion.
struct Frame {
depth: usize,
room: u64,
value: u64,
branch: u8,
}
let mut stack = vec![Frame { depth: 0, room: capacity, value: 0, branch: 0 }];
while let Some(frame) = stack.last_mut() {
let Frame { depth, room, value, branch } = *frame;
if depth == n || branch == 2 {
if value > best_value && depth == n {
best_value = value;
best_take.copy_from_slice(&take);
}
if depth == n && branch == 0 {
// Nothing to undo at a leaf.
}
stack.pop();
if let Some(parent) = stack.last() {
let i = order[parent.depth];
take[i] = false;
}
continue;
}
frame.branch += 1;
let i = order[depth];
let (next_room, next_value, feasible) = if branch == 0 {
// Take the item, if it fits.
(room.checked_sub(weights[i]), value + values[i], weights[i] <= room)
} else {
(Some(room), value, true)
};
if !feasible {
continue;
}
let room_left = next_room.unwrap_or(0);
if bound(depth + 1, room_left, next_value) <= best_value as f64 {
continue;
}
take[i] = branch == 0;
if next_value > best_value {
best_value = next_value;
best_take.copy_from_slice(&take);
}
stack.push(Frame { depth: depth + 1, room: room_left, value: next_value, branch: 0 });
}
(best_value, best_take)
}
// ---------------------------------------------------------------------------
// Subset sum and partition
// ---------------------------------------------------------------------------
/// Indices of a subset summing exactly to `target`, if one exists.
///
/// # Panics
/// Panics if the values are large enough that the table would not fit.
#[must_use]
pub fn subset_sum(xs: &[u64], target: u64) -> Option<Vec<usize>> {
let t = target as usize;
let n = xs.len();
let mut reachable = vec![vec![false; t + 1]; n + 1];
for row in reachable.iter_mut() {
row[0] = true;
}
for i in 1..=n {
let v = xs[i - 1] as usize;
for s in 0..=t {
reachable[i][s] = reachable[i - 1][s] || (v <= s && reachable[i - 1][s - v]);
}
}
if !reachable[n][t] {
return None;
}
let mut chosen = Vec::new();
let mut s = t;
for i in (1..=n).rev() {
let v = xs[i - 1] as usize;
if !reachable[i - 1][s] {
chosen.push(i - 1);
s -= v;
}
}
chosen.reverse();
Some(chosen)
}
/// How many subsets sum exactly to `target`.
///
/// Counted as a [`BigInt`], since the number of subsets of an `n`-element set
/// is `2^n` and the count routinely overflows a machine word well before the
/// table does.
#[must_use]
pub fn subset_sum_count(xs: &[u64], target: u64) -> BigInt {
let t = target as usize;
let mut counts = vec![BigInt::zero(); t + 1];
counts[0] = BigInt::one();
for &v in xs {
let v = v as usize;
if v > t {
continue;
}
// Descending, so each item is counted once per subset.
for s in (v..=t).rev() {
let carried = counts[s - v].clone();
counts[s] = counts[s].add(&carried);
}
}
counts[t].clone()
}
/// Splits the values into two groups whose totals are as close as possible.
///
/// Returns the difference and the membership flags. The problem is
/// NP-hard in general and solved here by the subset-sum table over half the
/// total, which is exact and pseudo-polynomial.
#[must_use]
pub fn partition_min_diff(xs: &[u64]) -> (u64, Vec<bool>) {
let total: u64 = xs.iter().sum();
let half = (total / 2) as usize;
let n = xs.len();
let mut reachable = vec![vec![false; half + 1]; n + 1];
for row in reachable.iter_mut() {
row[0] = true;
}
for i in 1..=n {
let v = xs[i - 1] as usize;
for s in 0..=half {
reachable[i][s] = reachable[i - 1][s] || (v <= s && reachable[i - 1][s - v]);
}
}
// The largest reachable total at or below half minimises the gap.
let best = (0..=half).rev().find(|&s| reachable[n][s]).unwrap_or(0);
let mut flags = vec![false; n];
let mut s = best;
for i in (1..=n).rev() {
let v = xs[i - 1] as usize;
if !reachable[i - 1][s] {
flags[i - 1] = true;
s -= v;
}
}
(total - 2 * best as u64, flags)
}
// ---------------------------------------------------------------------------
// Packing and covering
// ---------------------------------------------------------------------------
/// Bin packing by first-fit-decreasing: sort the items large to small and put
/// each into the first bin it fits.
///
/// Returns the item indices in each bin. The rule uses at most
/// `11/9 OPT + 6/9` bins, a bound that is tight -- so the tests check the
/// guarantee against an exact answer rather than checking the result merely
/// looks reasonable.
///
/// # Panics
/// Panics if any item exceeds the bin capacity, which makes packing
/// impossible rather than merely hard.
#[must_use]
pub fn bin_packing_ffd(sizes: &[f64], capacity: f64) -> Vec<Vec<usize>> {
assert!(capacity > 0.0, "bin_packing_ffd requires a positive capacity");
assert!(
sizes.iter().all(|&s| s <= capacity + 1e-12 && s >= 0.0),
"every item must fit in an empty bin"
);
let mut order: Vec<usize> = (0..sizes.len()).collect();
order.sort_by(|&a, &b| sizes[b].partial_cmp(&sizes[a]).unwrap_or(std::cmp::Ordering::Equal));
let mut bins: Vec<Vec<usize>> = Vec::new();
let mut room: Vec<f64> = Vec::new();
for &i in &order {
match room.iter().position(|&r| r >= sizes[i] - 1e-12) {
Some(b) => {
room[b] -= sizes[i];
bins[b].push(i);
}
None => {
room.push(capacity - sizes[i]);
bins.push(vec![i]);
}
}
}
bins
}
/// The fewest bins any packing could use: the total size divided by the
/// capacity, rounded up.
///
/// A valid lower bound because a bin holds at most `capacity`, so no packing
/// can use fewer. It is not always attainable -- three items of size 0.4 need
/// two bins though their total is 1.2 -- which is exactly why it is a bound
/// and not an answer.
#[must_use]
pub fn bin_packing_lower_bound(sizes: &[f64], capacity: f64) -> usize {
assert!(capacity > 0.0, "bin_packing_lower_bound requires a positive capacity");
let total: f64 = sizes.iter().sum();
(total / capacity).ceil().max(0.0) as usize
}
/// The exact minimum number of bins, by trying each count in turn.
///
/// Exponential, and meant for the small instances the tests use to check the
/// first-fit-decreasing guarantee. Returns the packing.
///
/// # Panics
/// Panics under the same conditions as [`bin_packing_ffd`].
#[must_use]
pub fn bin_packing_exact_small(sizes: &[f64], capacity: f64) -> Vec<Vec<usize>> {
assert!(capacity > 0.0, "bin_packing_exact_small requires a positive capacity");
assert!(sizes.len() <= 12, "bin_packing_exact_small is for instances of at most twelve items");
let n = sizes.len();
if n == 0 {
return Vec::new();
}
let lower = bin_packing_lower_bound(sizes, capacity).max(1);
for count in lower..=n {
let mut assignment = vec![usize::MAX; n];
let mut room = vec![capacity; count];
if pack(sizes, 0, &mut assignment, &mut room) {
let mut bins = vec![Vec::new(); count];
for (i, &b) in assignment.iter().enumerate() {
bins[b].push(i);
}
return bins;
}
}
// Every item in its own bin always works.
(0..n).map(|i| vec![i]).collect()
}
/// Depth-first placement for [`bin_packing_exact_small`].
fn pack(sizes: &[f64], i: usize, assignment: &mut Vec<usize>, room: &mut Vec<f64>) -> bool {
if i == sizes.len() {
return true;
}
for b in 0..room.len() {
if room[b] >= sizes[i] - 1e-12 {
room[b] -= sizes[i];
assignment[i] = b;
if pack(sizes, i + 1, assignment, room) {
return true;
}
room[b] += sizes[i];
assignment[i] = usize::MAX;
}
}
false
}
/// Greedy set cover: repeatedly take the set covering the most of what is
/// still uncovered.
///
/// Returns the indices of the chosen sets, or `None` if the sets do not cover
/// the universe at all. Greedy uses at most `H_n` times the optimal number of
/// sets, where `H_n` is the `n`-th harmonic number, and no polynomial
/// algorithm does asymptotically better unless P equals NP -- so this is not
/// a placeholder for something better.
#[must_use]
pub fn set_cover_greedy(universe_n: usize, sets: &[Vec<usize>]) -> Option<Vec<usize>> {
let mut covered = vec![false; universe_n];
let mut chosen = Vec::new();
let mut remaining = universe_n;
while remaining > 0 {
let best = (0..sets.len())
.filter(|i| !chosen.contains(i))
.max_by_key(|&i| sets[i].iter().filter(|&&e| e < universe_n && !covered[e]).count());
let best = best?;
let gain = sets[best].iter().filter(|&&e| e < universe_n && !covered[e]).count();
if gain == 0 {
return None;
}
for &e in &sets[best] {
if e < universe_n && !covered[e] {
covered[e] = true;
remaining -= 1;
}
}
chosen.push(best);
}
Some(chosen)
}
/// The exact minimum set cover, by trying every subset of the sets in order of
/// size.
///
/// For the small instances that make the greedy ratio checkable.
///
/// # Panics
/// Panics if there are more than 20 sets, where the enumeration stops being
/// reasonable.
#[must_use]
pub fn set_cover_exact_small(universe_n: usize, sets: &[Vec<usize>]) -> Option<Vec<usize>> {
assert!(sets.len() <= 20, "set_cover_exact_small is for at most twenty sets");
let m = sets.len();
let masks: Vec<u64> = sets
.iter()
.map(|s| s.iter().filter(|&&e| e < universe_n).fold(0u64, |acc, &e| acc | (1 << e)))
.collect();
let full = if universe_n >= 64 { u64::MAX } else { (1u64 << universe_n) - 1 };
for size in 0..=m {
for combination in 0u32..(1u32 << m) {
if combination.count_ones() as usize != size {
continue;
}
let mut union = 0u64;
for (i, &mask) in masks.iter().enumerate() {
if combination & (1 << i) != 0 {
union |= mask;
}
}
if union == full {
return Some((0..m).filter(|&i| combination & (1 << i) != 0).collect());
}
}
}
None
}
/// Uncapacitated facility location, solved greedily.
///
/// `open_costs[i]` is the fixed cost of opening facility `i` and
/// `serve_costs[(i, j)]` the cost of serving client `j` from it. Facilities
/// are opened one at a time, each time the one whose opening cost plus
/// improved service most reduces the total.
///
/// Returns the total cost and which facilities to open.
///
/// # Panics
/// Panics unless the shapes agree and there is at least one facility.
#[must_use]
pub fn facility_location_greedy(
open_costs: &[f64],
serve_costs: &crate::linalg::matrix::Matrix,
) -> (f64, Vec<bool>) {
let m = open_costs.len();
assert!(m > 0 && serve_costs.rows == m, "facility_location_greedy: shape mismatch");
let n = serve_costs.cols;
let mut open = vec![false; m];
let mut best_serve = vec![f64::INFINITY; n];
let mut total = f64::INFINITY;
loop {
let mut improvement: Option<(f64, usize, Vec<f64>)> = None;
for i in 0..m {
if open[i] {
continue;
}
let candidate: Vec<f64> =
(0..n).map(|j| best_serve[j].min(serve_costs.get(i, j))).collect();
let cost: f64 = open_costs[i]
+ candidate.iter().sum::<f64>()
+ (0..m).filter(|&k| open[k]).map(|k| open_costs[k]).sum::<f64>();
if cost < total && improvement.as_ref().is_none_or(|(best, _, _)| cost < *best) {
improvement = Some((cost, i, candidate));
}
}
let Some((cost, i, serve)) = improvement else { break };
open[i] = true;
best_serve = serve;
total = cost;
}
(total, open)
}
/// The cutting stock problem by column generation, relaxed.
///
/// Each cutting pattern is a column of the linear program, and there are far
/// too many to write down, so patterns are generated on demand: solve the
/// relaxation over the patterns in hand, read the dual prices off it, and ask
/// which single new pattern would be most profitable at those prices. That
/// subproblem is an unbounded knapsack, and when its best pattern is not
/// profitable the relaxation is optimal over *all* patterns without ever
/// having enumerated them.
///
/// Returns the relaxed number of stock lengths needed, which lower-bounds the
/// integer answer.
///
/// # Errors
/// Returns an error if the inputs disagree in length, or a piece is longer
/// than the stock.
pub fn cutting_stock_column_generation(
demand: &[u64],
lengths: &[u64],
stock_length: u64,
max_rounds: usize,
) -> Result<f64, GeomError> {
if demand.len() != lengths.len() || demand.is_empty() {
return Err(GeomError::InvalidArgument("cutting_stock: one demand per length"));
}
if lengths.iter().any(|&l| l == 0 || l > stock_length) {
return Err(GeomError::InvalidArgument("cutting_stock: a piece does not fit the stock"));
}
let n = lengths.len();
// Start with the trivial patterns: one length repeated as often as fits.
let mut patterns: Vec<Vec<f64>> = (0..n)
.map(|i| {
let mut p = vec![0.0; n];
p[i] = (stock_length / lengths[i]) as f64;
p
})
.collect();
let mut value = f64::INFINITY;
for _ in 0..max_rounds {
// min sum(x) s.t. each length's demand is met by the patterns used.
let mut a = crate::linalg::matrix::Matrix::zeros(n, patterns.len());
for (k, pattern) in patterns.iter().enumerate() {
for (i, &count) in pattern.iter().enumerate() {
a.set(i, k, count);
}
}
let p = LpProblem {
c: vec![1.0; patterns.len()],
a,
b: demand.iter().map(|&d| d as f64).collect(),
constraint_types: vec![Cmp::Ge; n],
bounds: vec![(0.0, f64::INFINITY); patterns.len()],
maximize: false,
};
let LpResult::Optimal { objective, duals, .. } = simplex(&p)? else {
return Err(GeomError::Degenerate("cutting_stock: the relaxation has no optimum"));
};
value = objective;
// Pricing: the most profitable new pattern is an unbounded knapsack
// with the duals as values and the piece lengths as weights.
let scale = 10_000.0;
let values: Vec<u64> = duals.iter().map(|&d| (d.max(0.0) * scale) as u64).collect();
let (best, counts) = knapsack_unbounded(&values, lengths, stock_length);
if best as f64 / scale <= 1.0 + 1e-6 {
// No pattern pays for the stock length it consumes: optimal.
break;
}
let column: Vec<f64> = counts.iter().map(|&c| c as f64).collect();
if patterns.contains(&column) {
break;
}
patterns.push(column);
}
Ok(value)
}
// ---------------------------------------------------------------------------
// Dynamic programming classics
// ---------------------------------------------------------------------------
/// The fewest coins summing to `amount`, or `None` if no combination does.
///
/// Returns how many of each denomination. Greedy is wrong for general
/// denominations -- with coins 1, 3, 4 and an amount of 6, greedy takes
/// 4 + 1 + 1 while two threes do it -- so this is a table, not a loop.
#[must_use]
pub fn coin_change_min(coins: &[u64], amount: u64) -> Option<Vec<u64>> {
let target = amount as usize;
let mut best = vec![usize::MAX; target + 1];
let mut used = vec![usize::MAX; target + 1];
best[0] = 0;
for s in 1..=target {
for (i, &c) in coins.iter().enumerate() {
let c = c as usize;
if c > 0 && c <= s && best[s - c] != usize::MAX && best[s - c] + 1 < best[s] {
best[s] = best[s - c] + 1;
used[s] = i;
}
}
}
if best[target] == usize::MAX {
return None;
}
let mut counts = vec![0u64; coins.len()];
let mut s = target;
while s > 0 {
let i = used[s];
counts[i] += 1;
s -= coins[i] as usize;
}
Some(counts)
}
/// How many combinations of coins sum to `amount`, order disregarded.
///
/// Iterating coins in the outer loop is what makes this count combinations
/// rather than permutations: each coin is considered once for the whole table,
/// so `1 + 2` and `2 + 1` are never both counted.
#[must_use]
pub fn coin_change_count(coins: &[u64], amount: u64) -> BigInt {
let target = amount as usize;
let mut ways = vec![BigInt::zero(); target + 1];
ways[0] = BigInt::one();
for &c in coins {
let c = c as usize;
if c == 0 {
continue;
}
for s in c..=target {
let carried = ways[s - c].clone();
ways[s] = ways[s].add(&carried);
}
}
ways[target].clone()
}
/// Indices of a longest strictly increasing subsequence, in `O(n log n)`.
///
/// The trick is to keep, for each length, the smallest value that can end a
/// subsequence of that length. That list is sorted by construction, so the
/// position each new element belongs at is a binary search rather than a scan
/// -- which is what turns the quadratic table into an `n log n` sweep.
#[must_use]
pub fn longest_increasing_subsequence(x: &[f64]) -> Vec<usize> {
let n = x.len();
if n == 0 {
return Vec::new();
}
// `tails[k]` is the index of the smallest tail of an increasing
// subsequence of length `k + 1`.
let mut tails: Vec<usize> = Vec::new();
let mut previous = vec![usize::MAX; n];
for i in 0..n {
let mut lo = 0usize;
let mut hi = tails.len();
while lo < hi {
let mid = (lo + hi) / 2;
if x[tails[mid]] < x[i] {
lo = mid + 1;
} else {
hi = mid;
}
}