阅读视图

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

[Python3/Java/C++/Go] 一题一解:记忆化搜索(清晰题解)

方法一:记忆化搜索

我们设计一个函数 $\text{dfs}(i, j, k, \textit{cnt})$,表示上一个位置为 $(i, j)$,当前方向为 $k$,剩余可转向次数为 $\textit{cnt}$ 时,返回最长的 V 形对角线段长度。

函数 $\text{dfs}$ 的执行逻辑如下:

我们首先基于上一个位置以及当前的方向,计算当前得到当前位置 $(x, y)$,然后计算当前目标值 $\textit{target}$。如果 $x$ 或 $y$ 不在矩阵范围内,或者 $\textit{grid}[x][y] \neq \textit{target}$,返回 $0$。

否则,我们有两种选择:

  1. 继续沿着当前方向前进。
  2. 在当前位置进行顺时针 90 度转向,然后继续前进。

我们可以通过改变方向来实现顺时针 90 度转向。具体来说,如果当前方向为 $k$,则顺时针 90 度转向后的新方向为 $(k + 1) \bmod 4$。最后,我们选择这两种选择中的最大值作为当前状态的结果。

在主函数中,我们遍历整个矩阵,对于每个值为 1 的位置,尝试四个方向的搜索,并更新答案。

遍历结束后,返回答案即可。

为了避免重复计算,我们使用记忆化搜索来缓存中间结果。

###python

class Solution:
    def lenOfVDiagonal(self, grid: List[List[int]]) -> int:
        @cache
        def dfs(i: int, j: int, k: int, cnt: int) -> int:
            x, y = i + dirs[k], j + dirs[k + 1]
            target = 2 if grid[i][j] == 1 else (2 - grid[i][j])
            if not 0 <= x < m or not 0 <= y < n or grid[x][y] != target:
                return 0
            res = dfs(x, y, k, cnt)
            if cnt > 0:
                res = max(res, dfs(x, y, (k + 1) % 4, 0))
            return 1 + res

        m, n = len(grid), len(grid[0])
        dirs = (1, 1, -1, -1, 1)
        ans = 0
        for i, row in enumerate(grid):
            for j, x in enumerate(row):
                if x == 1:
                    for k in range(4):
                        ans = max(ans, dfs(i, j, k, 1) + 1)
        return ans

###java

class Solution {
    private int m, n;
    private final int[] dirs = {1, 1, -1, -1, 1};
    private Integer[][][][] f;

    public int lenOfVDiagonal(int[][] grid) {
        m = grid.length;
        n = grid[0].length;
        f = new Integer[m][n][4][2];
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    for (int k = 0; k < 4; k++) {
                        ans = Math.max(ans, dfs(grid, i, j, k, 1) + 1);
                    }
                }
            }
        }
        return ans;
    }

    private int dfs(int[][] grid, int i, int j, int k, int cnt) {
        if (f[i][j][k][cnt] != null) {
            return f[i][j][k][cnt];
        }
        int x = i + dirs[k];
        int y = j + dirs[k + 1];
        int target = grid[i][j] == 1 ? 2 : (2 - grid[i][j]);
        if (x < 0 || x >= m || y < 0 || y >= n || grid[x][y] != target) {
            f[i][j][k][cnt] = 0;
            return 0;
        }
        int res = dfs(grid, x, y, k, cnt);
        if (cnt > 0) {
            res = Math.max(res, dfs(grid, x, y, (k + 1) % 4, 0));
        }
        f[i][j][k][cnt] = 1 + res;
        return 1 + res;
    }
}

###cpp

class Solution {
public:
    static constexpr int MAXN = 501;
    int f[MAXN][MAXN][4][2];

    int lenOfVDiagonal(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        int dirs[5] = {1, 1, -1, -1, 1};
        memset(f, -1, sizeof(f));

        auto dfs = [&](this auto&& dfs, int i, int j, int k, int cnt) -> int {
            if (f[i][j][k][cnt] != -1) {
                return f[i][j][k][cnt];
            }
            int x = i + dirs[k];
            int y = j + dirs[k + 1];
            int target = grid[i][j] == 1 ? 2 : (2 - grid[i][j]);
            if (x < 0 || x >= m || y < 0 || y >= n || grid[x][y] != target) {
                f[i][j][k][cnt] = 0;
                return 0;
            }
            int res = dfs(x, y, k, cnt);
            if (cnt > 0) {
                res = max(res, dfs(x, y, (k + 1) % 4, 0));
            }
            f[i][j][k][cnt] = 1 + res;
            return 1 + res;
        };

        int ans = 0;
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1) {
                    for (int k = 0; k < 4; ++k) {
                        ans = max(ans, dfs(i, j, k, 1) + 1);
                    }
                }
            }
        }
        return ans;
    }
};

###go

func lenOfVDiagonal(grid [][]int) int {
m, n := len(grid), len(grid[0])
dirs := []int{1, 1, -1, -1, 1}
f := make([][][4][2]int, m)
for i := range f {
f[i] = make([][4][2]int, n)
}

var dfs func(i, j, k, cnt int) int
dfs = func(i, j, k, cnt int) int {
if f[i][j][k][cnt] != 0 {
return f[i][j][k][cnt]
}

x := i + dirs[k]
y := j + dirs[k+1]

var target int
if grid[i][j] == 1 {
target = 2
} else {
target = 2 - grid[i][j]
}

if x < 0 || x >= m || y < 0 || y >= n || grid[x][y] != target {
f[i][j][k][cnt] = 0
return 0
}

res := dfs(x, y, k, cnt)
if cnt > 0 {
res = max(res, dfs(x, y, (k+1)%4, 0))
}
f[i][j][k][cnt] = res + 1
return res + 1
}

ans := 0
for i := 0; i < m; i++ {
for j := 0; j < n; j++ {
if grid[i][j] == 1 {
for k := 0; k < 4; k++ {
ans = max(ans, dfs(i, j, k, 1)+1)
}
}
}
}
return ans
}

时间复杂度 $O(m \times n)$,空间复杂度 $O(m \times n)$。其中 $m$ 和 $n$ 分别是矩阵的行数和列数。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-最长 V 形对角线段的长度🔴

给你一个大小为 n x m 的二维整数矩阵 grid,其中每个元素的值为 012

V 形对角线段 定义如下:

  • 线段从 1 开始。
  • 后续元素按照以下无限序列的模式排列:2, 0, 2, 0, ...
  • 该线段:
    • 起始于某个对角方向(左上到右下、右下到左上、右上到左下或左下到右上)。
    • 沿着相同的对角方向继续,保持 序列模式 
    • 在保持 序列模式 的前提下,最多允许 一次顺时针 90 度转向 另一个对角方向。

返回最长的 V 形对角线段 的 长度 。如果不存在有效的线段,则返回 0。

 

示例 1:

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

输出: 5

解释:

最长的 V 形对角线段长度为 5,路径如下:(0,2) → (1,3) → (2,4),在 (2,4) 处进行 顺时针 90 度转向 ,继续路径为 (3,3) → (4,2)

示例 2:

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

输出: 4

解释:

最长的 V 形对角线段长度为 4,路径如下:(2,3) → (3,2),在 (3,2) 处进行 顺时针 90 度转向 ,继续路径为 (2,1) → (1,0)

示例 3:

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

输出: 5

解释:

最长的 V 形对角线段长度为 5,路径如下:(0,0) → (1,1) → (2,2) → (3,3) → (4,4)

示例 4:

输入: grid = [[1]]

输出: 1

解释:

最长的 V 形对角线段长度为 1,路径如下:(0,0)

 

提示:

  • n == grid.length
  • m == grid[i].length
  • 1 <= n, m <= 500
  • grid[i][j] 的值为 012

记忆化搜索+最优性剪枝(Python/Java/C++/Go)

想象有一个人在网格图上移动,按照题目要求,移动经过的值必须为 $1,2,0,2,0,\dots$

由于至多转弯一次,所以移动路径不会形成环,可以写一个记忆化搜索,模拟人在网格图上的移动。

定义 $\textit{dfs}(i,j,k,\textit{canTurn},\textit{target})$ 表示在如下约束下的最长移动步数。

  • 上一步的位置在 $(i,j)$。(定义成上一步,方便编程实现)
  • 移动方向为 $\textit{DIRS}[k]$,其中 $\textit{DIRS}$ 是一个长为 $4$ 的方向数组。
  • 是否可以右转,用布尔值 $\textit{canTurn}$ 表示。
  • 当前位置的目标值必须等于 $\textit{target}$。

转移

  • 设 $(i',j')$ 是从 $(i,j)$ 向 $\textit{DIRS}[k]$ 方向移动一步后的位置。
  • 直行:递归到 $\textit{dfs}(i',j',k,\textit{canTurn}, 2-\textit{target})$。这里用 $2-\textit{target}$ 来实现 $2$ 和 $0$ 的切换。也可以写成 $\textit{target}\oplus 2$。
  • 右转:如果 $\textit{canTurn} = \texttt{true}$,那么递归到 $\textit{dfs}(i',j',(k+1)\bmod 4, \texttt{false}, 2-\textit{target})$。其中 $(k+1)\bmod 4$ 表示环形数组 $\textit{DIRS}$ 的下一个元素的下标。如果 $k<3$,那么 $k$ 更新为 $k+1$;否则 $k$ 更新为 $0$。
  • 返回二者的最大值加一。其中加一表示走了一步。

递归边界

  • 出界,返回 $0$。
  • 如果 $\textit{grid}[i'][j']\ne \textit{target}$,返回 $0$。

递归入口

  • 如果 $\textit{grid}[i][j]=1$,那么枚举 $k=0,1,2,3$,递归 $\textit{dfs}(i,j,k,\texttt{true},2)$。其中 $2$ 是因为下一步的值必须是 $2$。

计算所有 $\textit{dfs}(i,j,k,\texttt{true},2)+1$ 的最大值,即为答案。

注意:$\textit{target}$ 无需记忆化,因为知道 $(i,j)$ 就间接知道 $\textit{target}$ 是多少,代码只是为了方便实现,额外传入了 $\textit{target}$。

关于记忆化搜索的原理,请看 动态规划入门:从记忆化搜索到递推【基础算法精讲 17】

本题视频讲解,欢迎点赞关注~

优化前

###py

class Solution:
    def lenOfVDiagonal(self, grid: List[List[int]]) -> int:
        DIRS = (1, 1), (1, -1), (-1, -1), (-1, 1)
        m, n = len(grid), len(grid[0])

        # 上一步在 (i,j),移动方向为 DIRS[k],是否可以右转,当前位置目标值
        @cache  # 缓存装饰器,避免重复计算 dfs 的结果(一行代码实现记忆化)
        def dfs(i: int, j: int, k: int, can_turn: bool, target: int) -> int:
            i += DIRS[k][0]
            j += DIRS[k][1]
            if not (0 <= i < m and 0 <= j < n) or grid[i][j] != target:
                return 0
            res = dfs(i, j, k, can_turn, 2 - target)  # 直行
            if can_turn:
                res = max(res, dfs(i, j, (k + 1) % 4, False, 2 - target))  # 右转
            return res + 1  # 算上当前位置

        ans = 0
        for i, row in enumerate(grid):
            for j, x in enumerate(row):
                if x == 1:
                    # 枚举起始方向
                    for k in range(4):
                        ans = max(ans, dfs(i, j, k, True, 2) + 1)
        return ans

###java

class Solution {
    private static final int[][] DIRS = {{1, 1}, {1, -1}, {-1, -1}, {-1, 1}};

    public int lenOfVDiagonal(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        // 数组维度太多影响效率,这里把 k 和 canTurn 压缩成一个 int
        int[][][] memo = new int[m][n][1 << 3];
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    for (int k = 0; k < 4; k++) { // 枚举起始方向
                        ans = Math.max(ans, dfs(i, j, k, 1, 2, grid, memo) + 1);
                    }
                }
            }
        }
        return ans;
    }

    // 上一步在 (i,j),移动方向为 DIRS[k],是否可以右转,当前位置目标值
    private int dfs(int i, int j, int k, int canTurn, int target, int[][] grid, int[][][] memo) {
        i += DIRS[k][0];
        j += DIRS[k][1];
        if (i < 0 || i >= grid.length || j < 0 || j >= grid[i].length || grid[i][j] != target) {
            return 0;
        }
        int mask = k << 1 | canTurn;
        if (memo[i][j][mask] > 0) { // 之前计算过
            return memo[i][j][mask];
        }
        int res = dfs(i, j, k, canTurn, 2 - target, grid, memo); // 直行
        if (canTurn == 1) {
            res = Math.max(res, dfs(i, j, (k + 1) % 4, 0, 2 - target, grid, memo)); // 右转
        }
        return memo[i][j][mask] = res + 1; // 算上当前位置
    }
}

###cpp

class Solution {
    static constexpr int DIRS[4][2] = {{1, 1}, {1, -1}, {-1, -1}, {-1, 1}};

public:
    int lenOfVDiagonal(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        vector memo(m, vector<array<array<int, 2>, 4>>(n));

        // 上一步在 (i,j),移动方向为 DIRS[k],是否可以右转,当前位置目标值
        auto dfs = [&](this auto&& dfs, int i, int j, int k, bool can_turn, int target) -> int {
            i += DIRS[k][0];
            j += DIRS[k][1];
            if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != target) {
                return 0;
            }
            int& res = memo[i][j][k][can_turn]; // 注意这里是引用
            if (res) { // 之前计算过
                return res;
            }
            res = dfs(i, j, k, can_turn, 2 - target); // 直行
            if (can_turn) {
                res = max(res, dfs(i, j, (k + 1) % 4, false, 2 - target)); // 右转
            }
            return ++res; // 算上当前位置
        };

        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    for (int k = 0; k < 4; k++) { // 枚举起始方向
                        ans = max(ans, dfs(i, j, k, true, 2) + 1);
                    }
                }
            }
        }
        return ans;
    }
};

###go

var DIRS = [4][2]int{{1, 1}, {1, -1}, {-1, -1}, {-1, 1}}

func lenOfVDiagonal(grid [][]int) (ans int) {
m, n := len(grid), len(grid[0])
memo := make([][][4][2]int, m)
for i := range memo {
memo[i] = make([][4][2]int, n)
}

// 上一步在 (i,j),移动方向为 DIRS[k],是否可以右转,当前位置目标值
var dfs func(int, int, int, int, int) int
dfs = func(i, j, k, canTurn, target int) (res int) {
i += DIRS[k][0]
j += DIRS[k][1]
if i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != target {
return
}
p := &memo[i][j][k][canTurn]
if *p > 0 { // 之前计算过
return *p
}
defer func() { *p = res }() // 记忆化
res = dfs(i, j, k, canTurn, 2-target) // 直行
if canTurn == 1 {
res = max(res, dfs(i, j, (k+1)%4, 0, 2-target)) // 右转
}
return res + 1 // 算上当前位置
}

for i, row := range grid {
for j, x := range row {
if x == 1 {
for k := range 4 { // 枚举起始方向
ans = max(ans, dfs(i, j, k, 1, 2)+1)
}
}
}
}
return
}

最优性剪枝

优化一

在递归之前,如果发现即使走到底,移动步数也不会比目前算出的 $\textit{ans}$ 更大,那么不递归。

lc3459.jpg

看图,设当前在 $(i,j)$:

  • 如果一开始往右下走,无论是否右转,每一步都在往下走,所以至多走 $m-i$ 步。
  • 如果一开始往左下走,无论是否右转,每一步都在往左走,所以至多走 $j+1$ 步。
  • 如果一开始往左上走,无论是否右转,每一步都在往上走,所以至多走 $i+1$ 步。
  • 如果一开始往右上走,无论是否右转,每一步都在往右走,所以至多走 $n-j$ 步。

如果理论最大移动步数 $\textit{mx}$ 大于 $\textit{ans}$,那么递归,否则不递归。

优化二

同理,在递归中,如果要右转,可以先判断继续走的理论最大值是否大于 $\textit{res}$,如果大于则递归,否则不递归。

###py

class Solution:
    def lenOfVDiagonal(self, grid: List[List[int]]) -> int:
        DIRS = (1, 1), (1, -1), (-1, -1), (-1, 1)
        m, n = len(grid), len(grid[0])

        @cache
        def dfs(i: int, j: int, k: int, can_turn: bool, target: int) -> int:
            i += DIRS[k][0]
            j += DIRS[k][1]
            if not (0 <= i < m and 0 <= j < n) or grid[i][j] != target:
                return 0
            res = dfs(i, j, k, can_turn, 2 - target)
            if can_turn:
                maxs = (m - i - 1, j, i, n - j - 1)  # 理论最大值(走到底)
                k = (k + 1) % 4
                # 优化二:如果理论最大值没有超过 res,那么不递归
                if maxs[k] > res:
                    res = max(res, dfs(i, j, k, False, 2 - target))
            return res + 1

        ans = 0
        for i, row in enumerate(grid):
            for j, x in enumerate(row):
                if x != 1:
                    continue
                maxs = (m - i, j + 1, i + 1, n - j)  # 理论最大值(走到底)
                for k, mx in enumerate(maxs):  # 枚举起始方向
                    # 优化一:如果理论最大值没有超过 ans,那么不递归
                    if mx > ans:
                        ans = max(ans, dfs(i, j, k, True, 2) + 1)
        return ans

###java

class Solution {
    private static final int[][] DIRS = {{1, 1}, {1, -1}, {-1, -1}, {-1, 1}};

    public int lenOfVDiagonal(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        // 开太多维度影响效率,这里把 k 和 canTurn 压缩成一个 int
        int[][][] memo = new int[m][n][1 << 3];
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] != 1) {
                    continue;
                }
                int[] maxs = {m - i, j + 1, i + 1, n - j}; // 理论最大值(走到底)
                for (int k = 0; k < 4; k++) { // 枚举起始方向
                    if (maxs[k] > ans) { // 优化一:如果理论最大值没有超过 ans,那么不递归
                        ans = Math.max(ans, dfs(i, j, k, 1, 2, grid, memo) + 1);
                    }
                }
            }
        }
        return ans;
    }

    private int dfs(int i, int j, int k, int canTurn, int target, int[][] grid, int[][][] memo) {
        i += DIRS[k][0];
        j += DIRS[k][1];
        if (i < 0 || i >= grid.length || j < 0 || j >= grid[i].length || grid[i][j] != target) {
            return 0;
        }
        int mask = k << 1 | canTurn;
        if (memo[i][j][mask] > 0) {
            return memo[i][j][mask];
        }
        int res = dfs(i, j, k, canTurn, 2 - target, grid, memo);
        if (canTurn == 1) {
            int[] maxs = {grid.length - i - 1, j, i, grid[i].length - j - 1}; // 理论最大值(走到底)
            k = (k + 1) % 4;
            // 优化二:如果理论最大值没有超过 res,那么不递归
            if (maxs[k] > res) {
                res = Math.max(res, dfs(i, j, k, 0, 2 - target, grid, memo));
            }
        }
        return memo[i][j][mask] = res + 1;
    }
}

###cpp

class Solution {
    static constexpr int DIRS[4][2] = {{1, 1}, {1, -1}, {-1, -1}, {-1, 1}};

public:
    int lenOfVDiagonal(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        vector memo(m, vector<array<array<int, 2>, 4>>(n));

        auto dfs = [&](this auto&& dfs, int i, int j, int k, bool can_turn, int target) -> int {
            i += DIRS[k][0];
            j += DIRS[k][1];
            if (i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != target) {
                return 0;
            }
            int& res = memo[i][j][k][can_turn];
            if (res) {
                return res;
            }
            res = dfs(i, j, k, can_turn, 2 - target);
            if (can_turn) {
                int maxs[4] = {m - i - 1, j, i, n - j - 1}; // 理论最大值(走到底)
                k = (k + 1) % 4;
                // 优化二:如果理论最大值没有超过 res,那么不递归
                if (maxs[k] > res) {
                    res = max(res, dfs(i, j, k, false, 2 - target));
                }
            }
            return ++res;
        };

        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] != 1) {
                    continue;
                }
                int maxs[4] = {m - i, j + 1, i + 1, n - j}; // 理论最大值(走到底)
                for (int k = 0; k < 4; k++) { // 枚举起始方向
                    if (maxs[k] > ans) { // 优化一:如果理论最大值没有超过 ans,那么不递归
                        ans = max(ans, dfs(i, j, k, true, 2) + 1);
                    }
                }
            }
        }
        return ans;
    }
};

###go

var DIRS = [4][2]int{{1, 1}, {1, -1}, {-1, -1}, {-1, 1}}

func lenOfVDiagonal(grid [][]int) (ans int) {
m, n := len(grid), len(grid[0])
memo := make([][][4][2]int, m)
for i := range memo {
memo[i] = make([][4][2]int, n)
}

var dfs func(int, int, int, int, int) int
dfs = func(i, j, k, canTurn, target int) (res int) {
i += DIRS[k][0]
j += DIRS[k][1]
if i < 0 || i >= m || j < 0 || j >= n || grid[i][j] != target {
return
}
p := &memo[i][j][k][canTurn]
if *p > 0 {
return *p
}
defer func() { *p = res }()
res = dfs(i, j, k, canTurn, 2-target)
if canTurn == 1 {
maxs := [4]int{m - i - 1, j, i, n - j - 1} // 理论最大值(走到底)
k = (k + 1) % 4
// 优化二:如果理论最大值没有超过 res,那么不递归
if maxs[k] > res {
res = max(res, dfs(i, j, k, 0, 2-target))
}
}
return res + 1
}

for i, row := range grid {
for j, x := range row {
if x != 1 {
continue
}
maxs := [4]int{m - i, j + 1, i + 1, n - j} // 理论最大值(走到底)
for k, mx := range maxs { // 枚举起始方向
if mx > ans { // 优化一:如果理论最大值没有超过 ans,那么不递归
ans = max(ans, dfs(i, j, k, 1, 2)+1)
}
}
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(mn)$,其中 $m$ 和 $n$ 分别为 $\textit{grid}$ 的行数和列数。由于每个状态只会计算一次,动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。本题状态个数等于 $\mathcal{O}(mn)$,单个状态的计算时间为 $\mathcal{O}(1)$,所以总的时间复杂度为 $\mathcal{O}(mn)$。
  • 空间复杂度:$\mathcal{O}(mn)$。保存多少状态,就需要多少空间。

相似题目

更多相似题目,见下面动态规划题单的「二、网格图 DP」。

分类题单

如何科学刷题?

  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站@灵茶山艾府

「模拟」一次遍历O(n)

题目分析

题目给定了若干矩形,每个矩形用 [长, 宽] 表示。
需要找出 对角线最长的矩形,如果有多个矩形对角线长度相同,就返回 面积最大的那个矩形的面积

对角线长度公式:

$$
d = \sqrt{a^2 + b^2}
$$

其中 a, b 是矩形的长和宽。

但我们比较时 不需要真的开根号,因为比较 $\sqrt{a^2+b^2}$ 的大小等价于比较 $a^2+b^2$。

思路

  1. 平方和比较代替对角线

    • 每个矩形对角线长度:$\sqrt{a^2+b^2}$。
    • 比较时,只需比较 a^2+b^2,避免浮点数计算。
  2. 同时存储面积

    • 如果两个矩形的 a^2+b^2 相同,选面积 a*b 较大的。
  3. 逐个遍历

    • 用一个变量 mx 保存当前最优矩形(比较规则:先比对角线平方,再比面积)。

代码

###cpp

class Solution {
public:
    int areaOfMaxDiagonal(vector<vector<int>>& dimensions) {
        pair<int,int> mx{0, 0}; // {对角线平方, 面积}
        for (auto &d : dimensions) {
            int a = d[0], b = d[1];
            pair<int,int> cur = {a*a + b*b, a*b};
            mx = max(mx, cur); // pair 比较:先比 first,再比 second
        }
        return mx.second;
    }
};

复杂度分析

  • 时间复杂度:O(n),遍历一次矩形数组即可。
  • 空间复杂度:O(1),只需常数额外空间。

学习路线

更多算法学习内容以及算法竞赛题单请访问 「 算法特训 」

[Python3/Java/C++/Go/TypeScript] 一题一解:数学(清晰题解)

方法一:数学

根据勾股定理,矩形的对角线的平方为 $l^2 + w^2$,其中 $l$ 和 $w$ 分别是矩形的长度和宽度。

我们可以遍历所有矩形,计算它们的对角线长度的平方,并记录下最大的对角线长度和对应的面积。

遍历结束后,我们返回记录的最大面积。

###python

class Solution:
    def areaOfMaxDiagonal(self, dimensions: List[List[int]]) -> int:
        ans = mx = 0
        for l, w in dimensions:
            t = l**2 + w**2
            if mx < t:
                mx = t
                ans = l * w
            elif mx == t:
                ans = max(ans, l * w)
        return ans

###java

class Solution {
    public int areaOfMaxDiagonal(int[][] dimensions) {
        int ans = 0, mx = 0;
        for (var d : dimensions) {
            int l = d[0], w = d[1];
            int t = l * l + w * w;
            if (mx < t) {
                mx = t;
                ans = l * w;
            } else if (mx == t) {
                ans = Math.max(ans, l * w);
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int areaOfMaxDiagonal(vector<vector<int>>& dimensions) {
        int ans = 0, mx = 0;
        for (auto& d : dimensions) {
            int l = d[0], w = d[1];
            int t = l * l + w * w;
            if (mx < t) {
                mx = t;
                ans = l * w;
            } else if (mx == t) {
                ans = max(ans, l * w);
            }
        }
        return ans;
    }
};

###go

func areaOfMaxDiagonal(dimensions [][]int) (ans int) {
mx := 0
for _, d := range dimensions {
l, w := d[0], d[1]
t := l*l + w*w
if mx < t {
mx = t
ans = l * w
} else if mx == t {
ans = max(ans, l*w)
}
}
return
}

###ts

function areaOfMaxDiagonal(dimensions: number[][]): number {
    let [ans, mx] = [0, 0];
    for (const [l, w] of dimensions) {
        const t = l * l + w * w;
        if (mx < t) {
            mx = t;
            ans = l * w;
        } else if (mx === t) {
            ans = Math.max(ans, l * w);
        }
    }
    return ans;
}

时间复杂度 $O(n)$,其中 $n$ 是矩形的数量。空间复杂度 $O(1)$。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-对角线最长的矩形的面积🟢

给你一个下标从 0 开始的二维整数数组 dimensions

对于所有下标 i0 <= i < dimensions.length),dimensions[i][0] 表示矩形 i 的长度,而 dimensions[i][1] 表示矩形 i 的宽度。

返回对角线最 的矩形的 面积 。如果存在多个对角线长度相同的矩形,返回面积最的矩形的面积。

 

示例 1:

输入:dimensions = [[9,3],[8,6]]
输出:48
解释:
下标 = 0,长度 = 9,宽度 = 3。对角线长度 = sqrt(9 * 9 + 3 * 3) = sqrt(90) ≈ 9.487。
下标 = 1,长度 = 8,宽度 = 6。对角线长度 = sqrt(8 * 8 + 6 * 6) = sqrt(100) = 10。
因此,下标为 1 的矩形对角线更长,所以返回面积 = 8 * 6 = 48。

示例 2:

输入:dimensions = [[3,4],[4,3]]
输出:12
解释:两个矩形的对角线长度相同,为 5,所以最大面积 = 12。

 

提示:

  • 1 <= dimensions.length <= 100
  • dimensions[i].length == 2
  • 1 <= dimensions[i][0], dimensions[i][1] <= 100

枚举

解法:枚举

按题意枚举每个矩形并判断即可。可以改成记录对角线长度的平方,避免浮点数运算。

复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###c++

class Solution {
public:
    int areaOfMaxDiagonal(vector<vector<int>>& dimensions) {
        int ans1 = 0, ans2 = 0;
        for (auto &rect : dimensions) {
            int len = rect[0] * rect[0] + rect[1] * rect[1];
            int area = rect[0] * rect[1];
            if (len > ans1) ans1 = len, ans2 = area;
            else if (len == ans1 && area > ans2) ans2 = area;
        }
        return ans2;
    }
};

简洁写法,O(n) 一次遍历(Python/Java/C++/C/Go/JS/Rust)

设矩形长宽分别为 $x$ 和 $y$。

根据勾股定理,矩形对角线长度的平方为

$$
x^2+y^2
$$

根据题意,我们需要用双关键字比大小,找到最大的面积。

  • 第一关键字是对角线长度,直接用其平方值。如果遍历到更大的长度,则覆盖矩形面积。
  • 第二关键字是矩形面积,即 $x\cdot y$。如果遇到了和最长长度一样长的矩形,那么更新面积的最大值。

###py

class Solution:
    def areaOfMaxDiagonal(self, dimensions: List[List[int]]) -> int:
        return max((x * x + y * y, x * y) for x, y in dimensions)[1]

###java

class Solution {
    public int areaOfMaxDiagonal(int[][] dimensions) {
        int ans = 0, maxL = 0;
        for (int[] d : dimensions) {
            int x = d[0], y = d[1];
            int l = x * x + y * y;
            if (l > maxL || l == maxL && x * y > ans) {
                maxL = l;
                ans = x * y;
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int areaOfMaxDiagonal(vector<vector<int>>& dimensions) {
        pair<int, int> mx{};
        for (auto& d : dimensions) {
            int x = d[0], y = d[1];
            mx = max(mx, pair(x * x + y * y, x * y));
        }
        return mx.second;
    }
};

###c

int areaOfMaxDiagonal(int** dimensions, int dimensionsSize, int* dimensionsColSize) {
    int ans = 0, max_l = 0;
    for (int i = 0; i < dimensionsSize; i++) {
        int x = dimensions[i][0], y = dimensions[i][1];
        int l = x * x + y * y;
        if (l > max_l || l == max_l && x * y > ans) {
            max_l = l;
            ans = x * y;
        }
    }
    return ans;
}

###go

func areaOfMaxDiagonal(dimensions [][]int) (ans int) {
maxL := 0
for _, d := range dimensions {
x, y := d[0], d[1]
l := x*x + y*y
if l > maxL || l == maxL && x*y > ans {
maxL = l
ans = x * y
}
}
return
}

###js

var areaOfMaxDiagonal = function(dimensions) {
    let ans = 0, maxL = 0;
    for (const [x, y] of dimensions) {
        const l = x * x + y * y;
        if (l > maxL || l === maxL && x * y > ans) {
            maxL = l;
            ans = x * y;
        }
    }
    return ans;
};

###rust

impl Solution {
    pub fn area_of_max_diagonal(dimensions: Vec<Vec<i32>>) -> i32 {
        let mut mx = (0, 0);
        for d in dimensions {
            let x = d[0];
            let y = d[1];
            mx = mx.max((x * x + y * y, x * y));
        }
        mx.1
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 为 $\textit{dimensions}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

分类题单

如何科学刷题?

  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站@灵茶山艾府

每日一题-删掉一个元素以后全为 1 的最长子数组🟡

给你一个二进制数组 nums ,你需要从中删掉一个元素。

请你在删掉元素的结果数组中,返回最长的且只包含 1 的非空子数组的长度。

如果不存在这样的子数组,请返回 0 。

 

提示 1:

输入:nums = [1,1,0,1]
输出:3
解释:删掉位置 2 的数后,[1,1,1] 包含 3 个 1 。

示例 2:

输入:nums = [0,1,1,1,0,1,1,0,1]
输出:5
解释:删掉位置 4 的数字后,[0,1,1,1,1,1,0,1] 的最长全 1 子数组为 [1,1,1,1,1] 。

示例 3:

输入:nums = [1,1,1]
输出:2
解释:你必须要删除一个元素。

 

提示:

  • 1 <= nums.length <= 105
  • nums[i] 要么是 0 要么是 1

滑动窗口(Python/Java/C++/C/Go/JS/Rust)

删除后,子数组只有 $1$,也就是没有 $0$。那么删除之前呢?至多有一个 $0$。

所以问题相当于:

  • 求最长子数组的长度(减一),满足子数组至多有一个 $0$。

子数组越短,包含的 $0$ 越少,越能满足要求;子数组越长,包含的 $0$ 越多,越无法满足要求。所以当子数组的右端点增大时,左端点也随之增大(或者不变),这符合滑动窗口模型。如果你不了解滑动窗口,请看视频【基础算法精讲 03】

这个方法对于至多有 $k$ 个 $0$ 的题目,也是适用的。

注意:删除之前的子数组长度是 $\textit{right}-\textit{left}+1$,由于题目一定要删除一个数(尤其是在全为 $1$ 的情况下,一定要删除一个 $1$),所以删除后的子数组长度是 $\textit{right}-\textit{left}$,用其更新答案的最大值。

###py

class Solution:
    def longestSubarray(self, nums: List[int]) -> int:
        ans = cnt0 = left = 0
        for right, x in enumerate(nums):
            # 1. 入,nums[right] 进入窗口
            cnt0 += 1 - x  # 维护窗口中的 0 的个数
            while cnt0 > 1:  # 不符合题目要求
                # 2. 出,nums[left] 离开窗口
                cnt0 -= 1 - nums[left]  # 维护窗口中的 0 的个数
                left += 1
            # 3. 更新答案,注意不是 right-left+1,因为我们要删掉一个数
            ans = max(ans, right - left)
        return ans

###java

class Solution {
    public int longestSubarray(int[] nums) {
        int ans = 0;
        int cnt0 = 0;
        int left = 0;
        for (int right = 0; right < nums.length; right++) {
            // 1. 入,nums[right] 进入窗口
            cnt0 += 1 - nums[right]; // 维护窗口中的 0 的个数
            while (cnt0 > 1) { // 不符合题目要求
                // 2. 出,nums[left] 离开窗口
                cnt0 -= 1 - nums[left]; // 维护窗口中的 0 的个数
                left++;
            }
            // 3. 更新答案,注意不是 right-left+1,因为我们要删掉一个数
            ans = Math.max(ans, right - left);
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int longestSubarray(vector<int>& nums) {
        int ans = 0, cnt0 = 0, left = 0;
        for (int right = 0; right < nums.size(); right++) {
            // 1. 入,nums[right] 进入窗口
            cnt0 += 1 - nums[right]; // 维护窗口中的 0 的个数
            while (cnt0 > 1) { // 不符合题目要求
                // 2. 出,nums[left] 离开窗口
                cnt0 -= 1 - nums[left]; // 维护窗口中的 0 的个数
                left++;
            }
            // 3. 更新答案,注意不是 right-left+1,因为我们要删掉一个数
            ans = max(ans, right - left);
        }
        return ans;
    }
};

###c

#define MAX(a, b) ((b) > (a) ? (b) : (a))

int longestSubarray(int* nums, int numsSize) {
    int ans = 0, cnt0 = 0, left = 0;
    for (int right = 0; right < numsSize; right++) {
        // 1. 入,nums[right] 进入窗口
        cnt0 += 1 - nums[right]; // 维护窗口中的 0 的个数
        while (cnt0 > 1) { // 不符合题目要求
            // 2. 出,nums[left] 离开窗口
            cnt0 -= 1 - nums[left]; // 维护窗口中的 0 的个数
            left++;
        }
        // 3. 更新答案,注意不是 right-left+1,因为我们要删掉一个数
        ans = MAX(ans, right - left);
    }
    return ans;
}

###go

func longestSubarray(nums []int) (ans int) {
    cnt0, left := 0, 0
    for right, x := range nums {
        // 1. 入,nums[right] 进入窗口
        cnt0 += 1 - x  // 维护窗口中的 0 的个数
        for cnt0 > 1 { // 不符合题目要求
            // 2. 出,nums[left] 离开窗口
            cnt0 -= 1 - nums[left] // 维护窗口中的 0 的个数
            left++
        }
        // 3. 更新答案,注意不是 right-left+1,因为我们要删掉一个数
        ans = max(ans, right-left)
    }
    return
}

###js

var longestSubarray = function(nums) {
    let ans = 0, cnt0 = 0, left = 0;
    for (let right = 0; right < nums.length; right++) {
        // 1. 入,nums[right] 进入窗口
        cnt0 += 1 - nums[right]; // 维护窗口中的 0 的个数
        while (cnt0 > 1) { // 不符合题目要求
            // 2. 出,nums[left] 离开窗口
            cnt0 -= 1 - nums[left]; // 维护窗口中的 0 的个数
            left++;
        }
        // 3. 更新答案,注意不是 right-left+1,因为我们要删掉一个数
        ans = Math.max(ans, right - left);
    }
    return ans;
};

###rust

impl Solution {
    pub fn longest_subarray(nums: Vec<i32>) -> i32 {
        let mut ans = 0;
        let mut cnt0 = 0;
        let mut left = 0;
        for (right, &x) in nums.iter().enumerate() {
            // 1. 入,nums[right] 进入窗口
            cnt0 += 1 - x; // 维护窗口中的 0 的个数
            while cnt0 > 1 {
                // 2. 出,nums[left] 离开窗口
                cnt0 -= 1 - nums[left]; // 维护窗口中的 0 的个数
                left += 1;
            }
            // 3. 更新答案,注意不是 right-left+1,因为我们要删掉一个数
            ans = ans.max(right - left);
        }
        ans as _
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。虽然写了个二重循环,但是内层循环中对 $\textit{left}$ 加一的执行次数不会超过 $n$ 次,所以总的时间复杂度为 $\mathcal{O}(n)$。
  • 空间复杂度:$\mathcal{O}(1)$。

相似题目

见下面滑动窗口题单的「§2.1 越短越合法/求最长/最大」。

分类题单

如何科学刷题?

  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站@灵茶山艾府

删掉一个元素以后全为 1 的最长子数组

方法一:递推

思路

在删掉元素的结果数组中,最长的且只包含 $1$ 的非空子数组存在两种情况:

  • 这个子数组在原数组中本身就是连续的,无论删或者不删其他的元素,它都是最长的且只包含 $1$ 的非空子数组;

  • 这个子数组原本不连续,而是两个连续的全 $1$ 子数组,中间夹着一个 $0$,把这个 $0$ 删掉以后,左右两个子数组组合成一个最长的且只包含 $1$ 的非空子数组。

我们可以枚举被删除的位置,假设下标为 $i$,我们希望知道「以第 $i - 1$ 位结尾的最长连续全 $1$ 子数组」和「以第 $i + 1$ 位开头的最长连续全 $1$ 子数组」的长度分别是多少,这两个量的和就是删除第 $i$ 位之后最长的且只包含 $1$ 的非空子数组的长度。假设我们可以得到这两个量,我们只要枚举所有的 $i$,就可以得到最终的答案。

我们可以这样维护「以第 $i - 1$ 位结尾的最长连续全 $1$ 子数组」和「以第 $i + 1$ 位开头的最长连续全 $1$ 子数组」的长度:

  • 记原数组为 $a$

  • 记 ${\rm pre}(i)$ 为「以第 $i$ 位结尾的最长连续全 $1$ 子数组」,那么
    $$
    {\rm pre}(i) = \left{ \begin{aligned}
    & 0 , & a_i = 0 \
    & {\rm pre}(i - 1) + 1 , & a_i = 1
    \end{aligned} \right.
    $$

  • 记 ${\rm suf}(i)$ 为「以第 $i$ 位开头的最长连续全 $1$ 子数组」,那么
    $$
    {\rm suf}(i) = \left{ \begin{aligned}
    & 0 , & a_i = 0 \
    & {\rm suf}(i + 1) + 1 , & a_i = 1
    \end{aligned} \right.
    $$

我们可以对原数组做一次正向遍历,预处理出 $\rm pre$,再做一次反向遍历,预处理出 $\rm suf$。最后我们枚举所有的元素作为待删除的元素,计算出删除这些元素之后最长的且只包含 $1$ 的非空子数组的长度,比较并取最大值。

代码如下。

算法

###C++

class Solution {
public:
    int longestSubarray(vector<int>& nums) {
        int n = nums.size();

        vector<int> pre(n), suf(n);

        pre[0] = nums[0];
        for (int i = 1; i < n; ++i) {
            pre[i] = nums[i] ? pre[i - 1] + 1 : 0; 
        }

        suf[n - 1] = nums[n - 1];
        for (int i = n - 2; i >= 0; --i) {
            suf[i] = nums[i] ? suf[i + 1] + 1 : 0;
        }

        int ans = 0;
        for (int i = 0; i < n; ++i) {
            int preSum = i == 0 ? 0 : pre[i - 1];
            int sufSum = i == n - 1 ? 0 : suf[i + 1];
            ans = max(ans, preSum + sufSum);
        }

        return ans;
    }
};

###Java

class Solution {
    public int longestSubarray(int[] nums) {
        int n = nums.length;

        int[] pre = new int[n];
        int[] suf = new int[n];

        pre[0] = nums[0];
        for (int i = 1; i < n; ++i) {
            pre[i] = nums[i] != 0 ? pre[i - 1] + 1 : 0; 
        }

        suf[n - 1] = nums[n - 1];
        for (int i = n - 2; i >= 0; --i) {
            suf[i] = nums[i] != 0 ? suf[i + 1] + 1 : 0;
        }

        int ans = 0;
        for (int i = 0; i < n; ++i) {
            int preSum = i == 0 ? 0 : pre[i - 1];
            int sufSum = i == n - 1 ? 0 : suf[i + 1];
            ans = Math.max(ans, preSum + sufSum);
        }

        return ans;
    }
}

###Python

class Solution:
    def longestSubarray(self, nums: List[int]) -> int:
        n = len(nums)
        pre = [0] * n
        suf = [0] * n
        pre[0] = nums[0]
        for i in range(1, n):
            pre[i] = pre[i - 1] + 1 if nums[i] else 0
        suf[-1] = nums[-1]
        for i in range(n - 2, -1, -1):
            suf[i] = suf[i + 1] + 1 if nums[i] else 0

        ans = 0
        for i in range(n):
            pre_sum = pre[i - 1] if i > 0 else 0
            suf_sum = suf[i + 1] if i < n - 1 else 0
            ans = max(ans, pre_sum + suf_sum)

        return ans

###C#

public class Solution {
    public int LongestSubarray(int[] nums) {
        int n = nums.Length;
        int[] pre = new int[n];
        int[] suf = new int[n];
        pre[0] = nums[0];
        for (int i = 1; i < n; ++i) {
            pre[i] = nums[i] != 0 ? pre[i - 1] + 1 : 0;
        }
        suf[n - 1] = nums[n - 1];
        for (int i = n - 2; i >= 0; --i) {
            suf[i] = nums[i] != 0 ? suf[i + 1] + 1 : 0;
        }
        
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            int preSum = i == 0 ? 0 : pre[i - 1];
            int sufSum = i == n - 1 ? 0 : suf[i + 1];
            ans = Math.Max(ans, preSum + sufSum);
        }
        return ans;
    }
}

###Go

func longestSubarray(nums []int) int {
    n := len(nums)
    pre := make([]int, n)
    suf := make([]int, n)
    pre[0] = nums[0]
    for i := 1; i < n; i++ {
        if nums[i] != 0 {
            pre[i] = pre[i - 1] + 1
        } else {
            pre[i] = 0
        }
    }
    suf[n - 1] = nums[n - 1]
    for i := n - 2; i >= 0; i-- {
        if nums[i] != 0 {
            suf[i] = suf[i + 1] + 1
        } else {
            suf[i] = 0
        }
    }

    ans := 0
    for i := 0; i < n; i++ {
        preSum := 0
        if i != 0 {
            preSum = pre[i - 1]
        }
        sufSum := 0
        if i != n - 1 {
            sufSum = suf[i + 1]
        }
        if preSum + sufSum > ans {
            ans = preSum + sufSum
        }
    }

    return ans
}

###C

int longestSubarray(int* nums, int numsSize) {
    int* pre = (int*)malloc(numsSize * sizeof(int));
    int* suf = (int*)malloc(numsSize * sizeof(int));
    pre[0] = nums[0];
    for (int i = 1; i < numsSize; ++i) {
        pre[i] = nums[i] ? pre[i - 1] + 1 : 0;
    }
    suf[numsSize - 1] = nums[numsSize - 1];
    for (int i = numsSize - 2; i >= 0; --i) {
        suf[i] = nums[i] ? suf[i + 1] + 1 : 0;
    }

    int ans = 0;
    for (int i = 0; i < numsSize; ++i) {
        int preSum = i == 0 ? 0 : pre[i - 1];
        int sufSum = i == numsSize - 1 ? 0 : suf[i + 1];
        ans = fmax(ans, preSum + sufSum);
    }
    free(pre);
    free(suf);

    return ans;
}

###JavaScript

var longestSubarray = function(nums) {
    const n = nums.length;
    const pre = new Array(n);
    const suf = new Array(n);
    pre[0] = nums[0];
    for (let i = 1; i < n; ++i) {
        pre[i] = nums[i] ? pre[i - 1] + 1 : 0;
    }
    suf[n - 1] = nums[n - 1];
    for (let i = n - 2; i >= 0; --i) {
        suf[i] = nums[i] ? suf[i + 1] + 1 : 0;
    }

    let ans = 0;
    for (let i = 0; i < n; ++i) {
        const preSum = i === 0 ? 0 : pre[i - 1];
        const sufSum = i === n - 1 ? 0 : suf[i + 1];
        ans = Math.max(ans, preSum + sufSum);
    }

    return ans;
};

###TypeScript

function longestSubarray(nums: number[]): number {
    const n = nums.length;
    const pre: number[] = new Array(n);
    const suf: number[] = new Array(n);
    pre[0] = nums[0];
    for (let i = 1; i < n; ++i) {
        pre[i] = nums[i] ? pre[i - 1] + 1 : 0;
    }
    suf[n - 1] = nums[n - 1];
    for (let i = n - 2; i >= 0; --i) {
        suf[i] = nums[i] ? suf[i + 1] + 1 : 0;
    }

    let ans = 0;
    for (let i = 0; i < n; ++i) {
        const preSum = i === 0 ? 0 : pre[i - 1];
        const sufSum = i === n - 1 ? 0 : suf[i + 1];
        ans = Math.max(ans, preSum + sufSum);
    }

    return ans;
}

###Rust

impl Solution {
    pub fn longest_subarray(nums: Vec<i32>) -> i32 {
        let n = nums.len();
        let mut pre = vec![0; n];
        let mut suf = vec![0; n];
        pre[0] = nums[0];
        for i in 1..n {
            pre[i] = if nums[i] != 0 { pre[i - 1] + 1 } else { 0 };
        }
        suf[n - 1] = nums[n - 1];
        for i in (0..n - 1).rev() {
            suf[i] = if nums[i] != 0 { suf[i + 1] + 1 } else { 0 };
        }
        
        let mut ans = 0;
        for i in 0..n {
            let pre_sum = if i == 0 { 0 } else { pre[i - 1] };
            let suf_sum = if i == n - 1 { 0 } else { suf[i + 1] };
            ans = ans.max(pre_sum + suf_sum);
        }

        ans
    }
}

复杂度

假设原数组长度为 $n$。

  • 时间复杂度:$O(n)$。这里对原数组进行三次遍历,每次时间代价为 $O(n)$,故渐进时间复杂度为 $O(n)$。

  • 空间复杂度:$O(n)$。这里预处理 $\rm pre$ 和 $\rm suf$ 需要两个长度为 $n$ 的数组。

方法二:递推优化

思路

我们也可以修改递推的方式使用一次就可以得到答案。

记 $p_0(i)$ 为「以第 $i$ 位结尾的最长连续全 $1$ 子数组」,与方法一中的 ${\rm pre}(i)$ 相同,递推式为:

$$
p_0(i) = \left{ \begin{aligned}
& 0 , & a_i = 0 \
& p_0(i - 1) + 1 , & a_i = 1
\end{aligned} \right.
$$

同时,我们记 $p_1(i)$ 为「以第 $i$ 位结尾,并且可以在某个位置删除一个 $0$ 的最长连续全 $1$ 子数组」。注意这里我们规定了只删除 $0$,而不是任意一个元素,这是因为只要数组中的元素不全为 $1$,那么删除 $1$ 就没有任何意义。$p_1(i)$ 的递推式为:

$$
p_1(i) = \left{ \begin{aligned}
& p_0(i - 1) , & a_i = 0 \
& p_1(i - 1) + 1 , & a_i = 1
\end{aligned} \right.
$$

当我们遇到 $1$ 时,$p_1(i)$ 的递推式与 $p_0(i)$ 相同;而当我们遇到 $0$ 时,由于 $p_1(i)$ 允许删除一个 $0$,那么我们可以把这个 $0$ 删除,将 $p_0(i-1)$ 的值赋予 $p_1(i)$。

最后的答案即为 $p_1(i)$ 中的最大值。当遇到数组中的元素全为 $1$ 的特殊情况时,我们需要将答案减去 $1$,这是因为在这种情况下,我们不得不删除一个 $1$。注意到递推式中所有的 $p_0(i), p_1(i)$ 只和 $p_0(i-1), p_1(i-1)$ 相关,因此我们可以直接使用两个变量进行递推,减少空间复杂度。

算法

###C++

class Solution {
public:
    int longestSubarray(vector<int>& nums) {
        int ans = 0;
        int p0 = 0, p1 = 0;
        for (int num: nums) {
            if (num == 0) {
                p1 = p0;
                p0 = 0;
            }
            else {
                ++p0;
                ++p1;
            }
            ans = max(ans, p1);
        }
        if (ans == nums.size()) {
            --ans;
        }
        return ans;
    }
};

###Java

class Solution {
    public int longestSubarray(int[] nums) {
        int ans = 0;
        int p0 = 0, p1 = 0;
        for (int num : nums) {
            if (num == 0) {
                p1 = p0;
                p0 = 0;
            } else {
                ++p0;
                ++p1;
            }
            ans = Math.max(ans, p1);
        }
        if (ans == nums.length) {
            --ans;
        }
        return ans;
    }
}

###Python

class Solution:
    def longestSubarray(self, nums: List[int]) -> int:
        ans = 0
        p0 = p1 = 0
        for num in nums:
            if num == 0:
                p1, p0 = p0, 0
            else:
                p0 += 1
                p1 += 1
            ans = max(ans, p1)
        if ans == len(nums):
            ans -= 1
        return ans

###C#

public class Solution {
    public int LongestSubarray(int[] nums) {
        int ans = 0;
        int p0 = 0, p1 = 0;
        foreach (int num in nums) {
            if (num == 0) {
                p1 = p0;
                p0 = 0;
            }
            else {
                ++p0;
                ++p1;
            }
            ans = Math.Max(ans, p1);
        }
        if (ans == nums.Length) {
            --ans;
        }
        return ans;
    }
}

###Go

func longestSubarray(nums []int) int {
    ans := 0
    p0, p1 := 0, 0
    for _, num := range nums {
        if num == 0 {
            p1 = p0
            p0 = 0
        } else {
            p0++
            p1++
        }
        if p1 > ans {
            ans = p1
        }
    }
    if ans == len(nums) {
        ans--
    }
    return ans
}

###C

int longestSubarray(int* nums, int numsSize) {
    int ans = 0;
    int p0 = 0, p1 = 0;
    for (int i = 0; i < numsSize; i++) {
        if (nums[i] == 0) {
            p1 = p0;
            p0 = 0;
        } else {
            ++p0;
            ++p1;
        }
        ans = fmax(ans, p1);
    }
    if (ans == numsSize) {
        --ans;
    }
    return ans;
}

###JavaScript

var longestSubarray = function(nums) {
    let ans = 0;
    let p0 = 0, p1 = 0;
    for (const num of nums) {
        if (num === 0) {
            p1 = p0;
            p0 = 0;
        } else {
            p0++;
            p1++;
        }
        ans = Math.max(ans, p1);
    }
    if (ans === nums.length) {
        ans--;
    }
    return ans;
};

###TypeScript

function longestSubarray(nums: number[]): number {
    let ans = 0;
    let p0 = 0, p1 = 0;
    for (const num of nums) {
        if (num === 0) {
            p1 = p0;
            p0 = 0;
        } else {
            p0++;
            p1++;
        }
        ans = Math.max(ans, p1);
    }
    if (ans === nums.length) {
        ans--;
    }
    return ans;
}

###Rust

impl Solution {
    pub fn longest_subarray(nums: Vec<i32>) -> i32 {
        let mut ans = 0;
        let mut p0 = 0;
        let mut p1 = 0;
        for &num in nums.iter() {
            if num == 0 {
                p1 = p0;
                p0 = 0;
            } else {
                p0 += 1;
                p1 += 1;
            }
            ans = ans.max(p1);
        }
        if ans == nums.len() as i32 {
            ans -= 1;
        }
        ans
    }
}

复杂度

假设原数组长度为 $n$。

  • 时间复杂度:$O(n)$。这里对原数组进行一次遍历,时间代价为 $O(n)$,故渐进时间复杂度为 $O(n)$。

  • 空间复杂度:$O(1)$。

此题最优算法,不接受反驳

a 存中间有一个“非1”的和,b 存连续1的和,遇 1 两数自增,遇“非1” a=b;b=0。
扫描过程保存最大的 a 值,最后处理一下全1特例即可。

class Solution {
    public int longestSubarray(int[] nums) {
        int ret = 0;
        int a = 0;
        int b = 0;
        int n = nums.length;
        for (int i = 0; i < n; ++i) {
            if (nums[i] == 1) {
                ++a;
                ++b;
                ret = Math.max(ret, a);
            } else {
                a = b;
                b = 0;
            }
        }
        if (ret == n) ret--;
        return ret;
    }
}

[Python3/Java/C++/Go/TypeScript] 一题一解:求最小边界和最大边界(清晰题解)

方法一:求最小边界和最大边界

我们可以遍历 grid,找到所有 1 的最小边界,记为 $(x_1, y_1)$,最大边界,记为 $(x_2, y_2)$,那么最小矩形的面积就是 $(x_2 - x_1 + 1) \times (y_2 - y_1 + 1)$。

###python

class Solution:
    def minimumArea(self, grid: List[List[int]]) -> int:
        x1 = y1 = inf
        x2 = y2 = -inf
        for i, row in enumerate(grid):
            for j, x in enumerate(row):
                if x == 1:
                    x1 = min(x1, i)
                    y1 = min(y1, j)
                    x2 = max(x2, i)
                    y2 = max(y2, j)
        return (x2 - x1 + 1) * (y2 - y1 + 1)

###java

class Solution {
    public int minimumArea(int[][] grid) {
        int m = grid.length, n = grid[0].length;
        int x1 = m, y1 = n;
        int x2 = 0, y2 = 0;
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1) {
                    x1 = Math.min(x1, i);
                    y1 = Math.min(y1, j);
                    x2 = Math.max(x2, i);
                    y2 = Math.max(y2, j);
                }
            }
        }
        return (x2 - x1 + 1) * (y2 - y1 + 1);
    }
}

###cpp

class Solution {
public:
    int minimumArea(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        int x1 = m, y1 = n;
        int x2 = 0, y2 = 0;
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1) {
                    x1 = min(x1, i);
                    y1 = min(y1, j);
                    x2 = max(x2, i);
                    y2 = max(y2, j);
                }
            }
        }
        return (x2 - x1 + 1) * (y2 - y1 + 1);
    }
};

###go

func minimumArea(grid [][]int) int {
x1, y1 := len(grid), len(grid[0])
x2, y2 := 0, 0
for i, row := range grid {
for j, x := range row {
if x == 1 {
x1, y1 = min(x1, i), min(y1, j)
x2, y2 = max(x2, i), max(y2, j)
}
}
}
return (x2 - x1 + 1) * (y2 - y1 + 1)
}

###ts

function minimumArea(grid: number[][]): number {
    const [m, n] = [grid.length, grid[0].length];
    let [x1, y1] = [m, n];
    let [x2, y2] = [0, 0];
    for (let i = 0; i < m; ++i) {
        for (let j = 0; j < n; ++j) {
            if (grid[i][j] === 1) {
                x1 = Math.min(x1, i);
                y1 = Math.min(y1, j);
                x2 = Math.max(x2, i);
                y2 = Math.max(y2, j);
            }
        }
    }
    return (x2 - x1 + 1) * (y2 - y1 + 1);
}

时间复杂度 $O(m \times n)$,其中 $m$ 和 $n$ 分别是 grid 的行数和列数。空间复杂度 $O(1)$。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-包含所有 1 的最小矩形面积 I🟡

给你一个二维 二进制 数组 grid。请你找出一个边在水平方向和竖直方向上、面积 最小 的矩形,并且满足 grid 中所有的 1 都在矩形的内部。

返回这个矩形可能的 最小 面积。

 

示例 1:

输入: grid = [[0,1,0],[1,0,1]]

输出: 6

解释:

这个最小矩形的高度为 2,宽度为 3,因此面积为 2 * 3 = 6

示例 2:

输入: grid = [[0,0],[1,0]]

输出: 1

解释:

这个最小矩形的高度和宽度都是 1,因此面积为 1 * 1 = 1

 

提示:

  • 1 <= grid.length, grid[i].length <= 1000
  • grid[i][j] 是 0 或 1。
  • 输入保证 grid 中至少有一个 1 。

枚举

解法:枚举

经典题。求包含所有点的,边与坐标轴平行的矩形的最小面积。

矩形的上(下)边界即为所有点中纵坐标最大(小)的,矩形的左(右)边界即为所有点中横坐标最大(小)的。枚举所有点计算边界即可。复杂度 $\mathcal{O}(nm)$。

参考代码(c++)

###cpp

class Solution {
public:
    int minimumArea(vector<vector<int>>& grid) {
        int n = grid.size(), m = grid[0].size();
        int U = n, D = -1, L = m, R = -1;
        for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) if (grid[i][j]) {
            U = min(U, i); D = max(D, i);
            L = min(L, j); R = max(R, j);
        }
        return (D - U + 1) * (R - L + 1);
    }
};

寻找矩形的左右上下边界(Python/Java/C++/C/Go/JS/Rust)

遍历 $\textit{grid}$:

  • 找到最左最右的 $1$ 的列号,分别记作 $\textit{left},\textit{right}$,则矩形底边长至少为 $\textit{right}-\textit{left}+1$。
  • 找到最上最下的 $1$ 的行号,分别记作 $\textit{top},\textit{bottom}$,则矩形高至少为 $\textit{bottom} - \textit{top} + 1$。

矩形面积至少为

$$
(\textit{right} - \textit{left} + 1) \cdot (\textit{bottom} - \textit{top} + 1)
$$

###py

class Solution:
    def minimumArea(self, grid: List[List[int]]) -> int:
        left = top = inf
        right = bottom = 0
        for i, row in enumerate(grid):
            for j, x in enumerate(row):
                if x:
                    left = min(left, j)
                    right = max(right, j)
                    top = min(top, i)
                    bottom = i
        return (right - left + 1) * (bottom - top + 1)

###java

class Solution {
    public int minimumArea(int[][] grid) {
        int left = Integer.MAX_VALUE;
        int right = 0;
        int top = Integer.MAX_VALUE;
        int bottom = 0;
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[i].length; j++) {
                if (grid[i][j] == 1) {
                    left = Math.min(left, j);
                    right = Math.max(right, j);
                    top = Math.min(top, i);
                    bottom = i;
                }
            }
        }
        return (right - left + 1) * (bottom - top + 1);
    }
}

###cpp

class Solution {
public:
    int minimumArea(vector<vector<int>>& grid) {
        int left = INT_MAX, right = 0;
        int top = INT_MAX, bottom = 0;
        for (int i = 0; i < grid.size(); i++) {
            for (int j = 0; j < grid[i].size(); j++) {
                if (grid[i][j]) {
                    left = min(left, j);
                    right = max(right, j);
                    top = min(top, i);
                    bottom = i;
                }
            }
        }
        return (right - left + 1) * (bottom - top + 1);
    }
};

###c

#define MIN(a, b) ((b) < (a) ? (b) : (a))
#define MAX(a, b) ((b) > (a) ? (b) : (a))

int minimumArea(int** grid, int gridSize, int* gridColSize) {
    int left = INT_MAX, right = 0;
    int top = INT_MAX, bottom = 0;
    for (int i = 0; i < gridSize; i++) {
        for (int j = 0; j < gridColSize[i]; j++) {
            if (grid[i][j]) {
                left = MIN(left, j);
                right = MAX(right, j);
                top = MIN(top, i);
                bottom = i;
            }
        }
    }
    return (right - left + 1) * (bottom - top + 1);
}

###go

func minimumArea(grid [][]int) int {
left, right := math.MaxInt, 0
top, bottom := math.MaxInt, 0
for i, row := range grid {
for j, x := range row {
if x == 1 {
left = min(left, j)
right = max(right, j)
top = min(top, i)
bottom = i
}
}
}
return (right - left + 1) * (bottom - top + 1)
}

###js

var minimumArea = function(grid) {
    let left = Infinity, right = 0;
    let top = Infinity, bottom = 0;
    for (let i = 0; i < grid.length; i++) {
        for (let j = 0; j < grid[i].length; j++) {
            if (grid[i][j]) {
                left = Math.min(left, j);
                right = Math.max(right, j);
                top = Math.min(top, i);
                bottom = i;
            }
        }
    }
    return (right - left + 1) * (bottom - top + 1);
};

###rust

impl Solution {
    pub fn minimum_area(grid: Vec<Vec<i32>>) -> i32 {
        let mut left = usize::MAX;
        let mut right = 0;
        let mut top = usize::MAX;
        let mut bottom = 0;
        for (i, row) in grid.into_iter().enumerate() {
            for (j, x) in row.into_iter().enumerate() {
                if x != 0 {
                    left = left.min(j);
                    right = right.max(j);
                    top = top.min(i);
                    bottom = i;
                }
            }
        }
        ((right - left + 1) * (bottom - top + 1)) as _
    }
}

复杂度分析

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

分类题单

如何科学刷题?

  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站@灵茶山艾府

每日一题-统计全 1 子矩形🟡

给你一个 m x n 的二进制矩阵 mat ,请你返回有多少个 子矩形 的元素全部都是 1 。

 

示例 1:

输入:mat = [[1,0,1],[1,1,0],[1,1,0]]
输出:13
解释:
6 个 1x1 的矩形。
有 2 个 1x2 的矩形。
有 3 个 2x1 的矩形。
有 1 个 2x2 的矩形。
有 1 个 3x1 的矩形。
矩形数目总共 = 6 + 2 + 3 + 1 + 1 = 13 。

示例 2:

输入:mat = [[0,1,1,0],[0,1,1,1],[1,1,1,0]]
输出:24
解释:8 个 1x1 的子矩形。
有 5 个 1x2 的子矩形。
有 2 个 1x3 的子矩形。
有 4 个 2x1 的子矩形。
有 2 个 2x2 的子矩形。
有 2 个 3x1 的子矩形。
有 1 个 3x2 的子矩形。
矩形数目总共 = 8 + 5 + 2 + 4 + 2 + 2 + 1 = 24

 

提示:

  • 1 <= m, n <= 150
  • mat[i][j] 仅包含 0 或 1
❌