Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

723. Candy Crush - Leetcode Solution

Problem Description

The Candy Crush problem on Leetcode provides you with a 2D board of integers, where each integer represents a type of candy. The goal is to repeatedly "crush" (remove) any group of three or more adjacent candies of the same type, either horizontally or vertically. After crushing, candies above will fall down to fill the emptied spaces (gravity), and zeros will fill the top. This process repeats until no more candies can be crushed. The final board should be returned.

  • The input is a 2D integer array board of size m x n.
  • Crushing can happen in both horizontal and vertical directions.
  • You must keep crushing until the board is stable (no more crushes possible).
  • All moves must be performed in-place (mutate the board).
  • Return the final state of the board.

Thought Process

At first glance, you might consider iterating through the board and crushing candies as you find them. However, candies can be part of multiple crushes in a single round, and after gravity, new crushes may become possible. This suggests a need for:

  • Identifying all crushable groups in a single pass (without crushing immediately).
  • Applying gravity after each round of crushing.
  • Repeating the process until the board is stable.

A brute-force approach would be to check every cell for crushes, crush immediately, and then re-scan. However, this can lead to missing overlapping crushes or repeated work. Instead, we look for an optimized approach where we:

  • Mark all candies to be crushed in a pass using a marker (e.g., negative values).
  • After marking, perform the crush and gravity steps.
  • Continue until no candies are marked for crushing.

Solution Approach

Let's break down the solution step-by-step:

  1. Marking Candies to Crush:
    • Iterate over the board. For each cell, check horizontally and vertically for sequences of three or more same candies.
    • When such a sequence is found, mark those candies (e.g., by making their value negative).
    • This ensures we don't double-count overlapping crushes and don't crush immediately.
  2. Crushing and Gravity:
    • After marking, iterate column by column from bottom to top.
    • For each column, move non-crushed candies (positive values) down, filling from the bottom up.
    • Fill the remaining cells at the top with zeros.
  3. Repeat Until Stable:
    • If any candies were marked in the current round, repeat the process.
    • If none were marked, the board is stable, and we return it.

We use in-place marking to avoid extra space, and the process is repeated until no more crushes are possible.

Example Walkthrough

Suppose we have the following board:

    [
      [110, 5, 112, 113, 114],
      [210, 211, 5, 213, 214],
      [310, 311, 3, 313, 314],
      [410, 411, 412, 5, 414],
      [5,   1,   512, 3, 3],
      [610, 4,   1,   613, 614],
      [710, 1,   2,   713, 714],
      [810, 1,   2,   1,   1],
      [1,   1,   2,   2,   2],
      [4,   1,   4,   4,   1014]
    ]
  
  1. First Round:
    • Crush all horizontal and vertical groups of 3+ (e.g., bottom row has three 2's, [8][2], [8][3], [8][4]).
    • Mark these for crushing.
  2. Apply Gravity:
    • For each column, drop non-crushed candies to the bottom, fill with zeros at the top.
  3. Repeat:
    • New crushes may appear due to candies falling. Repeat the process.
  4. Stop:
    • When no more candies can be crushed, return the final board.

Each step ensures all crushes are found and gravity is applied before checking for new crushes.

Time and Space Complexity

  • Brute-force Approach:
    • Time: O((m*n)^2) per round, since you might rescan the board for each crush.
    • Space: O(1) if in-place, O(m*n) if using extra arrays.
  • Optimized Approach (as above):
    • Time: O((m*n)*k), where k is the number of rounds (usually small, since each round reduces candies).
    • Space: O(1), since all marking and gravity is done in-place.

The approach is efficient because each round guarantees at least one candy is crushed, and the board size is limited.

Summary

The Candy Crush problem requires repeated simulation of matching, crushing, and gravity. The key insight is to mark all crushable candies in a pass before applying gravity, ensuring all overlaps are handled correctly. By working in-place and repeating until stable, we achieve both correctness and efficiency. This approach elegantly models the actual game mechanics and avoids unnecessary rescans or data structures.

Code Implementation

class Solution:
    def candyCrush(self, board):
        m, n = len(board), len(board[0])
        changed = True
        while changed:
            changed = False
            # Mark candies to crush
            for i in range(m):
                for j in range(n-2):
                    v = abs(board[i][j])
                    if v != 0 and v == abs(board[i][j+1]) and v == abs(board[i][j+2]):
                        board[i][j] = board[i][j+1] = board[i][j+2] = -v
                        changed = True
            for j in range(n):
                for i in range(m-2):
                    v = abs(board[i][j])
                    if v != 0 and v == abs(board[i+1][j]) and v == abs(board[i+2][j]):
                        board[i][j] = board[i+1][j] = board[i+2][j] = -v
                        changed = True
            # Gravity
            for j in range(n):
                write_row = m-1
                for i in range(m-1, -1, -1):
                    if board[i][j] > 0:
                        board[write_row][j] = board[i][j]
                        write_row -= 1
                for i in range(write_row, -1, -1):
                    board[i][j] = 0
        return board
      
class Solution {
public:
    vector<vector<int>> candyCrush(vector<vector<int>>& board) {
        int m = board.size(), n = board[0].size();
        bool changed = true;
        while (changed) {
            changed = false;
            // Mark candies to crush
            for (int i = 0; i < m; ++i) {
                for (int j = 0; j < n - 2; ++j) {
                    int v = abs(board[i][j]);
                    if (v != 0 && v == abs(board[i][j+1]) && v == abs(board[i][j+2])) {
                        board[i][j] = board[i][j+1] = board[i][j+2] = -v;
                        changed = true;
                    }
                }
            }
            for (int j = 0; j < n; ++j) {
                for (int i = 0; i < m - 2; ++i) {
                    int v = abs(board[i][j]);
                    if (v != 0 && v == abs(board[i+1][j]) && v == abs(board[i+2][j])) {
                        board[i][j] = board[i+1][j] = board[i+2][j] = -v;
                        changed = true;
                    }
                }
            }
            // Gravity
            for (int j = 0; j < n; ++j) {
                int write_row = m - 1;
                for (int i = m - 1; i >= 0; --i) {
                    if (board[i][j] > 0) {
                        board[write_row--][j] = board[i][j];
                    }
                }
                for (int i = write_row; i >= 0; --i) {
                    board[i][j] = 0;
                }
            }
        }
        return board;
    }
};
      
class Solution {
    public int[][] candyCrush(int[][] board) {
        int m = board.length, n = board[0].length;
        boolean changed = true;
        while (changed) {
            changed = false;
            // Mark candies to crush
            for (int i = 0; i < m; ++i) {
                for (int j = 0; j < n - 2; ++j) {
                    int v = Math.abs(board[i][j]);
                    if (v != 0 && v == Math.abs(board[i][j+1]) && v == Math.abs(board[i][j+2])) {
                        board[i][j] = board[i][j+1] = board[i][j+2] = -v;
                        changed = true;
                    }
                }
            }
            for (int j = 0; j < n; ++j) {
                for (int i = 0; i < m - 2; ++i) {
                    int v = Math.abs(board[i][j]);
                    if (v != 0 && v == Math.abs(board[i+1][j]) && v == Math.abs(board[i+2][j])) {
                        board[i][j] = board[i+1][j] = board[i+2][j] = -v;
                        changed = true;
                    }
                }
            }
            // Gravity
            for (int j = 0; j < n; ++j) {
                int write_row = m - 1;
                for (int i = m - 1; i >= 0; --i) {
                    if (board[i][j] > 0) {
                        board[write_row--][j] = board[i][j];
                    }
                }
                for (int i = write_row; i >= 0; --i) {
                    board[i][j] = 0;
                }
            }
        }
        return board;
    }
}
      
var candyCrush = function(board) {
    let m = board.length, n = board[0].length;
    let changed = true;
    while (changed) {
        changed = false;
        // Mark candies to crush
        for (let i = 0; i < m; ++i) {
            for (let j = 0; j < n - 2; ++j) {
                let v = Math.abs(board[i][j]);
                if (v !== 0 && v === Math.abs(board[i][j+1]) && v === Math.abs(board[i][j+2])) {
                    board[i][j] = board[i][j+1] = board[i][j+2] = -v;
                    changed = true;
                }
            }
        }
        for (let j = 0; j < n; ++j) {
            for (let i = 0; i < m - 2; ++i) {
                let v = Math.abs(board[i][j]);
                if (v !== 0 && v === Math.abs(board[i+1][j]) && v === Math.abs(board[i+2][j])) {
                    board[i][j] = board[i+1][j] = board[i+2][j] = -v;
                    changed = true;
                }
            }
        }
        // Gravity
        for (let j = 0; j < n; ++j) {
            let write_row = m - 1;
            for (let i = m - 1; i >= 0; --i) {
                if (board[i][j] > 0) {
                    board[write_row--][j] = board[i][j];
                }
            }
            for (let i = write_row; i >= 0; --i) {
                board[i][j] = 0;
            }
        }
    }
    return board;
};