CF 102878G - Nim plus
The game is played with a single pile containing n paper balls. Long Long and Mao Mao do not share the same legal moves. Long Long has a set of numbers he is allowed to remove, and Mao Mao has another set.
Rating: -
Tags: -
Solve time: 41s
Verified: yes
Solution
Problem Understanding
The game is played with a single pile containing n paper balls. Long Long and Mao Mao do not share the same legal moves. Long Long has a set of numbers he is allowed to remove, and Mao Mao has another set. On a turn, a player chooses one number from their own set and removes exactly that many balls, but only if the pile contains enough balls. A player who cannot make a move loses. The task is to determine whether the first player, Long Long, can force a win with optimal play.
The input gives the initial number of balls, the size of both move sets, and the two sets themselves. The output is the winner's name in the required format. The important part of the problem is that the players have different move options, so ordinary impartial Nim theory does not directly apply. The state depends on whose turn it is.
The constraints are small enough for dynamic programming. The pile size can reach 5000, and each move set has at most 100 values. This means a solution around n * m, which is about 500,000 transitions, is easily fast enough. A simulation of every possible game sequence would be impossible because the number of possible move orders grows exponentially.
Several edge cases can break a careless implementation. If a player has a move larger than the current pile, that move cannot be used. For example:
5 1
6
7
The correct output is:
Mao Mao nb!
Long Long cannot remove 6 balls from a pile of size 5, so he loses immediately. An implementation that only checks whether a move exists in the set, without checking the current pile size, would give the wrong answer.
Another tricky case is when a player can move now but every resulting state gives the opponent a winning position. For example:
1 1
1
1
The correct output is:
Long Long nb!
Long Long removes the only ball, leaving Mao Mao unable to move. The algorithm must evaluate future positions, not only whether the current player has at least one legal move.
A third important situation appears when the two players have identical move sets. The winner is not determined by comparing the sets, because the first player advantage still depends on the current pile size. For example:
2 1
1
1
The correct output is:
Mao Mao nb!
Long Long removes one ball, leaving one ball for Mao Mao, who removes it and wins.
Approaches
A direct approach is to recursively explore the game tree. For every state, we try every legal move for the current player. If any move sends the opponent to a losing state, the current player wins. This is correct because every possible game continuation is considered.
The problem is the number of repeated states. The same pile size can appear through many different sequences of moves. Without memoization, the recursion can revisit the same positions exponentially many times. Even with memoization, the natural state has two possibilities for the player turn and up to 5000 pile sizes, so dynamic programming is the right way to organize the computation.
The key observation is that the entire history is irrelevant. The only information needed is the number of balls remaining and whose turn it is. We can compute these states in increasing order of the pile size. Since every move decreases the pile size, every transition points to a state that has already been solved.
Let winL[x] mean Long Long can win when there are x balls and it is his turn. Let winM[x] mean Mao Mao can win when there are x balls and it is his turn.
For Long Long, a state is winning if there exists a legal removal that leaves Mao Mao in a losing state:
winL[x] = true if some a satisfies a <= x and winM[x-a] = false.
The same idea applies to Mao Mao:
winM[x] = true if some b satisfies b <= x and winL[x-b] = false.
The base state is 0 balls. A player with zero balls cannot move, so that player loses immediately. This means both winL[0] and winM[0] are false.
| Approach | Time Complexity | Space Complexity | Verdict |
|---|---|---|---|
| Brute Force | Exponential | O(number of states) with memoization | Too slow without DP structure |
| Optimal | O(nm) | O(n) | Accepted |
Algorithm Walkthrough
- Create two dynamic programming arrays.
winL[i]stores whether Long Long wins withiballs before his move, andwinM[i]stores whether Mao Mao wins withiballs before his move. Both values start as false because a player with no balls cannot move. - Process pile sizes from
1ton. For each size, try every move available to Long Long. If a move is not larger than the current pile and the resulting Mao Mao state is losing, markwinL[i]as true. This follows the normal game rule that one winning move is enough. - For the same pile size, try every move available to Mao Mao. If one move leaves Long Long in a losing state, mark
winM[i]as true. - After all states are calculated, check
winL[n]. If it is true, Long Long can force a victory. Otherwise Mao Mao can force the win.
Why it works:
The invariant of the dynamic programming is that when we calculate state i, every possible next state has already been calculated because every move reduces the pile size. For a player to win, there must exist at least one move leading to an opponent state where the opponent cannot force a victory. The transitions check exactly these possibilities, so every state is classified correctly. Since the initial game is simply Long Long's turn with n balls, winL[n] gives the answer.
Python Solution
import sys
input = sys.stdin.readline
def solve():
n, m = map(int, input().split())
a = list(map(int, input().split()))
b = list(map(int, input().split()))
win_l = [False] * (n + 1)
win_m = [False] * (n + 1)
for i in range(1, n + 1):
for x in a:
if x <= i and not win_m[i - x]:
win_l[i] = True
break
for x in b:
if x <= i and not win_l[i - x]:
win_m[i] = True
break
if win_l[n]:
print("Long Long nb!")
else:
print("Mao Mao nb!")
if __name__ == "__main__":
solve()
The two arrays represent the two different players' turns. Keeping separate arrays is necessary because the same pile size can be winning for one player and losing for the other due to their different move sets.
The loops go upward from smaller piles to larger piles. When calculating win_l[i], every state win_m[i - x] is already known because i - x < i. The same property holds for Mao Mao's states.
The condition x <= i is the boundary check that prevents illegal removals. The early break is only an optimization. Once a winning move is found, checking other moves cannot change the result.
Worked Examples
Sample 1:
5 1
6
7
| Balls | Long Long state | Mao Mao state | Reason |
|---|---|---|---|
| 0 | Lose | Lose | No legal moves |
| 1 | Lose | Lose | Both moves are too large |
| 2 | Lose | Lose | Both moves are too large |
| 3 | Lose | Lose | Both moves are too large |
| 4 | Lose | Lose | Both moves are too large |
| 5 | Lose | Lose | Long Long cannot remove 6 |
The first player has no legal move at the initial state, so Mao Mao wins.
Sample 2:
20 3
3 7 10
2 6 7
| Balls | Long Long state | Mao Mao state | Reason |
|---|---|---|---|
| 0 | Lose | Lose | No moves |
| 1 | Lose | Lose | No legal removals |
| 3 | Win | Lose | Long Long can remove 3 |
| 10 | Win | Win | Both players have winning replies |
| 20 | Win | Unknown after checking | Long Long removes 10 and leaves 10 |
The important part of this trace is that a winning move does not need to empty the pile. Long Long only needs to move to a state where Mao Mao cannot force a win.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(nm) | Every pile size checks every move from both sets |
| Space | O(n) | Two boolean arrays store all solved states |
With n <= 5000 and m <= 100, the algorithm performs about one million simple checks in the worst case, which fits comfortably within the limits.
Test Cases
import sys
import io
def run(inp: str) -> str:
old_stdin = sys.stdin
old_stdout = sys.stdout
sys.stdin = io.StringIO(inp)
sys.stdout = io.StringIO()
solve()
result = sys.stdout.getvalue()
sys.stdin = old_stdin
sys.stdout = old_stdout
return result
# provided samples
assert run("""5 1
6
7
""") == "Mao Mao nb!\n", "sample 1"
assert run("""20 3
3 7 10
2 6 7
""") == "Long Long nb!\n", "sample 2"
# minimum size
assert run("""1 1
1
1
""") == "Long Long nb!\n", "single ball"
# all equal moves
assert run("""2 1
1
1
""") == "Mao Mao nb!\n", "same move set"
# large pile with repeated possible cycles
assert run("""10 2
1 10
1 10
""") == "Mao Mao nb!\n", "large equal moves"
# unavailable moves at start
assert run("""3 2
5 6
1 2
""") == "Mao Mao nb!\n", "illegal first moves"
| Test input | Expected output | What it validates |
|---|---|---|
1 1 / 1 / 1 |
Long Long nb! | Immediate winning move |
2 1 / 1 / 1 |
Mao Mao nb! | Alternating turns with equal moves |
10 2 / 1 10 / 1 10 |
Mao Mao nb! | Longer dependency chain |
3 2 / 5 6 / 1 2 |
Mao Mao nb! | Moves larger than the pile |
Edge Cases
For the case where Long Long has no usable move, the transition correctly leaves winL[n] false. In the input:
5 1
6
7
the only Long Long move is 6, which is greater than 5. The algorithm skips it because x <= i is false, so the state remains losing.
For the case where both players have the same moves, the algorithm does not make assumptions based on symmetry. In:
2 1
1
1
the state with one ball is winning for the player whose turn it is, but the state with two balls is losing because the first player must give the opponent the one-ball winning state.
For a state where a move removes all remaining balls, the algorithm handles it through the base case. If a player removes the entire pile, the opponent reaches state 0, which is losing. This makes the move correctly appear as a winning transition.