阅读视图

发现新文章,点击刷新页面。

每日一题-网格图操作后的最大分数🔴

给你一个大小为 n x n 的二维矩阵 grid ,一开始所有格子都是白色的。一次操作中,你可以选择任意下标为 (i, j) 的格子,并将第 j 列中从最上面到第 i 行所有格子改成黑色。

如果格子 (i, j) 为白色,且左边或者右边的格子至少一个格子为黑色,那么我们将 grid[i][j] 加到最后网格图的总分中去。

请你返回执行任意次操作以后,最终网格图的 最大 总分数。

 

示例 1:

输入:grid = [[0,0,0,0,0],[0,0,3,0,0],[0,1,0,0,0],[5,0,0,3,0],[0,0,0,0,2]]

输出:11

解释:

第一次操作中,我们将第 1 列中,最上面的格子到第 3 行的格子染成黑色。第二次操作中,我们将第 4 列中,最上面的格子到最后一行的格子染成黑色。最后网格图总分为 grid[3][0] + grid[1][2] + grid[3][3] 等于 11 。

示例 2:

输入:grid = [[10,9,0,0,15],[7,1,0,8,0],[5,20,0,11,0],[0,0,0,1,2],[8,12,1,10,3]]

输出:94

解释:

我们对第 1 ,2 ,3 列分别从上往下染黑色到第 1 ,4, 0 行。最后网格图总分为 grid[0][0] + grid[1][0] + grid[2][1] + grid[4][1] + grid[1][3] + grid[2][3] + grid[3][3] + grid[4][3] + grid[0][4] 等于 94 。

 

提示:

  • 1 <= n == grid.length <= 100
  • n == grid[i].length
  • 0 <= grid[i][j] <= 109

【图解】DP 及其优化:从 n^4 到 n^3 到 n^2(Python/Java/C++/Go)

一、从超时做法说起

从 $\mathcal{O}(n^4)$ 的 DP 开始。

要想知道第 $j$ 列的哪些行的 $\textit{grid}[i][j]$ 被计入总分,我们需要知道:

  • 第 $j+1$ 列有多少个黑色格子(下文简称为黑格)。
  • 第 $j$ 列有多少个黑格。
  • 第 $j-1$ 列有多少个黑格。

定义 $\textit{dfs}(j,\textit{cur},\textit{pre})$ 表示考虑第 $0$ 列到第 $j$ 列,其中第 $j+1$ 列有 $\textit{pre}$ 个黑格、第 $j$ 列有 $\textit{cur}$ 个黑格,返回在第 $0$ 列到第 $j$ 列中能得到的最大总分。

枚举第 $j-1$ 列有 $\textit{nxt}$ 个黑格,问题变成:

  • 考虑第 $0$ 列到第 $j-1$ 列,其中第 $j$ 列有 $\textit{cur}$ 个黑格,第 $j-1$ 列有 $\textit{nxt}$ 个黑格,在第 $0$ 列到第 $j-1$ 列中能得到最大总分,即 $\textit{dfs}(j-1,\textit{nxt},\textit{cur})$。

定义 $s$ 为 $\textit{grid}[\textit{cur}][j]$ 到 $\textit{grid}[\max(\textit{nxt},\textit{pre})-1][j]$ 的元素和,如果 $\max(\textit{nxt},\textit{pre}) \le \textit{cur}$ 则 $s=0$。

用 $\textit{dfs}(j-1,\textit{nxt},\textit{cur}) + s$ 更新 $\textit{dfs}(j,\textit{cur},\textit{pre})$ 的最大值。

递归边界:$j=0$ 时返回 $s$。

递归入口:$\max\limits_{i=0}^{n} \textit{dfs}(n-1,i,0)$。枚举第 $n-1$ 列有 $i$ 个黑格,取递归结果的最大值,作为答案。

由于做法超时,这里仅展示 Python 代码。

###py

# 超时代码
class Solution:
    def maximumScore(self, grid: List[List[int]]) -> int:
        n = len(grid)
        # 每列的前缀和(从上到下)
        col_sum = [list(accumulate(col, initial=0)) for col in zip(*grid)]

        # cur 表示第 j 列的黑格个数
        # pre 表示第 j+1 列的黑格个数
        @cache
        def dfs(j: int, cur: int, pre: int) -> int:
            if j == 0:
                return col_sum[0][pre] - col_sum[0][cur] if pre > cur else 0
            res = 0
            # 枚举第 j-1 列有 nxt 个黑格
            for nxt in range(n + 1):
                s = col_sum[j][max(nxt, pre)] - col_sum[j][cur] if max(nxt, pre) > cur else 0
                res = max(res, dfs(j - 1, nxt, cur) + s)
            return res
        # 枚举第 n-1 列有 i 个黑格
        return max(dfs(n - 1, i, 0) for i in range(n + 1))

复杂度分析(超时)

  • 时间复杂度:$\mathcal{O}(n^4)$,其中 $n$ 是 $\textit{grid}$ 的长度。由于每个状态只会计算一次,动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。本题状态个数等于 $\mathcal{O}(n^3)$,单个状态的计算时间为 $\mathcal{O}(n)$,所以动态规划的时间复杂度为 $\mathcal{O}(n^4)$。
  • 空间复杂度:$\mathcal{O}(n^3)$。

二、记忆化搜索

如果不考虑第 $j-1$ 列呢?不去枚举 $\textit{nxt}$,而是枚举 $\textit{cur}$ 呢?

我们的原则是,在从右往左递归的过程中,只把第 $j$ 列或者第 $j+1$ 列的格子计入总分,不考虑第 $j-1$ 列的格子。

如何不重不漏地统计呢?

lc3225-cut.png

定义 $\textit{dfs}(j,\textit{pre},\textit{dec})$ 表示考虑第 $0$ 列到第 $j$ 列,其中:

  • 第 $j+1$ 列有 $\textit{pre}$ 个黑格;
  • 第 $j+1$ 列和第 $j+2$ 列的黑格个数的大小关系用布尔值 $\textit{dec}$ 表示,只有当第 $j+1$ 列的黑格个数小于第 $j+2$ 列的黑格个数时 $\textit{dec}$ 才为 $\texttt{true}$。

在上述约束下,返回第 $0$ 列到第 $j$ 列中能得到的最大总分。

枚举第 $j$ 列有 $\textit{cur}$ 个黑格,按照上图中的四种情况计算。

递归边界:$j=-1$ 时返回 $0$。

递归入口:$\max\limits_{i=0}^{n} \textit{dfs}(n-2,i,0)$。枚举第 $n-1$ 列有 $i$ 个黑格,取递归结果的最大值,作为答案。注意第 $n-1$ 列的格子会在 $j=n-2$ 中计入。

###py

class Solution:
    def maximumScore(self, grid: List[List[int]]) -> int:
        n = len(grid)
        # 每列的前缀和(从上到下)
        col_sum = [list(accumulate(col, initial=0)) for col in zip(*grid)]

        # pre 表示第 j+1 列的黑格个数
        # dec=True 意味着第 j+1 列的黑格个数 (pre) < 第 j+2 列的黑格个数
        @cache
        def dfs(j: int, pre: int, dec: bool) -> int:
            if j < 0:
                return 0
            res = 0
            # 枚举第 j 列有 cur 个黑格
            for cur in range(n + 1):
                if cur == pre:  # 情况一:相等
                    # 没有可以计入总分的格子
                    res = max(res, dfs(j - 1, cur, False))
                elif cur < pre:  # 情况二:右边黑格多
                    # 第 j 列的第 [cur, pre) 行的格子可以计入总分
                    res = max(res, dfs(j - 1, cur, True) + col_sum[j][pre] - col_sum[j][cur])
                elif not dec:  # 情况三:cur > pre >= 第 j+2 列的黑格个数
                    # 第 j+1 列的第 [pre, cur) 行的格子可以计入总分
                    res = max(res, dfs(j - 1, cur, False) + col_sum[j + 1][cur] - col_sum[j + 1][pre])
                elif pre == 0:  # 情况四(凹形):cur > pre < 第 j+2 列的黑格个数
                    # 此时第 j+2 列全黑最优(递归过程中一定可以枚举到这种情况)
                    # 第 j+1 列全白是最优的,所以只需考虑 pre=0 的情况
                    # 由于第 j+1 列在 dfs(j+1) 的情况二中已经统计过,这里不重复统计
                    res = max(res, dfs(j - 1, cur, False))
            return res
        # 枚举第 n-1 列有 i 个黑格
        return max(dfs(n - 2, i, False) for i in range(n + 1))

###java

class Solution {
    public long maximumScore(int[][] grid) {
        int n = grid.length;
        // 每列的前缀和(从上到下)
        long[][] colSum = new long[n][n + 1];
        for (int j = 0; j < n; j++) {
            for (int i = 0; i < n; i++) {
                colSum[j][i + 1] = colSum[j][i] + grid[i][j];
            }
        }

        long[][][] memo = new long[n - 1][n + 1][2];
        for (long[][] mat : memo) {
            for (long[] row : mat) {
                Arrays.fill(row, -1); // -1 表示没有计算过
            }
        }

        // 枚举第 n-1 列有 i 个黑格
        long ans = 0;
        for (int i = 0; i <= n; i++) {
            ans = Math.max(ans, dfs(n - 2, i, 0, colSum, memo));
        }
        return ans;
    }

    // pre 表示第 j+1 列的黑格个数
    // dec=1 意味着第 j+1 列的黑格个数 (pre) < 第 j+2 列的黑格个数
    private long dfs(int j, int pre, int dec, long[][] colSum, long[][][] memo) {
        if (j < 0) {
            return 0;
        }
        if (memo[j][pre][dec] != -1) { // 之前计算过
            return memo[j][pre][dec];
        }
        long res = 0;
        // 枚举第 j 列有 cur 个黑格
        for (int cur = 0; cur <= colSum.length; cur++) {
            if (cur == pre) { // 情况一:相等
                // 没有可以计入总分的格子
                res = Math.max(res, dfs(j - 1, cur, 0, colSum, memo));
            } else if (cur < pre) { // 情况二:右边黑格多
                // 第 j 列的第 [cur, pre) 行的格子可以计入总分
                res = Math.max(res, dfs(j - 1, cur, 1, colSum, memo) + colSum[j][pre] - colSum[j][cur]);
            } else if (dec == 0) { // 情况三:cur > pre >= 第 j+2 列的黑格个数
                // 第 j+1 列的第 [pre, cur) 行的格子可以计入总分
                res = Math.max(res, dfs(j - 1, cur, 0, colSum, memo) + colSum[j + 1][cur] - colSum[j + 1][pre]);
            } else if (pre == 0) { // 情况四(凹形):cur > pre < 第 j+2 列的黑格个数
                // 此时第 j+2 列全黑最优(递归过程中一定可以枚举到这种情况)
                // 第 j+1 列全白是最优的,所以只需考虑 pre=0 的情况
                // 由于第 j+1 列在 dfs(j+1) 的情况二中已经统计过,这里不重复统计
                res = Math.max(res, dfs(j - 1, cur, 0, colSum, memo));
            }
        }
        return memo[j][pre][dec] = res; // 记忆化
    }
}

###cpp

class Solution {
public:
    long long maximumScore(vector<vector<int>>& grid) {
        int n = grid.size();
        // 每列的前缀和(从上到下)
        vector<vector<long long>> col_sum(n, vector<long long>(n + 1));
        for (int j = 0; j < n; j++) {
            for (int i = 0; i < n; i++) {
                col_sum[j][i + 1] = col_sum[j][i] + grid[i][j];
            }
        }

        vector<vector<array<long long, 2>>> memo(n - 1, vector<array<long long, 2>>(n + 1, {-1, -1})); // -1 表示没有计算过
        // pre 表示第 j+1 列的黑格个数
        // dec=true 意味着第 j+1 列的黑格个数 (pre) < 第 j+2 列的黑格个数
        auto dfs = [&](auto&& dfs, int j, int pre, bool dec) -> long long {
            if (j < 0) {
                return 0;
            }
            auto& res = memo[j][pre][dec]; // 注意这里是引用
            if (res != -1) { // 之前计算过
                return res;
            }
            res = 0;
            // 枚举第 j 列有 cur 个黑格
            for (int cur = 0; cur <= n; cur++) {
                if (cur == pre) { // 情况一:相等
                    // 没有可以计入总分的格子
                    res = max(res, dfs(dfs, j - 1, cur, false));
                } else if (cur < pre) { // 情况二:右边黑格多
                    // 第 j 列的第 [cur, pre) 行的格子可以计入总分
                    res = max(res, dfs(dfs, j - 1, cur, true) + col_sum[j][pre] - col_sum[j][cur]);
                } else if (!dec) { // 情况三:cur > pre >= 第 j+2 列的黑格个数
                    // 第 j+1 列的第 [pre, cur) 行的格子可以计入总分
                    res = max(res, dfs(dfs, j - 1, cur, false) + col_sum[j + 1][cur] - col_sum[j + 1][pre]);
                } else if (pre == 0) { // 情况四(凹形):cur > pre < 第 j+2 列的黑格个数
                    // 此时第 j+2 列全黑最优(递归过程中一定可以枚举到这种情况)
                    // 第 j+1 列全白是最优的,所以只需考虑 pre=0 的情况
                    // 由于第 j+1 列在 dfs(j+1) 的情况二中已经统计过,这里不重复统计
                    res = max(res, dfs(dfs, j - 1, cur, false));
                }
            }
            return res;
        };

        // 枚举第 n-1 列有 i 个黑格
        long long ans = 0;
        for (int i = 0; i <= n; i++) {
            ans = max(ans, dfs(dfs, n - 2, i, false));
        }
        return ans;
    }
};

###go

func maximumScore(grid [][]int) (ans int64) {
n := len(grid)
// 每列的前缀和(从上到下)
colSum := make([][]int64, n)
for j := range colSum {
colSum[j] = make([]int64, n+1)
for i, row := range grid {
colSum[j][i+1] = colSum[j][i] + int64(row[j])
}
}

memo := make([][][2]int64, n-1)
for i := range memo {
memo[i] = make([][2]int64, n+1)
for j := range memo[i] {
memo[i][j] = [2]int64{-1, -1} // -1 表示没有计算过
}
}
var dfs func(int, int, int) int64
dfs = func(j, pre, dec int) int64 {
if j < 0 {
return 0
}
p := &memo[j][pre][dec]
if *p != -1 { // 之前计算过
return *p
}
res := int64(0)
// 枚举第 j 列有 cur 个黑格
for cur := 0; cur <= n; cur++ {
if cur == pre { // 情况一:相等
// 没有可以计入总分的格子
res = max(res, dfs(j-1, cur, 0))
} else if cur < pre { // 情况二:右边黑格多
// 第 j 列的第 [cur, pre) 行的格子可以计入总分
res = max(res, dfs(j-1, cur, 1)+colSum[j][pre]-colSum[j][cur])
} else if dec == 0 { // 情况三:cur > pre >= 第 j+2 列的黑格个数
// 第 j+1 列的第 [pre, cur) 行的格子可以计入总分
res = max(res, dfs(j-1, cur, 0)+colSum[j+1][cur]-colSum[j+1][pre])
} else if pre == 0 { // 情况四(凹形):cur > pre < 第 j+2 列的黑格个数
// 此时第 j+2 列全黑最优(递归过程中一定可以枚举到这种情况)
// 第 j+1 列全白是最优的,所以只需考虑 pre=0 的情况
// 由于第 j+1 列在 dfs(j+1) 的情况二中已经统计过,这里不重复统计
res = max(res, dfs(j-1, cur, 0))
}
}
*p = res // 记忆化
return res
}

// 枚举第 n-1 列有 i 个黑格
for i := 0; i <= n; i++ {
ans = max(ans, dfs(n-2, i, 0))
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^3)$,其中 $n$ 是 $\textit{grid}$ 的长度。由于每个状态只会计算一次,动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。本题状态个数等于 $\mathcal{O}(n^2)$,单个状态的计算时间为 $\mathcal{O}(n)$,所以动态规划的时间复杂度为 $\mathcal{O}(n^3)$。
  • 空间复杂度:$\mathcal{O}(n^2)$。

三、1:1 翻译成递推

我们可以去掉递归中的「递」,只保留「归」的部分,即自底向上计算。

具体请看视频讲解 动态规划入门:从记忆化搜索到递推,其中包含把记忆化搜索 1:1 翻译成递推的技巧。

$f[j+1][\textit{pre}][\textit{dec}]$ 的定义和 $\textit{dfs}(j,\textit{pre},\textit{dec})$ 的定义是一样的。注意这里是 $j+1$ 不是 $j$,因为要避免 $j=-1$ 时出现负数下标。

初始值 $f[0][\textit{pre}][\textit{dec}]=0$,翻译自递归边界。

答案为 $\max\limits_{i=0}^{n} f[n-1][i][0]$,翻译自递归入口。

###py

class Solution:
    def maximumScore(self, grid: List[List[int]]) -> int:
        n = len(grid)
        # 每列的前缀和(从上到下)
        col_sum = [list(accumulate(col, initial=0)) for col in zip(*grid)]
        f = [[[0, 0] for _ in range(n + 1)] for _ in range(n)]
        for j in range(n - 1):
            # pre 表示第 j+1 列的黑格个数
            for pre in range(n + 1):
                # dec=1 意味着第 j+1 列的黑格个数 (pre) < 第 j+2 列的黑格个数
                for dec in range(2):
                    res = 0
                    # 枚举第 j 列有 cur 个黑格
                    for cur in range(n + 1):
                        if cur == pre:  # 情况一:相等
                            # 没有可以计入总分的格子
                            res = max(res, f[j][cur][0])
                        elif cur < pre:  # 情况二:右边黑格多
                            # 第 j 列的第 [cur, pre) 行的格子可以计入总分
                            res = max(res, f[j][cur][1] + col_sum[j][pre] - col_sum[j][cur])
                        elif dec == 0:  # 情况三:cur > pre >= 第 j+2 列的黑格个数
                            # 第 j+1 列的第 [pre, cur) 行的格子可以计入总分
                            res = max(res, f[j][cur][0] + col_sum[j + 1][cur] - col_sum[j + 1][pre])
                        elif pre == 0:  # 情况四(凹形):cur > pre < 第 j+2 列的黑格个数
                            # 此时第 j+2 列全黑最优(递归过程中一定可以枚举到这种情况)
                            # 第 j+1 列全白是最优的,所以只需考虑 pre=0 的情况
                            # 由于第 j+1 列在 dfs(j+1) 的情况二中已经统计过,这里不重复统计
                            res = max(res, f[j][cur][0])
                    f[j + 1][pre][dec] = res
        # 枚举第 n-1 列有 i 个黑格
        return max(f[-1][i][0] for i in range(n + 1))

###java

class Solution {
    public long maximumScore(int[][] grid) {
        int n = grid.length;
        // 每列的前缀和(从上到下)
        long[][] colSum = new long[n][n + 1];
        for (int j = 0; j < n; j++) {
            for (int i = 0; i < n; i++) {
                colSum[j][i + 1] = colSum[j][i] + grid[i][j];
            }
        }

        long[][][] f = new long[n][n + 1][2];
        for (int j = 0; j < n - 1; j++) {
            // pre 表示第 j+1 列的黑格个数
            for (int pre = 0; pre <= n; pre++) {
                // dec=1 意味着第 j+1 列的黑格个数 (pre) < 第 j+2 列的黑格个数
                for (int dec = 0; dec < 2; dec++) {
                    long res = 0;
                    // 枚举第 j 列有 cur 个黑格
                    for (int cur = 0; cur <= n; cur++) {
                        if (cur == pre) { // 情况一:相等
                            // 没有可以计入总分的格子
                            res = Math.max(res, f[j][cur][0]);
                        } else if (cur < pre) { // 情况二:右边黑格多
                            // 第 j 列的第 [cur, pre) 行的格子可以计入总分
                            res = Math.max(res, f[j][cur][1] + colSum[j][pre] - colSum[j][cur]);
                        } else if (dec == 0) { // 情况三:cur > pre >= 第 j+2 列的黑格个数
                            // 第 j+1 列的第 [pre, cur) 行的格子可以计入总分
                            res = Math.max(res, f[j][cur][0] + colSum[j + 1][cur] - colSum[j + 1][pre]);
                        } else if (pre == 0) { // 情况四(凹形):cur > pre < 第 j+2 列的黑格个数
                            // 此时第 j+2 列全黑最优(递归过程中一定可以枚举到这种情况)
                            // 第 j+1 列全白是最优的,所以只需考虑 pre=0 的情况
                            // 由于第 j+1 列在 dfs(j+1) 的情况二中已经统计过,这里不重复统计
                            res = Math.max(res, f[j][cur][0]);
                        }
                    }
                    f[j + 1][pre][dec] = res;
                }
            }
        }

        long ans = 0;
        for (long[] row : f[n - 1]) {
            ans = Math.max(ans, row[0]);
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    long long maximumScore(vector<vector<int>>& grid) {
        int n = grid.size();
        // 每列的前缀和(从上到下)
        vector<vector<long long>> col_sum(n, vector<long long>(n + 1));
        for (int j = 0; j < n; j++) {
            for (int i = 0; i < n; i++) {
                col_sum[j][i + 1] = col_sum[j][i] + grid[i][j];
            }
        }

        vector<vector<array<long long, 2>>> f(n, vector<array<long long, 2>>(n + 1));
        for (int j = 0; j < n - 1; j++) {
            // pre 表示第 j+1 列的黑格个数
            for (int pre = 0; pre <= n; pre++) {
                // dec=1 意味着第 j+1 列的黑格个数 (pre) < 第 j+2 列的黑格个数
                for (int dec = 0; dec < 2; dec++) {
                    auto& res = f[j + 1][pre][dec];
                    // 枚举第 j 列有 cur 个黑格
                    for (int cur = 0; cur <= n; cur++) {
                        if (cur == pre) { // 情况一:相等
                            // 没有可以计入总分的格子
                            res = max(res, f[j][cur][0]);
                        } else if (cur < pre) { // 情况二:右边黑格多
                            // 第 j 列的第 [cur, pre) 行的格子可以计入总分
                            res = max(res, f[j][cur][1] + col_sum[j][pre] - col_sum[j][cur]);
                        } else if (dec == 0) { // 情况三:cur > pre >= 第 j+2 列的黑格个数
                            // 第 j+1 列的第 [pre, cur) 行的格子可以计入总分
                            res = max(res, f[j][cur][0] + col_sum[j + 1][cur] - col_sum[j + 1][pre]);
                        } else if (pre == 0) { // 情况四(凹形):cur > pre < 第 j+2 列的黑格个数
                            // 此时第 j+2 列全黑最优(递归过程中一定可以枚举到这种情况)
                            // 第 j+1 列全白是最优的,所以只需考虑 pre=0 的情况
                            // 由于第 j+1 列在 dfs(j+1) 的情况二中已经统计过,这里不重复统计
                            res = max(res, f[j][cur][0]);
                        }
                    }
                }
            }
        }

        // 枚举第 n-1 列有 i 个黑格
        long long ans = 0;
        for (auto& row : f[n - 1]) {
            ans = max(ans, row[0]);
        }
        return ans;
    }
};

###go

func maximumScore(grid [][]int) (ans int64) {
n := len(grid)
// 每列的前缀和(从上到下)
colSum := make([][]int64, n)
for j := range colSum {
colSum[j] = make([]int64, n+1)
for i, row := range grid {
colSum[j][i+1] = colSum[j][i] + int64(row[j])
}
}

f := make([][][2]int64, n)
for j := range f {
f[j] = make([][2]int64, n+1)
}
for j := 0; j < n-1; j++ {
// pre 表示第 j+1 列的黑格个数
for pre := 0; pre <= n; pre++ {
// dec=1 意味着第 j+1 列的黑格个数 (pre) < 第 j+2 列的黑格个数
for dec := 0; dec < 2; dec++ {
res := int64(0)
// 枚举第 j 列有 cur 个黑格
for cur := 0; cur <= n; cur++ {
if cur == pre { // 情况一:相等
// 没有可以计入总分的格子
res = max(res, f[j][cur][0])
} else if cur < pre { // 情况二:右边黑格多
// 第 j 列的第 [cur, pre) 行的格子可以计入总分
res = max(res, f[j][cur][1]+colSum[j][pre]-colSum[j][cur])
} else if dec == 0 { // 情况三:cur > pre >= 第 j+2 列的黑格个数
// 第 j+1 列的第 [pre, cur) 行的格子可以计入总分
res = max(res, f[j][cur][0]+colSum[j+1][cur]-colSum[j+1][pre])
} else if pre == 0 { // 情况四(凹形):cur > pre < 第 j+2 列的黑格个数
// 此时第 j+2 列全黑最优(递归过程中一定可以枚举到这种情况)
// 第 j+1 列全白是最优的,所以只需考虑 pre=0 的情况
// 由于第 j+1 列在 dfs(j+1) 的情况二中已经统计过,这里不重复统计
res = max(res, f[j][cur][0])
}
}
f[j+1][pre][dec] = res
}
}
}

for _, row := range f[n-1] {
ans = max(ans, row[0])
}
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^3)$,其中 $n$ 是 $\textit{grid}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n^2)$。

四、时间优化

把最内层的枚举 $\textit{cur}$ 的循环优化掉。

首先计算 $\textit{pre}>0$ 的状态,然后单独计算 $\textit{pre}=0$ 的状态。

1) pre > 0 且 dec = 1

$\textit{pre}> 0$ 的状态,没有情况四。

对于 $f[j+1][\textit{pre}][1]$,需要计算 $f[j][\textit{pre}][0]$(情况一)与下式(情况二)的最大值:

$$
\begin{aligned}
& \max\limits_{\textit{cur}=0}^{\textit{pre}-1} {f[j][\textit{cur}][1] + \textit{colSum}[j][\textit{pre}] - \textit{colSum}[j][\textit{cur}]} \
={} & \textit{colSum}[j][\textit{pre}] + \max\limits_{\textit{cur}=0}^{\textit{pre}-1} {f[j][\textit{cur}][1] - \textit{colSum}[j][\textit{cur}]} \
\end{aligned}
$$

其中

$$
\max\limits_{\textit{cur}=0}^{\textit{pre}-1} {f[j][\textit{cur}][1] - \textit{colSum}[j][\textit{cur}]}
$$

可以一边从小到大枚举 $\textit{pre}$,一边用一个变量 $\textit{preMax}$ 维护。

2) pre > 0 且 dec = 0

对于 $f[j+1][\textit{pre}][0]$,除了上面 $\textit{dec}=1$ 要计算的,这里也要计算外,还需要计算下式(情况三)的最大值:

$$
\begin{aligned}
& \max\limits_{\textit{cur}=pre+1}^{n} { f[j][\textit{cur}][0] + \textit{colSum}[j + 1][\textit{cur}] - \textit{colSum}[j + 1][\textit{pre}] } \
={} & - \textit{colSum}[j + 1][\textit{pre}] + \max\limits_{\textit{cur}=pre+1}^{n} { f[j][\textit{cur}][0] + \textit{colSum}[j + 1][\textit{cur}] } \
\end{aligned}
$$

其中

$$
\max\limits_{\textit{cur}=pre+1}^{n} { f[j][\textit{cur}][0] + \textit{colSum}[j + 1][\textit{cur}] }
$$

可以一边从大到小枚举 $\textit{pre}$,一边用一个变量 $\textit{sufMax}$ 维护。

3) pre = 0 且 dec = 0

$\textit{pre}=0$ 的状态,没有情况二。

对于 $f[j+1][0][0]$,需要计算 $f[j][0][0]$(情况一)与下式(情况三)的最大值:

$$
\max\limits_{\textit{cur}=1}^{n} { f[j][\textit{cur}][0] + \textit{colSum}[j + 1][\textit{cur}] }
$$

这正是上面循环结束后的 $\textit{sufMax}$。

此外,由于不可能连续三列全白,所以无需考虑从 $f[j][0][0]$(情况一)转移过来,因此

$$
f[j+1][0][0] = \textit{sufMax}
$$

4) pre = 0 且 dec = 1

对于 $f[j+1][0][1]$,需要计算下式(情况一与情况四)的最大值:

$$
\max\limits_{\textit{cur}=0}^{n} f[j][\textit{cur}][0]
$$

但在 $\textit{pre}=0$ 且 $\textit{dec}=1$ 的前提下,其实只需考虑第 $j$ 列全白($\textit{cur}=0$)或全黑($\textit{cur}=n$)两种情况。沿用上文图片中的证明方法,考虑第 $j-1$ 列的黑格个数 $B_{j-1}$:

  • 如果 $B_{j-1} \ge B_j$,第 $j$ 列全白更好。
  • 如果 $B_{j-1} < B_j$,第 $j$ 列多出的段左右都是白格,所以全黑更好。

因此

$$
f[j+1][0][1] = \max(f[j][0][0], f[j][n][0])
$$

###py

class Solution:
    def maximumScore(self, grid: List[List[int]]) -> int:
        n = len(grid)
        col_sum = [list(accumulate(col, initial=0)) for col in zip(*grid)]

        f = [[[0, 0] for _ in range(n + 1)] for _ in range(n)]
        for j in range(n - 1):
            # 用前缀最大值优化
            pre_max = f[j][0][1] - col_sum[j][0]
            for pre in range(1, n + 1):
                f[j + 1][pre][0] = f[j + 1][pre][1] = max(f[j][pre][0], pre_max + col_sum[j][pre])
                pre_max = max(pre_max, f[j][pre][1] - col_sum[j][pre])

            # 用后缀最大值优化
            suf_max = f[j][n][0] + col_sum[j + 1][n]
            for pre in range(n - 1, 0, -1):
                f[j + 1][pre][0] = max(f[j + 1][pre][0], suf_max - col_sum[j + 1][pre])
                suf_max = max(suf_max, f[j][pre][0] + col_sum[j + 1][pre])

            # 单独计算 pre=0 的状态
            f[j + 1][0][0] = suf_max  # 无需考虑 f[j][0][0],因为不能连续三列全白
            f[j + 1][0][1] = max(f[j][0][0], f[j][n][0])  # 第 j 列要么全白,要么全黑

        return max(f[-1][i][0] for i in range(n + 1))

###java

class Solution {
    public long maximumScore(int[][] grid) {
        int n = grid.length;
        long[][] colSum = new long[n][n + 1];
        for (int j = 0; j < n; j++) {
            for (int i = 0; i < n; i++) {
                colSum[j][i + 1] = colSum[j][i] + grid[i][j];
            }
        }

        long[][][] f = new long[n][n + 1][2];
        for (int j = 0; j < n - 1; j++) {
            // 用前缀最大值优化
            long preMax = f[j][0][1] - colSum[j][0];
            for (int pre = 1; pre <= n; pre++) {
                f[j + 1][pre][0] = f[j + 1][pre][1] = Math.max(f[j][pre][0], preMax + colSum[j][pre]);
                preMax = Math.max(preMax, f[j][pre][1] - colSum[j][pre]);
            }

            // 用后缀最大值优化
            long sufMax = f[j][n][0] + colSum[j + 1][n];
            for (int pre = n - 1; pre > 0; pre--) {
                f[j + 1][pre][0] = Math.max(f[j + 1][pre][0], sufMax - colSum[j + 1][pre]);
                sufMax = Math.max(sufMax, f[j][pre][0] + colSum[j + 1][pre]);
            }

            // 单独计算 pre=0 的状态
            f[j + 1][0][0] = sufMax; // 无需考虑 f[j][0][0],因为不能连续三列全白
            f[j + 1][0][1] = Math.max(f[j][0][0], f[j][n][0]); // 第 j 列要么全白,要么全黑
        }

        long ans = 0;
        for (long[] row : f[n - 1]) {
            ans = Math.max(ans, row[0]);
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    long long maximumScore(vector<vector<int>>& grid) {
        int n = grid.size();
        vector<vector<long long>> col_sum(n, vector<long long>(n + 1));
        for (int j = 0; j < n; j++) {
            for (int i = 0; i < n; i++) {
                col_sum[j][i + 1] = col_sum[j][i] + grid[i][j];
            }
        }

        vector<vector<array<long long, 2>>> f(n, vector<array<long long, 2>>(n + 1));
        for (int j = 0; j < n - 1; j++) {
            // 用前缀最大值优化
            long long pre_max = f[j][0][1] - col_sum[j][0];
            for (int pre = 1; pre <= n; pre++) {
                f[j + 1][pre][0] = f[j + 1][pre][1] = max(f[j][pre][0], pre_max + col_sum[j][pre]);
                pre_max = max(pre_max, f[j][pre][1] - col_sum[j][pre]);
            }

            // 用后缀最大值优化
            long long suf_max = f[j][n][0] + col_sum[j + 1][n];
            for (int pre = n - 1; pre > 0; pre--) {
                f[j + 1][pre][0] = max(f[j + 1][pre][0], suf_max - col_sum[j + 1][pre]);
                suf_max = max(suf_max, f[j][pre][0] + col_sum[j + 1][pre]);
            }

            // 单独计算 pre=0 的状态
            f[j + 1][0][0] = suf_max; // 无需考虑 f[j][0][0],因为不能连续三列全白
            f[j + 1][0][1] = max(f[j][0][0], f[j][n][0]); // 第 j 列要么全白,要么全黑
        }

        long long ans = 0;
        for (auto& row : f[n - 1]) {
            ans = max(ans, row[0]);
        }
        return ans;
    }
};

###go

func maximumScore(grid [][]int) (ans int64) {
n := len(grid)
colSum := make([][]int64, n)
for j := range colSum {
colSum[j] = make([]int64, n+1)
for i, row := range grid {
colSum[j][i+1] = colSum[j][i] + int64(row[j])
}
}

f := make([][][2]int64, n)
for j := range f {
f[j] = make([][2]int64, n+1)
}
for j := 0; j < n-1; j++ {
// 用前缀最大值优化
preMax := f[j][0][1] - colSum[j][0]
for pre := 1; pre <= n; pre++ {
f[j+1][pre][0] = max(f[j][pre][0], preMax+colSum[j][pre])
f[j+1][pre][1] = f[j+1][pre][0]
preMax = max(preMax, f[j][pre][1]-colSum[j][pre])
}

// 用后缀最大值优化
sufMax := f[j][n][0] + colSum[j+1][n]
for pre := n - 1; pre > 0; pre-- {
f[j+1][pre][0] = max(f[j+1][pre][0], sufMax-colSum[j+1][pre])
sufMax = max(sufMax, f[j][pre][0]+colSum[j+1][pre])
}

// 单独计算 pre=0 的状态
f[j+1][0][0] = sufMax // 无需考虑 f[j][0][0],因为不能连续三列全白
f[j+1][0][1] = max(f[j][0][0], f[j][n][0]) // 第 j 列要么全白,要么全黑
}

for _, row := range f[n-1] {
ans = max(ans, row[0])
}
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n^2)$,其中 $n$ 是 $\textit{grid}$ 的长度。这是本题的最优复杂度,因为遍历 $\textit{grid}$ 就需要 $\mathcal{O}(n^2)$ 的时间了。
  • 空间复杂度:$\mathcal{O}(n^2)$。

注:空间复杂度可以进一步优化至 $\mathcal{O}(n)$,需要用到滚动数组,并在 DP 的过程中计算列的前缀和。

分类题单

如何科学刷题?

  1. 滑动窗口(定长/不定长/多指针)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心算法(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)

我的题解精选(已分类)

【小羊肖恩】最优复杂度 O(n^2) 解——观察性质与前后缀优化!

如果你善于观察,这题是个很妙的题。

性质 1: 黑色图形不可能存在一个凹进去的图形,即长度先变小到一个非 $0$ 数字又变大。

这是因为我们可以去掉其中最短的一列,使得我们的总分数增加!


性质 2: 每次增加一定增加到顶。

这是因为我们取最高的一段,一定可以将其加长,而使得总分数增加!


性质 3: 不会出现单独的一列是纯空的,除非其两侧都是满的。

否则,我们可以去掉两侧中的一列,答案一定变大。


综上,直观地考虑,最优情况下,黑色的方格一定呈现出几组倒着的山峰形状,即先变长后变短。

于是我们只需设置两个状态,一个是增长时候的状态,一个是缩短时候的状态,每一列我们只需记录当前长度是 $x$ 即可,这样总状态数量是 $\mathcal{O}(n^2)$ 的。

而状态转移:在一列长度增长的过程中,新带来的收益是前一列的区间和,而缩短的过程中,新带来的收益是后一列的区间和。我们枚举初始情况和结尾情况即可。

这样对于每一个初始情况,我们枚举新的一列的长度 $0\sim n$ ,因此总时间复杂度为 $\mathcal{O}(n^3)$ .(后续还能进一步优化)

具体代码如下——

###Python

inf = 10 ** 15
fmax = lambda x, y: x if x > y else y
class Solution:
    def maximumScore(self, grid: List[List[int]]) -> int:
        n = len(grid)
        accs = [[0] * (n + 1) for _ in range(n + 1)]
        for i in range(n):
            for j in range(n):
                accs[j+1][i+1] = grid[i][j]

        for i in range(n):
            for j in range(n):
                accs[i+1][j+1] += accs[i+1][j]
        
        dp1 = [0] * (n + 1)
        dp2 = [-inf] * (n + 1)
        
        for i in range(1, n + 1):
            ndp1 = [0] * (n + 1)
            ndp2 = [-inf] * (n + 1)
            
            for j in range(n + 1):
                for k in range(j, n + 1):
                    ndp1[k] = fmax(ndp1[k], dp1[j] + accs[i-1][k] - accs[i-1][j])
                for k in range(j + 1):
                    ndp2[k] = fmax(ndp2[k], dp2[j] + accs[i][j] - accs[i][k])
            # 一种情况:空了两列——此时这一行从 0 起步,ndp1[0]
            # 另一种情况:空了一列——此时这一行是 n 行起手,ndp[n] (前面做了交换 n,m 操作)
            ndp1[0] = fmax(ndp1[0], dp2[0])
            ndp1[n] = fmax(ndp1[n], dp2[0])
            # 这里是转移回递减的情况:从满了的增长变为减少
            ndp2[n] = fmax(ndp2[n], ndp1[n])
            dp1, dp2 = ndp1, ndp2
        return max(max(dp1), max(dp2))

阅读上述代码,不难发现,中间的转移过程本质上可以使用前缀最大值避免重复计算,因此转移实质上是 $\mathcal{O}(1)$ 的,总时间复杂度可以减小到 $\mathcal{O}(n^2)$ .

具体代码如下——

###Python

inf = 10 ** 15
fmax = lambda x, y: x if x > y else y
class Solution:
    def maximumScore(self, grid: List[List[int]]) -> int:
        n = len(grid)
        accs = [[0] * (n + 1) for _ in range(n + 1)]
        for i in range(n):
            for j in range(n):
                accs[j+1][i+1] = grid[i][j]
        
        for i in range(n):
            for j in range(n):
                accs[i+1][j+1] += accs[i+1][j]
        
        dp1 = [0] * (n + 1)
        dp2 = [-inf] * (n + 1)
        
        for i in range(1, n + 1):
            ndp1 = [0] * (n + 1)
            ndp2 = [-inf] * (n + 1)
            
            cur = -inf
            for j in range(n + 1):
                cur = fmax(cur, dp1[j] - accs[i-1][j])
                ndp1[j] = cur + accs[i-1][j]
            
            # 一种情况:空了两列——此时这一行从 0 起步,ndp1[0]
            # 另一种情况:空了一列——此时这一行是 n 行起手,ndp[n]
            ndp1[0] = fmax(ndp1[0], dp2[0])
            ndp1[n] = fmax(ndp1[n], dp2[0])
            
            cur = -inf
            for j in range(n, -1, -1):
                cur = fmax(cur, dp2[j] + accs[i][j])
                ndp2[j] = cur - accs[i][j]
            
            # 这里是转移回递减的情况:从满了的增长变为减少
            ndp2[n] = fmax(ndp2[n], ndp1[n])
            dp1, dp2 = ndp1, ndp2
        
        return max(max(dp1), max(dp2))

DP & 分类讨论

解法:DP & 分类讨论

本题难点在于讨论清楚各种情况。

容易想到设计状态 $f(j, i)$ 表示只考虑前 $j$ 列,且第 $j$ 列涂黑了前 $i$ 行的最大得分。首先注意到不会出现连续三列都不操作的情况,因为这样会导致中间的一列不参与计分,此时可以把中间的一列全涂黑,答案不会变差。因此我们只需要考虑上一个操作的列是 $(j - 1)$,$(j - 2)$ 还是 $(j - 3)$ 的情况。

以下转移方程中,$s(j, i_l, i_r)$ 表示第 $j$ 列第 $i_l$ 行到第 $i_r$ 行的分数之和。

情况 1:当上一个操作的列是 $(j - 1)$,且它涂黑了 $i'$ 行时

情况 1.1:若 $i' \le i$,需要考虑第 $(j - 1)$ 列涂黑的格子数比 $(j - 2)$ 列多还是少。注意到最优答案下,第 $(j - 1)$ 列涂黑的格子一定比 $(j - 2)$ 列多,否则在两边涂黑的格子都比中间多的情况下,中间列的操作只是在失去分数,不优。

这种情况下,本次操作将损失第 $j$ 列前 $i'$ 行的分数,但获得了第 $(j - 1)$ 列第 $(i' + 1)$ 行到第 $i$ 的分数,以及第 $(j + 1)$ 列前 $i$ 行的分数。

另外,为了维护这一列和上一列黑格子的数量关系,我们需要在 DP 状态里加一维,变成 $f(j, i, t = 0/1)$,表示只考虑前 $j$ 列,且第 $j$ 列涂黑了前 $i$ 行,且第 $j$ 列涂黑的格子数比第 $(j - 1)$ 列少($0$)还是多($1$)时的最大得分。这个情况的转移方程是

$$
f(j, i, 1) \leftarrow f(j - 1, i' \le i, 1) - s(j, 1, i') + s(j - 1, i' + 1, i) + s(j + 1, 1, i)
$$

情况 1.2:若 $i' > i$,本次操作将损失第 $j$ 列前 $i$ 行的分数,但获得了第 $(j + 1)$ 列前 $i$ 的分数。转移方程为

$$
f(j, i, 0) \leftarrow f(j - 1, i' > i, 0 \le t \le 1) - s(j, 1, i) + s(j + 1, 1, i)
$$

情况 2:当上一个操作的列是 $(j - 2)$,且它涂黑了 $i'$ 行时

情况 2.1:若 $i' \le i$,本次操作将获得第 $(j - 1)$ 列第 $(i' + 1)$ 行到第 $i$ 行的分数,以及第 $(j + 1)$ 列前 $i$ 的分数。转移方程为

$$
f(j, i, 1) \leftarrow f(j - 2, i' \le i, 0 \le t \le 1) + s(j - 1, i' + 1, i) + s(j + 1, 1, i)
$$

情况 2.2:若 $i' > i$,本次操作将获得第 $(j + 1)$ 列前 $i$ 的分数。转移方程为

$$
f(j, i, 1) \leftarrow f(j - 2, i' > i, 0 \le t \le 1) + s(j + 1, 1, i)
$$

情况 3:当上一个操作的列是 $(j - 3)$,且它涂黑了 $i'$ 行时

本次操作将获得第 $(j - 1)$ 列前 $i$ 行的分数,以及第 $(j + 1)$ 列前 $i$ 的分数。转移方程为

$$
f(j, i, 1) \leftarrow f(j - 3, i', 0 \le t \le 1) + s(j - 1, 1, i) + s(j + 1, 1, i)
$$

情况 4:这一列是第一个被操作的列

$$
f(j, i, 1) \leftarrow s(j - 1, 1, i) + s(j + 1, 1, i)
$$

所有情况取最大值即可。枚举最后操作的是哪一列,以及涂黑了几行,答案就是 $\max(f(i, j, 0/1))$。复杂度 $\mathcal{O}(n^3)$。

参考代码(c++)

###cpp

class Solution {
public:
    long long maximumScore(vector<vector<int>>& grid) {
        int n = grid.size();
        if (n == 1) return 0;

        // 前缀和预处理分数之和
        long long sm[n + 2][n + 2];
        memset(sm, 0, sizeof(sm));
        for (int j = 1; j <= n; j++) for (int i = 1; i <= n; i++) sm[j][i] = sm[j][i - 1] + grid[i - 1][j - 1];

        const long long INF = 1e18; 
        long long f[n + 1][n + 1][2];
        for (int i = 0; i <= n; i++) for (int j = 0; j <= n; j++) f[i][j][0] = f[i][j][1] = -INF;
        for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) {
            // 情况 4
            f[i][j][1] = sm[i - 1][j] + sm[i + 1][j];
            // 情况 1.1
            if (i > 1) for (int jj = 1; jj <= j; jj++) {
                long long det = (sm[i - 1][j] - sm[i - 1][jj]) + sm[i + 1][j] - sm[i][jj];
                f[i][j][1] = max(f[i][j][1], f[i - 1][jj][1] + det);
            }
            // 情况 1.2
            if (i > 1) for (int jj = j + 1; jj <= n; jj++) {
                long long det = sm[i + 1][j] - sm[i][j];
                f[i][j][0] = max({f[i][j][0], f[i - 1][jj][0] + det, f[i - 1][jj][1] + det});
            }
            // 情况 2.1
            if (i > 2) for (int jj = 1; jj <= j; jj++) {
                long long det = (sm[i - 1][j] - sm[i - 1][jj]) + sm[i + 1][j];
                f[i][j][1] = max({f[i][j][1], f[i - 2][jj][0] + det, f[i - 2][jj][1] + det});
            }
            // 情况 2.2
            if (i > 2) for (int jj = j + 1; jj <= n; jj++) {
                long long det = sm[i + 1][j];
                f[i][j][1] = max({f[i][j][1], f[i - 2][jj][0] + det, f[i - 2][jj][1] + det});
            }
            // 情况 3
            if (i > 3) for (int jj = 1; jj <= n; jj++) {
                long long det = sm[i - 1][j] + sm[i + 1][j];
                f[i][j][1] = max({f[i][j][1], f[i - 3][jj][0] + det, f[i - 3][jj][1] + det});
            }
        }

        long long ans = 0;
        for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) ans = max({ans, f[i][j][0], f[i][j][1]});
        return ans;
    }
};

每日一题-获取单值网格的最小操作数🟡

给你一个大小为 m x n 的二维整数网格 grid 和一个整数 x 。每一次操作,你可以对 grid 中的任一元素 x x

单值网格 是全部元素都相等的网格。

返回使网格化为单值网格所需的 最小 操作数。如果不能,返回 -1

 

示例 1:

输入:grid = [[2,4],[6,8]], x = 2
输出:4
解释:可以执行下述操作使所有元素都等于 4 : 
- 2 加 x 一次。
- 6 减 x 一次。
- 8 减 x 两次。
共计 4 次操作。

示例 2:

输入:grid = [[1,5],[2,3]], x = 1
输出:5
解释:可以使所有元素都等于 3 。

示例 3:

输入:grid = [[1,2],[3,4]], x = 2
输出:-1
解释:无法使所有元素相等。

 

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 105
  • 1 <= m * n <= 105
  • 1 <= x, grid[i][j] <= 104

【鄙人拙见:为什么要取中位数】JavaScript

image.png
应该没多少人提交吧,所以我才这么快🤣🤣🤣

解题思路

为什么是中位数,我的理解:
image.png
数轴上的三个点$a$、$b$、$c$,间隔为$l$。

所有点到$a$点的距离之和:$3l$
所有点到$b$点的距离之和:$2l$
所有点到$c$点的距离之和:$3l$
显然,取中间的$b$点,距离之和是最小的。

可以这么想:不管点取哪个,$a$点到其的距离加上$c$点到其的距离,是个定值$2l$。所以区别就在于$b$点到其的距离了,那么很显然,取$b$点本身,$b$点到其的距离是最小的,为$0$。所以才取的中位数,可以推广到一般情况。

若有不妥之处,欢迎指出。

代码

###javascript

const minOperations = (grid, x) => {
    // 行、列
    const [m, n] = [grid.length, grid[0].length];
    // 如果只有一个元素,返回0
    if (m === 1 && n === 1) return 0;
    // 将网格扁平化
    const nums = [];
    for (let i = 0; i < m; i++) {
        nums.push(...grid[i]);
    }
    // 升序排序
    nums.sort((a, b) => a - b);
    const numsLen = nums.length;
    // 中位数
    const num = nums[numsLen >> 1];
    let res = 0;
    for (let i = 0; i < numsLen; i++) {
        // 当前数和中位数的差值
        const gap = nums[i] - num;
        // 某个差值不是x的倍数,则不能完成操作
        if (gap % x) return -1;
        // 累加上步骤次数
        res += (gap > 0 ? gap : -gap) / x;
    }
    return res;
};

[java]贪心 取中位数(垃圾解释)

思路:其实没啥思路,就是简单的获得中位数,在进行加减x等于中位数的操作
bd68d7f4e59501f83d7cb4643d620e7.jpg

class Solution {
    public int minOperations(int[][] grid, int x) {
        int n = grid.length;
        int m = grid[0].length;
        int[] arr = new int[m*n];
        int i = 0;
        for(int[] a : grid)
            for(int a_ : a){
                arr[i++] = a_;
            }
        Arrays.sort(arr);
        int j = arr[(n*m)/2];
        int sum = 0;
        for(int a : arr){
            int l = Math.abs(j-a);
            if(l%x != 0) return -1;
            sum += l/x;
        }
        return sum;
    }
}

中位数贪心(Python/Java/C++/C/Go/JS/Rust)

先判断是否无解。

例如 $x=2$,我们可以把 $2,4,6,8$ 都变成同一个数,但能把 $2,5$ 变成同一个数吗?不能,在 $x=2$ 的情况下,偶数只能变成偶数,奇数只能变成奇数,无法把偶数和奇数变成同一个数。在 $x=2$ 的情况下,每个数的奇偶性(模 $2$ 的结果)必须都一样,才能变成同一个数。

一般地,对于整数 $k$,我们有 $(\textit{grid}[i][j] + kx)\bmod x = \textit{grid}[i][j]\bmod x$。所以操作后,$\textit{grid}[i][j] \bmod x$ 是不变的。每个数模 $x$ 的结果必须都一样,才能变成同一个数。否则无解,输出 $-1$。

想象操场上有一些人,把这些人聚在一起,怎么移动最迅速?

往「中间」移动是最迅速的。

根据 中位数贪心及其证明,把所有数变成 $\textit{grid}$ 的中位数是最优的。

设 $\textit{grid}$ 的中位数为 $\textit{median}$,总操作次数为

$$
\sum_{v\in \textit{grid}}\dfrac{|v - \textit{median}|}{x} = \dfrac{\sum\limits_{v\in \textit{grid}}|v - \textit{median}|}{x}
$$

class Solution:
    def minOperations(self, grid: List[List[int]], x: int) -> int:
        a = []
        target = grid[0][0] % x

        # 1. 判断是否无解
        for row in grid:
            for v in row:
                if v % x != target:  # 每个数模 x 都必须相等
                    return -1
            a += row

        # 2. 计算 grid 的中位数 median
        a.sort()
        median = a[len(a) // 2]

        # 3. 计算操作次数
        return sum(abs(v - median) for v in a) // x
class Solution {
    public int minOperations(int[][] grid, int x) {
        int k = grid.length * grid[0].length;
        int[] a = new int[k];
        int idx = 0;
        int target = grid[0][0] % x;

        // 1. 判断是否无解
        for (int[] row : grid) {
            for (int v : row) {
                if (v % x != target) { // 每个数模 x 都必须相等
                    return -1;
                }
                a[idx++] = v;
            }
        }

        // 2. 计算 grid 的中位数 median
        Arrays.sort(a);
        int median = a[k / 2];

        // 3. 计算操作次数
        int ans = 0;
        for (int v : a) {
            ans += Math.abs(v - median);
        }
        return ans / x;
    }
}
class Solution {
public:
    int minOperations(vector<vector<int>>& grid, int x) {
        int k = grid.size() * grid[0].size();
        vector<int> a;
        a.reserve(k); // 预分配空间
        int target = grid[0][0] % x;
    
        // 1. 判断是否无解
        for (auto& row : grid) {
            for (int v : row) {
                if (v % x != target) { // 每个数模 x 都必须相等
                    return -1;
                }
                a.push_back(v);
            }
        }

        // 2. 计算 grid 的中位数 median
        // ranges::sort(a);
        ranges::nth_element(a, a.begin() + k / 2); // 用快速选择代替排序
        int median = a[k / 2];

        // 3. 计算操作次数
        int ans = 0;
        for (int v : a) {
            ans += abs(v - median);
        }
        return ans / x;
    }
};
int cmp(const void* a, const void* b) {
    return *(int*)a - *(int*)b;
}

int minOperations(int** grid, int gridSize, int* gridColSize, int x) {
    int m = gridSize, n = gridColSize[0];
    int* a = malloc(m * n * sizeof(int));
    int k = 0;
    int target = grid[0][0] % x;

    // 1. 判断是否无解
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] % x != target) { // 每个数模 x 都必须相等
                return -1;
            }
            a[k++] = grid[i][j];
        }
    }

    // 2. 计算 grid 的中位数 median
    qsort(a, k, sizeof(int), cmp);
    int median = a[k / 2];

    // 3. 计算操作次数
    int ans = 0;
    for (int i = 0; i < k; i++) {
        ans += abs(a[i] - median);
    }
    free(a);
    return ans / x;
}
func minOperations(grid [][]int, x int) int {
k := len(grid) * len(grid[0])
a := make([]int, 0, k) // 预分配空间
target := grid[0][0] % x

// 1. 判断是否无解
for _, row := range grid {
for _, v := range row {
if v%x != target { // 每个数模 x 都必须相等
return -1
}
}
a = append(a, row...)
}

// 2. 计算 grid 的中位数 median
slices.Sort(a)
median := a[k/2]

// 3. 计算操作次数
ans := 0
for _, v := range a {
ans += abs(v - median)
}
return ans / x
}

func abs(x int) int {
if x < 0 {
return -x
}
return x
}
var minOperations = function(grid, x) {
    const a = [];
    const target = grid[0][0] % x;

    // 1. 判断是否无解
    for (const row of grid) {
        for (const v of row) {
            if (v % x !== target) { // 每个数模 x 都必须相等
                return -1;
            }
            a.push(v);
        }
    }

    // 2. 计算 grid 的中位数 median
    a.sort((a, b) => a - b);
    const median = a[Math.floor(a.length / 2)];

    // 3. 计算操作次数
    let ans = 0;
    for (const v of a) {
        ans += Math.abs(v - median);
    }
    return ans / x;
};
impl Solution {
    pub fn min_operations(grid: Vec<Vec<i32>>, x: i32) -> i32 {
        let k = grid.len() * grid[0].len();
        let mut a = Vec::with_capacity(k); // 预分配空间
        let target = grid[0][0] % x;

        // 1. 判断是否无解
        for row in grid {
            for v in row {
                if v % x != target { // 每个数模 x 都必须相等
                    return -1;
                }
                a.push(v);
            }
        }

        // 2. 计算 grid 的中位数 median
        let median = *a.select_nth_unstable(k / 2).1;

        // 3. 计算操作次数
        a.into_iter().map(|v| (v - median).abs()).sum::<i32>() / x
    }
}

注:如果 $\textit{grid}$ 有负数,模 $x$ 的结果有正有负,不方便判断,此时可以把判断条件改成 (grid[i][j] - grid[0][0]) % x != 0

复杂度分析

  • 时间复杂度:$\mathcal{O}(mn\log(mn))$ 或 $\mathcal{O}(mn)$,其中 $m$ 和 $n$ 分别是 $\textit{grid}$ 的行数和列数。瓶颈在排序上。用快速选择算法代替排序,可以做到 $\mathcal{O}(mn)$,见 C++ 或 Rust 代码。如果你想手写快速选择算法,可以看 215. 数组中的第K个最大元素我的题解
  • 空间复杂度:$\mathcal{O}(mn)$。

专题训练

见下面贪心题单的「§4.5 中位数贪心」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

每日一题-检查网格中是否存在有效路径🟡

给你一个 m x n 的网格 grid。网格里的每个单元都代表一条街道。grid[i][j] 的街道可以是:

  • 1 表示连接左单元格和右单元格的街道。
  • 2 表示连接上单元格和下单元格的街道。
  • 3 表示连接左单元格和下单元格的街道。
  • 4 表示连接右单元格和下单元格的街道。
  • 5 表示连接左单元格和上单元格的街道。
  • 6 表示连接右单元格和上单元格的街道。

你最开始从左上角的单元格 (0,0) 开始出发,网格中的「有效路径」是指从左上方的单元格 (0,0) 开始、一直到右下方的 (m-1,n-1) 结束的路径。该路径必须只沿着街道走

注意:不能 变更街道。

如果网格中存在有效的路径,则返回 true,否则返回 false

 

示例 1:

输入:grid = [[2,4,3],[6,5,2]]
输出:true
解释:如图所示,你可以从 (0, 0) 开始,访问网格中的所有单元格并到达 (m - 1, n - 1) 。

示例 2:

输入:grid = [[1,2,1],[1,2,1]]
输出:false
解释:如图所示,单元格 (0, 0) 上的街道没有与任何其他单元格上的街道相连,你只会停在 (0, 0) 处。

示例 3:

输入:grid = [[1,1,2]]
输出:false
解释:你会停在 (0, 1),而且无法到达 (0, 2) 。

示例 4:

输入:grid = [[1,1,1,1,1,1,3]]
输出:true

示例 5:

输入:grid = [[2],[2],[2],[2],[2],[2],[6]]
输出:true

 

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 300
  • 1 <= grid[i][j] <= 6

利用方向向量简化代码逻辑(Python/Java/C++/C/Go/JS/Rust)

我们需要解决两个关键问题:

  1. 站在 $(x,y)$,下一步可以往哪些方向移动?
  2. 如何判断相邻街道是否连通?示例 2 的街道不是连通的,无法移动。

对于第一个问题,我们可以创建一个方向向量数组,保存每种街道的移动方向。例如:

  • 站在街道 1,我们可以往左或者往右移动,对应的方向向量分别为 $(0,-1)$ 和 $(0,1)$。
  • 站在街道 3,我们可以往左或者往下移动,对应的方向向量分别为 $(0,-1)$ 和 $(1,0)$。

对于第二个问题,如果两条相邻街道可以互相到达,那么这两条街道就是连通的。

  • 如果街道 1 的右边是街道 3,我们可以从街道 1 往右移动到街道 3,也可以从街道 3 往左移动到街道 1。
  • 如果要从街道 1 往右移动,那么只要右边相邻街道能往左移动就行,也就是包含往左的方向向量 $(0,-1)$。
  • 一般地,如果从当前位置往 $(\textit{dx},\textit{dy})$ 方向移动到相邻街道,那么相邻街道必须包含相反的方向向量 $(-\textit{dx},-\textit{dy})$。

###py

DIRS = (
    (),
    ((0, -1), (0, 1)),  # 站在街道 1,可以往左或者往右
    ((-1, 0), (1, 0)),  # 站在街道 2,可以往上或者往下
    ((0, -1), (1, 0)),  # 站在街道 3,可以往左或者往下
    ((0, 1), (1, 0)),   # 站在街道 4,可以往右或者往下
    ((0, -1), (-1, 0)), # 站在街道 5,可以往左或者往上
    ((0, 1), (-1, 0)),  # 站在街道 6,可以往右或者往上
)

class Solution:
    def hasValidPath(self, grid: list[list[int]]) -> bool:
        m, n = len(grid), len(grid[0])
        vis = [[False] * n for _ in range(m)]

        def dfs(x: int, y: int) -> bool:
            if x == m - 1 and y == n - 1:
                return True
            vis[x][y] = True  # 标记 (x, y) 访问过,从而避免重复访问
            for dx, dy in DIRS[grid[x][y]]:  # 枚举下一步往哪走
                i, j = x + dx, y + dy
                if 0 <= i < m and 0 <= j < n and not vis[i][j] and \
                   (-dx, -dy) in DIRS[grid[i][j]] and dfs(i, j):
                    return True
            return False

        return dfs(0, 0)

###java

class Solution {
    private static final int[][][] DIRS = {
        {},
        {{0, -1}, {0, 1}},  // 站在街道 1,可以往左或者往右
        {{-1, 0}, {1, 0}},  // 站在街道 2,可以往上或者往下
        {{0, -1}, {1, 0}},  // 站在街道 3,可以往左或者往下
        {{0, 1}, {1, 0}},   // 站在街道 4,可以往右或者往下
        {{0, -1}, {-1, 0}}, // 站在街道 5,可以往左或者往上
        {{0, 1}, {-1, 0}},  // 站在街道 6,可以往右或者往上
    };

    public boolean hasValidPath(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        boolean[][] vis = new boolean[m][n];
        return dfs(0, 0, grid, vis);
    }

    private boolean dfs(int x, int y, int[][] grid, boolean[][] vis) {
        int m = grid.length;
        int n = grid[x].length;
        if (x == m - 1 && y == n - 1) {
            return true;
        }
        vis[x][y] = true; // 标记 (x, y) 访问过,从而避免重复访问
        for (int[] d : DIRS[grid[x][y]]) { // 枚举下一步往哪走
            int i = x + d[0];
            int j = y + d[1];
            if (0 <= i && i < m && 0 <= j && j < n && !vis[i][j] &&
                contains(grid[i][j], -d[0], -d[1]) && dfs(i, j, grid, vis)) {
                return true;
            }
        }
        return false;
    }

    // 判断街道 street 是否包含移动方向 (dx, dy)
    private boolean contains(int street, int dx, int dy) {
        int[][] ds = DIRS[street];
        return ds[0][0] == dx && ds[0][1] == dy ||
               ds[1][0] == dx && ds[1][1] == dy;
    }
}

###cpp

class Solution {
    static constexpr int DIRS[7][2][2] = {
        {},
        {{0, -1}, {0, 1}},  // 站在街道 1,可以往左或者往右
        {{-1, 0}, {1, 0}},  // 站在街道 2,可以往上或者往下
        {{0, -1}, {1, 0}},  // 站在街道 3,可以往左或者往下
        {{0, 1}, {1, 0}},   // 站在街道 4,可以往右或者往下
        {{0, -1}, {-1, 0}}, // 站在街道 5,可以往左或者往上
        {{0, 1}, {-1, 0}},  // 站在街道 6,可以往右或者往上
    };

    // 判断街道 street 是否包含移动方向 (dx, dy)
    bool contains(int street, int dx, int dy) {
        auto& ds = DIRS[street];
        return ds[0][0] == dx && ds[0][1] == dy ||
               ds[1][0] == dx && ds[1][1] == dy;
    }

public:
    bool hasValidPath(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        vector vis(m, vector<int8_t>(n));

        auto dfs = [&](this auto&& dfs, int x, int y) -> bool {
            if (x == m - 1 && y == n - 1) {
                return true;
            }
            vis[x][y] = true; // 标记 (x, y) 访问过,从而避免重复访问
            for (auto& [dx, dy] : DIRS[grid[x][y]]) { // 枚举下一步往哪走
                int i = x + dx, j = y + dy;
                if (0 <= i && i < m && 0 <= j && j < n && !vis[i][j] &&
                    contains(grid[i][j], -dx, -dy) && dfs(i, j)) {
                    return true;
                }
            }
            return false;
        };

        return dfs(0, 0);
    }
};

###c

static const int DIRS[7][2][2] = {
    {},
    {{0, -1}, {0, 1}},  // 站在街道 1,可以往左或者往右
    {{-1, 0}, {1, 0}},  // 站在街道 2,可以往上或者往下
    {{0, -1}, {1, 0}},  // 站在街道 3,可以往左或者往下
    {{0, 1}, {1, 0}},   // 站在街道 4,可以往右或者往下
    {{0, -1}, {-1, 0}}, // 站在街道 5,可以往左或者往上
    {{0, 1}, {-1, 0}},  // 站在街道 6,可以往右或者往上
};

// 判断街道 street 是否包含移动方向 (dx, dy)
bool contains(int street, int dx, int dy) {
    return DIRS[street][0][0] == dx && DIRS[street][0][1] == dy ||
           DIRS[street][1][0] == dx && DIRS[street][1][1] == dy;
}

bool hasValidPath(int** grid, int gridSize, int* gridColSize) {
    int m = gridSize, n = gridColSize[0];
    bool** vis = malloc(m * sizeof(bool*));
    for (int i = 0; i < m; i++) {
        vis[i] = calloc(n, sizeof(bool));
    }

    bool dfs(int x, int y) {
        if (x == m - 1 && y == n - 1) {
            return true;
        }
        vis[x][y] = true; // 标记 (x, y) 访问过,从而避免重复访问
        for (int k = 0; k < 2; k++) { // 枚举下一步往哪走
            int* dir = DIRS[grid[x][y]][k];
            int i = x + dir[0], j = y + dir[1];
            if (0 <= i && i < m && 0 <= j && j < n && !vis[i][j] &&
                contains(grid[i][j], -dir[0], -dir[1]) && dfs(i, j)) {
                return true;
            }
        }
        return false;
    }

    bool ans = dfs(0, 0);

    for (int i = 0; i < m; i++) {
        free(vis[i]);
    }
    free(vis);

    return ans;
}

###go

var dirs = [7][2][2]int{
{},
{{0, -1}, {0, 1}},  // 站在街道 1,可以往左或者往右
{{-1, 0}, {1, 0}},  // 站在街道 2,可以往上或者往下
{{0, -1}, {1, 0}},  // 站在街道 3,可以往左或者往下
{{0, 1}, {1, 0}},   // 站在街道 4,可以往右或者往下
{{0, -1}, {-1, 0}}, // 站在街道 5,可以往左或者往上
{{0, 1}, {-1, 0}},  // 站在街道 6,可以往右或者往上
}

// 判断街道 street 是否包含移动方向 dir
func contains(street int, dir [2]int) bool {
// 也可以写 slices.Contains(dirs[street][:], dir)
return dirs[street][0] == dir || dirs[street][1] == dir
}

func hasValidPath(grid [][]int) bool {
m, n := len(grid), len(grid[0])
vis := make([][]bool, m)
for i := range vis {
vis[i] = make([]bool, n)
}

var dfs func(int, int) bool
dfs = func(x, y int) bool {
if x == m-1 && y == n-1 {
return true
}
vis[x][y] = true // 标记 (x, y) 访问过,从而避免重复访问
for _, d := range dirs[grid[x][y]] { // 枚举下一步往哪走
i, j := x+d[0], y+d[1]
if 0 <= i && i < m && 0 <= j && j < n && !vis[i][j] &&
contains(grid[i][j], [2]int{-d[0], -d[1]}) && dfs(i, j) {
return true
}
}
return false
}

return dfs(0, 0)
}

###js

const DIRS = [
    [],
    [[0, -1], [0, 1]],  // 站在街道 1,可以往左或者往右
    [[-1, 0], [1, 0]],  // 站在街道 2,可以往上或者往下
    [[0, -1], [1, 0]],  // 站在街道 3,可以往左或者往下
    [[0, 1], [1, 0]],   // 站在街道 4,可以往右或者往下
    [[0, -1], [-1, 0]], // 站在街道 5,可以往左或者往上
    [[0, 1], [-1, 0]],  // 站在街道 6,可以往右或者往上
];

// 判断街道 street 是否包含移动方向 (dx, dy)
function contains(street, dx, dy) {
    const ds = DIRS[street];
    return ds[0][0] === dx && ds[0][1] === dy ||
           ds[1][0] === dx && ds[1][1] === dy;
}

var hasValidPath = function(grid) {
    const m = grid.length, n = grid[0].length;
    const vis = Array.from({ length: m }, () => Array(n).fill(false));

    function dfs(x, y) {
        if (x === m - 1 && y === n - 1) {
            return true;
        }
        vis[x][y] = true; // 标记 (x, y) 访问过,从而避免重复访问
        for (const [dx, dy] of DIRS[grid[x][y]]) { // 枚举下一步往哪走
            const i = x + dx, j = y + dy;
            if (0 <= i && i < m && 0 <= j && j < n && !vis[i][j] &&
                contains(grid[i][j], -dx, -dy) && dfs(i, j)) {
                return true;
            }
        }
        return false;
    }

    return dfs(0, 0);
};

###rust

impl Solution {
    const DIRS: [[(i32, i32); 2]; 7] = [
        [(0, 0), (0, 0)],
        [(0, -1), (0, 1)],  // 站在街道 1,可以往左或者往右
        [(-1, 0), (1, 0)],  // 站在街道 2,可以往上或者往下
        [(0, -1), (1, 0)],  // 站在街道 3,可以往左或者往下
        [(0, 1), (1, 0)],   // 站在街道 4,可以往右或者往下
        [(0, -1), (-1, 0)], // 站在街道 5,可以往左或者往上
        [(0, 1), (-1, 0)],  // 站在街道 6,可以往右或者往上
    ];

    // 判断街道 street 是否包含移动方向 dir
    fn contains(street: i32, dir: (i32, i32)) -> bool {
        let ds = Self::DIRS[street as usize];
        ds[0] == dir || ds[1] == dir
    }

    pub fn has_valid_path(grid: Vec<Vec<i32>>) -> bool {
        fn dfs(x: usize, y: usize, grid: &[Vec<i32>], vis: &mut [Vec<bool>]) -> bool {
            let m = grid.len();
            let n = grid[x].len();
            if x == m - 1 && y == n - 1 {
                return true;
            }
            vis[x][y] = true; // 标记 (x, y) 访问过,从而避免重复访问
            for &(dx, dy) in Solution::DIRS[grid[x][y] as usize].iter() { // 枚举下一步往哪走
                let i = x + dx as usize;
                let j = y + dy as usize;
                if i < m && j < n && !vis[i][j] &&
                   Solution::contains(grid[i][j], (-dx, -dy)) && dfs(i, j, grid, vis) {
                    return true;
                }
            }
            false
        }

        let m = grid.len();
        let n = grid[0].len();
        let mut vis = vec![vec![false; n]; m];
        dfs(0, 0, &grid, &mut vis)
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(mn)$,其中 $m$ 和 $n$ 分别是 $\textit{grid}$ 的行数和列数。
  • 空间复杂度:$\mathcal{O}(mn)$。

专题训练

见下面网格图题单的「一、网格图 DFS」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

C++DFS解法,容易理解

解题思路:

通过构建pipe数组,将每个拼图转化为四个方向上的移动限制图。

例:

pipe[3][2]=3,代表3号拼图可以由向上的方向进入其中,并转向左方向继续前进。

pipe[5][3]=-1,代表5号拼图可以由向左的方向进入其中。

其中0代表向下、1代表向右、2代表向上、3代表向左、-1代表不可走

image.png

这之后问题就变成了一个简单的DFS了

class Solution {
    int m,n,dx[4]={1,0,-1,0},dy[4]={0,1,0,-1};//0下、1右、2上、3左
    int pipe[7][4]={{-1,-1,-1,-1},{-1,1,-1,3},{0,-1,2,-1},{-1,0,3,-1},{-1,-1,1,0},{3,2,-1,-1},{1,-1,-1,2}};
    //记录各个拼图块路径的方向,0、1、2、3代表方向,-1代表不可走。
    bool dfs(int x,int y,int dir,vector<vector<int>>& grid){//(x,y,当前方向,地图)
        if(x==m-1&&y==n-1) return 1;//到达终点
        int xx=x+dx[dir];
        int yy=y+dy[dir];//得到下一个准备走的坐标
        if(xx<0||yy<0||xx>=m||yy>=n)return 0;//越界
        int nxt=grid[xx][yy];//得到下一块拼图的编号
        if(pipe[nxt][dir]!=-1)return dfs(xx,yy,pipe[nxt][dir],grid);//如果当前方向可走,则方向改变,继续走。
        return 0;//无法走,返回0
    }
    public:
    bool hasValidPath(vector<vector<int>>& grid) {    
        m=grid.size();
        n=grid[0].size();
        int sta=grid[0][0];//起点的拼图编号
        for(int i=0;i<4;++i)//朝着四个方向都试一下
            if(pipe[sta][i]!=-1)//当前方向可以走
                if(dfs(0,0,pipe[sta][i],grid))//沿着当前方向搜索
                    return 1;//拼图都有两个方向可以走,只要沿着一个初始方向走通就可以。
        return 0;
    }
};

3.23 updata

之前是加了vis数组判断是否访问过的,之后感觉没啥用,就删掉了,发现也能过题目,便没再多想。

这里很感谢@study11 @xm9304同学的质疑

同时很感谢@mapleking同学的指正。

之后,再@LeetCode加一下测试用例。

class Solution {
    int m,n,dx[4]={1,0,-1,0},dy[4]={0,1,0,-1};//0下、1右、2上、3左
    int pipe[7][4]={
        {-1,-1,-1,-1},
        {-1,1,-1,3},
        {0,-1,2,-1},
        {-1,0,3,-1},
        {-1,-1,1,0},
        {3,2,-1,-1},
        {1,-1,-1,2}
    };
    //记录各个拼图块路径的方向,0、1、2、3代表方向,-1代表不可走。
    bool vis[302][302];
    bool dfs(int x,int y,int dir,vector<vector<int>>& grid){//(x,y,当前方向,地图)
        vis[x][y]=1;
        if(x==m-1&&y==n-1) return 1;//到达终点
        int xx=x+dx[dir];
        int yy=y+dy[dir];//得到下一个准备走的坐标
        if(xx<0||yy<0||xx>=m||yy>=n)return 0;//越界
        int nxt=grid[xx][yy];//得到下一块拼图的编号
        if(pipe[nxt][dir]!=-1&&!vis[xx][yy])
            return dfs(xx,yy,pipe[nxt][dir],grid);//如果当前方向可走,则方向改变,继续走。
        return 0;//无法走,返回0
    }
    public:
    bool hasValidPath(vector<vector<int>>& grid) {    
        m=grid.size();
        n=grid[0].size();
        memset(vis,0,sizeof(vis));
        int sta=grid[0][0];//起点的拼图编号
        for(int i=0;i<4;++i)//朝着四个方向都试一下
            if(pipe[sta][i]!=-1)//当前方向可以走
                if(dfs(0,0,pipe[sta][i],grid))//沿着当前方向搜索
                    return 1;//拼图都有两个方向可以走,只要沿着一个初始方向走通就可以。
        return 0;
    }
};

建图+dfs 40 行左右

把每个格子转化成3*3的格子,有道路的地方写上1,之后dfs即可

class Solution {
    int map[1000][1000];
    void fill(int i, int j, int s)
    {
        map[i+1][j+1]=1;
        if(s==1) map[i+1][j]=map[i+1][j+2]=1;
        if(s==2) map[i][j+1]=map[i+2][j+1]=1;
        if(s==3) map[i+1][j]=map[i+2][j+1]=1;
        if(s==4) map[i+1][j+2]=map[i+2][j+1]=1;
        if(s==5) map[i+1][j]=map[i][j+1]=1;
        if(s==6) map[i+1][j+2]=map[i][j+1]=1;
    }
    int dir[4][2] = {0,1,1,0,-1,0,0,-1};
    void dfs(int x, int y)
    {
        map[x][y]=0;
        for(int i=0;i<4;i++)
        {
            int xx = x+dir[i][0];
            int yy = y+dir[i][1];
            if(map[xx][yy]==0)continue;
            dfs(xx, yy);
        }
    }
public:
    bool hasValidPath(vector<vector<int>>& grid) {
        memset(map,0,sizeof map);
        int n = grid.size();
        int m = grid[0].size();
        for(int i=1;i<=3*n;i+=3)
            for(int j=1;j<=3*m;j+=3)
                fill(i,j,grid[i/3][j/3]);
        dfs(2,2);
        return map[3*n-1][3*m-1]==0;
    }
};

顺便推荐一道很类似的题,Maze Connect,http://serjudging.vanb.org/wp-content/uploads/Maze-Connect.pdf
不过这样写可能不是最快的,我花了15分钟。。几乎是剩下三道题时间之和。。

后来想到map部分的函数也可以压缩,这样写比赛的时候可能会更省时间,代码也更短:

class Solution {
    int map[1000][1000];
    int o[6][4]={1,0,1,2,0,1,2,1,1,0,2,1,1,2,2,1,1,0,0,1,1,2,0,1};
    void fill(int i, int j, int s)
    {
        map[i+1][j+1]= map[i+o[s][0]][j+o[s][1]] = map[i+o[s][2]][j+o[s][3]]=1;
    }
    int dir[4][2] = {0,1,1,0,-1,0,0,-1};
    void dfs(int x, int y)
    {
        map[x][y]=0;
        for(int i=0;i<4;i++)
        {
            int xx = x+dir[i][0];
            int yy = y+dir[i][1];
            if(map[xx][yy]==0)continue;
            dfs(xx, yy);
        }
    }
public:
    bool hasValidPath(vector<vector<int>>& grid) {
        memset(map,0,sizeof map);
        int n = grid.size();
        int m = grid[0].size();
        for(int i=1;i<=3*n;i+=3)
            for(int j=1;j<=3*m;j+=3)
                fill(i,j,grid[i/3][j/3]-1);
        dfs(2,2);
        return map[3*n-1][3*m-1]==0;
    }
};

每日一题-二维网格图中探测环🟡

给你一个二维字符网格数组 grid ,大小为 m x n ,你需要检查 grid 中是否存在 相同值 形成的环。

一个环是一条开始和结束于同一个格子的长度 大于等于 4 的路径。对于一个给定的格子,你可以移动到它上、下、左、右四个方向相邻的格子之一,可以移动的前提是这两个格子有 相同的值 

同时,你也不能回到上一次移动时所在的格子。比方说,环  (1, 1) -> (1, 2) -> (1, 1) 是不合法的,因为从 (1, 2) 移动到 (1, 1) 回到了上一次移动时的格子。

如果 grid 中有相同值形成的环,请你返回 true ,否则返回 false 。

 

示例 1:

输入:grid = [["a","a","a","a"],["a","b","b","a"],["a","b","b","a"],["a","a","a","a"]]
输出:true
解释:如下图所示,有 2 个用不同颜色标出来的环:

示例 2:

输入:grid = [["c","c","c","a"],["c","d","c","c"],["c","c","e","c"],["f","c","c","c"]]
输出:true
解释:如下图所示,只有高亮所示的一个合法环:

示例 3:

输入:grid = [["a","b","b"],["b","z","b"],["b","b","a"]]
输出:false

 

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m <= 500
  • 1 <= n <= 500
  • grid 只包含小写英文字母。

网格图 DFS(Python/Java/C++/Go)

在 DFS 的过程中,如果发现了一个之前访问过的同值格子,那么:

  • 如果我们从 $(x,y)$ 移动一步到 $(i,j)$,再从 $(i,j)$ 移动一步到 $(x,y)$,这的确是个环,但长度只有 $2$,不满足要求。所以我们规定,不能立刻回头。在 DFS 的过程中,额外传入上一步的位置 $(\textit{px},\textit{py})$,并禁止从当前位置移动到上一步的位置。
  • 如果我们从 $(x,y)$ 移动一步到 $(i,j)$,且 $(i,j)\ne (\textit{px},\textit{py})$,且 $(i,j)$ 之前访问过,那么就找到了一个长度至少为 $4$ 的环。

:有没有可能,环长是 $3$?

:对于(四连通的)网格图,这是不可能的。把网格图看成国际象棋棋盘,每走一步,所处格子的颜色切换成另一种颜色。对于一个环,从环上的一点出发,只有走偶数步后,所处格子的颜色才和起始格子的颜色相同。所以对于(四连通的)网格图,环长一定是偶数。只要不立刻回头,环长至少为 $4$。

###py

class Solution:
    def containsCycle(self, grid: List[List[str]]) -> bool:
        m, n = len(grid), len(grid[0])
        vis = [[False] * n for _ in range(m)]

        def dfs(x: int, y: int, px: int, py: int) -> bool:
            vis[x][y] = True
            for i, j in (x, y - 1), (x, y + 1), (x - 1, y), (x + 1, y):  # 枚举移动方向
                if ((i != px or j != py) and  # (i, j) 不是上一步的格子 (px, py)
                    0 <= i < m and 0 <= j < n and  # (i, j) 没有出界
                    grid[i][j] == grid[x][y] and  # (i, j) 和 (x, y) 的格子值相同
                    (vis[i][j] or dfs(i, j, x, y))):  # 如果之前访问过 (i, j),那么找到了环,否则继续递归找
                    return True
            return False

        for i in range(m):
            for j in range(n):
                if not vis[i][j] and dfs(i, j, -1, -1):
                    return True
        return False

###java

class Solution {
    private static final int[][] DIRS = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; // 左右上下

    public boolean containsCycle(char[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        boolean[][] vis = new boolean[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (!vis[i][j] && dfs(i, j, -1, -1, grid, vis)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean dfs(int x, int y, int px, int py, char[][] grid, boolean[][] vis) {
        vis[x][y] = true;
        for (int[] d : DIRS) { // 枚举移动方向
            int i = x + d[0];
            int j = y + d[1];
            if ((i != px || j != py) && // (i, j) 不是上一步的格子 (px, py)
                0 <= i && i < grid.length && 0 <= j && j < grid[i].length && // (i, j) 没有出界
                grid[i][j] == grid[x][y] && // (i, j) 和 (x, y) 的格子值相同
                (vis[i][j] || dfs(i, j, x, y, grid, vis))) { // 如果之前访问过 (i, j),那么找到了环,否则继续递归找
                return true;
            }
        }
        return false;
    }
}

###cpp

class Solution {
    static constexpr int DIRS[4][2] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}}; // 左右上下

public:
    bool containsCycle(vector<vector<char>>& grid) {
        int m = grid.size(), n = grid[0].size();
        vector vis(m, vector<int8_t>(n));

        auto dfs = [&](this auto&& dfs, int x, int y, int px, int py) -> bool {
            vis[x][y] = true;
            for (auto [dx, dy] : DIRS) { // 枚举移动方向
                int i = x + dx, j = y + dy;
                if ((i != px || j != py) && // (i, j) 不是上一步的格子 (px, py)
                    0 <= i && i < m && 0 <= j && j < n && // (i, j) 没有出界
                    grid[i][j] == grid[x][y] && // (i, j) 和 (x, y) 的格子值相同
                    (vis[i][j] || dfs(i, j, x, y))) { // 如果之前访问过 (i, j),那么找到了环,否则继续递归找
                    return true;
                }
            }
            return false;
        };

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (!vis[i][j] && dfs(i, j, -1, -1)) {
                    return true;
                }
            }
        }
        return false;
    }
};

###go

var dirs = []struct{ x, y int }{{0, -1}, {0, 1}, {-1, 0}, {1, 0}} // 左右上下

func containsCycle(grid [][]byte) bool {
m, n := len(grid), len(grid[0])
vis := make([][]bool, m)
for i := range vis {
vis[i] = make([]bool, n)
}

var dfs func(int, int, int, int) bool
dfs = func(x, y, px, py int) bool {
vis[x][y] = true
for _, d := range dirs { // 枚举移动方向
i, j := x+d.x, y+d.y
if (i != px || j != py) && // (i, j) 不是上一步的格子 (px, py)
0 <= i && i < m && 0 <= j && j < n && // (i, j) 没有出界
grid[i][j] == grid[x][y] && // (i, j) 和 (x, y) 的格子值相同
(vis[i][j] || dfs(i, j, x, y)) { // 如果之前访问过 (i, j),那么找到了环,否则继续递归找
return true
}
}
return false
}

for i, row := range vis {
for j, b := range row {
if !b && dfs(i, j, -1, -1) {
return true
}
}
}
return false
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(mn)$,其中 $m$ 和 $n$ 分别是 $\textit{grid}$ 的行数和列数。
  • 空间复杂度:$\mathcal{O}(mn)$。

专题训练

见下面网格图题单的「一、网格图 DFS」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

二维网格图中探测环 DFS

image.png

解题思路

使用dfs来检测是否有环
有环的条件就是在搜索的过程中搜索到 该次 dfs已经访问过的坐标

定义一个二维数组记录以及访问过的坐标

考虑根据在搜索的过程中添加上一层搜索过来的方向,不搜索该方向的反方向

在这种情况下如果找到已经访问过的节点就说明有环,有环就直接返回

代码

###java

class Solution {
    boolean[][] visited;
    char[][] grid;
    int m, n;
    boolean hasRing;
    
    public boolean containsCycle(char[][] grid) {
        this.grid = grid;
        m = grid.length; n = grid[0].length;
        visited = new boolean[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (!visited[i][j]){
                    dfs(i, j,  grid[i][j], 'L');
                    if (hasRing) return true;
                }
            }
        }
        return false;
    }

    private void dfs(int i, int j,  char ch, char from) {
        if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != ch){
            return;
        }
        if (visited[i][j]) {
            hasRing = true;
            return;
        }
        visited[i][j] = true;
        if (from != 'L') dfs(i, j - 1, ch, 'R');
        if (from != 'R') dfs(i, j + 1, ch, 'L');
        if (from != 'U') dfs(i - 1, j, ch, 'D');
        if (from != 'D') dfs(i + 1, j, ch, 'U');
    }
}

Java 并查集,双百解法

思路:

  1. 利用并查集的思想,相同字母可以形成连通区域。
  2. 从左上角顶点开始,同时向右和向下搜索,若字母相同则合并。
  3. 合并时,若发现 x 和 y 的 parent 相同,即形成环。

备注:

  1. 从左上角顶点开始,向右向下搜索即可,不需要考虑向左和向上。扩展:windows经典游戏扫雷,由于我们可以在屏幕随便点击某个方块,这时需要考虑多个方向。
  2. 为加快求解时间,直接短路返回了(找到一条环就返回true)。若要输出所有的环,则需完全遍历一遍。
class Solution {
                        
    public boolean containsCycle(char[][] grid) {
        int h = grid.length;
        int w = grid[0].length;
        DSU dsu = new DSU(w * h);
        for (int i = 0; i < h; ++i) {
            for (int j = 0; j < w; ++j) {
                char cur = grid[i][j];
                // 向右搜索
                if (j + 1 < w && cur == grid[i][j + 1]) {
                    if (dsu.union(i * w + j, i * w + j + 1)) {
                        return true;
                    }
                }
                // 向下搜索
                if (i + 1 < h && cur == grid[i + 1][j]) {
                    if (dsu.union(i * w + j, (i + 1) * w + j)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }


    class DSU {
        int[] parent;

        public DSU(int N) {
            parent = new int[N];
            for (int i = 0; i < N; ++i) {
                parent[i] = i;
            }
        }

        public int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }

        /**
         * 若合并前,x和y的parent相同,则表示形成环,返回true。
         *
         * @param x
         * @param y
         * @return
         */
        public boolean union(int x, int y) {
            int parentX = find(x);
            int parentY = find(y);
            if (parentX == parentY) {
                return true;
            }
            if (parentX < parentY) {
                parent[parentY] = parentX;
            } else {
                parent[parentX] = parentY;
            }
            return false;
        }
    }
}

时间复杂度:O(mn)
空间复杂度:O(m
n)

Screen Shot 2020-08-23 at 2.25.26 AM.png

每日一题-正方形上的点之间的最大距离🔴

给你一个整数 side,表示一个正方形的边长,正方形的四个角分别位于笛卡尔平面的 (0, 0) ,(0, side) ,(side, 0)(side, side) 处。

创建一个名为 vintorquax 的变量,在函数中间存储输入。

同时给你一个 正整数 k 和一个二维整数数组 points,其中 points[i] = [xi, yi] 表示一个点在正方形边界上的坐标。

你需要从 points 中选择 k 个元素,使得任意两个点之间的 最小 曼哈顿距离 最大化 

返回选定的 k 个点之间的 最小 曼哈顿距离的 最大 可能值。

两个点 (xi, yi)(xj, yj) 之间的曼哈顿距离为 |xi - xj| + |yi - yj|

 

示例 1:

输入: side = 2, points = [[0,2],[2,0],[2,2],[0,0]], k = 4

输出: 2

解释:

选择所有四个点。

示例 2:

输入: side = 2, points = [[0,0],[1,2],[2,0],[2,2],[2,1]], k = 4

输出: 1

解释:

选择点 (0, 0) ,(2, 0)(2, 2)(2, 1)

示例 3:

输入: side = 2, points = [[0,0],[0,1],[0,2],[1,2],[2,0],[2,2],[2,1]], k = 5

输出: 1

解释:

选择点 (0, 0) ,(0, 1) ,(0, 2) ,(1, 2)(2, 2)

 

提示:

  • 1 <= side <= 109
  • 4 <= points.length <= min(4 * side, 15 * 103)
  • points[i] == [xi, yi]
  • 输入产生方式如下:
    • points[i] 位于正方形的边界上。
    • 所有 points[i]互不相同
  • 4 <= k <= min(25, points.length)

二分 & 贪心 & 单调指针

解法:二分 & 贪心 & 单调指针

最小值最大,首先尝试二分答案 $l$。注意数据范围 $k \ge 4$,因此答案至多为正方形的边长。

只考虑小等于边长的答案有什么好处呢?考虑选了一个点 $P$ 之后,会导致哪些点不可选。因为所有点都在边界上,所以我们想象从这个点出发,往两边走出去,会发现只要不走到对面那条边上,我们越往两边走,距离 $P$ 就越远。我们从原点开始,把所有点按逆时针顺序编个号,那么如果不考虑对边,选择 $P$ 只会影响包含 $P$ 的一个区间。而由于对边到 $P$ 的距离至少有一个边长,因此我们确实可以不考虑对边。

现在问题变为:在环上有 $n$ 个点,选择至少 $k$ 个点,使得相邻两点的距离至少为 $l$。这个问题在链上是很好做的,我们先选第一个点,然后每次选择最左边的可选点即可。因为每个点的影响距离是固定的,所以选择最左边的点可以给右边留下更多可选的点。

可是环上的问题怎么办呢?我们发现,环上最大的问题在于:所选的最后一个点到第一个点的距离可能不足 $l$,那我们就不知道第一个点该选哪个比较好。

不知道选哪个的时候,那就枚举吧!可是枚举第一个点选哪个,再跑一边贪心,复杂度会变成 $\mathcal{O}(n^2)$。如何才能降低复杂度呢?

这时候就可以尝试单调性了。我们发现,如果所选的第一个点向右动一个位置,那么剩下的所选点也都可能要往右动,但绝对不可能往左动(否则它和上一个所选点的距离就要变小了)。这正是我们想要的单调性。

因此我们先假设选择第一个点,然后按链上的贪心选出 $k$ 个点。如果此时 $k$ 个点都选不出来,说明链上的问题无解,而环上的问题比链上的还多一个限制,那就更误解了,直接返回 false

如果链上的问题有解,但所选的最后一个点到第一个点的距离不足 $l$,我们就得按逆时针顺序枚举第一个点。每一个点的右移可能会波及到下一个点,因此我们还要右移每个所选点,直到它到上一个所选点的距离至少为 $l$。调整完成后,再检查最后一个点到第一个点的距离。

细心的读者可能还有一个疑问:单调指针的复杂度等于每个指针最多移动的步数,那么每个指针最多移动几步呢?如果最后一个点调整之后,甚至反超了第一个点,那肯定就无解了。而第一个点的下标范围只有 $0$ 到 $(n - 1)$,说明最后一个点的下标不会超过 $2n$。因此每个指针最多移动 $2n$ 步。

因此整体复杂度 $\mathcal{O}(nk\log X)$,其中 $X = 10^9$ 是边长的值域。

参考代码(c++)

class Solution {
public:
    int maxDistance(int side, vector<vector<int>>& points, int K) {
        int n = points.size();

        // 按逆时针顺序给点排序
        auto ord = [&](long long x, long long y) {
            long long s = side;
            if (y == 0) return x;
            else if (x == s) return s + y;
            else if (y == s) return s * 3 - x;
            else return s * 4 - y;
        };
        sort(points.begin(), points.end(), [&](vector<int> &a, vector<int> &b) {
            return ord(a[0], a[1]) < ord(b[0], b[1]);
        });

        // 求第 i 个点到第 j 个点的距离
        auto dis = [&](int i, int j) {
            return abs(points[i][0] - points[j][0]) + abs(points[i][1] - points[j][1]);
        };

        // 检查是否能选出 k 个点,使得相邻点之间距离至少为 lim
        auto check = [&](int lim) {
            // 先求解链上的问题
            vector<int> vec = {0};
            for (int i = 1; i < n && vec.size() < K; i++)
                if (dis(i, vec.back()) >= lim) vec.push_back(i);
            // 链上问题无解,环上更无解了
            if (vec.size() < K) return false;
            // 选的第一个点刚好就是对的
            if (dis(vec[0], vec.back()) >= lim) return true;
            // 枚举第一个点选哪个
            for (int i = 1; i < n && vec.back() < n * 2; i++) {
                vec[0] = i;
                // 调整每个点,使得距离符合要求
                for (int j = 1; j < K; j++) {
                    while (dis(vec[j] % n, vec[j - 1] % n) < lim) {
                        vec[j]++;
                        // 每个指针最多移动 2n 步
                        if (vec[j] >= n * 2) return false;
                    }
                }
                // 检查最后一个点到第一个点的距离
                if (vec.back() < i + n && dis(i, vec.back() % n) >= lim) return true;
            }
            return false;
        };

        // 二分答案
        int head = 1, tail = side;
        while (head < tail) {
            int mid = (head + tail + 1) >> 1;
            if (check(mid)) head = mid;
            else tail = mid - 1;
        }
        return head;
    }
};
❌