algos/cs/dynamic/
knuth_optimization.rs

1//! lib.rs
2
3/// Computes the minimum cost to merge consecutive segments (files) with sizes given in `weights`
4/// using Knuth Optimization for an O(n^2) solution.
5///
6/// # Arguments
7///
8/// - `weights`: a slice of non-negative sizes (e.g., file sizes).
9///
10/// # Returns
11///
12/// - `dp[0][n-1]`: the minimal total cost to merge all segments into one.
13/// - an optional 2D array `opt` that records the partition points (the "k" values).
14///   You can use `reconstruct_optimal_merge` (below) to build an actual merge order
15///   if you desire, though many applications only need the minimal cost.
16///
17/// # Examples
18///
19/// ```
20/// use algos::cs::dynamic::knuth_optimization::min_merge_cost_knuth;
21///
22/// let weights = vec![10, 20, 30];
23/// let (cost, _) = min_merge_cost_knuth(&weights);
24/// assert_eq!(cost, 90);
25/// // Explanation:
26/// // - Merge (10, 20) cost=30, new array [30, 30]
27/// // - Merge (30, 30) cost=60
28/// // total=90
29/// ```
30pub fn min_merge_cost_knuth(weights: &[usize]) -> (usize, Vec<Vec<usize>>) {
31    let n = weights.len();
32    if n == 0 {
33        return (0, Vec::new());
34    }
35    if n == 1 {
36        return (0, vec![vec![0; 1]; 1]); // no cost to "merge" a single item
37    }
38
39    // Prefix sums for quick subarray cost calculation: prefix_sum[i] = sum of weights[..i].
40    // So sum of weights[i..j] = prefix_sum[j] - prefix_sum[i].
41    let mut prefix_sum = vec![0; n + 1];
42    for i in 0..n {
43        prefix_sum[i + 1] = prefix_sum[i] + weights[i];
44    }
45
46    // dp[i][j] = minimal cost to merge subarray i..j.
47    let mut dp = vec![vec![0_usize; n]; n];
48    // opt[i][j] = the best "k" that achieves the min cost for dp[i][j].
49    // We'll fill it in to enable Knuth's bounding technique.
50    let mut opt = vec![vec![0_usize; n]; n];
51
52    // Base case: dp[i][i] = 0 (no cost to have a single element).
53    for i in 0..n {
54        dp[i][i] = 0;
55        opt[i][i] = i;
56    }
57
58    // cost function
59    let cost = |i: usize, j: usize| prefix_sum[j + 1] - prefix_sum[i];
60
61    // For subarray length from 2 to n
62    for length in 2..=n {
63        for i in 0..=n - length {
64            let j = i + length - 1;
65
66            // We only need to check k from opt[i][j-1]..=opt[i+1][j]
67            // Because of Knuth's monotonic queue bounding.
68            // But we must ensure these indices are within [i..j-1].
69            let start_k = opt[i][j.saturating_sub(1)].max(i);
70            let end_k = opt
71                .get(i + 1)
72                .map(|row| row.get(j).cloned().unwrap_or(j.saturating_sub(1)))
73                .unwrap_or(j.saturating_sub(1))
74                .min(j.saturating_sub(1));
75
76            let mut min_val = usize::MAX;
77            let mut best_k = start_k;
78            for k in start_k..=end_k {
79                let val = dp[i][k] + dp[k + 1][j];
80                if val < min_val {
81                    min_val = val;
82                    best_k = k;
83                }
84            }
85            dp[i][j] = min_val + cost(i, j);
86            opt[i][j] = best_k;
87        }
88    }
89
90    (dp[0][n - 1], opt)
91}
92
93/// Reconstructs one optimal merge sequence using the `opt` table from `min_merge_cost_knuth`.
94///
95/// Returns a list of merges in the form `(start, mid, end)` meaning "merge subarray `[start..=mid]` with `[mid+1..=end]`".
96/// This is one way to represent the merge strategy, but in practice you might
97/// only need the minimal cost.
98///
99/// # Arguments
100/// - `opt`: the 2D table from `min_merge_cost_knuth`
101/// - `i`: start index of the subarray
102/// - `j`: end index of the subarray
103///
104/// # Examples
105///
106/// ```
107/// use algos::cs::dynamic::knuth_optimization::{min_merge_cost_knuth, reconstruct_optimal_merge};
108///
109/// let weights = vec![10, 20, 30];
110/// let (_, s) = min_merge_cost_knuth(&weights);
111/// let merges = reconstruct_optimal_merge(&s, 0, weights.len() - 1);
112/// assert_eq!(merges.len(), 2); // Two merge operations needed
113/// ```
114pub fn reconstruct_optimal_merge(
115    opt: &Vec<Vec<usize>>,
116    i: usize,
117    j: usize,
118) -> Vec<(usize, usize, usize)> {
119    let mut result = Vec::new();
120    reconstruct_optimal_merge_rec(opt, i, j, &mut result);
121    result
122}
123
124fn reconstruct_optimal_merge_rec(
125    opt: &Vec<Vec<usize>>,
126    i: usize,
127    j: usize,
128    merges: &mut Vec<(usize, usize, usize)>,
129) {
130    if i >= j {
131        return;
132    }
133    let k = opt[i][j];
134    // Record the merge of [i..=k] with [k+1..=j]
135    merges.push((i, k, j));
136    reconstruct_optimal_merge_rec(opt, i, k, merges);
137    reconstruct_optimal_merge_rec(opt, k + 1, j, merges);
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_single_file() {
146        let weights = vec![10];
147        let (cost, _opt) = min_merge_cost_knuth(&weights);
148        assert_eq!(cost, 0, "Merging 1 file is zero cost");
149    }
150
151    #[test]
152    fn test_two_files() {
153        let weights = vec![10, 20];
154        let (cost, _opt) = min_merge_cost_knuth(&weights);
155        // Merging [10, 20] => cost=10+20=30
156        assert_eq!(cost, 30);
157    }
158
159    #[test]
160    fn test_three_files() {
161        // Common example: [10, 20, 30]
162        // (10 + 20)=30 => cost=30
163        // merges to [30, 30], final merge cost=60 => total=90
164        let weights = vec![10, 20, 30];
165        let (cost, opt) = min_merge_cost_knuth(&weights);
166        assert_eq!(cost, 90);
167
168        let merges = reconstruct_optimal_merge(&opt, 0, weights.len() - 1);
169        // merges might be something like:
170        //    (0,0,1) => merges subarray [0..=0] with [1..=1]
171        //    (0,1,2) => merges subarray [0..=1] with [2..=2]
172        // The order can vary depending on how ties are broken.
173        assert_eq!(merges.len(), 2);
174    }
175
176    #[test]
177    fn test_four_files() {
178        // Example: [1,2,3,4]
179        // There's more than one merge order but let's just check the final cost is correct.
180        // Known minimal cost is 19:
181        // e.g., merge(1,2)=3 -> cost=3, new array [3,3,4], merge(3,3)=6 -> cost=6, new array [6,4], final merge=10 => total=3+6+10=19
182        let weights = vec![1, 2, 3, 4];
183        let (cost, _opt) = min_merge_cost_knuth(&weights);
184        assert_eq!(cost, 19);
185    }
186}