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 boxi
is initially open (1
) or closed (0
)candies[i]
: The number of candies contained in boxi
keys[i]
: A list of box labels that you can unlock after opening boxi
containedBoxes[i]
: A list of other boxes found inside boxi
You start with an array initialBoxes
containing the labels of boxes you initially possess.
The rules for collecting candies are:
- You can take candies from any open box that you have
- When you open a box, you obtain all the keys inside it (which can be used to open other closed boxes)
- 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:
- We need to explore all reachable boxes level by level
- We process boxes as they become available (when we get their keys or find them)
- We want to collect candies from all accessible boxes systematically
- 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.
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:
- We collect the candies inside it
- We obtain keys that might unlock other boxes we already have
- 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:
-
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
- Unlock the corresponding box by setting
-
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
- Add it to our possession (
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 EvaluatorExample 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
- Add to queue:
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
- Box 1 not in
- Key 2: Set
status[2] = 1
(box 2 already open, no change)- Box 2 not in
has
yet, so no action
- Box 2 not in
- Key 1: Set
- 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 1 is now open (we just unlocked it) and not in
- 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
- Box 2 is open (status[2] = 1) and not in
- Box 1: Add to
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
- Box 3: Add to
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
- Box 4 not in
- Key 4: Set
- 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:
- Opening box 0 gave us access to boxes 1 and 2, plus the key to box 1
- The key from box 0 allowed us to immediately open box 1 when we found it
- Box 3 was discovered but remained locked throughout
- 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 mostn
boxeshas
(set): Can contain at mostn
boxestook
(set): Can contain at mostn
boxesstatus
array: Modified in-place, but originally sizen
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.
Which algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Donβt Miss This!