Taro Logo

House Robber IV

Medium
Google logo
Google
4 views
Topics:
ArraysBinary SearchGreedy Algorithms

There are several consecutive houses along a street, each of which has some money inside. A robber wants to steal from these homes but refuses to steal from adjacent houses. The robber's capability is the maximum amount of money stolen from any single house they robbed.

You are given an integer array nums representing the amount of money in each house (nums[i] dollars in the i-th house). You are also given an integer k, representing the minimum number of houses the robber must steal from. Assume it's always possible to steal at least k houses.

Return the minimum capability of the robber out of all possible ways to steal at least k houses.

Example 1:

Input: nums = [2, 3, 5, 9], k = 2 Output: 5

Explanation: Possible ways to rob at least 2 houses:

  • Houses at indices 0 and 2. Capability is max(nums[0], nums[2]) = 5.
  • Houses at indices 0 and 3. Capability is max(nums[0], nums[3]) = 9.
  • Houses at indices 1 and 3. Capability is max(nums[1], nums[3]) = 9. Therefore, return min(5, 9, 9) = 5.

Example 2:

Input: nums = [2, 7, 9, 3, 1], k = 2 Output: 2

Explanation: The way which leads to minimum capability is to rob the house at index 0 and 4. Return max(nums[0], nums[4]) = 2.

Solution


Clarifying Questions

When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:

  1. What are the constraints on the size of the `nums` array and the value of `k`? Are there any limits I should be aware of?
  2. Can the values in the `nums` array be zero? Are they guaranteed to be non-negative as stated, or could they be negative?
  3. If it's impossible to rob at least `k` houses, what value should I return? Is there a specified error code or a defined maximum value I should use?
  4. Can you provide an example where the optimal capacity isn't immediately obvious, perhaps with multiple houses having the same amount of money?
  5. Is there a requirement to minimize the number of houses robbed, or is it sufficient to rob any set of at least `k` houses with the minimum capacity?

Brute Force Solution

Approach

The goal is to determine the minimum capacity needed to rob a given number of houses. The brute force approach involves trying every possible combination of houses to rob, and checking if the selected houses meet the minimum house count.

Here's how the algorithm would work step-by-step:

  1. Start by considering all possible subsets of houses.
  2. For each subset, count the number of houses selected to be robbed.
  3. If the number of selected houses is less than the required minimum, discard this subset because it's not a valid solution.
  4. If the number of selected houses is equal to or greater than the required minimum, check if any two robbed houses are adjacent to each other.
  5. If adjacent houses are robbed, discard the subset, because we cannot rob adjacent houses.
  6. If the subset is valid (meets the house count and no adjacent houses are robbed), calculate the maximum amount stolen, which will be equal to the largest value from the capacity of the houses robbed in that subset.
  7. Keep track of the minimum of the maximum stolen amounts obtained from all valid subsets.
  8. After checking every possible subset, the minimum of maximum stolen amounts will be the answer.

Code Implementation

def house_robber_brute_force(house_capacities, number_of_houses_to_rob):
    number_of_houses = len(house_capacities)
    minimum_maximum_stolen = float('inf')

    for i in range(1 << number_of_houses):
        houses_robbed = []
        for j in range(number_of_houses):
            if (i >> j) & 1:
                houses_robbed.append(j)

        number_of_houses_robbed = len(houses_robbed)

        # Discard if not enough houses are robbed
        if number_of_houses_robbed < number_of_houses_to_rob:
            continue

        adjacent_houses_robbed = False

        # Checking for adjacent robbed houses
        for k in range(number_of_houses_robbed - 1):
            if houses_robbed[k+1] == houses_robbed[k] + 1:
                adjacent_houses_robbed = True
                break

        # Discard if adjacent houses are robbed
        if adjacent_houses_robbed:
            continue

        # Calculate the maximum stolen capacity for this subset
        maximum_stolen = 0
        for house_index in houses_robbed:
            maximum_stolen = max(maximum_stolen, house_capacities[house_index])

        # Update the minimum of maximum stolen amounts
        minimum_maximum_stolen = min(minimum_maximum_stolen, maximum_stolen)

    if minimum_maximum_stolen == float('inf'):
        return -1
    else:
        return minimum_maximum_stolen

Big(O) Analysis

Time Complexity
O(2^n * n)The brute force approach explores all possible subsets of houses. For 'n' houses, there are 2^n possible subsets. For each subset, we iterate through the subset to check if it is a valid solution. Validity checks include checking the subset size (at most O(n)) and checking for adjacent houses being robbed (at most O(n)). Thus, we have 2^n subsets and for each we spend O(n) time checking its validity. Therefore the time complexity is O(2^n * n).
Space Complexity
O(2^N)The brute force approach involves generating all possible subsets of houses. Each subset requires storage, and in the worst case, we need to store all 2^N possible subsets to determine the valid ones and their maximum stolen amounts. The size of each subset is at most N (the number of houses), however we need space to hold potentially all 2^N of them. Therefore, the auxiliary space complexity is dominated by the storage of these subsets, which is O(2^N).

Optimal Solution

Approach

The best way to solve this problem is by using a guessing game based on what the smallest amount we are willing to steal could be. We then check if that amount is actually possible to steal without robbing adjacent houses, and we repeat until we find the smallest possible amount.

Here's how the algorithm would work step-by-step:

  1. First, figure out the range of possible amounts we could steal. The smallest possible amount is 0, and the largest possible amount is just the value of the most expensive house.
  2. Now, let's pick a number in the middle of that range and imagine that's the smallest amount we're willing to steal from any single house.
  3. Go through all the houses and decide whether to rob them or not, given this minimum amount. Remember, we can't rob houses next to each other. If a house has a value less than our minimum, we simply don't rob it. If a house is greater than our minimum, we consider robbing it, so long as its neighbours aren't robbed.
  4. After checking every house, see if we managed to rob at least the required number of houses.
  5. If we robbed enough houses, it means we were too strict with our minimum. We can try a smaller minimum next time by shrinking our search range from the middle downwards. But if we didn't rob enough houses, it means we weren't strict enough, so we can try a larger minimum next time by shrinking our search range upwards.
  6. Keep repeating this guessing process until we find the smallest possible amount we could steal while still robbing the required number of houses. That amount is the answer.

Code Implementation

def house_robber_iv(nums, houses_to_rob):
    left_bound = 0
    right_bound = max(nums)

    while left_bound <= right_bound:
        minimum_stolen_amount = (left_bound + right_bound) // 2
        
        # Check if robbing houses w/ min amount is possible.
        if is_possible_to_rob(nums, houses_to_rob, minimum_stolen_amount):
            right_bound = minimum_stolen_amount - 1
        
        # Need to consider larger amounts next time.
        else:
            left_bound = minimum_stolen_amount + 1

    return left_bound

def is_possible_to_rob(house_values, houses_to_rob, minimum_stolen_amount):
    robbed_houses_count = 0
    previous_robbed = False

    for house_value in house_values:
        if house_value >= minimum_stolen_amount and not previous_robbed:
            robbed_houses_count += 1
            previous_robbed = True
        else:
            previous_robbed = False

    # Return if we robbed enough houses.
    return robbed_houses_count >= houses_to_rob

Big(O) Analysis

Time Complexity
O(n log m)The algorithm uses binary search to find the minimum amount to steal, where m is the maximum value of any house. Inside each binary search iteration, it iterates through all n houses to determine if it's possible to rob at least k houses with the current minimum amount. Therefore, the time complexity is O(n) for each binary search step. The binary search performs approximately log m steps, where m is the range of possible minimum amounts (0 to max(nums)). Combining these gives a total time complexity of O(n log m).
Space Complexity
O(1)The described algorithm performs a binary search and checks if a 'minimum' amount is achievable. It does not create any auxiliary data structures like arrays, lists, or hash maps to store intermediate results. The space used is limited to variables for the binary search (range boundaries, middle point) and the boolean logic to track if a house should be robbed, all of which take constant space. Therefore, the auxiliary space complexity is independent of the input size N (number of houses) and is O(1).

Edge Cases

CaseHow to Handle
Empty input array (nums is null or has length 0)Return 0 if k is 0, otherwise return -1 or throw an exception as robbing k houses is impossible.
k is 0Return 0, as no houses need to be robbed, so no capacity is needed.
k is greater than the number of houses that can be robbed (more than half rounded up)Return -1 or throw an exception, as it is impossible to rob k houses under the adjacency constraint.
Array contains only one element and k is 1Return the value of the single element in the array.
All houses have the same amount of moneyThe binary search for capacity will still work correctly, converging to the correct minimum capacity.
Large input array size (performance consideration)The binary search approach will handle large arrays reasonably well with logarithmic time complexity for the search, but the feasibility check must be O(n).
Extreme values in the input array causing integer overflowUse long data type for calculations involving house values to prevent potential overflow.
All house values are zeroIf k > 0, return 0 because you can still rob the houses; if k==0, then return 0 as well.