游戏

  • [529. 扫雷游戏](https://leetcode.cn/problems/minesweeper/)
  • 486. 预测赢家
  • 方法一:记忆化递归
  • 方法二:动态规划
  • ★488. 祖玛游戏
  • 方法一:广度优先搜索
  • 笛卡尔积(Cartesian Product)
  • 495. 提莫攻击
  • 877. 石子游戏
  • 方法一:记忆化递归
  • 方法二:动态规划
  • 25、1140. 石子游戏 II
  • 方法一:递归
  • 方法二:动态规划
  • 2029. 石子游戏 IX
  • [1263. 推箱子](https://leetcode.cn/problems/minimum-moves-to-move-a-box-to-their-target-location/)


46.permutations

529. 扫雷游戏

class Solution:
    def updateBoard(self, board: List[List[str]], click: List[int]) -> List[List[str]]:
                          
        def calc(r, c):
            val = 0
            for x, y in dirs:
                i, j = x + r, y + c
                if 0 <= i < m and 0 <= j < n and board[i][j] == 'M': val += 1
            return val

        def dfs(r, c):
            cnt = calc(r, c)
            if cnt == 0:
                board[r][c] = 'B'
                for x, y in dirs:
                    i, j = x + r, y + c
                    if 0 <= i < m and 0 <= j < n and board[i][j] == 'E': 
                        dfs(i, j)
            else: board[r][c] = str(cnt)

        def bfs(r, c):
            q = deque([(r, c)])
            while q:                                
                r, c = q.popleft()
                cnt = calc(r, c)
                if cnt == 0:
                    board[r][c] = 'B'
                    for x, y in dirs:
                        i, j = x + r, y + c
                        if 0 <= i < m and 0 <= j < n and board[i][j] == 'E': 
                            q.append((i, j))
                            board[i][j] = '*' # 先标记一下,表示访问过,后面会更新,否则超时。
                else: board[r][c] = str(cnt)
                    
            
        dirs = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]
        m, n = len(board), len(board[0])        
        r, c = click
        if board[r][c] == 'M': board[r][c] = 'X'
        else: 
            bfs(r, c)
            #dfs(r, c)
            
        return board

486. 预测赢家

Leetcode

方法一:记忆化递归

class Solution:
    def PredictTheWinner(self, nums: List[int]) -> bool:
        
        def dfs(i, j):
            if i == j:  return nums[i]
            if memo[i][j] != -inf: return memo[i][j]
            start = nums[i] - dfs(i + 1, j)
            end = nums[j] - dfs(i, j - 1)
            res = max(start, end)
            memo[i][j] = res
            return res
        
        n = len(nums)
        memo = [[-inf] * n for _ in range(n)]
        
        return dfs(0, n - 1) >= 0

方法二:动态规划

class Solution:
    def PredictTheWinner(self, nums: List[int]) -> bool:
        n = len(nums)
        dp = [[-inf] * n for _ in range(n)]
        for i in range(n):
            dp[i][i] = nums[i]

        for i in range(n - 2, -1, -1):
            for j in range(i + 1, n):
                dp[i][j]  = max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]) 
        
        return dp[0][-1] >= 0

dp[i][j] 的值只和 dp[i+1][j] 与 dp[i][j−1] 有关,即在计算 dp 的第 i 行的值时,只需要使用到 dp 的第 i 行和第 i+1 行的值,因此可以使用一维数组代替二维数组,对空间进行优化。

class Solution:
    def PredictTheWinner(self, nums: List[int]) -> bool:
        n = len(nums)
        dp = nums[:]
        
        for i in range(n - 2, -1, -1):
            for j in range(i + 1, n):
                dp[j] = max(nums[i] - dp[j], nums[j] - dp[j - 1])

        return dp[n - 1] >= 0

★488. 祖玛游戏

Leetcode1047. 删除字符串中的所有相邻重复项

方法一:广度优先搜索

使用广度优先搜索得到可以消除桌面上所有球的方案时就直接返回结果,不需要继续遍历。

class Solution:
    def findMinStep(self, board: str, hand: str) -> int:
        # if board == "RRYGGYYRRYYGGYRR" and hand == "GGBBB" : return 5
        # "RRYGGYYR(B)RYYGGYRR" 同色插入异色 分割
        # if board == "RRGGBBYYWWRRGGBB" and hand == "RGBYW" : return -1
        # 异色不需要插入异色,

        def clean(s):
            n = 1
            while n:
                s, n = re.subn(r"(.)\1{2,}", "", s)
            return s

        d = defaultdict(int)
        for c in board:  d[c] += 1
        for c in hand:
            if c in d:  d[c] += 1
        if min(d.values()) < 3: return -1 
        
        hand = ''.join(sorted(hand)) 

        seen = {(board, hand)}
        queue = deque([(board, hand, 1)])

        while queue:
            b, h, step = queue.popleft()
            m, n = len(b), len(h)
            for i in range(m + 1):
                for j in range(n):  
                    # 剪枝一:手中的同色球只需要遍历一个 
                    if j > 0 and h[j] == h[j - 1]: continue
                    # 剪枝二:连续同色球在的开头或结尾插入同色球
                    if i > 0 and b[i-1] == h[j]: continue
                    # 剪枝三:前一个、当前和插入的球互不同色 (主要)
                    if i < m and h[j] != b[i] and b[i] != b[i-1]: continue

                    nb = clean(b[:i] + h[j] + b[i:])
                    nh = h[:j] + h[j + 1:]
                    
                    if not nb: return step
                    if not nh: break
                    
                    if (nb, nh) not in seen:
                        queue.append((nb, nh, step + 1))
                        seen.add((nb, nh))

        return -1

连续同色开头插入同色,同色中插入异色(分割)。

class Solution:
    def findMinStep(self, board: str, hand: str) -> int:
        # if board == "RRYGGYYRRYYGGYRR" and hand == "GGBBB" : return 5
        # "RRYGGYYR(B)RYYGGYRR" 同色插入异色 分割
        # "RRWWRRBBR(w)R" "WB"
        # if board == "RRGGBBYYWWRRGGBB" and hand == "RGBYW" : return -1
        # 异色不需要插入异色,

        def clean(s):
            n = 1
            while n:
                s, n = re.subn(r"(.)\1{2,}", "", s)
            return s

        seen = {(board, hand)}
        queue = deque([(board, hand, 1)])

        while queue:
            b, h, step = queue.popleft()           
  
            for i, j in product(range(len(b)), range(len(h))):
                # 连续同色开头插入同色,同色中插入异色(分割)。
                if h[j] == b[i] and (i == 0 or i > 0 and b[i-1] != b[i]) \
                or i > 0 and b[i-1] == b[i] and h[j] != b[i]:

                    nb = clean(b[:i] + h[j] + b[i:])
                    nh = h[:j] + h[j + 1:]
                    
                    if not nb: return step
                    
                    if (nb, nh) not in seen:
                        queue.append((nb, nh, step + 1))
                        seen.add((nb, nh))

        return -1

笛卡尔积(Cartesian Product)

product 用于求多个可迭代对象的笛卡尔积(Cartesian Product),等价嵌套的 for 循环。即:
product(A, B) 和 ((x,y) for x in A for y in B)一样。

itertools.product(*iterables, repeat=1)
iterables 是可迭代对象,repeat 指定 iterable 重复几次,即:

product(A, repeat=3)等价于 product(A, A, A)

大概的实现逻辑如下(真正的内部实现不保存中间值):

def product(*args, repeat=1):
    # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy
    # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111
    pools = [tuple(pool) for pool in args] * repeat
    result = [[]]
    for pool in pools:
        result = [x+[y] for x in result for y in pool]
    for prod in result:
        yield tuple(prod)

495. 提莫攻击

Leetcode

class Solution:
    def findPoisonedDuration(self, timeSeries: List[int], duration: int) -> int:
        timeSeries.append(10 ** 8) # 添加哨兵
        res, n = 0, len(timeSeries)

        for i in range(n-1):
            res += min(timeSeries[i+1]-timeSeries[i], duration)
        
        # res += duration # 补最后一个

        return res

877. 石子游戏

Leetcode **区间博弈论动态规划问题。 「相对分数」**先手后手差。

方法一:记忆化递归

class Solution:
    def stoneGame(self, piles: List[int]) -> bool:
        # 计算子区间 [left, right] 里先手能够得到的分数
        def dfs(left, right):
            if left == right: return piles[left]        
            if memo[left][right] != -inf: return memo[left][right]

            chooseLeft = piles[left] - dfs(left + 1, right)
            chooseRight = piles[right] - dfs(left, right - 1)
            res = max(chooseLeft, chooseRight)
            memo[left][right] = res
            return res

        n = len(piles)
        # 由于是相对分数,因此初始化的时候不能为 0
        memo = [[-inf] * n for _ in range(n)]
       
        return dfs(0, n - 1) > 0

方法二:动态规划

状态定义: dp[i][j] 表示区间 [i, j] 内先手可以获得的相对分数;
状态转移方程:dp[i][j] = max(piles[i] - dp[i + 1, j] , piles[j] - dp[i, j - 1]) 。
遍历方向:在计算状态的时候,一定要保证左边一格和下边一格的值先计算出来。

n, k = 6, 1
for j in range(1, n):
    for i in range(j - 1, -1, -1):
        print(i, j, k)
        k += 1

游戏(Games)_leetcode

n, k = 6, 1

for i in range(n - 2, -1, -1):
    for j in range(i + 1, n):
        print(i, j, k)
        k += 1

游戏(Games)_List_02

class Solution:
    def stoneGame(self, piles: List[int]) -> bool:
        n = len(piles)        
        dp = [[-inf] * n for _ in range(n)]  

        # 遍历方向一:
        # for j in range(1, n):
        #     for i in range(j - 1, -1, -1):

        # 遍历方向一:
        # for i in range(n - 2, -1, -1):
        #     for j in range(i + 1, n):

        # 遍历方向三:
        '''
        dis 代表间隔距离,dis = 1,会不断得到相邻 2 个石头堆的最优选择策略,[1,2,3,4],会得到 [1,2]、[2、3]、[3、4]。
        '''
        for dis in range(1, n):
            for i in range(n - dis):            
                j = i + dis

                dp[i][j] = max(piles[i] - dp[i + 1][j], piles[j] - dp[i][j - 1])

        return dp[0][-1] > 0

25、1140. 石子游戏 II

Leetcode

方法一:递归

class Solution:
    def stoneGameII(self, piles: List[int]) -> int:
    
        def dfs(i, m):
            if (i, m) in memo: return memo[(i, m)]           
            if i >= n: return 0            
            if i + m * 2 >= n: return s[i]

            res = max(s[i] - dfs(i + x, max(x, m)) for x in range(1, 2*m + 1))
            memo[(i, m)] = res
            return res
        
        n, memo = len(piles), dict()       
        
        s = [0] * (n + 1) # s[i] 后缀和
        for i in range(n - 1, -1, -1):
            s[i] = s[i + 1] + piles[i]
            
        return dfs(0, 1)

方法二:动态规划

class Solution:
    def stoneGameII(self, piles: List[int]) -> int:

        n= len(piles)
        if n == 1:return piles[0]
        
        s = [0] * (n + 1) # s[i] 后缀和
        for i in range(n - 1, -1, -1):
            s[i] = s[i + 1] + piles[i]

        dp = [[0]*n for _ in range(n + 1)]
        for i in range(n - 1, -1, -1):            
            for m in range(1, n):
                if i + 2 * m >= n:
                    dp[i][m] = s[i]
                else:
                    for x in range(1, 2 * m + 1):
                        dp[i][m] = max(dp[i][m], s[i] - dp[i + x][max(m, x)])
        
        return dp[0][1]

2029. 石子游戏 IX

Leetcode 石子按价值除以 3 的余数分成三类 0, 1, 2。
0 可以争得先手
1 移除石子的类型序列为:1121212121⋯
后手只能选 1,先手从第二次开始后面只能选 2。
2 移除石子的类型序列为:2212121212⋯ 后手只能选 2,先手从第二次开始后面只能选 1。
先手要想获胜,只有留给对手不能选择的类型 ≥ 2。
类型 0 有偶数个: 1 和 2 都至少有 1 个,先手选最少的开始
否则: 1 和 2 至少差 2 个

class Solution:
    def stoneGameIX(self, stones: List[int]) -> bool:
        cnt = [0, 0, 0]               
        for val in stones:
            cnt[val % 3] += 1
        
        if cnt[0] % 2 == 0: return cnt[1] > 0 and cnt[2] > 0 
        return abs(cnt[1] - cnt[2]) > 2

1263. 推箱子

方法一:bfs + 双端队列
本质上是最短路径问题,求箱子最少移动多少次可以到达目标位置。
同一个点可以被玩家多次访问,因为某个点能否被访问同时取决于玩家和箱子的状态。 设置四维数组 vis[playerx][playery][boxx][boxy] 来防止重复访问, 若本次访问的位置 <px, py, bx, by> 之前被访问过,则本次到达此位置的路径一定比之前到达此位置路径长,因此不再从此点继续扩展。
BFS 是逐层遍历,每遍历一层距离增加 1, 最终访问到目的节点,获得最小距离。
玩家移动 --> 距离不会增加,而箱子移动 --> 距离增加,因此为了保证遍历过程中路径长度从小到大:
箱子移动后,距离 + 1,放入队列末尾;
玩家移动,而箱子未移动,距离不变,放入队列首部;
每次从队列首部取元素。
队列中的所有点的距离最多只会出现两种可能, d 和 d + 1, 因为每次都是先访问距离较小的点,距离为 d 的点未访问结束,不可能访问距离为 d + 1 的点,队列中一定不会出现距离为 d + 2 的点。

class Solution:
    def minPushBox(self, grid: List[List[str]]) -> int:
        
        def check(x, y): # 检测位置是否合法
            return m > x >= 0 and n > y >= 0 and grid[x][y] != '#'

        dirs = (1, 0, -1, 0, 1)
        m, n = len(grid), len(grid[0])
        vis = [[[[False] * 20 for _ in range(20)] for _ in range(20)] for _ in range(20)]

        boxX = boxY = targetX = targetY = startX = startY = 0
        for i in range(m):
            for j in range(n):
                if grid[i][j] == 'S': startX, startY = i, j
                elif grid[i][j] == 'B': boxX, boxY = i, j
                elif grid[i][j] == 'T': targetX, targetY = i, j

        q = deque([(startX, startY, boxX, boxY, 0)])  # 玩家坐标、箱子坐标, 距离        
        vis[startX][startY][boxX][boxY] = True

        while q:
            px, py, bx, by, d = q.popleft()
            if bx == targetX and by == targetY: return d # 箱子到达目的地
            for k in range(4):
                npx, npy = px + dirs[k], py + dirs[k + 1]
                if not check(npx, npy): continue
                if npx == bx and npy == by: # 人能到达箱子的位置,箱子同方向移动
                    nbx, nby = bx + dirs[k], by + dirs[k + 1]
                    if not check(nbx, nby) or vis[npx][npy][nbx][nby]: continue
                    q.append((npx, npy, nbx, nby, d + 1));  # 箱子移动,放入队尾部
                elif not vis[npx][npy][bx][by]: # 只能人不动箱子,直到人能到达箱子的位置
                    q.appendleft((npx, npy, bx, by, d)) # 箱子未动,放入队首
                vis[npx][npy][bx][by] = True

        return -1
class Solution {
    int n, m;  
    public int minPushBox(char[][] grid) {        
        m = grid.length; n = grid[0].length;
        Deque<int[]> q = new ArrayDeque();
        int[] dir = new int[]{1,0,-1,0,1};
        boolean[][][][] vis = new boolean[20][20][20][20];
        int tx = 0, ty = 0, sx = 0, sy = 0, bx = 0, by = 0;
        for(int i = 0; i < m; i++){
            for(int j = 0; j < n; j++){
                if(grid[i][j] == 'S'){
                    sx = i; sy = j;
                }else if(grid[i][j] == 'T'){
                    tx = i; ty = j;
                }else if(grid[i][j] == 'B'){
                    bx = i; by = j;
                }
            }
        }
        q.add(new int[]{sx, sy, bx, by, 0});
        while(!q.isEmpty()){
            int[] c = q.poll();
            if(c[2] == tx && c[3] == ty) return c[4];
            vis[c[0]][c[1]][c[2]][c[3]] = true;
            for(int k = 0; k < 4; k++){
                int px = c[0] + dir[k], py = c[1] + dir[k + 1];
                if(!check(px, py, grid)) continue;
                if(px == c[2] && py == c[3]){
                    bx = c[2] + dir[k]; by = c[3] + dir[k + 1];
                    if(!check(bx, by, grid) || vis[px][py][bx][by]) continue;
                    q.add(new int[]{px, py, bx, by, c[4] + 1});
                } else if(!vis[px][py][c[2]][c[3]]){
                    q.addFirst(new int[]{px, py, c[2], c[3], c[4]});
                }
            }
        }
        return -1;
    }
    boolean check(int x, int y, char[][] grid){
        return x >= 0 && x < m && y >= 0 && y < n && grid[x][y] != '#';
    }
}

30、 1406. 石子游戏 III
31、 1510. 石子游戏 IV
33、 1563. 石子游戏 V
34、 1686. 石子游戏 VI
35、 1690. 石子游戏 VII
40、 1872. 石子游戏 VIII

22、 1033. 移动石子直到连续
23、 1040. 移动石子直到连续 II
24、 1097. 游戏玩法分析 V
37、 1753. 移除石子的最大得分
38、 1823. 找出游戏的获胜者

41、 1908. Nim 游戏 II
42、 1927. 求和游戏
43、 1962. 移除石子使总数最小
44、 1996. 游戏中弱角色的数量
45、 2017. 网格游戏
46、 2018. 判断单词是否能放入填字游戏内
11、 511. 游戏玩法分析 I
12、 512. 游戏玩法分析 II
13、 520. 检测大写字母
14、 529. 扫雷游戏
15、 534. 游戏玩法分析 III
16、 550. 游戏玩法分析 IV
17、 679. 24 点游戏
18、 794. 有效的井字游戏
19、 810. 黑板异或游戏
20、 822. 翻转卡片游戏
32、 1535. 找出数组游戏的赢家
3、 174. 地下城游戏
4、 289. 生命游戏
5、 292. Nim 游戏
6、 293. 翻转游戏
7、 294. 翻转游戏 II
8、 299. 猜数字游戏
9、 390. 消除游戏
48、 LCP 21. 追逐游戏
49、 LCP 24. 数字游戏
50、 LCP 30. 魔塔游戏
51、 LCP 49. 环形闯关游戏
26、 1145. 二叉树着色游戏