Zuma Game — Hard Problem & Solution
A row of coloured balls lies on the board; each ball is red 'R', yellow 'Y', blue 'B', green 'G' or white 'W'. You also hold some balls in your hand.
- Difficulty: Hard
- Topics: Strings, Dynamic Programming, Breadth-First Search, Memoization
- Asked at: Amazon, Google
- Time limit: 2 s
- Memory limit: 256 MB
- Languages: JavaScript, TypeScript, Python, Java, C++, C, C#, Go, Kotlin, Swift, Rust, PHP and Ruby
Problem statement
A row of coloured balls lies on the board; each ball is red 'R', yellow 'Y', blue 'B', green 'G' or white 'W'. You also hold some balls in your hand.
In one turn you take any ball from your hand and insert it anywhere in the row — between two balls or at either end. Then, while the row contains a group of three or more consecutive balls of the same colour, that group is removed (removing one group can make its neighbours touch and form a new group, which is removed too).
Return the minimum number of balls you must insert to clear the whole board, or -1 if the balls in your hand are not enough. The starting row never contains three or more consecutive balls of one colour.
Example 1
Input: board = "WRRBBW", hand = "RB"
Output: -1
Explanation: Inserting R then B leaves `WW`, and there is nothing left to finish it.
Example 2
Input: board = "WWRRBBWW", hand = "WRBRW"
Output: 2
Explanation: `WWRR[R]BBWW` clears the reds, then `WWBB[B]WW` clears the blues and the whites merge into four.
Example 3
Input: board = "G", hand = "GGGGG"
Output: 2
Constraints
1 <= board.length <= 161 <= hand.length <= 5board and hand consist of the characters 'R', 'Y', 'B', 'G' and 'W'The initial board has no group of three or more consecutive balls of the same colour
How to solve Zuma Game
Breadth-first search over game states (row, sorted hand), so the first time the row becomes empty we have used the fewest balls. Pruning the insertion points to the two useful kinds keeps the branching small.
Approach
- Sort
hand; push(board, hand)into the queue and the visited set. - Process the queue level by level (level = balls used). For each state, for each insertion index
iand each distinct colourcin the hand: - skip if the ball before
iis alreadyc; keep it only ifboard[i] == c, or ifboard[i - 1] == board[i] != c(splitting a pair). - Insert, run
clean; if the row is empty, return the current level. Otherwise, if(row, hand minus c)is new, enqueue it. - If the queue empties, return -1.
Why it works
Breadth-first order guarantees minimality. The pruning is safe: putting c next to a same-coloured ball can always be done at the left end of that group (identical results), and putting c between two different-coloured neighbours, neither equal to c nor equal to each other, never creates or destroys anything that a later, better-placed insertion could not achieve — while splitting an equal pair can matter (it can let the two halves merge with other balls later), so that case is kept.
Complexity
- Time —
Exponential in the hand size (≤ 5) — small in practice with the visited set - Space —
O(number of distinct states)
Pitfalls
- Only inserting next to same-coloured balls is not enough:
RRWWRRBBRRwith handWBneeds aWbetween the last twoRs. - After a removal the neighbours may form a new group of 3+; keep cleaning until nothing changes.
- Two identical balls in the hand lead to identical states — try each colour once per position.
Reference solution
Python
def findMinStep(board: str, hand: str) -> int:
def clean(s):
while True:
changed = False
i = 0
while i < len(s):
j = i
while j < len(s) and s[j] == s[i]:
j += 1
if j - i >= 3:
s = s[:i] + s[j:]
changed = True
break
i = j
if not changed:
return s
start = ''.join(sorted(hand))
level = [(board, start)]
seen = {board + '#' + start}
step = 0
while level:
step += 1
nxt = []
for b, h in level:
for i in range(len(b) + 1):
for j in range(len(h)):
c = h[j]
if j > 0 and c == h[j - 1]:
continue
if i > 0 and b[i - 1] == c:
continue
worth = (i < len(b) and b[i] == c) or (0 < i < len(b) and b[i - 1] == b[i] and b[i] != c)
if not worth:
continue
nb = clean(b[:i] + c + b[i:])
if not nb:
return step
nh = h[:j] + h[j + 1:]
key = nb + '#' + nh
if key not in seen:
seen.add(key)
nxt.append((nb, nh))
level = nxt
return -1JavaScript
var findMinStep = function(board, hand) {
var clean = function(s) {
for (;;) {
var changed = false;
var i = 0;
while (i < s.length) {
var j = i;
while (j < s.length && s.charAt(j) === s.charAt(i)) j++;
if (j - i >= 3) {
s = s.substring(0, i) + s.substring(j);
changed = true;
break;
}
i = j;
}
if (!changed) return s;
}
};
var start = hand.split('').sort().join('');
var level = [[board, start]];
var seen = new Set([board + '#' + start]);
var step = 0;
while (level.length) {
step++;
var nxt = [];
for (var q = 0; q < level.length; q++) {
var b = level[q][0], h = level[q][1];
for (var i = 0; i <= b.length; i++) {
for (var j = 0; j < h.length; j++) {
var c = h.charAt(j);
if (j > 0 && c === h.charAt(j - 1)) continue;
if (i > 0 && b.charAt(i - 1) === c) continue;
var worth = (i < b.length && b.charAt(i) === c) ||
(i > 0 && i < b.length && b.charAt(i - 1) === b.charAt(i) && b.charAt(i) !== c);
if (!worth) continue;
var nb = clean(b.substring(0, i) + c + b.substring(i));
if (nb.length === 0) return step;
var nh = h.substring(0, j) + h.substring(j + 1);
var key = nb + '#' + nh;
if (!seen.has(key)) {
seen.add(key);
nxt.push([nb, nh]);
}
}
}
}
level = nxt;
}
return -1;
};Also on the editorial tab: C, C#, C++, Go, Java, Kotlin, PHP, Ruby, Rust, Swift, TypeScript.
All 424 strings problems · the whole catalogue
Learn the technique: Strings · Dynamic Programming