Start learning free

LESSON 253 OF 347 · BINARY SEARCH

The Factory in C#: O(n log(k * T)) Binary Search on the Answer

Practice O(n * log(k * T)) Read the full explanation

There are N workers in a factory. Each worker packs the same type of product, and for each of them we know the time it takes them to pack one product.

Find out the minimum amount of time the workers need to pack at least K products.

Example:

Input: times = [4, 7, 3, 6, 7, 1], K = 60
Output:30
Explanation:
1st worker packs 7 products in 4 * 7 = 28 units of time
2nd worker packs 4 products in 7 * 4 = 28 units of time
3rd worker packs 10 products in 3 * 10 = 30 units of time
4th worker packs 5 products in 6 * 5 = 30 units of time
5th worker packs 4 products in 7 * 4 = 28 units of time
6th worker packs 30 products in 1 * 30 = 30 units of time
We have 7 + 4 + 10 + 5 + 4 + 30 = 60 total products
in max(28, 28, 30, 30, 28, 30) = 30 units of time
and this is the minimum amount of time
since in 29 units of time we can pack only
7 + 4 + 9 + 4 + 4 + 29 = 57 products

Note:

Your algorithm should run in O(n * log(k * T)) time and use O(1) extra space.


STEP 1 OF 13
    Ctrl + Enter to check

    C# · BINARY SEARCH

    The Factory in C#, explained

    Problem overview

    MinTime receives an int[] times, where times[i] is how many time units worker i needs to pack one product, and an int K. All workers pack at once. The method returns the smallest time after which they have packed at least K products between them.

    For times = {4, 7, 3, 6, 7, 1} and K = 60, the answer is 30. In 30 units the workers pack 7, 4, 10, 5, 4 and 30 products, which is exactly 60. In 29 units they pack only 7 + 4 + 9 + 4 + 4 + 29 = 57.

    A worker who needs t units per product packs time / t whole products in time units. With int operands, C# division already drops the fraction.

    The lesson asks for O(n * log(k * T)) time and O(1) extra space. Trying every time from 0 upward would be too slow when the answer is large.

    Approach: binary search on the answer

    The number of packed products never decreases as the time grows. So the times split into two groups: the short ones, which pack fewer than K products, and the long ones, which pack at least K. The answer is the first time of the second group. A sorted split like this is exactly what binary search needs, even though there is no array of times to look into. The search runs over the numbers 1, 2, 3, ... themselves, and the helper CountProducts plays the role of the comparison.

    Two bounds are needed. The lower one is 1. For the upper one, the fastest worker alone packs K products in minValue * K units, so the workers together never need more than that. The answer is therefore somewhere in [1, minValue * K].

    On each step, take the middle time mid and count the products:

    • If the count is below K, then mid is too short, and so is everything below it. Search the upper half.
    • Otherwise mid works, but a smaller time might too. Remember mid and keep searching the lower half.

    For the example, the bounds are 1 and 1 * 60 = 60. The search takes six rounds of halving and ends with time = 30, the same answer as the brute-force version in the previous lesson.

    Step by step

    1. Find the fastest worker. Start minValue at times[0] and lower it with Math.Min(minValue, t) in a foreach loop.
    2. Set the bounds. Declare left = 1 and right = minValue * K in one statement.
    3. Declare time as 0. It will hold the smallest good time found so far.
    4. Loop while left <= right.
    5. Compute mid as (left + right) / 2. Integer division drops the fraction.
    6. If CountProducts(times, mid) < K, the time is too short, so set left = mid + 1.
    7. Otherwise record time = mid first, then set right = mid - 1 to look for something shorter. Record before shrinking: the loop may end on a mid that was too short, so the last good one has to be saved.
    8. Return time after the loop.
    9. Write CountProducts above MinTime, with products starting at 0.
    10. Loop over the workers with foreach (int t in times) and add time / t to products.
    11. Return products.

    The solution in C#

    class Solution {
        public int CountProducts(int[] times, int time) {
            int products = 0;
            foreach (int t in times) {
                products += time / t;
            }
            return products;
        }
    
        public int MinTime(int[] times, int K) {
            int minValue = times[0];
            foreach (int t in times) {
                minValue = Math.Min(minValue, t);
            }
    
            int left = 1, right = minValue * K;
            int time = 0;
            while (left <= right) {
                int mid = (left + right) / 2;
                if (CountProducts(times, mid) < K) {
                    left = mid + 1;
                } else {
                    time = mid;
                    right = mid - 1;
                }
            }
            return time;
        }
    }
    

    C# details worth knowing

    • Two variables in one declaration. int left = 1, right = minValue * K; declares both as int, and the second initializer can use minValue, which exists already.
    • Math.Min. It lives in the System namespace and returns the smaller of two numbers. Names in C# are case-sensitive, so Math.min does not exist.
    • int overflow is silent. C# does not stop on overflow by default. If minValue * K exceeds about 2.1 billion, the product wraps around to a wrong number. For example, with times = {1000000} and K = 5000, the true bound is 5,000,000,000, which doesn't fit in an int. The wrapped bound is 705032704, and this solution then returns 0, which is wrong. Computing the bound as a long avoids this. Likewise, (left + right) / 2 can overflow when both bounds are large, and left + (right - left) / 2 is the safe form. Within the sizes this lesson uses, the plain versions are fine.
    • Dividing by a worker's time. A time of 0 would throw a DivideByZeroException, so the problem assumes positive times.

    Complexity

    The search range has about minValue * K values, and each iteration halves it, so the loop runs about log(minValue * K) times. Each iteration calls CountProducts, which is a pass over the n workers. The total time is O(n * log(k * T)), where T stands for the size of a worker's time. The first pass that finds the fastest worker costs O(n) and doesn't change the total.

    The solution keeps only a handful of int variables, so the extra space is O(1).

    Edge cases and common mistakes

    • Forgetting to record time. The loop can finish on a mid that was too short, so the result has to be the last good mid saved in the else branch, not left or right.
    • Setting right = mid or left = mid. With the left <= right loop, each branch must move past mid (mid + 1 or mid - 1), or the search can repeat the same mid forever.
    • Using <= in the count test. A count of exactly K is good enough, so the comparison is a strict <.
    • A too-small upper bound. Starting right below the true answer makes the search return a wrong time. The fastest worker alone is a bound that is always large enough.
    • One worker. For times = {5} and K = 1, the bounds are 1 and 5, and the method returns 5.

    Testing

    timesKresult
    {4, 7, 3, 6, 7, 1}6030
    {5}15
    {3, 3}13
    Keep going with C#About 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free