4

I've came across some similar problems to this one in the past, and I still haven't got good idea how to solve this problem. Problem goes like this:

You are given an positive integer array with size n <= 1000 and k <= n which is the number of contiguous subarrays that you will have to split your array into. You have to output minimum m, where m = max{s[1],..., s[k]}, and s[i] is the sum of the i-th subarray. All integers in the array are between 1 and 100. Example :

Input:                           Output:
5  3  >> n = 5 k = 3             3
2 1 1 2 3

Splitting array into 2+1 | 1+2 | 3 will minimize the m.

My brute force idea was to make first subarray end at position i (for all possible i) and then try to split the rest of the array in k-1 subarrays in the best way possible. However, this is exponential solution and will never work.

So I'm looking for good ideas to solve it. If you have one please tell me.

Thanks for your help.

Mike Plott
  • 327
  • 1
  • 2
  • 6

6 Answers6

5

You can use dynamic programming to solve this problem, but you can actually solve with greedy and binary search on the answer. This algorithm's complexity is O(n log d), where d is the output answer. (An upper bound would be the sum of all the elements in the array.) (or O( n d ) in the size of the output bits)

The idea is to binary search on what your m would be - and then greedily move forward on the array, adding the current element to the partition unless adding the current element pushes it over the current m -- in that case you start a new partition. The current m is a success (and thus adjust your upper bound) if the numbers of partition used is less than or equal to your given input k. Otherwise, you used too many partitions, and raise your lower bound on m.

Some pseudocode:

// binary search
binary_search ( array, N, k ) {
    lower = max( array ), upper = sum( array )

    while lower < upper {
        mid = ( lower + upper ) / 2

        // if the greedy is good
        if partitions( array, mid ) <= k
           upper = mid
        else
           lower = mid
    }
 }

 partitions( array, m ) {
    count = 0
    running_sum = 0

    for x in array {
       if running_sum + x > m
          running_sum = 0
          count++
       running_sum += x
    }
    if running_sum > 0
       count++
    return count
 }

This should be easier to come up with conceptually. Also note that because of the monotonic nature of the partitions function, you can actually skip the binary search and do a linear search, if you are sure that the output d is not too big:

 for i = 0 to infinity
    if partitions( array, i ) <= k
       return i
Larry
  • 4,491
  • 2
  • 24
  • 16
  • Wow, this is such easy solution to implement and hard to come up with (at least for me). Thanks for the help. – Mike Plott Dec 21 '11 at 19:54
  • Honestly, for certain problems, I find myself being more confident with dynamic programming just because it's easier (for me) to prove optimality than proving the greedy is correct. – Larry Dec 21 '11 at 21:55
  • nice one Larry, this is great – FUD Dec 22 '11 at 03:21
3

Dynamic programming. Make an array

int best[k+1][n+1];

where best[i][j] is the best you can achieve splitting the first j elements of the array int i subarrays. best[1][j] is simply the sum of the first j array elements. Having row i, you calculate row i+1 as follows:

for(j = i+1; j <= n; ++j){
    temp = min(best[i][i], arraysum[i+1 .. j]);
    for(h = i+1; h < j; ++h){
        if (min(best[i][h], arraysum[h+1 .. j]) < temp){
            temp = min(best[i][h], arraysum[h+1 .. j]);
        }
    }
    best[i+1][j] = temp;
}

best[m][n] will contain the solution. The algorithm is O(n^2*k), probably something better is possible.

Edit: a combination of the ideas of ChingPing, toto2, Coffee on Mars and rds (in the order they appear as I currently see this page).

Set A = ceiling(sum/k). This is a lower bound for the minimum. To find a good upper bound for the minimum, create a good partition by any of the mentioned methods, moving borders until you don't find any simple move that still decreases the maximum subsum. That gives you an upper bound B, not much larger than the lower bound (if it were much larger, you'd find an easy improvement by moving a border, I think). Now proceed with ChingPing's algorithm, with the known upper bound reducing the number of possible branches. This last phase is O((B-A)*n), finding B unknown, but I guess better than O(n^2).

Daniel Fischer
  • 181,706
  • 17
  • 308
  • 431
  • 1
    I think this will work :D just a suggestion since there is a limit on the value of every element as 100...we can precompute value of arraysum[0...j] for j = 0 to n.. and then arraysum[i...j]==arraysum[0...j]-arraysum[0...i]..which bring it down to O(n*k) – FUD Dec 21 '11 at 16:13
  • 1
    Yes, I would also store the cumulative sum in an array, so `arraysum[a .. b]` would become `cum[b] - cum[a-1]`, but that doesn't make it O(n*k), the quadratic in `n` behaviour comes from the fact that we must check `j-i` possible positions for the last subarray to find `best[i+1][j]`. Of course one can shave off some constant factors by short-cutting. – Daniel Fischer Dec 21 '11 at 16:24
  • err..sorry you are right..just one other thing ..do you think it should be best[i+1][j] = min( best[i+1][j],temp ) – FUD Dec 21 '11 at 16:44
  • No, I think it shouldn't, `best[i+1][j]` is only set once after finding the minimum in `temp` (so, actually, we could eliminate `temp`, that might be clearer). – Daniel Fischer Dec 21 '11 at 17:12
  • @ChingPing ah, I was mentally on the wrong line and completely misunderstood. Of course you're right, we need `min` there. – Daniel Fischer Dec 21 '11 at 18:28
2

I have a sucky branch and bound algorithm ( please dont downvote me )

First take the sum of array and dvide by k, which gives you the best case bound for you answer i.e. the average A. Also we will keep a best solution seen so far for any branch GO ( global optimal ).Lets consider we put a divider( logical ) as a partition unit after some array element and we have to put k-1 partitions. Now we will put the partitions greedily this way,

Traverse the array elements summing them up until you see that at the next position we will exceed A, now make two branches one where you put the divider at this position and other where you put at next position, Do this recursiely and set GO = min (GO, answer for a branch ). If at any point in any branch we have a partition greater then GO or the no of position are less then the partitions left to be put we bound. In the end you should have GO as you answer.

EDIT: As suggested by Daniel, we could modify the divider placing strategy a little to place it until you reach sum of elements as A or the remaining positions left are less then the dividers.

FUD
  • 5,114
  • 7
  • 39
  • 61
  • I think this will in general do pretty well. An enhancement would be to recalculate the possible optimum at each point where you would cross it. We start with `A = ceiling(sum/k)`. At the first point where `running_sum[i]` would exceed `A`, calculate `B = ceiling((sum-running_sum[i-1])/(k-1))`. If `B >= running_sum[i]`, you don't need to branch and can update `A = running_sum[i]`. Similarly for later crossing points, if the remaining average would be larger than what you get by crossing, you don#t need to branch. – Daniel Fischer Dec 21 '11 at 16:40
  • 1
    Thanks guys for generous upvotes but my solution is not so good in all cases ... consider 1 1 1 9 as array and 3 partitions , my solution never reaches answer ..i wouldnt mind undoing the upvotes.. :) – FUD Dec 21 '11 at 16:49
  • 1
    Okay, work in extreme cases like that, `A = max{ max array, ceiling(sum/k) }`, don't run so far that you have fewer elements than subarrays left. Then I still think it's pretty good. – Daniel Fischer Dec 21 '11 at 17:16
1

This is just a sketch of an idea... I'm not sure that it works, but it's very easy (and probably fast too).

You start say by putting the separations evenly distributed (it does not actually matter how you start).

Make the sum of each subarray.
Find the subarray with the largest sum.
Look at the right and left neighbor subarrays and move the separation on the left by one if the subarray on the left has a lower sum than the one on the right (and vice-versa).
Redo for the subarray with the current largest sum.

You'll reach some situation where you'll keep bouncing the separation between the same two positions which will probably mean that you have the solution.

EDIT: see the comment by @rds. You'll have to think harder about bouncing solutions and the end condition.

toto2
  • 5,306
  • 21
  • 24
  • 1
    This is not correct. Counter example: [1; 40; 50; 1; 2; 40] with k=3. Start at (1;40)(50;1)(2;40). Maximum in the middle. Left is lower. Goes to (1;40;90)(1)(2;40). Goes back to (1;40)(50;1)(2;40). Ends. Hoever there is (1;40)(50)(1;2;40). – rds Dec 21 '11 at 17:10
  • @rds Thanks. That's why I had "will **probably** mean that you have the solution". I'll add an edit. – toto2 Dec 21 '11 at 17:32
0

If your array has random numbers, you can hope that a partition where each subarray has n/k is a good starting point.

From there

  1. Evaluate this candidate solution, by computing the sums
  2. Store this candidate solution. For instance with:
    • an array of the indexes of every sub-arrays
    • the corresponding maximum of sum over sub-arrays
  3. Reduce the size of the max sub-array: create two new candidates: one with the sub-array starting at index+1 ; one with sub-array ending at index-1
  4. Evaluate the new candidates.
    • If their maximum is higher, discard
    • If their maximum is lower, iterate on 2, except if this candidate was already evaluated, in which case it is the solution.
rds
  • 26,253
  • 19
  • 107
  • 134
0

My idea, which unfortunately does not work:

  1. Split the array in N subarrays
  2. Locate the two contiguous subarrays whose sum is the least
  3. Merge the subarrays found in step 2 to form a new contiguous subarray
  4. If the total number of subarrays is greater than k, iterate from step 2, else finish.
Coffee on Mars
  • 988
  • 6
  • 21
  • Unfortunately, that doesn't always work. For the example, this would produce `2 | 1 1 | 2 3` as the first step, and end with `2 1 1 | 2 | 3` or `2 | 1 1 2 | 3`. You would then need to check if you can decrease the maximum by moving borders. It would give you a good starting position for moving borders probably, and in most cases, you'd quickly find the optimum, but I'm not sure if there can be cases where you'd get stuck at a local but not global minimum. – Daniel Fischer Dec 21 '11 at 16:49
  • Yes, it would seem that the problem of having two little values close always brings to a local minimum, because the algorithm would merge them together rather than to the closer ones. – Coffee on Mars Dec 21 '11 at 17:02