723. Candy Crush
Problem Description
This problem requires you to simulate the game "Candy Crush". Given a 2D grid representing a candy board of size m x n
, where each cell contains an integer that represents a different type of candy, your task is to implement an algorithm that performs a series of crushing actions according to the game's rules.
The crushing rules are as follows:
- When three or more candies of the same type are adjacent either vertically or horizontally, they all get crushed at once, and their positions are set to zero, indicating an empty cell.
- Once candies are crushed, gravity comes into play. Any candies above the empty cells will fall down to fill the empty spaces.
- New candies do not enter the board from the top. Only existing candies drop down.
- The crushing and dropping process continues until no more crushing can occur—i.e., no groups of three or more adjacent candies of the same type are left.
The goal is to keep applying these rules until the board reaches a stable state with no more possible crushes. The final stable board should be returned.
Intuition
The solution is iterative and simulates the game's mechanics step by step. Here's how the intuition unfolds:
-
Initial Check for Crushes: Start by scanning the board to find any rows or columns of three or more candies that can be crushed. We keep a boolean flag,
run
, tracking if we performed a crushing operation during the current iteration. This flag indicates whether we need to continue processing the board. -
Marking Candies for Crushing: When we find three or more candies in a row or column that match, we mark them by negating their value. It allows us to distinguish between candies that will be crushed and the rest of the board.
-
Crushing Candies: Once we are done checking the entire board and have marked all candies to be crushed, we perform the actual crushing by replacing the marked candies (negative numbers) with zeros.
-
Implementing Gravity: After crushing, simulate gravity by letting the uncrushed candies (positive numbers) fall down to fill any gaps. This is done by moving candies down within each column, starting from the bottom up.
-
Repeating the Process: Once we've completed a crush and gravity step, we need to check if more crushes can be made because the falling candies might create new sequences of three or more matching candies. We repeat the process as long as we keep crushing candies in each iteration. The
run
flag helps us to determine this; if set to True, at least one crushing occurred, so we need another pass. -
Stable Board: Finally, when no more crushes occur in a whole pass (i.e.,
run
is False), the board is stable, and we return it.
By iterating through these steps, the given algorithm ensures that all possible crushes are made and that the board reaches a stable state according to Candy Crush rules.
Learn more about Two Pointers patterns.
Solution Approach
The implementation of the solution involves several important steps, each taking advantage of basic algorithms and data structures.
-
Marking Candies to Crush: We loop through the entire board, searching for sequences of three or more similar candies either horizontally or vertically. We use two nested loops for this - one for rows and one for columns. When we find such a sequence, we mark the candies by negating their values using
abs(board[i][j])
to indicate these will be crushed. The use of negation allows us to retain the candy type information, which is important to check for subsequent crushes in later iterations. -
Horizontal Crushing: This part checks for three or more adjacent candies horizontally. The following snippet from the code performs horizontal checks and marks the candies:
for i in range(m): for j in range(n - 2): if (board[i][j] != 0 and abs(board[i][j]) == abs(board[i][j + 1]) and abs(board[i][j]) == abs(board[i][j + 2])): run = True board[i][j] = board[i][j + 1] = board[i][j + 2] = -abs(board[i][j])
-
Vertical Crushing: Similar to the horizontal checks, we perform vertical checks as well, identifying candies that should be crushed and marking them as negative numbers. Code snippet for the vertical check:
for j in range(n): for i in range(m - 2): if (board[i][j] != 0 and abs(board[i][j]) == abs(board[i + 1][j]) and abs(board[i][j]) == abs(board[i + 2][j])): run = True board[i][j] = board[i + 1][j] = board[i + 2][j] = -abs(board[i][j])
-
Candies Falling (Gravity): Once all possible candies are marked, we need to let all the unmarked (positive) candies fall down. We iterate through each column starting from the bottom row and move unmarked candies down to the ‘curr’ position, which represents the next available uncrushed position from the bottom. The code for simulating gravity is as follows:
for j in range(n): curr = m - 1 for i in range(m - 1, -1, -1): if board[i][j] > 0: board[curr][j] = board[i][j] curr -= 1 while curr > -1: board[curr][j] = 0 curr -= 1
-
Repeating Until Stable: The outer
while run:
loop allows us to repeat this process until no further candies can be crushed. Therun
flag is set toTrue
whenever a crush happens, prompting another iteration. When an iteration completes with no candies crushed (run
isFalse
), the board is stable, and the loop will terminate. -
Returning the Result: After the loop exits, we return the stabilized board. There are no extra data structures used outside of the input
board
, which is mutated in place to save space, and simple variables for iteration control.
By iterating over the marked candies and applying gravity as long as there are candies to crush, the algorithm ensures an accurate portrayal of the Candy Crush mechanism.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach using a simple 3x3
grid to demonstrate the Candy Crush algorithm.
Suppose our initial 3x3
grid is as follows:
1 1 1 2 2 3 3 3 2
Following the solution approach:
-
Marking Candies to Crush: First, we look for horizontal and vertical sequences of three or more similar candies to mark for crushing. We see that the top row has three
1
s horizontally.After marking, our grid looks like this:
-1 -1 -1 2 2 3 3 3 2
There are no vertical sequences to mark, so we continue to the next step.
-
Horizontal and Vertical Crushing: Since we've already marked the candies in step 1, we now set those positions to zero, simulating the crush.
Our grid now looks like this:
0 0 0 2 2 3 3 3 2
-
Candies Falling (Gravity): We apply gravity, allowing the candies to fall down to fill the empty spaces. Starting from the bottom of each column, we move positive numbers down.
After gravity, our grid becomes:
0 0 0 0 0 0 2 2 3
Now we have uncrushed candies at the bottom with zeros representing the empty cells above them.
-
Repeating Until Stable: We need to check again if any new sequences of three or more similar candies have formed due to gravity. Since no such sequences exist, no further action is required, and the
run
flag remainsFalse
. -
Returning the Result: With no further crushes possible, and the board in a stable state, this is the final board state:
0 0 0 0 0 0 2 2 3
And this is the state we return as our answer, illustrating a simple run-through of the Candy Crush algorithm.
Solution Implementation
1from typing import List
2
3class Solution:
4 def candyCrush(self, board: List[List[int]]) -> List[List[int]]:
5 # Dimensions of the board
6 num_rows, num_cols = len(board), len(board[0])
7
8 # Flag to indicate if we should continue crushing candies
9 should_crush = True
10
11 # Keep crushing candies until no more moves can be made
12 while should_crush:
13 should_crush = False # Reset the flag for each iteration
14
15 # Mark the candies to be crushed horizontally
16 for row in range(num_rows):
17 for col in range(num_cols - 2):
18 candy_value = abs(board[row][col])
19 # Check if three consecutive candies have the same value
20 if candy_value != 0 and candy_value == abs(board[row][col + 1]) == abs(board[row][col + 2]):
21 should_crush = True # We will need another pass after crushing
22 # Mark the candies for crushing by negating their value
23 board[row][col] = board[row][col + 1] = board[row][col + 2] = -candy_value
24
25 # Mark the candies to be crushed vertically
26 for col in range(num_cols):
27 for row in range(num_rows - 2):
28 candy_value = abs(board[row][col])
29 # Check if three consecutive candies vertically have the same value
30 if candy_value != 0 and candy_value == abs(board[row + 1][col]) == abs(board[row + 2][col]):
31 should_crush = True # We will need another pass after crushing
32 # Mark the candies for crushing
33 board[row][col] = board[row + 1][col] = board[row + 2][col] = -candy_value
34
35 # Drop the candies to fill the empty spaces caused by crushing
36 if should_crush:
37 for col in range(num_cols):
38 # Pointer to where the next non-crushed candy will fall
39 write_row = num_rows - 1
40 for row in range(num_rows - 1, -1, -1):
41 # If the candy is not marked for crushing, bring it down
42 if board[row][col] > 0:
43 board[write_row][col] = board[row][col]
44 write_row -= 1
45 # Fill the remaining spaces at the top with zeros
46 while write_row >= 0:
47 board[write_row][col] = 0
48 write_row -= 1
49
50 # Return the modified board after all possible crushes are completed
51 return board
52
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 // Loop until no more candies can be crushed
8 while (shouldContinue) {
9 shouldContinue = false;
10
11 // Mark candies that need to be crushed in the horizontal direction
12 for (int i = 0; i < rows; ++i) {
13 for (int j = 0; j < cols - 2; ++j) {
14 int value = Math.abs(board[i][j]);
15 if (value != 0 && value == Math.abs(board[i][j + 1]) && value == Math.abs(board[i][j + 2])) {
16 shouldContinue = true;
17 board[i][j] = board[i][j + 1] = board[i][j + 2] = -value;
18 }
19 }
20 }
21
22 // Mark candies that need to be crushed in the vertical direction
23 for (int j = 0; j < cols; ++j) {
24 for (int i = 0; i < rows - 2; ++i) {
25 int value = Math.abs(board[i][j]);
26 if (value != 0 && value == Math.abs(board[i + 1][j]) && value == Math.abs(board[i + 2][j])) {
27 shouldContinue = true;
28 board[i][j] = board[i + 1][j] = board[i + 2][j] = -value;
29 }
30 }
31 }
32
33 // Crush the candies and let new ones fall down
34 if (shouldContinue) {
35 for (int j = 0; j < cols; ++j) {
36 // Start from bottom of the board
37 int writeIndex = rows - 1;
38 for (int i = rows - 1; i >= 0; --i) {
39 // If this candy is not marked to be crushed, bring it down
40 if (board[i][j] > 0) {
41 board[writeIndex][j] = board[i][j];
42 writeIndex--;
43 }
44 }
45 // Fill the remaining spaces with 0s to indicate empty spaces
46 while (writeIndex > -1) {
47 board[writeIndex][j] = 0;
48 writeIndex--;
49 }
50 }
51 }
52 }
53
54 return board;
55 }
56}
57
1class Solution {
2public:
3 vector<vector<int>> candyCrush(vector<vector<int>>& board) {
4 int numRows = board.size(), numCols = board[0].size();
5 bool foundCrushableCandies = true;
6
7 // Continue the process as long as we find crushable candies
8 while (foundCrushableCandies) {
9 foundCrushableCandies = false;
10
11 // Mark the crushable candies in rows by making their values negative
12 for (int row = 0; row < numRows; ++row) {
13 for (int col = 0; col < numCols - 2; ++col) {
14 int candyValue = abs(board[row][col]);
15 if (candyValue != 0 &&
16 candyValue == abs(board[row][col + 1]) &&
17 candyValue == abs(board[row][col + 2])) {
18 foundCrushableCandies = true;
19 board[row][col] = board[row][col + 1] = board[row][col + 2] = -candyValue;
20 }
21 }
22 }
23
24 // Mark the crushable candies in columns by making their values negative
25 for (int col = 0; col < numCols; ++col) {
26 for (int row = 0; row < numRows - 2; ++row) {
27 int candyValue = abs(board[row][col]);
28 if (candyValue != 0 &&
29 candyValue == abs(board[row + 1][col]) &&
30 candyValue == abs(board[row + 2][col])) {
31 foundCrushableCandies = true;
32 board[row][col] = board[row + 1][col] = board[row + 2][col] = -candyValue;
33 }
34 }
35 }
36
37 // Crush the marked candies and let the candies fall down to fill the empty spaces
38 if (foundCrushableCandies) {
39 for (int col = 0; col < numCols; ++col) {
40 int fillPosition = numRows - 1;
41
42 // Move non-crushable candies down
43 for (int row = numRows - 1; row >= 0; --row) {
44 if (board[row][col] > 0) {
45 board[fillPosition][col] = board[row][col];
46 fillPosition--;
47 }
48 }
49
50 // Fill the remaining spaces with 0s
51 while (fillPosition >= 0) {
52 board[fillPosition][col] = 0;
53 fillPosition--;
54 }
55 }
56 }
57 }
58
59 return board;
60 }
61};
62
1// Define the matrix type
2type Matrix = number[][];
3
4// Find and crush the crushable candies, then return the updated board
5function candyCrush(board: Matrix): Matrix {
6 let numRows = board.length;
7 let numCols = board[0].length;
8 let foundCrushableCandies = true;
9
10 // Continue the process as long as we find crushable candies
11 while (foundCrushableCandies) {
12 foundCrushableCandies = false;
13
14 // Mark the crushable candies in rows by making their values negative
15 for (let row = 0; row < numRows; ++row) {
16 for (let col = 0; col < numCols - 2; ++col) {
17 let candyValue = Math.abs(board[row][col]);
18 if (candyValue !== 0 &&
19 candyValue === Math.abs(board[row][col + 1]) &&
20 candyValue === Math.abs(board[row][col + 2])) {
21 foundCrushableCandies = true;
22 board[row][col] = board[row][col + 1] = board[row][col + 2] = -candyValue;
23 }
24 }
25 }
26
27 // Mark the crushable candies in columns by making their values negative
28 for (let col = 0; col < numCols; ++col) {
29 for (let row = 0; row < numRows - 2; ++row) {
30 let candyValue = Math.abs(board[row][col]);
31 if (candyValue !== 0 &&
32 candyValue === Math.abs(board[row + 1][col]) &&
33 candyValue === Math.abs(board[row + 2][col])) {
34 foundCrushableCandies = true;
35 board[row][col] = board[row + 1][col] = board[row + 2][col] = -candyValue;
36 }
37 }
38 }
39
40 // Crush the marked candies and let the candies fall down to fill the empty spaces
41 if (foundCrushableCandies) {
42 for (let col = 0; col < numCols; ++col) {
43 let fillPosition = numRows - 1;
44
45 // Move non-crushable candies down
46 for (let row = numRows - 1; row >= 0; --row) {
47 if (board[row][col] > 0) {
48 board[fillPosition][col] = board[row][col];
49 fillPosition--;
50 }
51 }
52
53 // Fill the remaining spaces with 0s
54 for (let fill = fillPosition; fill >= 0; fill--) {
55 board[fill][col] = 0;
56 }
57 }
58 }
59 }
60
61 return board;
62}
63
Time and Space Complexity
Time Complexity
The time complexity of the given code is primarily determined by the nested loops and the operations performed within them. Let's analyze it step by step.
-
The outer
while
loop that repeats until no more candies can be crushed (run = False
) will run until the board stabilizes, which relates to the dimensions of the board. In the worst case, this might beO(m * n)
because each candy might need to fall all the way down the board in the most extreme case. -
Inside the while loop, there are two sets of nested
for
loops: one for checking rows and one for checking columns for crushable candies. Both of these sets involve iterating over all elements on the board once, giving us aO(m * n)
for each full scan. -
Crushing candies and making them negative take
O(1)
time each, but since it's inside the nested loops, it doesn't add more than a constant factor to the overall complexity of scanning the board. -
The final part within the while loop is another nested loop that handles gravity, making candies fall down. This, again, requires going over each column and, in the worst case, moving each candy down to the bottom, which takes
O(m)
for each column. Multiplying byn
columns gives usO(m * n)
.
Combining these, in the worst case, the while loop may run m * n
times due to the possibility of each iteration only crushing one candy and making others fall one place. This is a pessimistic estimate, but it serves as an upper boundary. Thus, the time complexity of the nested loops, taken together, results in O((m * n) * (m * n))
or O((m * n)^2)
.
Space Complexity
The space complexity of the code is:
-
O(1)
for the variablesm
,n
,run
,curr
, andi
,j
inside loops, as they do not depend on the size of the board and use a constant amount of space. -
The input board is modified in-place and no additional data structures of significant size are created or used in the process, meaning we do not use additional space proportional to the size of the input.
Hence, the space complexity of the algorithm is O(1)
.
Note: While the actual number of iterations needed to stabilize the board might be much less than m * n
, we are considering the worst-case time complexity here for completeness.
Learn more about how to find time and space complexity quickly using problem constraints.
Which algorithm is best for finding the shortest distance between two points in an unweighted graph?
Recommended Readings
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
LeetCode 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 we
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https algomonster s3 us east 2 amazonaws com recursion jpg You first
Want a Structured Path to Master System Design Too? Don’t Miss This!