Facebook Pixel

723. Candy Crush πŸ”’

MediumArrayTwo PointersMatrixSimulation
Leetcode Link

Problem Description

This problem asks you to implement the basic elimination mechanism from the game Candy Crush on a 2D grid.

You're given an m x n integer array board where each board[i][j] represents a type of candy (positive integers), and 0 represents an empty cell. Your task is to simulate the candy crushing process until the board reaches a stable state.

The crushing rules work as follows:

  1. Identify crushable candies: Find any group of three or more candies of the same type that are adjacent either horizontally or vertically. All such groups should be marked for crushing simultaneously.

  2. Crush the candies: Remove all marked candies at once by making those positions empty (setting them to 0).

  3. Apply gravity: After crushing, candies above empty spaces should fall down to fill the gaps. Candies drop straight down until they hit another candy or reach the bottom of the board. Multiple candies can fall simultaneously.

  4. Repeat until stable: After candies drop, new crushing opportunities may appear. Keep repeating steps 1-3 until no more candies can be crushed.

The solution uses a clever marking technique - instead of immediately removing candies, it marks them with negative values first. This allows the algorithm to identify all crushable groups in one pass before actually removing them. The gravity simulation is implemented by using a two-pointer approach to shift non-empty candies downward in each column.

For example, if you have a row like [1, 1, 1, 2], the three consecutive 1s would be crushed, leaving [0, 0, 0, 2] after applying gravity. The process continues until no more groups of three or more identical candies exist in any row or column.

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

Intuition

The key insight is that we need to simulate the actual Candy Crush game mechanics, which involves repeatedly finding matches and applying gravity until no more moves are possible.

The first challenge is how to handle simultaneous crushing. If we immediately remove candies as we find them, we might miss overlapping groups. For instance, if we have a cross pattern where both a horizontal and vertical group of three share a candy, we need to crush all of them together. The clever trick here is to use negative numbers as markers. When we find a group of three or more matching candies, instead of removing them immediately, we mark them by making their values negative. This preserves the candy type information (through abs()) while flagging them for removal.

Why negative numbers? Because:

  • They're distinguishable from positive candy values
  • We can still identify the candy type using abs()
  • Empty cells (0) remain unaffected
  • We can check multiple patterns without losing information

The second insight is about the order of operations. We first scan horizontally across all rows, then vertically down all columns. This ensures we catch all possible matches in a single iteration. We check for patterns of three consecutive candies by comparing board[i][j], board[i][j-1], and board[i][j-2] for horizontal matches (and similar for vertical).

For the gravity simulation, we use a two-pointer technique for each column. Starting from the bottom of each column, we maintain a write pointer k that tracks where the next non-crushed candy should be placed. As we scan upward, whenever we find a positive value (non-crushed candy), we move it to position k and decrement k. After processing all candies, any remaining positions above k are filled with zeros.

The overall flow becomes: mark crushable candies β†’ apply gravity β†’ check if anything was crushed β†’ repeat if necessary. The run flag tracks whether any crushing occurred in the current iteration, determining whether we need another round.

Learn more about Two Pointers patterns.

Solution Approach

The implementation uses a simulation approach with a marking and dropping mechanism. Let's walk through the key components:

Main Loop Structure: The solution uses a while loop controlled by a run flag that continues until the board reaches a stable state (no more candies can be crushed).

Step 1: Mark Candies for Crushing

First, we scan horizontally across each row:

for i in range(m):
    for j in range(2, n):
        if board[i][j] and abs(board[i][j]) == abs(board[i][j-1]) == abs(board[i][j-2]):
  • Start from column index 2 since we need to check three consecutive elements
  • Check if current position is non-empty (board[i][j]) and matches the two positions to its left
  • Use abs() to handle both positive values and previously marked negative values

When a match is found, mark all three positions as negative:

board[i][j] = board[i][j-1] = board[i][j-2] = -abs(board[i][j])

Then scan vertically down each column using similar logic:

for j in range(n):
    for i in range(2, m):
        if board[i][j] and abs(board[i][j]) == abs(board[i-1][j]) == abs(board[i-2][j]):

Step 2: Apply Gravity (Drop Candies)

If any candies were marked (run = True), process each column to drop candies:

for j in range(n):
    k = m - 1  # Write pointer starting from bottom
    for i in range(m - 1, -1, -1):  # Scan from bottom to top
        if board[i][j] > 0:  # Found a non-crushed candy
            board[k][j] = board[i][j]
            k -= 1

The two-pointer technique works as follows:

  • k represents where the next candy should be placed (starting from the bottom)
  • i scans upward looking for positive values (non-crushed candies)
  • When found, place the candy at position k and move k up
  • After processing all candies, fill remaining positions with zeros:
while k >= 0:
    board[k][j] = 0
    k -= 1

Step 3: Check for Stability

The run flag determines if another iteration is needed:

  • Initially set to False at the start of each iteration
  • Set to True whenever candies are marked for crushing
  • If it remains False after scanning, the board is stable

Time Complexity: O(m * n * k) where k is the number of iterations needed to stabilize the board. In the worst case, k could be O(max(m, n)).

Space Complexity: O(1) as we modify the board in-place without using additional data structures.

The elegance of this solution lies in using negative marking to handle simultaneous crushing and the efficient two-pointer technique for simulating gravity, avoiding the need for temporary arrays or complex data structures.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

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

Initial Board:

[1, 3, 5, 5, 2]
[3, 4, 3, 3, 1]
[3, 2, 4, 5, 2]
[2, 4, 4, 5, 5]
[1, 4, 4, 1, 1]

Iteration 1 - Marking Phase:

First, scan horizontally (checking groups of 3+ in rows):

  • Row 0: No matches
  • Row 1: No matches
  • Row 2: No matches
  • Row 3: No matches
  • Row 4: No matches

Then, scan vertically (checking groups of 3+ in columns):

  • Column 0: No matches
  • Column 1: positions [1,1], [2,1], [3,1], [4,1] all have value 4 β†’ Mark as -4
  • Column 2: positions [2,2], [3,2], [4,2] all have value 4 β†’ Mark as -4
  • Column 3: positions [0,3], [1,3], [2,3], [3,3] have different values (5,3,5,5) - no consecutive match of 3
  • Column 4: No matches

After marking:

[1, 3, 5, 5, 2]
[3,-4, 3, 3, 1]
[3,-4,-4, 5, 2]
[2,-4,-4, 5, 5]
[1,-4,-4, 1, 1]

Iteration 1 - Gravity Phase:

Process each column from bottom to top, moving positive values down:

Column 0: [1,3,3,2,1] β†’ No changes needed (no negatives)

Column 1: [3,-4,-4,-4,-4]

  • k=4 (start at bottom)
  • i=4: board[4][1]=-4 (skip)
  • i=3: board[3][1]=-4 (skip)
  • i=2: board[2][1]=-4 (skip)
  • i=1: board[1][1]=-4 (skip)
  • i=0: board[0][1]=3 (positive!) β†’ board[4][1]=3, k=3
  • Fill remaining: board[3][1]=0, board[2][1]=0, board[1][1]=0, board[0][1]=0

Column 2: [5,3,-4,-4,-4]

  • k=4, process similarly
  • Result: [0,0,5,3,4] (the 4 at bottom came from board[0][2]=5, wait that's wrong...)

Let me recalculate Column 2: [5,3,-4,-4,-4]

  • k=4 (start at bottom)
  • i=4: board[4][2]=-4 (skip)
  • i=3: board[3][2]=-4 (skip)
  • i=2: board[2][2]=-4 (skip)
  • i=1: board[1][2]=3 (positive!) β†’ board[4][2]=3, k=3
  • i=0: board[0][2]=5 (positive!) β†’ board[3][2]=5, k=2
  • Fill remaining: board[2][2]=0, board[1][2]=0, board[0][2]=0

After gravity:

[0, 0, 0, 5, 2]
[0, 0, 0, 3, 1]
[1, 0, 0, 5, 2]
[3, 0, 5, 5, 5]
[3, 3, 3, 1, 1]

Iteration 2 - Marking Phase:

Scan horizontally:

  • Row 3: positions [3,2], [3,3], [3,4] all have value 5 β†’ Mark as -5
  • Row 4: positions [4,0], [4,1], [4,2] all have value 3 β†’ Mark as -3

Scan vertically:

  • Column 3: positions [2,3], [3,3] have value 5, but [3,3] is already -5
  • Column 4: No new matches

After marking:

[0, 0, 0, 5, 2]
[0, 0, 0, 3, 1]
[1, 0, 0,-5, 2]
[3, 0,-5,-5,-5]
[-3,-3,-3, 1, 1]

Iteration 2 - Gravity Phase:

After applying gravity to each column:

[0, 0, 0, 0, 0]
[0, 0, 0, 0, 2]
[0, 0, 0, 5, 1]
[1, 0, 0, 3, 2]
[3, 0, 0, 1, 1]

Iteration 3 - Marking Phase:

No groups of 3+ found horizontally or vertically. The run flag stays False.

Final stable board:

[0, 0, 0, 0, 0]
[0, 0, 0, 0, 2]
[0, 0, 0, 5, 1]
[1, 0, 0, 3, 2]
[3, 0, 0, 1, 1]

The process demonstrates how:

  1. Negative marking preserves candy type while flagging for removal
  2. Gravity drops all candies simultaneously in each column
  3. The process repeats until no more matches exist

Solution Implementation

1class Solution:
2    def candyCrush(self, board: List[List[int]]) -> List[List[int]]:
3        """
4        Simulates a candy crush game where matching candies are removed and 
5        remaining candies drop down due to gravity.
6      
7        Args:
8            board: 2D grid representing the candy board
9          
10        Returns:
11            The stable board after all crushes are complete
12        """
13        rows, cols = len(board), len(board[0])
14        should_continue = True
15      
16        while should_continue:
17            should_continue = False
18          
19            # Mark horizontal matches (3 or more consecutive same candies)
20            for row in range(rows):
21                for col in range(2, cols):
22                    # Check if current position has candy and matches with previous two
23                    if (board[row][col] and 
24                        abs(board[row][col]) == abs(board[row][col - 1]) == abs(board[row][col - 2])):
25                        should_continue = True
26                        # Mark candies as negative to indicate they should be crushed
27                        board[row][col] = -abs(board[row][col])
28                        board[row][col - 1] = -abs(board[row][col - 1])
29                        board[row][col - 2] = -abs(board[row][col - 2])
30          
31            # Mark vertical matches (3 or more consecutive same candies)
32            for col in range(cols):
33                for row in range(2, rows):
34                    # Check if current position has candy and matches with previous two
35                    if (board[row][col] and 
36                        abs(board[row][col]) == abs(board[row - 1][col]) == abs(board[row - 2][col])):
37                        should_continue = True
38                        # Mark candies as negative to indicate they should be crushed
39                        board[row][col] = -abs(board[row][col])
40                        board[row - 1][col] = -abs(board[row - 1][col])
41                        board[row - 2][col] = -abs(board[row - 2][col])
42          
43            # Apply gravity: drop candies and remove crushed ones
44            if should_continue:
45                for col in range(cols):
46                    # Write pointer starting from bottom
47                    write_row = rows - 1
48                  
49                    # Scan from bottom to top
50                    for row in range(rows - 1, -1, -1):
51                        # Keep only positive values (non-crushed candies)
52                        if board[row][col] > 0:
53                            board[write_row][col] = board[row][col]
54                            write_row -= 1
55                  
56                    # Fill remaining top positions with zeros (empty spaces)
57                    while write_row >= 0:
58                        board[write_row][col] = 0
59                        write_row -= 1
60      
61        return board
62
1class Solution {
2    public int[][] candyCrush(int[][] board) {
3        int rows = board.length;
4        int cols = board[0].length;
5        boolean shouldContinue = true;
6
7        while (shouldContinue) {
8            shouldContinue = false;
9          
10            // Mark horizontal matches (3 or more consecutive same candies in a row)
11            for (int row = 0; row < rows; row++) {
12                for (int col = 2; col < cols; col++) {
13                    // Check if current position and previous 2 positions have same candy type
14                    if (board[row][col] != 0 && 
15                        Math.abs(board[row][col]) == Math.abs(board[row][col - 1]) &&
16                        Math.abs(board[row][col]) == Math.abs(board[row][col - 2])) {
17                      
18                        shouldContinue = true;
19                        int candyValue = Math.abs(board[row][col]);
20                        // Mark these positions as negative to indicate they should be crushed
21                        board[row][col] = -candyValue;
22                        board[row][col - 1] = -candyValue;
23                        board[row][col - 2] = -candyValue;
24                    }
25                }
26            }
27          
28            // Mark vertical matches (3 or more consecutive same candies in a column)
29            for (int col = 0; col < cols; col++) {
30                for (int row = 2; row < rows; row++) {
31                    // Check if current position and previous 2 positions have same candy type
32                    if (board[row][col] != 0 && 
33                        Math.abs(board[row][col]) == Math.abs(board[row - 1][col]) &&
34                        Math.abs(board[row][col]) == Math.abs(board[row - 2][col])) {
35                      
36                        shouldContinue = true;
37                        int candyValue = Math.abs(board[row][col]);
38                        // Mark these positions as negative to indicate they should be crushed
39                        board[row][col] = -candyValue;
40                        board[row - 1][col] = -candyValue;
41                        board[row - 2][col] = -candyValue;
42                    }
43                }
44            }
45          
46            // Drop candies down after crushing marked candies
47            if (shouldContinue) {
48                for (int col = 0; col < cols; col++) {
49                    int writePosition = rows - 1;
50                  
51                    // Move all positive values (uncrushed candies) down
52                    for (int row = rows - 1; row >= 0; row--) {
53                        if (board[row][col] > 0) {
54                            board[writePosition][col] = board[row][col];
55                            writePosition--;
56                        }
57                    }
58                  
59                    // Fill remaining top positions with 0 (empty spaces)
60                    while (writePosition >= 0) {
61                        board[writePosition][col] = 0;
62                        writePosition--;
63                    }
64                }
65            }
66        }
67
68        return board;
69    }
70}
71
1class Solution {
2public:
3    vector<vector<int>> candyCrush(vector<vector<int>>& board) {
4        int rows = board.size();
5        int cols = board[0].size();
6        bool shouldContinue = true;
7      
8        while (shouldContinue) {
9            shouldContinue = false;
10          
11            // Mark horizontal matches (3 or more consecutive same candies)
12            for (int row = 0; row < rows; ++row) {
13                for (int col = 0; col < cols - 2; ++col) {
14                    // Check if current position has candy and next two positions have same candy type
15                    if (board[row][col] != 0 && 
16                        abs(board[row][col]) == abs(board[row][col + 1]) && 
17                        abs(board[row][col]) == abs(board[row][col + 2])) {
18                        shouldContinue = true;
19                        // Mark candies as negative to indicate they should be crushed
20                        board[row][col] = -abs(board[row][col]);
21                        board[row][col + 1] = -abs(board[row][col + 1]);
22                        board[row][col + 2] = -abs(board[row][col + 2]);
23                    }
24                }
25            }
26          
27            // Mark vertical matches (3 or more consecutive same candies)
28            for (int col = 0; col < cols; ++col) {
29                for (int row = 0; row < rows - 2; ++row) {
30                    // Check if current position has candy and next two positions have same candy type
31                    if (board[row][col] != 0 && 
32                        abs(board[row][col]) == abs(board[row + 1][col]) && 
33                        abs(board[row][col]) == abs(board[row + 2][col])) {
34                        shouldContinue = true;
35                        // Mark candies as negative to indicate they should be crushed
36                        board[row][col] = -abs(board[row][col]);
37                        board[row + 1][col] = -abs(board[row + 1][col]);
38                        board[row + 2][col] = -abs(board[row + 2][col]);
39                    }
40                }
41            }
42          
43            // Drop candies and remove crushed ones
44            if (shouldContinue) {
45                for (int col = 0; col < cols; ++col) {
46                    int writePosition = rows - 1;  // Position to write the next valid candy
47                  
48                    // Move all positive (non-crushed) candies down
49                    for (int row = rows - 1; row >= 0; --row) {
50                        if (board[row][col] > 0) {
51                            board[writePosition][col] = board[row][col];
52                            --writePosition;
53                        }
54                    }
55                  
56                    // Fill remaining positions with 0 (empty spaces)
57                    while (writePosition >= 0) {
58                        board[writePosition][col] = 0;
59                        --writePosition;
60                    }
61                }
62            }
63        }
64      
65        return board;
66    }
67};
68
1/**
2 * Simulates a candy crush game by finding and eliminating consecutive candies
3 * @param board - 2D array representing the game board with candy values
4 * @returns The board after all possible crushes have been performed
5 */
6function candyCrush(board: number[][]): number[][] {
7    const rows: number = board.length;
8    const cols: number = board[0].length;
9    let shouldContinue: boolean = true;
10  
11    while (shouldContinue) {
12        shouldContinue = false;
13      
14        // Mark horizontal matches (3 or more consecutive candies in a row)
15        for (let row = 0; row < rows; row++) {
16            for (let col = 2; col < cols; col++) {
17                // Check if current position and previous two positions have same candy type
18                if (board[row][col] !== 0 &&
19                    Math.abs(board[row][col]) === Math.abs(board[row][col - 1]) &&
20                    Math.abs(board[row][col]) === Math.abs(board[row][col - 2])) {
21                  
22                    shouldContinue = true;
23                    const candyValue: number = Math.abs(board[row][col]);
24                    // Mark candies as negative to indicate they should be crushed
25                    board[row][col] = -candyValue;
26                    board[row][col - 1] = -candyValue;
27                    board[row][col - 2] = -candyValue;
28                }
29            }
30        }
31      
32        // Mark vertical matches (3 or more consecutive candies in a column)
33        for (let col = 0; col < cols; col++) {
34            for (let row = 2; row < rows; row++) {
35                // Check if current position and previous two positions have same candy type
36                if (board[row][col] !== 0 &&
37                    Math.abs(board[row][col]) === Math.abs(board[row - 1][col]) &&
38                    Math.abs(board[row][col]) === Math.abs(board[row - 2][col])) {
39                  
40                    shouldContinue = true;
41                    const candyValue: number = Math.abs(board[row][col]);
42                    // Mark candies as negative to indicate they should be crushed
43                    board[row][col] = -candyValue;
44                    board[row - 1][col] = -candyValue;
45                    board[row - 2][col] = -candyValue;
46                }
47            }
48        }
49      
50        // Drop candies down after crushing marked candies
51        if (shouldContinue) {
52            for (let col = 0; col < cols; col++) {
53                let writePosition: number = rows - 1;
54              
55                // Move all positive (non-crushed) candies down
56                for (let row = rows - 1; row >= 0; row--) {
57                    if (board[row][col] > 0) {
58                        board[writePosition][col] = board[row][col];
59                        writePosition--;
60                    }
61                }
62              
63                // Fill remaining top positions with zeros (empty spaces)
64                while (writePosition >= 0) {
65                    board[writePosition][col] = 0;
66                    writePosition--;
67                }
68            }
69        }
70    }
71  
72    return board;
73}
74

Time and Space Complexity

Time Complexity: O(mΒ² Γ— nΒ²)

The algorithm uses a while loop that continues until no more candies can be crushed. In the worst case, we might need to perform O(m Γ— n) iterations (crushing one candy at a time in a cascading manner). Within each iteration:

  • We scan the entire board twice to mark candies for crushing: O(m Γ— n)
  • We drop candies in each column: O(m Γ— n)

Therefore, the overall time complexity is O(m Γ— n) Γ— O(m Γ— n) = O(mΒ² Γ— nΒ²), where m is the number of rows and n is the number of columns of the board.

Space Complexity: O(1)

The algorithm modifies the board in-place without using any additional data structures that scale with the input size. We only use a few constant variables (run, i, j, k) regardless of the board dimensions, resulting in constant space complexity.

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

Common Pitfalls

1. Not Using Absolute Values When Checking Matches

One of the most common mistakes is forgetting to use abs() when comparing candy values during the matching phase. This is crucial because candies that were marked for crushing in a previous part of the same iteration are represented as negative numbers.

Incorrect Code:

# This will miss matches involving already-marked candies
if board[i][j] == board[i][j-1] == board[i][j-2]:
    # Mark for crushing

Why it fails: If board[i][j-1] was already marked as -3 and board[i][j] and board[i][j-2] are 3, the comparison 3 == -3 == 3 returns False, missing a valid match.

Correct Code:

if board[i][j] and abs(board[i][j]) == abs(board[i][j-1]) == abs(board[i][j-2]):
    # Mark for crushing

2. Incorrect Gravity Implementation - Not Scanning from Bottom

Another frequent error is implementing the gravity drop incorrectly by scanning from top to bottom instead of bottom to top.

Incorrect Code:

for col in range(cols):
    write_row = 0
    for row in range(rows):  # Wrong direction!
        if board[row][col] > 0:
            board[write_row][col] = board[row][col]
            write_row += 1
    while write_row < rows:
        board[write_row][col] = 0
        write_row += 1

Why it fails: This places candies at the top of the board instead of letting them fall to the bottom, violating the gravity rule.

Correct Code:

for col in range(cols):
    write_row = rows - 1  # Start from bottom
    for row in range(rows - 1, -1, -1):  # Scan bottom to top
        if board[row][col] > 0:
            board[write_row][col] = board[row][col]
            write_row -= 1
    while write_row >= 0:
        board[write_row][col] = 0
        write_row -= 1

3. Crushing Candies Immediately Instead of Marking First

A subtle but critical mistake is crushing candies immediately when found instead of marking all crushable candies first.

Incorrect Approach:

# Wrong: Crushing immediately
for row in range(rows):
    for col in range(2, cols):
        if board[row][col] == board[row][col-1] == board[row][col-2]:
            board[row][col] = board[row][col-1] = board[row][col-2] = 0  # Immediate removal!

Why it fails: This can cause cascading issues where:

  • A candy that's part of both a horizontal and vertical match might only be counted once
  • The board state changes during scanning, potentially missing valid matches
  • Example: In a T-shape or L-shape formation of identical candies, immediate removal would prevent detecting the perpendicular match

Correct Approach: Mark all matches with negative values first, then remove them all at once during the gravity phase. This ensures all valid matches are identified before any board modifications occur.

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

Which data structure is used to implement priority queue?


Recommended Readings

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

Load More