Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1199. Minimum Time to Build Blocks - Leetcode Solution

Problem Description

You are given an array blocks where blocks[i] is the time needed to build the i-th block. You also have a fixed split time, which is the time it takes to split your current team of workers into two smaller teams (doubling the number of workers).

Initially, you have only one worker. Each worker can build only one block at a time, but you can split your team as many times as you want (each split doubles your number of workers, but takes split time). Your task is to determine the minimum total time needed to build all the blocks.

  • Each block must be built by a single worker
  • Workers can split at any point, and splits can be performed in parallel by multiple workers
  • The goal is to minimize the total time to complete all blocks

Constraints:

  • 1 <= blocks.length <= 1000
  • 1 <= blocks[i], split <= 10^5

Thought Process

At first glance, you might consider assigning each worker to a block one by one. However, since you can split workers (but splitting takes time), you need to balance between spending time splitting and spending time building blocks.

The brute-force approach would be to try every possible way of splitting and assigning workers, but this quickly becomes infeasible as the number of blocks increases.

The key insight is that splits allow us to process more blocks in parallel, but each split costs time. So, we want to use splits wisely to minimize the overall time. This is similar to the problem of scheduling jobs on parallel machines, where splitting is analogous to provisioning new machines (with a cost).

We need an efficient way to decide when to split and which blocks to assign to which workers, aiming to minimize the maximum time any worker spends (since that's our total time).

Solution Approach

We can approach this problem using a greedy and recursive strategy, often implemented with a priority queue (min-heap). Here's how:

  1. Sort the blocks in descending order:
    • We want to build the largest blocks as early as possible, so that their long build times can overlap with splits and other work.
  2. Use a min-heap to simulate parallel work:
    • Each entry in the heap represents the current total time a worker will take to finish their assigned blocks.
    • Initially, all blocks are in the heap (since each block needs to be built).
  3. Iteratively combine the two smallest jobs:
    • At each step, remove the two smallest times from the heap (these represent two workers finishing their blocks).
    • Combine them by simulating a split: the new time is max(time1, time2) + split.
    • Push this new time back into the heap.
    • This simulates two workers finishing, splitting, and then one worker handling the combined work.
  4. Repeat until one time remains:
    • When only one time is left in the heap, that's the minimum total time needed to build all blocks.

This approach works because it always combines the smallest jobs first, minimizing the time spent waiting for long jobs to finish and efficiently using splits.

Example Walkthrough

Let's consider blocks = [1, 2, 3, 4] and split = 1.

  1. Sort blocks descending: [4, 3, 2, 1]
  2. Initialize heap: [1, 2, 3, 4]
  3. Pop two smallest: 1 and 2. Combine: max(1, 2) + 1 = 3. Heap: [3, 3, 4]
  4. Pop two smallest: 3 and 3. Combine: max(3, 3) + 1 = 4. Heap: [4, 4]
  5. Pop two smallest: 4 and 4. Combine: max(4, 4) + 1 = 5. Heap: [5]
  6. Only one time remains: 5

The minimum total time to build all blocks is 5.

Time and Space Complexity

  • Brute-force: The number of ways to split and assign blocks grows exponentially with the number of blocks, making it infeasible for large inputs (O(2^n)).
  • Optimized (heap-based):
    • Each heap operation (push/pop) is O(\log n).
    • We perform n-1 combinations, so total time is O(n \log n).
    • Space complexity is O(n) for the heap.

Summary

In summary, the problem is about efficiently scheduling block builds with the option to split workers, where each split incurs a cost. The optimal solution uses a min-heap to always combine the smallest jobs, simulating parallel work and splits, and ensuring the minimum total build time. The approach is efficient and leverages greedy and heap data structures for optimal job scheduling.

Code Implementation

import heapq

class Solution:
    def minBuildTime(self, blocks, split):
        heapq.heapify(blocks)
        while len(blocks) > 1:
            x = heapq.heappop(blocks)
            y = heapq.heappop(blocks)
            heapq.heappush(blocks, max(x, y) + split)
        return blocks[0]
      
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;

class Solution {
public:
    int minBuildTime(vector<int>& blocks, int split) {
        priority_queue<int, vector<int>, greater<int>> pq(blocks.begin(), blocks.end());
        while (pq.size() > 1) {
            int x = pq.top(); pq.pop();
            int y = pq.top(); pq.pop();
            pq.push(max(x, y) + split);
        }
        return pq.top();
    }
};
      
import java.util.*;

class Solution {
    public int minBuildTime(int[] blocks, int split) {
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for (int b : blocks) pq.offer(b);
        while (pq.size() > 1) {
            int x = pq.poll();
            int y = pq.poll();
            pq.offer(Math.max(x, y) + split);
        }
        return pq.peek();
    }
}
      
class MinHeap {
    constructor() {
        this.heap = [];
    }
    push(val) {
        this.heap.push(val);
        this._siftUp();
    }
    pop() {
        if (this.heap.length === 1) return this.heap.pop();
        const top = this.heap[0];
        this.heap[0] = this.heap.pop();
        this._siftDown();
        return top;
    }
    _siftUp() {
        let idx = this.heap.length - 1;
        while (idx > 0) {
            let parent = Math.floor((idx - 1) / 2);
            if (this.heap[parent] <= this.heap[idx]) break;
            [this.heap[parent], this.heap[idx]] = [this.heap[idx], this.heap[parent]];
            idx = parent;
        }
    }
    _siftDown() {
        let idx = 0, len = this.heap.length;
        while (true) {
            let left = 2 * idx + 1, right = 2 * idx + 2, smallest = idx;
            if (left < len && this.heap[left] < this.heap[smallest]) smallest = left;
            if (right < len && this.heap[right] < this.heap[smallest]) smallest = right;
            if (smallest === idx) break;
            [this.heap[smallest], this.heap[idx]] = [this.heap[idx], this.heap[smallest]];
            idx = smallest;
        }
    }
    size() { return this.heap.length; }
    top() { return this.heap[0]; }
}

var minBuildTime = function(blocks, split) {
    const heap = new MinHeap();
    for (let b of blocks) heap.push(b);
    while (heap.size() > 1) {
        let x = heap.pop();
        let y = heap.pop();
        heap.push(Math.max(x, y) + split);
    }
    return heap.top();
};