LeetCode 723 - Candy Crush
This problem is essentially a simulation of the Candy Crush game, where we need to repeatedly crush candies in a grid until the board reaches a stable state. The input is an m x n matrix of integers representing different types of candies, and 0 represents empty cells.
Difficulty: 🟡 Medium
Topics: Array, Two Pointers, Matrix, Simulation
Solution
Problem Understanding
This problem is essentially a simulation of the Candy Crush game, where we need to repeatedly crush candies in a grid until the board reaches a stable state. The input is an m x n matrix of integers representing different types of candies, and 0 represents empty cells. Each integer from 1 to 2000 represents a distinct candy type. The rules are that if three or more candies of the same type are aligned consecutively in a row or a column, they "crush" and disappear, leaving empty cells. After candies crush, any candy above an empty cell falls down until it reaches another candy or the bottom of the board. This process repeats until no further candies can be crushed.
The expected output is the final stable board after all possible crushing and falling actions. Constraints indicate that the board size is at most 50x50, which is small enough for a straightforward simulation approach to be feasible. Edge cases include boards that are already stable, boards with multiple overlapping crushes, and cases where new crushes are triggered after falling candies.
Approaches
The brute-force approach would be to iterate through the board, detect all crushable sequences (horizontal and vertical), mark them, then crush them and let candies fall. Repeat this process until no more candies can be crushed. While correct, this approach may repeatedly scan the entire board, making it less efficient. Each full scan could take O(mn) and repeated scans could lead to higher overall runtime.
The optimal approach leverages a systematic marking and dropping strategy. Instead of using complex data structures, we mark candies to crush using negative values or a separate boolean board, then crush all marked candies in one pass. For falling candies, we process each column independently using a "write pointer" from the bottom, moving candies down efficiently. We repeat the process until no crushes occur. This reduces unnecessary repeated iterations over non-crushable areas and makes the solution straightforward and efficient.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O((mn)^2) | O(mn) | Repeatedly scans the board entirely for crushes |
| Optimal | O((mn)^2) | O(1) | Marks crushes, drops candies column-wise, repeats until stable |
Algorithm Walkthrough
-
Initialize a flag
foundto indicate whether any candies are crushed in the current iteration. Set it toTrueto start the loop. -
While
foundisTrue, do the following: -
Set
foundtoFalse. Iterate over the board to identify horizontal and vertical sequences of three or more identical candies. Mark these positions for crushing, either by using a negative value or a separate boolean marker. If any sequence is found, setfoundtoTrue. -
After marking, iterate over the board again and crush all marked candies by setting them to
0. -
Process each column independently to drop candies. Start from the bottom of each column with a "write pointer". For each non-zero candy encountered from bottom to top, move it to the
writeposition and decrementwrite. After moving all candies, fill remaining positions abovewritewith0. -
Repeat the above loop until no new crushes are found (
foundremainsFalse).
Why it works: The loop invariant is that after each iteration, all crushable candies are crushed and all remaining candies are in the lowest possible positions. By repeatedly applying these steps, we ensure no new crushable sequences remain, guaranteeing stability.
Python Solution
from typing import List
class Solution:
def candyCrush(self, board: List[List[int]]) -> List[List[int]]:
m, n = len(board), len(board[0])
found = True
while found:
found = False
crush = [[False]*n for _ in range(m)]
# Mark horizontal crushes
for i in range(m):
for j in range(n-2):
if board[i][j] != 0 and board[i][j] == board[i][j+1] == board[i][j+2]:
crush[i][j] = crush[i][j+1] = crush[i][j+2] = True
found = True
# Mark vertical crushes
for i in range(m-2):
for j in range(n):
if board[i][j] != 0 and board[i][j] == board[i+1][j] == board[i+2][j]:
crush[i][j] = crush[i+1][j] = crush[i+2][j] = True
found = True
# Crush candies
for i in range(m):
for j in range(n):
if crush[i][j]:
board[i][j] = 0
# Drop candies
for j in range(n):
write = m - 1
for i in range(m-1, -1, -1):
if board[i][j] != 0:
board[write][j] = board[i][j]
write -= 1
for i in range(write, -1, -1):
board[i][j] = 0
return board
The Python implementation closely follows the algorithm. Horizontal and vertical crushes are marked separately, and a crush matrix avoids in-place conflicts. Dropping is handled efficiently with a bottom-up write pointer per column.
Go Solution
func candyCrush(board [][]int) [][]int {
m, n := len(board), len(board[0])
found := true
for found {
found = false
crush := make([][]bool, m)
for i := range crush {
crush[i] = make([]bool, n)
}
// Horizontal crush
for i := 0; i < m; i++ {
for j := 0; j < n-2; j++ {
if board[i][j] != 0 && board[i][j] == board[i][j+1] && board[i][j] == board[i][j+2] {
crush[i][j], crush[i][j+1], crush[i][j+2] = true, true, true
found = true
}
}
}
// Vertical crush
for i := 0; i < m-2; i++ {
for j := 0; j < n; j++ {
if board[i][j] != 0 && board[i][j] == board[i+1][j] && board[i][j] == board[i+2][j] {
crush[i][j], crush[i+1][j], crush[i+2][j] = true, true, true
found = true
}
}
}
// Crush candies
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
if crush[i][j] {
board[i][j] = 0
}
}
}
// Drop candies
for j := 0; j < n; j++ {
write := m - 1
for i := m - 1; i >= 0; i-- {
if board[i][j] != 0 {
board[write][j] = board[i][j]
write--
}
}
for i := write; i >= 0; i-- {
board[i][j] = 0
}
}
}
return board
}
In Go, slices are used to represent the board and the crush markers. The approach mirrors Python, with explicit initialization of 2D boolean slices. The same logic applies, handling edge cases and repeated crushing naturally.
Worked Examples
Example 1:
Starting 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]]
First iteration: detect crushes in last row (three 2s horizontally). Set crush flags. Drop candies above, resulting in intermediate board with 0s. Repeat the loop until no crushable candies remain. Final stable board:
[[0,0,0,0,0],
[0,0,0,0,0],
[0,0,0,0,0],
[110,0,0,0,114],
[210,0,0,0,214],
[310,0,0,113,314],
[410,0,0,213,414],
[610,211,112,313,614],
[710,311,412,613,714],
[810,411,512,713,1014]]
Example 2:
Input: [[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]]
Detect horizontal crushes at positions [0,2],[0,3],[0,4] etc.,