无限棋局

标签: 数组 数学 枚举 博弈

难度: Hard

小力正在通过残局练习来备战「力扣挑战赛」中的「五子棋」项目,他想请你能帮他预测当前残局的输赢情况。棋盘中的棋子分布信息记录于二维数组 `pieces` 中,其中 `pieces[i] = [x,y,color]` 表示第 `i` 枚棋子的横坐标为 `x`,纵坐标为 `y`,棋子颜色为 `color`(`0` 表示黑棋,`1` 表示白棋)。假如黑棋先行,并且黑棋和白棋都按最优策略落子,请你求出当前棋局在三步(按 **黑、白、黑** 的落子顺序)之内的输赢情况(三步之内先构成同行、列或对角线连续同颜色的至少 5 颗即为获胜): - 黑棋胜, 请返回 `"Black"` - 白棋胜, 请返回 `"White"` - 仍无胜者, 请返回 `"None"` **注意:** - 和传统的五子棋项目不同,「力扣挑战赛」中的「五子棋」项目 **不存在边界限制**,即可在 **任意位置** 落子; - 黑棋和白棋均按 3 步内的输赢情况进行最优策略的选择 - 测试数据保证所给棋局目前无胜者; - 测试数据保证不会存在坐标一样的棋子。 **示例 1:** > 输入: > `pieces = [[0,0,1],[1,1,1],[2,2,0]]` > > 输出:`"None"` > > 解释:无论黑、白棋以何种方式落子,三步以内都不会产生胜者。 **示例 2:** > 输入: > `pieces = [[1,2,1],[1,4,1],[1,5,1],[2,1,0],[2,3,0],[2,4,0],[3,2,1],[3,4,0],[4,2,1],[5,2,1]]` > > 输出:`"Black"` > > 解释:三步之内黑棋必胜,以下是一种可能的落子情况: >![902b87df29998b1c181146c8fdb3a4b6.gif](https://pic.leetcode-cn.com/1629800639-KabOfY-902b87df29998b1c181146c8fdb3a4b6.gif){:width="300px"} **提示:** - `0 <= pieces.length <= 1000` - `pieces[i].length = 3` - `-10^9 <= pieces[i][0], pieces[i][1] <=10^9` - `0 <= pieces[i][2] <=1`

Submission

运行时间: 200 ms

内存: 19.6 MB

from sortedcontainers import SortedList
class Solution:
    def gobang(self, pieces: List[List[int]]) -> str:
        B = [defaultdict(SortedList) for _ in range(4)] # 黑棋的四类直线:水平, 垂直, 斜率为1, 斜率为-1
        W = [defaultdict(SortedList) for _ in range(4)] # 白棋的四类直线:水平, 垂直, 斜率为1, 斜率为-1

        def add_piece(x, y, c):
            P = W if c else B
            P[0][y].add(x)
            P[1][x].add(y)
            P[2][y - x].add(x)
            P[3][y + x].add(x)

        def get_pos(k, v, d):
            if d == 0: x, y = v, k
            elif d == 1: x, y = k, v
            elif d == 2: x, y = v, k + v
            elif d == 3: x, y = v, k - v
            return (x, y)

        # 找到P的棋子中,冲np(3或4)的点(填上这个点就必胜,并且没有Q的棋子阻挡)
        def find_win_ps(P, Q, np) -> defaultdict(set):
            win_points = defaultdict(set)
            for d in range(4):
                for k in P[d]:
                    ps, n = P[d][k], len(P[d][k])
                    if n < np: continue
                    for i in range(n + 1 - np):
                        dif = ps[i + np - 1] - ps[i]
                        if dif < 5: # <5 说明能成5
                            # 找出空缺的v。找规律发现在[ps[i]-(4-dif), ps[i] + 5],不是已有的v的点
                            vs = [v for v in range(ps[i] + dif - 4, ps[i] + 5) if
                                  v not in ps[i:i + np] and not has_piece(Q, (k, v, d))]
                            nvs = len(vs)
                            if nvs < 5 - np: continue  # 3->2, 4->1
                            dt = 4 - np
                            for j in range(nvs - dt):
                                v1, v2 = vs[j], vs[j + dt]
                                if v2 - v1 > 4: continue
                                xy1, xy2 = get_pos(k, v1, d), get_pos(k, v2, d)
                                win_points[xy1].add(xy2)
                                win_points[xy2].add(xy1)
                                if np == 4 and len(win_points) > 1: return win_points
            return win_points

        def has_piece(X, kvd) -> bool:
            k, v, d = kvd
            return k in X[d] and v in X[d][k]

        # 开始,棋子先存好
        for x, y, c in pieces:
            add_piece(x, y, c)

        # 1. 黑先手有4连黑子,并且有空位放下能成5连黑子,就黑胜
        if len(find_win_ps(B, W, 4)) > 0:
            return 'Black'

        # 白如果有多个冲4就赢
        live_w4 = find_win_ps(W, B, 4)
        if len(live_w4) > 1:
            return 'White'

        # 白只有1个冲4,黑先堵上;如果黑有多个冲4就赢,否则None
        if len(live_w4) == 1:
            x, y = list(live_w4.keys())[0]
            add_piece(x, y, 0)
            return 'Black' if len(find_win_ps(B, W, 4)) > 1 else 'None'

        # 检查最开始如果有形成双4的点,就黑赢
        return 'Black' if any(len(ds) > 1 for ds in find_win_ps(B, W, 3).values()) else 'None'

Explain

本题解采用了搜索和评估的策略来预测五子棋的输赢情况。核心思路是通过哈希表记录各类直线(水平、垂直、两个对角线)上的棋子位置。通过这种数据结构可以快速地评估和更新棋局。具体方法包括:1. 初始化黑白棋的数据结构来记录棋子位置;2. 为每种颜色的棋子定义添加函数,便于更新棋局;3. 评估每种直线上的棋子配置,查找可以直接赢得比赛的落子点;4. 根据当前棋局使用最优策略预测接下来的三步棋,包括黑白双方的最优防守和进攻。根据棋局动态评估,判断棋局的输赢情况。

时间复杂度: O(m * n * log n)

空间复杂度: O(n)

from sortedcontainers import SortedList
from collections import defaultdict

class Solution:
    def gobang(self, pieces: List[List[int]]) -> str:
        B = [defaultdict(SortedList) for _ in range(4)]  # Black pieces lines: horizontal, vertical, diagonal with slope 1 and -1
        W = [defaultdict(SortedList) for _ in range(4)]  # White pieces lines

        def add_piece(x, y, c):
            P = W if c else B  # Select color array
            P[0][y].add(x)  # Add to horizontal line
            P[1][x].add(y)  # Add to vertical line
            P[2][y - x].add(x)  # Add to diagonal line slope 1
            P[3][y + x].add(x)  # Add to diagonal line slope -1

        def get_pos(k, v, d):  # Helper to get the actual position based on line and index
            if d == 0: x, y = v, k
            elif d == 1: x, y = k, v
            elif d == 2: x, y = v, k + v
            elif d == 3: x, y = v, k - v
            return (x, y)

        # Search for winning moves, where adding a piece leads to a victory
        def find_win_ps(P, Q, np) -> defaultdict(set):
            win_points = defaultdict(set)
            for d in range(4):
                for k in P[d]:
                    ps, n = P[d][k], len(P[d][k])
                    if n < np: continue  # Not enough pieces to form a line
                    for i in range(n + 1 - np):
                        dif = ps[i + np - 1] - ps[i]
                        if dif < 5:  # Potential win by filling gaps
                            vs = [v for v in range(ps[i] + dif - 4, ps[i] + 5) if v not in ps[i:i + np] and not has_piece(Q, (k, v, d))]
                            nvs = len(vs)
                            if nvs < 5 - np: continue  # Not enough spots to win
                            dt = 4 - np
                            for j in range(nvs - dt):
                                v1, v2 = vs[j], vs[j + dt]
                                if v2 - v1 > 4: continue  # Too far apart to form a line
                                xy1, xy2 = get_pos(k, v1, d), get_pos(k, v2, d)
                                win_points[xy1].add(xy2)
                                win_points[xy2].add(xy1)
                                if np == 4 and len(win_points) > 1: return win_points  # Immediate win for 4 in a row
            return win_points

        def has_piece(X, kvd) -> bool:  # Check if a position is occupied
            k, v, d = kvd
            return k in X[d] and v in X[d][k]

        for x, y, c in pieces:
            add_piece(x, y, c)  # Add initial pieces to the board

        if len(find_win_ps(B, W, 4)) > 0:
            return 'Black'  # Black wins with a potential 5 in a row

        live_w4 = find_win_ps(W, B, 4)
        if len(live_w4) > 1:
            return 'White'  # White wins if multiple 4 in a row options exist

        if len(live_w4) == 1:
            x, y = list(live_w4.keys())[0]
            add_piece(x, y, 0)  # Block white's potential 4 in a row
            return 'Black' if len(find_win_ps(B, W, 4)) > 1 else 'None'  # Check if blocking leads to a new win or a draw

        return 'Black' if any(len(ds) > 1 for ds in find_win_ps(B, W, 3).values()) else 'None'  # Check for possible double 4's for black

Explore

在评估棋局时,通过检查棋局的每一行、每一列以及两个对角线方向上的棋子配置来确定是否存在可以立即获胜的落子点。对于对角线(正斜率和负斜率),使用映射来将斜线上的点映射到一维数组,从而使问题转化为一维线性问题。具体来说,对于正斜率对角线,使用`y - x`作为键值来归类所有对应的棋子位置;对于负斜率对角线,使用`y + x`作为键。这样,可以通过查找每个键值下的棋子数组,使用同样的逻辑来评估是否存在连续的五个棋子或是存在空位可以通过落子立即形成五子连线,从而判断胜负。

在寻找赢棋点时,差值`dif`小于5意味着在这个区间内的棋子数目足够密集,可能存在空缺但可以通过填补这些空缺来形成连续的五个棋子,从而获胜。`dif`是指在当前检查的线段(无论是水平、垂直还是对角线)上连续棋子的最远端和最近端的位置差。如果这个差值小于5,意味着这两个棋子之间(包括这两个点自身)的距离最多只有四个位置,因此只要适当地填补中间未被占据的位置(如果有的话),就有可能形成五子连线。

在函数`find_win_ps`中,变量`np`代表当前考虑的棋子连线的长度。这是一个关键参数,用于指定需要检查的连续棋子的数量。例如,在搜索四子连线的情况下,`np`会被设定为4。根据`np`的值,函数将搜索所有可能的线段,看是否存在连续的`np`个棋子,且这些棋子之间的空隙足够通过落子在接下来的动作中形成五子连线。这样的逻辑使得算法可以灵活地根据不同的棋局状态评估接近获胜条件的棋子布局,进而预测和制定相应的进攻或防守策略。