The Painter’s Partition Problem Part I
April 5, 2011 in dynamic programming
You have to paint N boards of length {A0, A1, A2 … AN-1}. There are K painters available and you are also given how much time a painter takes to paint 1 unit of board. You have to get this job done as soon as possible under the constraints that any painter will only paint continuous sections of board, say board {2, 3, 4} or only board {1} or nothing but not board {2, 4, 5}.
This is a problem which is fairly difficult. Try your very best to really think through it. You will improve your problem solving technique by struggling through it!
It turns out that this problem is the same as “FairWorkload” from TopCoder’s division 1 level 2 problem in SRM 169. If you find the problem statement above is difficult to understand, head over to TopCoder’s FairWorkload problem statement for a clearer description with examples. This problem is an interesting problem because it has several approaches, which I will outline them one by one here.
For most people, the first natural approach is to brute force. Let’s look at an example where you have nine boards, each of length 100, and you have three painters. How would you divide the boards?
Very easy, just divide it into three equal-sized partitions!
100 100 100 | 100 100 100 | 100 100 100
But what if the boards are of different lengths? If we just divide the boards into three equal-sized partitions, the below configuration would be divided as:
100 200 300 | 400 500 600 | 700 800 900
The above allocation is clearly not appropriate, as it is extremely unfair to the third painter. The third painter has to paint 2400 units while the first painter paints only 600 units! The fairest possible way to divide the boards among the three painters should be:
100 200 300 400 500 | 600 700 | 800 900
In this way, the largest board to paint is 1700 units whilst the smallest board to paint is 1300 units. Compared to the previous allocation, this allocation allows painting to finish faster because the maximum over the partitions has decreased from 2400 to 1700 units.
Now that we understand the problem better, we can re-state the above problem as follow:
Divide A into k or fewer partitions,
such that the maximum sum over all the partitions is minimized.
By now, I am sure a handful of people would start thinking a heuristic approach to solve this problem. How about just computing the average size of a partition (ie, ∑{i=0..n-1} Ai / k) and try to match each partition as closely to the average as possible? Although this seemed to work for the above examples, there is no way you could evaluate all possibilities systematically and thus do not always produce the correct solution.
A more systematic way is to use the brute force approach to evaluate all possibilities. However, brute force in this case is not as straight forward as it used to be. Stop for awhile and think how you would do an exhaustive search for all possibilities. (Hint: Try to think recursively)
Recursion can be utilized to solve this problem easily. Imagine that we already have (k-1) partitions in place (the left partition) and we are trying to put the last separator (the (k-1)th separator). What are the possible locations to put the last separator? It must lie somewhere between the (j-1)th and jth element, where 1 = j = n. Once we have the last separator in place, we have completed all k partitions. The cost of this partition arrangement can be readily calculated as the larger of the two quantities below:
i) The cost of the last partition, ∑{i=j..n-1} Ai
ii) The cost of the largest partition formed to the left of j
(ie, the left partition = { A0, A1, ..., Aj-1 } ).Now, how do we find ii)? It turns out that we want to place the remaining (k-2) separators such that the left partition is divided as fairly as possible, which leads to another valid sub-problem itself. Therefore, we can solve it recursively!
By defining M[n, k] as the optimum cost of a partition arrangement with n total blocks from the first block and k partitions, the recurrence relation can be stated as follow:
n n-1 M[n, k] = min { max { M[j, k-1], ∑ Ai } } j=1 i=j
The base cases are:
M[1, k] = A0
n-1
M[n, 1] = ∑ Ai
i=0Therefore, the brute force solution can then be coded directly from the recurrence relation above:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | int sum(int A[], int from, int to) { int total = 0; for (int i = from; i <= to; i++) total += A[i]; return total; } int partition(int A[], int n, int k) { if (k == 1) return sum(A, 0, n-1); if (n == 1) return A[0]; int best = INT_MAX; for (int j = 1; j <= n; j++) best = min(best, max(partition(A, j, k-1), sum(A, j, n-1))); return best; } |
As you already knew, the brute force solution is definitely not very efficient. It is exponential in run time complexity due to re-computation of the same values over and over again. We can do much better by caching our results in a kxN table utilizing Dynamic programming (DP). The table has to be constructed in a way such that the sub-problems are calculated first in a bottom-up fashion.
Each entry in the table needs O(N2) time to calculate. This is due to the summation term (∑{i=j..n-1} Ai) needs O(N) time, and on top of that you need to find the minimum across all partitions which increases the complexity by an order to O(N2). Since there are a total of kN entries in the table, the overall run time complexity has to be O(kN3).
It is easy to further reduce the complexity down to O(kN2). The extra calculation of the summation term (∑{i=j..n-1} Ai) can be easily avoided by caching the results using an array that stores cumulative sums.
Below is the DP solution with O(kN2) run time and using O(kN) space complexity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | const int MAX_N = 100; int findMax(int A[], int n, int k) { int M[MAX_N+1][MAX_N+1] = {0}; int cum[MAX_N+1] = {0}; for (int i = 1; i <= n; i++) cum[i] = cum[i-1] + A[i-1]; for (int i = 1; i <= n; i++) M[i][1] = cum[i]; for (int i = 1; i <= k; i++) M[1][i] = A[0]; for (int i = 2; i <= k; i++) { for (int j = 2; j <= n; j++) { int best = INT_MAX; for (int p = 1; p <= j; p++) { best = min(best, max(M[p][i-1], cum[j]-cum[p])); } M[j][i] = best; } } return M[n][k]; } |
Follow up:
Could you think of another non DP algorithm which doesn’t requires any extra space?
» Continue reading Part II: The Painter’s Partition Problem.
The Painter's Partition Problem Part I,
Vanessa said on April 7, 2011
Hi, I like your posts very much, here just a simple question:
For this:
for (int j = 1; j <= n; j++)
best = min(best, max(partition(A, j, k-1), sum(A, j, n-1)));
Should it be fine if j start from k-1 rather than 1?
1337c0d3r said on April 7, 2011
Hey Vanessa,
Yes this is fine, and a sharp observation from you
I chose to start j from 1 instead of k-1, since this solves the more general case. Note that the original problem states that a painter can choose to do nothing, which translates to the following equivalent problem:
Given an array of non-negative integers A and a positive integer k, we want to:
Divide A into k or fewer partitions.
such that the maximum sum over all the partitions is minimized.
It is true that in the optimal case, we always divide A into exactly k partitions, not fewer.
This is because fewer partitions (less painter) will always lead to a direct increase in the cost of time to finish painting (which is not optimal).
coder said on April 7, 2011
How are you compensating for each painter’s speed?
1337c0d3r said on April 7, 2011
Assume each painter’s speed is the same. If not then it’s a much harder problem.
Sparsebase said on April 10, 2011
Just one observation. In the DP method, since the M[i,k-1] for i=n:1, and the cumulative sum are both monotonic, one should be able to use the binary search to further speed up the minimum search.
Eric said on April 16, 2011
“There are K painters available and you are also given how much time a painter takes to paint 1 unit of board.”
Doesn’t that imply that each painter has a different speed?
K said on April 18, 2011
I don’t understand how the following constraint was dealt with in the solution:
“…under the constraints that any painter will only paint continuous sections of board, say board {2, 3, 4} or only board {1} or nothing but not board {2, 4, 5}”
Ajit said on April 19, 2011
Ur explaination is awesome..:)
. and the kind of challeneg to pose to the visitor, they are jus facinating..:)
Thanks a lot..:) and keep posting..:)
lopp said on May 12, 2011
lets say if you have two boards with lengths 100 and 100 and two painters with speeds 1 and 100.
If you give to each of them one board then the total time will be 100 while if you give both of the boards to the second painter the total time will be 2.
Ming said on July 21, 2011
best = min(best, max(partition(A, j, k-1), sum(A, j, n-1)));
should be
best = min(best, max(partition(A, j-1, k-1), sum(A, j, n-1)));
anatoliy said on August 11, 2011
Seems like you get ‘index out of bound’ in the first code example.
Observe that when j reaches n in the loop, you are going to call
.
Machine Learning said on August 21, 2011
I don’t understand the problem itself. If I interpret the statement as it is, why can’t we just ask the fastest painter to paint all the boards?
mjb4 said on January 15, 2012
Im not sure but your recursivly defined Algorithm didnt give me the perfect solution for n=[4000-400], k=2 and elements from 0 to 10000. Maybe i did sth wrong converting ur code into php…
here a simple example given a couple of pictures:
A := {95, 31, 13, 87, 71, 48, 37, 73, 64, 93}
Sum of this is 612 best will be 306
My Result was not perfect but
M := {31, 93, 37, 87, 64}
Sum of this is 300 so difference is 12
The result with my implementation of ur code is 226 wich is far off in fact by 160
upi said on March 26, 2012
What about solution by binary searching + greedy?
by binary search we choose time, then in greedy way we assign boards to painters.
int l = 0, r= INT_INF ;
while (l > 1;
for (int i = 0, kk = k, mm = m ; i mm)
{
mm = m;
kk–;
i–;
}else mm -= A[i];
}
if (!k) l = m + 1;
else r = m;
}
l == r – answer
upi said on March 26, 2012
Problem with code displaying
upi said on March 26, 2012
Again
Code is on http://codepaste.ru/9865/
Moussa said on May 5, 2012
For this:
for (int j = 1; j <= n; j++)
best = min(best, max(partition(A, j, k-1), sum(A, j, n-1)));
Should it be j<n rather than j<=n?
Venki said on June 3, 2012
The code can be reduced one step further in the inner loop.