Maximum product cutting algorithm

Viewed 970

I am looking at the problem posted on https://www.geeksforgeeks.org/maximum-product-cutting-dp-36/#:~:text=Given%20a%20rope%20of%20length,is%20more%20than%202%20meters.

The problem statement is

Given a rope of length n meters, cut the rope in different parts of integer lengths in a way that maximizes product of lengths of all parts. You must make at least one cut. Assume that the length of rope is more than 2 meters.

It can be fairly simply solved with dynamic programming. This is their solution

// A Dynamic Programming solution for Max Product Problem 
int maxProd(int n) 
{ 
   int val[n+1]; 
   val[0] = val[1] = 0; 
   
   // Build the table val[] in bottom up manner and return 
   // the last entry from the table 
   for (int i = 1; i <= n; i++) 
   { 
      int max_val = 0; 
      for (int j = 1; j <= i/2; j++) 
         max_val = max(max_val, (i-j)*j, j*val[i-j]); 
      val[i] = max_val; 
   } 
   return val[n]; 
}

My question is, why is it valid for the inner loop to only go to i/2 instead of i - 1? This seems to be taken advantage of symmetry. However the max function is also over j * val[i - j], and it seems we are disgarding j = i/2 + 1, ..., i - 1.

2 Answers

Let’s take a step back from the DP solution here for a moment, since I think this follows from a mathematical property of the solution.

Let’s imagine that you have to make k cuts to a rope of length n. This is a bit more restrictive than the problem you’ve stated, in which you can make any (positive) number of cuts. What would the ideal solution look like? If you’re allowed to make any cut you want, not just integer cuts, the best answer would be to cut the rope into k pieces of size n/k. Why is this? Well, suppose you don’t do this. That means that some piece must be bigger than the average (let’s say its size is at least n/k + ε) and some piece must be smaller than the average (let’s say its size is at most n/k - ε). Then the product of these two pieces is at most

(n/k + ε)(n/k - ε)

= (n/k)2 - ε2.

Note that the bigger ε gets here - that is, the bigger the disparity in the sizes of the pieces - the smaller this product gets. That means that you’d be better off changing how these pieces were cut so that their sizes were closer to the average n/k.

The same logic applies even if the cuts have to be integer sizes. Imagine, for example, that two pieces have sizes that differ by at least two. Write one piece’s size as at least m + d and the other’s as at most m - d. Then the product of these pieces is m2 - d2, so you’d be better off making them as close to their average value as possible to keep d small.

So why the upper bound of n/2? Well, the above reasoning suggests we should keep the values as close together as possible if we’re making k cuts, and in particular with k cuts no piece should have a size bigger than ⌈n / k⌉. In the case where k = 2, the largest piece should never be bigger than ⌈k/2⌉. I think that’s where this bound comes from.

In fact, I’m fairly sure that this argument implies there’s a much faster solution to this problem. Rather than chopping off arbitrary amounts at each point and talking about making an arbitrary number of cuts, instead, iterate over the number of cuts made. For each number of cuts, we know what the sizes of the cuts ought to be - make the pieces’ sizes as close to the average as possible. There’s no need to use any DP to do that. In pseudocode:

max_cut = 0
for cuts = 2 to n:
    average_rounded_down = n // cuts
    pieces_above_average = n % cuts
    pieces_below_average = cuts - pieces_below_average
    value_of_this_cut = (average_rounded_down) ** pieces_below_average + (average_rounded_down + 1) ** pieces_above_average
    if max_cut < value_of_this_cut:
         max_cut = value_of_this_cut

This runs in time O(n), ignoring the cost of raising the values to the different powers (which, in fairness, isn’t O(1)).

I’m fairly sure this can be improved even further by using something like a binary search to improve the search for the optimal number of cuts, but alas I don’t have time right this minute to work out how to do that. :-)

Explanation to the algorithm given by the problem source:

We can see that there are many subproblems which are solved again and again. Since same subproblems are called again, this problem has Overlapping Subproblems property.... Like other typical Dynamic Programming(DP) problems, recomputations of same subproblems can be avoided by constructing a temporary array val[] in bottom up manner.

Although this does technically explain the reasoning, the explanation does not show everything. The original writer did not explain their thought process for the actual code very well. So the best any one could do would be to guess at what they were thinking. If you look at only the i<=5 cases, one could argue that the writer saw that symmetry and cut it off. This argument can be substantiated by the fact that they used the n=5 case as the example in the post.

To explain further, the following image shows the output of the max function as the loops iterate. The yellow indicates iterations that would be skipped by having the j <= i/2 as opposed to j <= i. The j=0 row represents the val array before the loops begin and the '-' value indicates an uninitialized value.

enter image description here

These results will also show how they further simplified the program. The results show that for every i greater than 4, the maximum value is always in the j=3 row.

If we see some examples of this problems, we can easily observe following pattern. The maximum product can be obtained be repeatedly cutting parts of size 3 while size is greater than 4, keeping the last part as size of 2 or 3 or 4. For example, n = 10, the maximum product is obtained by 3, 3, 4. For n = 11, the maximum product is obtained by 3, 3, 3, 2.

Related