Dynamic Programming · 46 of 60
Coin game winner where every player has three choices
Two friends take 1, x or y coins in turns from a pile, and whoever takes the last coin wins: decide who wins if both play perfectly.
- Simple way
- time O(3^n)
room O(n) - Clever way
- time O(n)
room O(n)
The problem, simply
There is a pile of n coins and two players, A and B. A goes first. On a turn, a player must take exactly 1, x or y coins (only if the pile has that many). The player who takes the last coin wins.
Both players are perfect: they always pick the best move. Say who wins, A or B.
A story to remember it
Picture it
Each dot is a pile size. An arrow is a move: take 1, 3 or 4 coins. The player who moves to 0 took the last coin and wins.
Examples
A wins
- In
n = 5, x = 3, y = 4- Out
A
A takes 3 and leaves 2. B can only take 1, leaving 1, and A takes the last coin.
Too few coins for the big moves
- In
n = 2, x = 3, y = 4- Out
B
Nobody can take 3 or 4. A takes 1, B takes the last 1.
B wins on a bigger pile
- In
n = 7, x = 3, y = 4- Out
B
A can leave 6, 4 or 3. All three are winning piles for B.
The 1, 2, 3 game
- In
n = 4, x = 2, y = 3- Out
B
With moves 1, 2 and 3, every multiple of 4 is a losing pile for the player to move. A must leave 3, 2 or 1, and B takes the rest.
One coin
- In
n = 1, x = 3, y = 4- Out
A
A takes it and wins at once.
Way 1: the simple way
Ask "can the player to move win with n coins?" with recursion.
- If
n == 0, the player to move has lost. Return false. - Try taking 1,
xandy(if there are enough coins). - If any move leaves a pile where the other player cannot win, return true.
- If none does, return false.
Plain recursion for 5 coins, picks 1, 3 or 4. It stops trying as soon as it finds a move that leaves the friend losing. Faded circles are piles it already solved before and solves again.
- seen
- rejected
class Solution {
// can the player who moves now win with n coins left?
boolean canWin(int n, int x, int y) {
if (n == 0) return false; // the other player took the last coin
int[] moves = {1, x, y};
for (int m : moves) {
if (m <= n && !canWin(n - m, x, y)) return true; // leave a losing pile
}
return false; // every move helps the other player
}
public String findWinner(int n, int x, int y) {
return canWin(n, x, y) ? "A" : "B";
}
}How long it takes: each call can make three more calls, so it can grow like O(3^n). Extra room: the chain of waiting calls is up to n long, O(n).
Way 2: the clever way
A pile size has the same answer every time it shows up, no matter who is moving. So write W or L for every pile size from 0 up to n, once.
win[0] = false.- For
ifrom 1 ton:win[i]is true ifwin[i - 1],win[i - x]orwin[i - y]is false (only for moves that fit). - If
win[n]is true, A wins. Otherwise B wins.
Mark every pile size as win or lose, from small to big
01 / 09
A pile of 0 means the other player just took the last coin. Whoever has to move now has lost. Mark 0 as L.
- looking now
- checking
- done
- answer
- rejected
- ignored
class Solution {
public String findWinner(int n, int x, int y) {
boolean[] win = new boolean[n + 1]; // win[i]: player to move with i coins wins
win[0] = false; // no coins: you already lost
for (int i = 1; i <= n; i++) {
// win if some move leaves the other player a losing pile
if (!win[i - 1]) win[i] = true;
else if (i >= x && !win[i - x]) win[i] = true;
else if (i >= y && !win[i - y]) win[i] = true;
}
return win[n] ? "A" : "B";
}
}How long it takes: three checks per pile size, O(n). Extra room: one card per pile size, O(n). You only ever look back max(x, y) cards, so a ring of that size is enough if memory is tight.
Watch out for
- Check
i >= xandi >= ybefore looking back, or you read outside the array. xorycan equal 1 or each other. The code still works, it just checks the same move twice.- The answer says who wins, not how many coins were taken. Return "A" when
win[n]is true.
What interviewers ask next
- Is there a pattern? Yes, the W and L marks always repeat after a while, because each mark only depends on the last
max(x, y)marks. For moves 1, 2, 3 the losing piles are exactly the multiples of 4, so the answer isn % 4 != 0. - What if the player who takes the last coin loses? Then a pile of 0 is a win for the player to move (the other player was forced to take the last coin). Start with
win[0] = trueand keep the same rule. - Many piles at once, where you take from any one pile? That is Nim-style. You need Grundy numbers: for each pile size, the smallest number not reached by any move, and you XOR them.
- What about picking from either end of a row of coins with values? That is "Optimal strategy for a game", a different DP over ranges.
Remember this
Grown-up words: W and L are winning and losing positions, the dots and arrows are the game graph, "both play perfectly" is optimal play, and the repeating marks come from the sequence being periodic.