Post

Game

Math

Chalkboard XOR Game

1
2
3
4
5
6
7
8
9
10
11
12
public boolean xorGame(int[] nums) {
    int xor = 0;
    for (int num : nums) {
        xor ^= num;
    }

    // when the number of elements are even
    // not all elements are same, otherwise xor == 0
    // if nums[i] and nums[j] are distinct
    // Alice can erase either and not close at current turn
    return xor == 0 || nums.length % 2 == 0;
}

Sum Game

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean sumGame(String num) {
    // left - right
    int diff = 0, qDiff = 0;
    for (int i = 0; i < num.length(); i++) {
        int c = i < num.length() / 2 ? 1 : -1;
        if (num.charAt(i) == '?') {
            qDiff += c;
        } else {
            diff += c * (num.charAt(i) - '0');
        }
    }

    // if qDiff is odd, Alice can always win
    // checks parity by qDiff & 1 to cover negative cases

    // 1. '?' on the opposite sides cancel each out
    // 2. For two '?' on the same side, Bob can always make ? + ? == 9, regardless of how Alice plays.
    // so if we have n '?'s, Bob can only win if:
    // smaller + n * 9 / 2 == larger
    return (qDiff & 1) == 1 || diff + qDiff * 9 / 2 != 0;
}

Stone Game IX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public boolean stoneGameIX(int[] stones) {
    int[] remainders = new int[3];
    for (int s : stones) {
        remainders[s % 3]++;
    }

    // Alice will always win if remainders[0] == 0 by picking the mod with less frequency
    // if remainders[0] is even, Alice can always pick a 3 when Bob picks a 3
    // so that's equivalent
    if (remainders[0] % 2 == 0) {
        return remainders[1] > 0 && remainders[2] > 0;
    }

    // because of the extra 3, Alice needs to pick mod with more frequency instead
    // after popping the more frequency mod twice,
    // Alice should keep the invariant that the less frequency mod is still the less frequency mod
    return Math.abs(remainders[1] - remainders[2]) > 2;
}

Dynamic Programming

Bottom-up

Stone Game

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean stoneGame(int[] piles) {
    int n = piles.length;
    // the largest number of stones one can get more than opponent
    int[][] dp  = new int[n][n];
    for (int i = 0; i < n; i++) {
        dp[i][i] = piles[i];
    }

    for (int d = 1; d < n; d++) {
        for (int i = 0; i + d < n; i++) {
            dp[i][i + d] = Math.max(piles[i] - dp[i + 1][i + d], piles[i + d] - dp[i][i + d - 1]);
        }
    }
    return dp[0][n - 1] > 0;
}

1D:

1
2
3
4
5
6
7
8
9
10
public boolean stoneGame(int[] piles) {
    int n = piles.length;
    int[] dp = Arrays.copyOf(piles, n);
    for (int d = 1; d < n; d++) {
        for (int i = 0; i + d < n; i++) {
            dp[i] = Math.max(piles[i] - dp[i + 1], piles[i + d] - dp[i]);
        }
    }
    return dp[0] > 0;
}

Alex can always take either all odd piles or all even piles, and one of the colors must have a sum number of stones larger than the other color.

Stone Game IV

1
2
3
4
5
6
7
8
9
10
11
12
public boolean winnerSquareGame(int n) {
    boolean[] dp = new boolean[n + 1];
    for (int i = 1; i <= n; i++) {
        for (int k = 1; k * k <= i; k++) {
            if (!dp[i - k * k]) {
                dp[i] = true;
                break;
            }
        }
    }
    return dp[n];
}

Top-down

Can I Win

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private int maxChoosableInteger;
private Map<Integer, Boolean> map = new HashMap<>();

public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
    this.maxChoosableInteger = maxChoosableInteger;

    if (desiredTotal == 0) {
        return true;
    }

    if (maxChoosableInteger * (maxChoosableInteger + 1) / 2 < desiredTotal) {
        return false;
    }

    return dfs(desiredTotal, 0);
}

private boolean dfs(int total, int state) {
    if (map.containsKey(state)) {
        return map.get(state);
    }

    boolean canWin = false;
    for (int i = maxChoosableInteger; i > 0; i--) {
        if ((state & (1 << (i - 1))) == 0) {
            // "can't force a win" == "lose"
            // because both players play optimally
            if (i >= total || !dfs(total - i, state | (1 << (i - 1)))) {
                canWin = true;
                break;
            }
        }
    }
    map.put(state, canWin);
    return canWin;
}

Flip Game II

Stone Game II

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private int[] p;
private int[][] memo;

public int stoneGameII(int[] piles) {
    p = new int[piles.length + 1];
    memo = new int[piles.length][piles.length + 1];

    for (int i = 0; i < piles.length; i++) {
        p[i + 1] = p[i] + piles[i];
    }

    return helper(piles, 0, 1);
}

private int helper(int[] piles, int start, int m) {
    if (start == piles.length) {
        return 0;
    }

    if (memo[start][m] > 0) {
        return memo[start][m];
    }

    int max = 0;
    for (int x = 1; x <= Math.min(2 * m, piles.length - start); x++) {
        max = Math.max(max, p[p.length - 1] - p[start] - helper(piles, start + x, Math.max(m, x)));
    }

    return memo[start][m] = max;
}

Cat and Mouse II

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public static final int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

private int n, m, available, catJump, mouseJump;
private String[] grid;
// {cell position, cat r, cat c, mouse r, mouse c}
private Boolean[][][][][] memo;

public boolean canMouseWin(String[] grid, int catJump, int mouseJump) {
    this.grid = grid;
    this.n = grid.length;
    this.m = grid[0].length();
    this.catJump = catJump;
    this.mouseJump = mouseJump;

    // positions of Cat and Mouse
    int[] cat = {-1, -1}, mouse = {-1, -1};
    // count of cells that Cat or Mouse can move to, i.e. non-wall cells
    available = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            if (grid[i].charAt(j) != '#') {
                available++;
            }

            if (grid[i].charAt(j) == 'C') {
                cat = new int[]{i, j};
            } else if (grid[i].charAt(j) == 'M') {
                mouse = new int[]{i, j};
            }
        }
    }
    memo = new Boolean[available * 2 + 1][n][m][n][m];
    return dfs(0, cat, mouse);
}

private boolean dfs(int turn, int[] cat, int[] mouse) {
    if (memo[turn][cat[0]][cat[1]][mouse[0]][mouse[1]] != null) {
        return memo[turn][cat[0]][cat[1]][mouse[0]][mouse[1]];
    }

    // 2 * available is the max turns
    // it's a rough estimate
    if (turn == available * 2) {
        return false;
    }

    // Mouse's turn
    if (turn % 2 == 0) {
        for (int[] d : DIRECTIONS) {
            for (int i = 0; i <= mouseJump; i++) {
                int r = mouse[0] + i * d[0], c = mouse[1] + i * d[1];

                if (r < 0 || r == n || c < 0 || c == m || grid[r].charAt(c) == '#') {
                    break;
                }

                if (grid[r].charAt(c) == 'F' || dfs(turn + 1, cat, new int[]{r, c})) {
                    return memo[turn][cat[0]][cat[1]][mouse[0]][mouse[1]] = true;
                }
            }
        }

        return memo[turn][cat[0]][cat[1]][mouse[0]][mouse[1]] = false;
    }

    // Cat's turn
    for (int[] d : DIRECTIONS) {
        for (int i = 0; i <= catJump; i++) {
            int r = cat[0] + i * d[0], c = cat[1] + i * d[1];

            if (r < 0 || r == n || c < 0 || c == m || grid[r].charAt(c) == '#') {
                break;
            }

            if ((r == mouse[0] && c == mouse[1]) || grid[r].charAt(c) == 'F' || !dfs(turn + 1, new int[]{r, c}, mouse)) {
                return memo[turn][cat[0]][cat[1]][mouse[0]][mouse[1]] = false;
            }
        }
    }

    return memo[turn][cat[0]][cat[1]][mouse[0]][mouse[1]] = true;
}

Bottom-up Percolation

Cat and Mouse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
private enum Color {
    DRAW,
    MOUSE,
    CAT
}

// minimax
// P-complete
public int catMouseGame(int[][] graph) {
    int n = graph.length;

    // state: (cat location, mouse location, move)
    // mouse move = 0, cat move = 1
    // these states form a directed graph
    Color[][][] color = new Color[n][n][2];
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            Arrays.fill(color[i][j], Color.DRAW);
        }
    }

    int[][][] outdegree = new int[n][n][2];
    for (int i = 0; i < n; i++) {   // cat
        for (int j = 0; j < n; j++) {   // mouse
            outdegree[i][j][0] = graph[j].length;
            outdegree[i][j][1] = graph[i].length;

            for (int next : graph[i]) {
                // excludes the hole from outdegree[i][j][1] (cat move)
                if (next == 0) {
                    outdegree[i][j][1]--;
                    break;
                }
            }
        }
    }

    // bottom-up percolation
    // enqueues ending nodes with definite results
    // {cat location, mouse location, move, color}
    Queue<int[]> q = new LinkedList<>();
    for (int i = 1; i < n; i++) {
        for (int k = 0; k < 2; k++) {
            // Mouse reaches the hole
            color[i][0][k] = Color.MOUSE;
            q.offer(new int[]{i, 0, k, Color.MOUSE.ordinal()});

            // Cat occupies the same node as Mouse
            color[i][i][k] = Color.CAT;
            q.offer(new int[]{i, i, k, Color.CAT.ordinal()});
        }
    }

    while (!q.isEmpty()) {
        int[] node = q.poll();
        int cat = node[0], mouse = node[1], move = node[2], c = node[3];

        // starting node
        if (cat == 2 && mouse == 1 && move == 0) {
            return c;
        }

        int prevMove = 1 - move;
        for (int prev : graph[prevMove == 0 ? mouse : cat]) {
            // if previous move is mouse, then cat remains
            // otherwise cat is prev
            int prevCat = prevMove == 0 ? cat : prev;
            // if previous move is mouse, then mouse is prev
            // otherwise mouse remains
            int prevMouse = prevMove == 0 ? prev : mouse;

            // Cat can't be in the hole
            if (prevCat == 0) {
                continue;
            }

            // if previous state is not DRAW, then it's already visited
            if (color[prevCat][prevMouse][prevMove] != Color.DRAW) {
                continue;
            }

            // for each DRAW node, there are two types of coloring
            // (suppose the move is mouse move)
            // - immediate coloring: if there's a child that's colored MOUSE,
            //   then this node will also be colored MOUSE
            // - eventual coloring: if all children are colored CAT,
            //   then this node will also be colored CAT
            if ((prevMove == 0 && c == Color.MOUSE.ordinal()) ||    // immediate coloring
                (prevMove == 1 && c == Color.CAT.ordinal()) ||      // immediate coloring
                --outdegree[prevCat][prevMouse][prevMove] == 0) {   // eventual coloring
                color[prevCat][prevMouse][prevMove] = Color.values()[c];
                q.offer(new int[]{prevCat, prevMouse, prevMove, c});
            }
        }
    }

    return color[2][1][0].ordinal();
}

Sprague–Grundy Theorem

Combinatorial game theory

Combinatorial game theory (CGT) has been largely confined to two-player games that have a position in which the players take turns changing in defined ways or moves to achieve a defined winning condition.

Impartial game

In combinatorial game theory, an impartial game is a game in which the allowable moves depend only on the position and not on which of the two players is currently moving, and where the payoffs are symmetric. In other words, the only difference between player 1 and player 2 is that player 1 goes first.

Normal play convention

In combinatorial game theory, the normal play convention of an impartial game is that the last player able to move is the winner.

Nim

Nim is a mathematical game of strategy in which two players take turns removing (or “nimming”) objects from distinct heaps or piles. On each turn, a player must remove at least one object, and may remove any number of objects provided they all come from the same heap or pile. The game is to take the last object.

Nim-sum is the bitwise xor of the heap sizes.

Theorem: In a normal Nim game, the player making the first move has a winning strategy if and only if the nim-sum of the sizes of the heaps is not zero. Otherwise, the second player has a winning strategy.

Strategy: the winning strategy is to finish every move with a nim-sum of 0. This is always possible if the nim-sum is not zero before the move. To find out which move to make, let X be the nim-sum of all the heap sizes. Find a heap where X \xor heap-size < heap-size; the winning strategy is to play in such a heap, reducing that heap to X \xor heap-size.

Nimber

In mathematics, the nimbers, also called Grundy numbers, are the values of heaps in the game Nim.

The Grundy Number/ nimber is equal to 0 for a game that is lost immediately by the first player and is equal to Mex of the nimbers of all possible next positions for any other game.

Mex

In mathematics, the mex of a subset of a well-ordered set is the smallest value from the whole set that does not belong to the subset. That is, it is the minimum value of the complement set. The name “mex” is shorthand for “minimum excluded” value.

Sprague–Grundy theorem

In combinatorial game theory, the Sprague–Grundy theorem states that every impartial game under the normal play convention is equivalent to a one-heap game of nim, or to an infinite generalization of nim.

Then Sprague-Grundy Theorem says that if both A and B play optimally, then the player starting first is guaranteed to win if the XOR of the grundy numbers of position in each sub-games at the beginning of the game is non-zero. Otherwise, if the XOR evaluates to zero, then player A will lose definitely, no matter what.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// O(n ^ 2)
public boolean canWin(String s) {
    int sg = 0;
    List<Integer> nimbers = new ArrayList<>();
    // DP to get nimbers
    for (String t : s.split("-+")) {
        int n = t.length();
        // t is consecutive '+'s
        if (n != 0) {
            while (nimbers.size() <= n) {
                char[] chars = t.toCharArray();
                int i = 0, j = nimbers.size() - 2;
                // g(t) = mex(g(0, n - 2), g(1, n - 3), ...)
                //      = mex(g(0) ^ g(n - 2), g(1) ^ g(n - 3), ...)
                //
                // chars marks the presence of nimbers of all sub-game states
                // chars[i] == '-': sub-game nimber i exists
                while (i <= j) {
                    chars[nimbers.get(i++) ^ nimbers.get(j--)] = '-';
                }  
                // mex
                nimbers.add(new String(chars).indexOf('+'));
            }
            sg ^= nimbers.get(n);
        }
    }
    return sg != 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// O(n)
public boolean canWin(String s) {
    // https://oeis.org/A002187
    // Has period 34 with the only exceptions at n=0, 14, 16, 17, 31, 34 and 51.
    // our definition is shifted by 1, so the last exception is nimbers[52]
    int p = 34, n = 51 + 1 + p;
    int[] nimbers = new int[n + 1];
    for (int k = 2; k <= n; k++) {
        char[] chars = "+".repeat(k).toCharArray();
        int i = 0, j = k - 2;
        while (i <= j) {
            chars[nimbers[i++] ^ nimbers[j--]] = '-';
        }
        nimbers[k] = new String(chars).indexOf('+');
    }

    int sg = 0;
    for (String t : s.split("-+")) {
        // t is consecutive '+'s
        int len = t.length();
        while (len > n) {
            len -= p;
        }
        sg ^= nimbers[len];
    }
    return sg != 0;
}

Subtree Removal Game with Fibonacci Tree

Colon principle: when branches come together at a vertex, one may replace the branches by a non-branching stalk of length equal to their nim sum.

1
2
3
4
5
6
7
8
public boolean findGameWinner(int n) {
    // The Grundy value of a node is the nim sum of the Grundy values of its children.
    // SG(1) = 0
    // SG(2) = 1
    // SG(n) = (SG(n - 1) + 1) xor (SG(n - 2) + 1)
    // SG: [null, 0, 1, 3, 6, 3, 3, 0, 5, 7, 14, 7, 7, 0, 9, 11, 6, 11, 11, 0, 13, ...]
    return n % 6 != 1;
}
This post is licensed under CC BY 4.0 by the author.