Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1298. Maximum Candies You Can Get from Boxes - Leetcode Solution

Problem Description

The Maximum Candies You Can Get from Boxes problem requires you to maximize the number of candies you can collect from a set of boxes, given certain rules:

  • You are given several boxes, each of which may contain candies, keys to other boxes, or other boxes.
  • Some boxes are initially open, while others are locked.
  • You can only open a box if you have the box and it is open, or you have a key to unlock it.
  • When you open a box, you collect all candies inside, all keys, and all contained boxes.
  • Your goal is to maximize the total number of candies you can collect, starting from the initial set of boxes.

Constraints:

  • You cannot reuse a box once opened.
  • You may find keys and boxes inside other boxes, which can be used to unlock/open them later.
  • There is always at least one way to proceed, but you must optimize for the maximum candies.
Input:
  • candies: An array where candies[i] is the number of candies in box i
  • keys: keys[i] is a list of keys inside box i
  • containedBoxes: containedBoxes[i] is a list of boxes inside box i
  • initialBoxes: The list of boxes you start with
  • status: status[i] is 1 if box i is open, 0 if locked

Thought Process

The problem is essentially about exploring a graph of boxes, where each box may unlock new boxes or keys to other boxes. The naive approach would be to try every possible sequence of opening boxes, but this quickly becomes infeasible due to the exponential number of possibilities.

To optimize, we can think of this as a Breadth-First Search (BFS) or queue-based exploration: we start with the boxes we have, open those that are unlocked, and as we find keys or new boxes, we add them to our "frontier" of things to process. We must also ensure we do not revisit boxes we've already opened, to avoid cycles or overcounting candies.

The main challenge is to manage the dependencies between boxes and keys efficiently, so that whenever we obtain a new key, we can check if we now have access to a previously locked box.

Solution Approach

We solve this problem using a BFS strategy, treating boxes as nodes in a graph. Here's how:

  1. Initialize a queue with all boxes from initialBoxes.
  2. Maintain a set of opened boxes to avoid revisiting.
  3. Maintain a set of available keys so we know which boxes can be unlocked.
  4. For each box in the queue:
    • If the box is open or we have its key, and it hasn't been opened yet:
      • Open it, collect candies.
      • Add any new keys found to our key set.
      • Add any contained boxes to the queue.
    • If the box is locked and we don't have its key, skip it for now (but keep it in a "waiting" queue to try again when we get new keys).
  5. Repeat until there are no more boxes to process.
  6. Return the total number of candies collected.

Design Choices:

  • We use a queue (FIFO) to process boxes in the order we gain access to them, ensuring we don't miss any accessible boxes.
  • A set for opened boxes ensures we don't double-count candies or get stuck in cycles.
  • A set for keys provides O(1) lookup for unlocking boxes as soon as we get the key.
  • We may need to process some boxes multiple times: if a box is locked, we may revisit it after we get its key.

Example Walkthrough

Example:
Suppose:

  • candies = [1, 0, 5, 2]
  • status = [1, 0, 1, 0]
  • keys = [[], [2], [], []]
  • containedBoxes = [[1,2], [3], [], []]
  • initialBoxes = [0]
Step-by-step:
  1. Start with box 0 (open). Open it, collect 1 candy. Find boxes 1 and 2 inside.
  2. Box 1 is locked (status 0), so we can't open it yet. Box 2 is open (status 1), so open it, collect 5 candies.
  3. Box 2 contains no boxes or keys.
  4. Box 1 contains box 3 and key to box 2. But we can't open box 1 yet (no key).
  5. We have no keys, so we're stuck. But wait, box 1 was inside box 0, and box 0 is open, so we already processed its contents. But we still can't open box 1, as we have no key.
  6. So, the process ends here. Total candies collected: 1 (box 0) + 5 (box 2) = 6.

If you get a key during the process, you revisit any boxes that were previously locked but now can be opened.

Time and Space Complexity

Brute-force approach: Would try every possible order of opening boxes, which is exponential: O(2^n) for n boxes.

Optimized BFS approach:

  • Each box is processed at most once, so O(n) time for n boxes.
  • For each box, we may process all its keys and contained boxes, but the total number of keys and contained boxes is bounded by the total number of boxes, so still O(n).
  • Space complexity is O(n) for the queue, sets, and storage.

Thus, our approach is efficient and scales linearly with the number of boxes.

Summary

We modeled the problem as a graph traversal, using BFS to systematically explore boxes as we unlock them. By tracking opened boxes and available keys, we ensure we never double-count or get stuck. The solution is efficient, elegant, and leverages classic data structures (queue and set) to manage dependencies and maximize the number of candies collected.

Code Implementation

from collections import deque

class Solution:
    def maxCandies(self, status, candies, keys, containedBoxes, initialBoxes):
        n = len(status)
        queue = deque(initialBoxes)
        has_key = set()
        opened = set()
        boxes = set(initialBoxes)
        res = 0

        while queue:
            size = len(queue)
            progress = False
            for _ in range(size):
                box = queue.popleft()
                if box in opened:
                    continue
                if status[box] == 1 or box in has_key:
                    res += candies[box]
                    opened.add(box)
                    # Add any keys found
                    for k in keys[box]:
                        if k not in has_key:
                            has_key.add(k)
                            if k in boxes and k not in opened:
                                queue.append(k)
                    # Add any contained boxes
                    for b in containedBoxes[box]:
                        if b not in boxes:
                            boxes.add(b)
                            queue.append(b)
                    progress = True
                else:
                    queue.append(box)  # Try again later
            if not progress:
                break  # No new boxes opened this round, so stop
        return res
      
#include <vector>
#include <queue>
#include <unordered_set>
using namespace std;

class Solution {
public:
    int maxCandies(vector<int>& status, vector<int>& candies, vector<vector<int>>& keys, vector<vector<int>>& containedBoxes, vector<int>& initialBoxes) {
        int n = status.size();
        queue<int> q;
        unordered_set<int> hasKey, opened, boxes;
        int res = 0;
        for (int b : initialBoxes) {
            q.push(b);
            boxes.insert(b);
        }
        while (!q.empty()) {
            int size = q.size();
            bool progress = false;
            for (int i = 0; i < size; ++i) {
                int box = q.front(); q.pop();
                if (opened.count(box)) continue;
                if (status[box] == 1 || hasKey.count(box)) {
                    res += candies[box];
                    opened.insert(box);
                    for (int k : keys[box]) {
                        if (!hasKey.count(k)) {
                            hasKey.insert(k);
                            if (boxes.count(k) && !opened.count(k))
                                q.push(k);
                        }
                    }
                    for (int b : containedBoxes[box]) {
                        if (!boxes.count(b)) {
                            boxes.insert(b);
                            q.push(b);
                        }
                    }
                    progress = true;
                } else {
                    q.push(box);
                }
            }
            if (!progress) break;
        }
        return res;
    }
};
      
import java.util.*;

class Solution {
    public int maxCandies(int[] status, int[] candies, int[][] keys, int[][] containedBoxes, int[] initialBoxes) {
        int n = status.length;
        Queue<Integer> queue = new LinkedList<>();
        Set<Integer> hasKey = new HashSet<>();
        Set<Integer> opened = new HashSet<>();
        Set<Integer> boxes = new HashSet<>();
        int res = 0;
        for (int b : initialBoxes) {
            queue.offer(b);
            boxes.add(b);
        }
        while (!queue.isEmpty()) {
            int size = queue.size();
            boolean progress = false;
            for (int i = 0; i < size; i++) {
                int box = queue.poll();
                if (opened.contains(box)) continue;
                if (status[box] == 1 || hasKey.contains(box)) {
                    res += candies[box];
                    opened.add(box);
                    for (int k : keys[box]) {
                        if (!hasKey.contains(k)) {
                            hasKey.add(k);
                            if (boxes.contains(k) && !opened.contains(k))
                                queue.offer(k);
                        }
                    }
                    for (int b : containedBoxes[box]) {
                        if (!boxes.contains(b)) {
                            boxes.add(b);
                            queue.offer(b);
                        }
                    }
                    progress = true;
                } else {
                    queue.offer(box);
                }
            }
            if (!progress) break;
        }
        return res;
    }
}
      
/**
 * @param {number[]} status
 * @param {number[]} candies
 * @param {number[][]} keys
 * @param {number[][]} containedBoxes
 * @param {number[]} initialBoxes
 * @return {number}
 */
var maxCandies = function(status, candies, keys, containedBoxes, initialBoxes) {
    const n = status.length;
    const queue = [...initialBoxes];
    const hasKey = new Set();
    const opened = new Set();
    const boxes = new Set(initialBoxes);
    let res = 0;

    while (queue.length) {
        let size = queue.length;
        let progress = false;
        for (let i = 0; i < size; i++) {
            let box = queue.shift();
            if (opened.has(box)) continue;
            if (status[box] === 1 || hasKey.has(box)) {
                res += candies[box];
                opened.add(box);
                for (let k of keys[box]) {
                    if (!hasKey.has(k)) {
                        hasKey.add(k);
                        if (boxes.has(k) && !opened.has(k)) {
                            queue.push(k);
                        }
                    }
                }
                for (let b of containedBoxes[box]) {
                    if (!boxes.has(b)) {
                        boxes.add(b);
                        queue.push(b);
                    }
                }
                progress = true;
            } else {
                queue.push(box);
            }
        }
        if (!progress) break;
    }
    return res;
};