Facebook Pixel

1298. Maximum Candies You Can Get from Boxes

Problem Description

You are given n boxes labeled from 0 to n - 1. Each box has properties defined by four arrays:

  • status[i]: Indicates whether box i is initially open (1) or closed (0)
  • candies[i]: The number of candies contained in box i
  • keys[i]: A list of box labels that you can unlock after opening box i
  • containedBoxes[i]: A list of other boxes found inside box i

You start with an array initialBoxes containing the labels of boxes you initially possess.

The rules for collecting candies are:

  1. You can take candies from any open box that you have
  2. When you open a box, you obtain all the keys inside it (which can be used to open other closed boxes)
  3. When you open a box, you also obtain all the boxes contained within it

A box can be opened if:

  • It was already open (status is 1), OR
  • You have a key for it (obtained from another box)

The goal is to find the maximum number of candies you can collect by strategically opening boxes and using the keys and contained boxes you find.

For example, you might initially have a closed box that you cannot open. But if you open another box that contains a key to the first box, you can then go back and open it to collect its candies. Similarly, a box might contain other boxes inside it, which you can then open if they are unlocked or if you have their keys.

The challenge is to explore all possible boxes you can access through this chain of opening boxes, finding keys, and discovering new boxes, ultimately maximizing the total number of candies collected.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: This problem can be modeled as a graph where boxes are nodes. The relationships between boxes (through keys and contained boxes) form edges. When you open a box, you can access other boxes either through keys or by finding them inside.

Is it a tree?

  • No: The structure is not a tree because there can be multiple paths to reach the same box (e.g., a box could be contained in one box while its key is in another box).

Is the problem related to directed acyclic graphs?

  • No: While there are directed relationships (box A contains box B, or box A has key to box B), the problem isn't specifically about DAG properties like topological ordering.

Is the problem related to shortest paths?

  • No: We're not looking for the shortest path between boxes. We want to explore and open as many boxes as possible to maximize candy collection.

Does the problem involve connectivity?

  • No: We're not checking if boxes are connected or finding connected components. We're exploring reachable boxes from initial boxes.

Does the problem have small constraints?

  • No: The problem doesn't specifically mention small constraints that would make brute force feasible. We need an efficient exploration strategy.

BFS

  • Yes: BFS is the appropriate choice here because:
    1. We need to explore all reachable boxes level by level
    2. We process boxes as they become available (when we get their keys or find them)
    3. We want to collect candies from all accessible boxes systematically
    4. The order of exploration doesn't affect the final candy count, making BFS suitable

Conclusion: The flowchart leads us to use BFS for this problem. We start with the initial boxes and explore outward, opening boxes as we gain access to them (either they're already open or we find their keys), collecting candies, and discovering new boxes and keys along the way.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is to think of this problem as an exploration game where we gradually expand our access to more boxes. Initially, we have some boxes but not all of them can be opened - some are locked and need keys.

Consider what happens when we open a box:

  1. We collect the candies inside it
  2. We obtain keys that might unlock other boxes we already have
  3. We discover new boxes that were contained inside

This creates a ripple effect - opening one box can trigger a chain reaction of newly accessible boxes. A box that was previously inaccessible might become available through two different ways:

  • We find its key in another box
  • We find the box itself inside another box (and it's already open or we have its key)

The challenge is that we might encounter boxes before we have the ability to open them. For example, we might have a locked box from the beginning, but only find its key after opening several other boxes. This means we need to keep track of:

  • Which boxes we currently possess (but might not be able to open yet)
  • Which boxes we've already opened (to avoid processing them twice)
  • The current status of each box (open or locked)

BFS naturally fits this exploration pattern because:

  • We process boxes in waves - first the initially accessible boxes, then the boxes that become accessible after opening those, and so on
  • Each "level" of BFS represents boxes that became accessible at the same stage of exploration
  • We don't need to find an optimal path; we just need to explore all reachable boxes

The algorithm mimics how you would solve this in real life: start with what you can open immediately, collect resources (keys and new boxes), then check if any previously inaccessible boxes can now be opened with your new keys. Repeat this process until no new boxes can be opened.

The beauty of this approach is that it guarantees we'll find all accessible candies because we systematically explore every possible box that can be reached through any combination of keys and contained boxes.

Learn more about Breadth-First Search and Graph patterns.

Solution Approach

We implement the BFS exploration using a queue and hash sets to efficiently track our progress through the boxes.

Data Structures:

  • Queue q: Stores boxes that are currently accessible and ready to be processed
  • Set has: Tracks all boxes we currently possess (whether open or closed)
  • Set took: Records boxes we've already processed to avoid duplicates
  • Variable ans: Accumulates the total candies collected

Initial Setup: We start by adding all initialBoxes to the has set since these are the boxes we possess from the beginning. For each initial box, if it's already open (status[box] == 1), we:

  • Add it to the queue q for processing
  • Mark it as processed in took
  • Collect its candies and add to ans

BFS Processing: While the queue is not empty, we process each box:

  1. Process Keys: For each key in keys[box]:

    • Unlock the corresponding box by setting status[k] = 1
    • Check if this newly unlocked box is one we already possess (k in has) but haven't processed yet (k not in took)
    • If so, add it to the queue, mark as processed, and collect its candies
  2. Process Contained Boxes: For each box in containedBoxes[box]:

    • Add it to our possession (has.add(b))
    • If this box is open (status[b] == 1) and we haven't processed it yet (b not in took):
      • Add it to the queue for processing
      • Mark as processed
      • Collect its candies

Key Insights:

  • A box enters the queue only when two conditions are met: we possess it AND it's open
  • Keys permanently unlock boxes by changing their status, making them available for future processing
  • The took set prevents processing the same box multiple times, even if we obtain it through different paths
  • We collect candies exactly once per box, at the moment we add it to the queue

The algorithm terminates when the queue is empty, meaning we've explored all accessible boxes. The total candies collected in ans represents the maximum possible candies we can obtain.

This approach ensures completeness because:

  • Every box we can access will eventually be processed
  • Keys found later can unlock boxes we possessed earlier
  • Boxes found inside other boxes are immediately checked for accessibility

The time complexity is O(n) where n is the number of boxes, as each box is processed at most once. The space complexity is also O(n) for the queue and sets.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Given:

  • 5 boxes labeled 0 to 4
  • status = [1, 0, 1, 0, 0] (boxes 0 and 2 are open, others are closed)
  • candies = [7, 5, 4, 100, 6]
  • keys = [[1,2], [], [4], [], []] (box 0 has keys to boxes 1 and 2, box 2 has key to box 4)
  • containedBoxes = [[1,2], [3], [], [], []] (box 0 contains boxes 1 and 2, box 1 contains box 3)
  • initialBoxes = [0] (we start with only box 0)

Step-by-step BFS process:

Initial Setup:

  • Add box 0 to has: has = {0}
  • Box 0 is open (status[0] = 1), so:
    • Add to queue: q = [0]
    • Mark as processed: took = {0}
    • Collect candies: ans = 7

Iteration 1: Process box 0

  • Dequeue box 0
  • Process keys [1, 2]:
    • Key 1: Set status[1] = 1 (unlock box 1)
      • Box 1 not in has yet, so no action
    • Key 2: Set status[2] = 1 (box 2 already open, no change)
      • Box 2 not in has yet, so no action
  • Process contained boxes [1, 2]:
    • Box 1: Add to has β†’ has = {0, 1}
      • Box 1 is now open (we just unlocked it) and not in took
      • Add to queue: q = [1]
      • Mark as processed: took = {0, 1}
      • Collect candies: ans = 7 + 5 = 12
    • Box 2: Add to has β†’ has = {0, 1, 2}
      • Box 2 is open (status[2] = 1) and not in took
      • Add to queue: q = [1, 2]
      • Mark as processed: took = {0, 1, 2}
      • Collect candies: ans = 12 + 4 = 16

Iteration 2: Process box 1

  • Dequeue box 1
  • Process keys [] (no keys)
  • Process contained boxes [3]:
    • Box 3: Add to has β†’ has = {0, 1, 2, 3}
      • Box 3 is closed (status[3] = 0) and we have no key
      • Cannot add to queue

Iteration 3: Process box 2

  • Dequeue box 2
  • Process keys [4]:
    • Key 4: Set status[4] = 1 (unlock box 4)
      • Box 4 not in has yet, so no action
  • Process contained boxes [] (no contained boxes)

Queue is empty, algorithm terminates

Final Result:

  • We collected candies from boxes 0, 1, and 2
  • Total candies = 7 + 5 + 4 = 16
  • Box 3 remains inaccessible (we have it but no key)
  • Box 4 was never obtained (we have its key but not the box itself)

This example demonstrates how:

  1. Opening box 0 gave us access to boxes 1 and 2, plus the key to box 1
  2. The key from box 0 allowed us to immediately open box 1 when we found it
  3. Box 3 was discovered but remained locked throughout
  4. Having a key (for box 4) doesn't help if we never obtain the box itself

Solution Implementation

1from collections import deque
2from typing import List
3
4class Solution:
5    def maxCandies(
6        self,
7        status: List[int],
8        candies: List[int],
9        keys: List[List[int]],
10        containedBoxes: List[List[int]],
11        initialBoxes: List[int],
12    ) -> int:
13        """
14        Calculate maximum candies that can be collected from boxes.
15      
16        Args:
17            status: List where status[i] = 1 if box i is open, 0 if locked
18            candies: List where candies[i] is the number of candies in box i
19            keys: List where keys[i] contains keys found in box i
20            containedBoxes: List where containedBoxes[i] contains boxes found inside box i
21            initialBoxes: List of boxes available at the start
22          
23        Returns:
24            Maximum number of candies that can be collected
25        """
26        # Initialize queue for BFS traversal
27        queue = deque()
28      
29        # Track boxes we have and boxes we've already opened
30        boxes_owned = set(initialBoxes)
31        boxes_opened = set()
32      
33        # Total candies collected
34        total_candies = 0
35      
36        # Process initial boxes - open any that are already unlocked
37        for box in initialBoxes:
38            if status[box] == 1:  # Box is open/unlocked
39                queue.append(box)
40                boxes_opened.add(box)
41                total_candies += candies[box]
42      
43        # BFS to process all reachable boxes
44        while queue:
45            current_box = queue.popleft()
46          
47            # Process keys found in current box
48            for key in keys[current_box]:
49                if status[key] == 0:  # Box was locked
50                    status[key] = 1  # Unlock the box
51                  
52                    # If we own this box and haven't opened it yet, open it now
53                    if key in boxes_owned and key not in boxes_opened:
54                        queue.append(key)
55                        boxes_opened.add(key)
56                        total_candies += candies[key]
57          
58            # Process boxes found inside current box
59            for contained_box in containedBoxes[current_box]:
60                boxes_owned.add(contained_box)
61              
62                # If this box is unlocked and we haven't opened it yet, open it
63                if status[contained_box] == 1 and contained_box not in boxes_opened:
64                    queue.append(contained_box)
65                    boxes_opened.add(contained_box)
66                    total_candies += candies[contained_box]
67      
68        return total_candies
69
1class Solution {
2    public int maxCandies(int[] status, int[] candies, int[][] keys, int[][] containedBoxes, int[] initialBoxes) {
3        // Queue to process boxes that can be opened
4        Deque<Integer> boxQueue = new ArrayDeque<>();
5        // Set to track which boxes we currently possess
6        Set<Integer> possessedBoxes = new HashSet<>();
7        // Set to track which boxes we've already opened (to avoid duplicates)
8        Set<Integer> openedBoxes = new HashSet<>();
9        // Total candies collected
10        int totalCandies = 0;
11      
12        // Process initial boxes
13        for (int boxId : initialBoxes) {
14            possessedBoxes.add(boxId);
15            // If the box is already open (status = 1), process it immediately
16            if (status[boxId] == 1) {
17                boxQueue.offer(boxId);
18                openedBoxes.add(boxId);
19                totalCandies += candies[boxId];
20            }
21        }
22      
23        // BFS to process all boxes that can be opened
24        while (!boxQueue.isEmpty()) {
25            int currentBox = boxQueue.poll();
26          
27            // Process all keys found in the current box
28            for (int keyForBox : keys[currentBox]) {
29                // If the box was locked, unlock it
30                if (status[keyForBox] == 0) {
31                    status[keyForBox] = 1;
32                    // If we possess this box and haven't opened it yet, open it now
33                    if (possessedBoxes.contains(keyForBox) && !openedBoxes.contains(keyForBox)) {
34                        boxQueue.offer(keyForBox);
35                        openedBoxes.add(keyForBox);
36                        totalCandies += candies[keyForBox];
37                    }
38                }
39            }
40          
41            // Process all boxes contained within the current box
42            for (int containedBox : containedBoxes[currentBox]) {
43                possessedBoxes.add(containedBox);
44                // If the contained box is unlocked and not yet opened, open it
45                if (status[containedBox] == 1 && !openedBoxes.contains(containedBox)) {
46                    boxQueue.offer(containedBox);
47                    openedBoxes.add(containedBox);
48                    totalCandies += candies[containedBox];
49                }
50            }
51        }
52      
53        return totalCandies;
54    }
55}
56
1class Solution {
2public:
3    int maxCandies(
4        vector<int>& status,
5        vector<int>& candies,
6        vector<vector<int>>& keys,
7        vector<vector<int>>& containedBoxes,
8        vector<int>& initialBoxes) {
9      
10        // Queue for BFS traversal of boxes we can open
11        queue<int> openableBoxes;
12      
13        // Set of boxes we currently possess (but may not be able to open)
14        unordered_set<int> possessedBoxes;
15      
16        // Set of boxes we have already opened (to avoid processing twice)
17        unordered_set<int> openedBoxes;
18      
19        // Total candies collected
20        int totalCandies = 0;
21
22        // Process initial boxes
23        for (int boxId : initialBoxes) {
24            possessedBoxes.insert(boxId);
25          
26            // If box is already open (status = 1), process it immediately
27            if (status[boxId] == 1) {
28                openableBoxes.push(boxId);
29                openedBoxes.insert(boxId);
30                totalCandies += candies[boxId];
31            }
32        }
33
34        // BFS to process all openable boxes
35        while (!openableBoxes.empty()) {
36            int currentBox = openableBoxes.front();
37            openableBoxes.pop();
38
39            // Process all keys found in current box
40            for (int keyForBox : keys[currentBox]) {
41                // Unlock the box if it wasn't already unlocked
42                if (status[keyForBox] == 0) {
43                    status[keyForBox] = 1;
44                  
45                    // If we possess this box and haven't opened it yet, open it now
46                    if (possessedBoxes.count(keyForBox) && !openedBoxes.count(keyForBox)) {
47                        openableBoxes.push(keyForBox);
48                        openedBoxes.insert(keyForBox);
49                        totalCandies += candies[keyForBox];
50                    }
51                }
52            }
53
54            // Process all boxes contained within current box
55            for (int containedBox : containedBoxes[currentBox]) {
56                possessedBoxes.insert(containedBox);
57              
58                // If this new box is unlocked and we haven't opened it yet, open it
59                if (status[containedBox] == 1 && !openedBoxes.count(containedBox)) {
60                    openableBoxes.push(containedBox);
61                    openedBoxes.insert(containedBox);
62                    totalCandies += candies[containedBox];
63                }
64            }
65        }
66
67        return totalCandies;
68    }
69};
70
1/**
2 * Calculates the maximum number of candies that can be collected from boxes
3 * @param status - Array where status[i] = 1 if box i is open, 0 if closed
4 * @param candies - Array where candies[i] is the number of candies in box i
5 * @param keys - Array where keys[i] contains the indices of boxes that box i has keys for
6 * @param containedBoxes - Array where containedBoxes[i] contains the indices of boxes inside box i
7 * @param initialBoxes - Array of indices of boxes initially available
8 * @returns The maximum number of candies that can be collected
9 */
10function maxCandies(
11    status: number[],
12    candies: number[],
13    keys: number[][],
14    containedBoxes: number[][],
15    initialBoxes: number[],
16): number {
17    // Queue for processing boxes that can be opened
18    const boxQueue: number[] = [];
19  
20    // Set of boxes currently in possession
21    const boxesInPossession: Set<number> = new Set<number>();
22  
23    // Set of boxes already opened and processed
24    const openedBoxes: Set<number> = new Set<number>();
25  
26    // Total candies collected
27    let totalCandies: number = 0;
28
29    // Process initial boxes
30    for (const boxIndex of initialBoxes) {
31        boxesInPossession.add(boxIndex);
32      
33        // If box is already open, add it to queue for processing
34        if (status[boxIndex] === 1) {
35            boxQueue.push(boxIndex);
36            openedBoxes.add(boxIndex);
37            totalCandies += candies[boxIndex];
38        }
39    }
40
41    // Process boxes using BFS approach
42    while (boxQueue.length > 0) {
43        const currentBox: number = boxQueue.pop()!;
44
45        // Process keys found in current box
46        for (const keyForBox of keys[currentBox]) {
47            // Unlock the box if it was closed
48            if (status[keyForBox] === 0) {
49                status[keyForBox] = 1;
50              
51                // If we have this box and haven't opened it yet, process it
52                if (boxesInPossession.has(keyForBox) && !openedBoxes.has(keyForBox)) {
53                    boxQueue.push(keyForBox);
54                    openedBoxes.add(keyForBox);
55                    totalCandies += candies[keyForBox];
56                }
57            }
58        }
59
60        // Process boxes contained within current box
61        for (const containedBox of containedBoxes[currentBox]) {
62            boxesInPossession.add(containedBox);
63          
64            // If contained box is open and not yet processed, add to queue
65            if (status[containedBox] === 1 && !openedBoxes.has(containedBox)) {
66                boxQueue.push(containedBox);
67                openedBoxes.add(containedBox);
68                totalCandies += candies[containedBox];
69            }
70        }
71    }
72
73    return totalCandies;
74}
75

Time and Space Complexity

Time Complexity: O(n)

The algorithm uses BFS to process boxes. Each box can be added to the queue at most once (tracked by the took set), and for each box processed:

  • We iterate through its keys list: O(keys[box]) operations
  • We iterate through its contained boxes list: O(containedBoxes[box]) operations

Since each box is processed at most once and the total number of keys and contained boxes across all boxes is bounded by O(n), the overall time complexity is O(n).

Space Complexity: O(n)

The algorithm uses the following data structures:

  • q (deque): Can contain at most n boxes
  • has (set): Can contain at most n boxes
  • took (set): Can contain at most n boxes
  • status array: Modified in-place, but originally size n

The total space used is O(n) where n is the total number of boxes.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Not Re-checking Previously Owned but Locked Boxes After Finding Keys

A critical mistake is failing to check if a newly found key unlocks a box we already possessed but couldn't open earlier. When we find a key, we must check ALL boxes we currently own, not just newly discovered ones.

Incorrect approach:

# Wrong: Only checking if the key unlocks future boxes
for key in keys[current_box]:
    status[key] = 1  # Unlock the box
    # Missing: Not checking if we already own this box!

Correct approach:

for key in keys[current_box]:
    if status[key] == 0:
        status[key] = 1  # Unlock the box
        # Critical: Check if we already own this now-unlocked box
        if key in boxes_owned and key not in boxes_opened:
            queue.append(key)
            boxes_opened.add(key)
            total_candies += candies[key]

Pitfall 2: Processing Boxes Multiple Times

Another common error is adding the same box to the queue multiple times, leading to counting candies more than once. This can happen when:

  • A box is contained in multiple other boxes
  • Multiple keys unlock the same box
  • The same box appears in both initial boxes and contained boxes

Incorrect approach:

# Wrong: Not tracking which boxes have been processed
for contained_box in containedBoxes[current_box]:
    if status[contained_box] == 1:
        queue.append(contained_box)  # May add duplicates!
        total_candies += candies[contained_box]  # May count twice!

Correct approach:

# Use a set to track processed boxes
for contained_box in containedBoxes[current_box]:
    boxes_owned.add(contained_box)
    if status[contained_box] == 1 and contained_box not in boxes_opened:
        queue.append(contained_box)
        boxes_opened.add(contained_box)  # Mark as processed
        total_candies += candies[contained_box]

Pitfall 3: Modifying Status Array Without Checking Current State

Blindly setting status[key] = 1 without checking if the box was already open can lead to inefficient operations and potential logic errors in more complex scenarios.

Better practice:

for key in keys[current_box]:
    if status[key] == 0:  # Only modify if currently locked
        status[key] = 1
        # ... rest of logic

This optimization avoids unnecessary operations and makes the code's intent clearer.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More