leetcode 2713. 矩阵中严格递增的单元格数

🔗leetcode 题目链接
题目描述:

给你一个下标从 1 开始、大小为 m x n 的整数矩阵 mat,你可以选择任一单元格作为 起始单元格 。 从起始单元格出发,你可以移动到
同一行或同一列 中的任何其他单元格,但前提是目标单元格的值 严格大于 当前单元格的值。
你可以多次重复这一过程,从一个单元格移动到另一个单元格,直到无法再进行任何移动。 请你找出从某个单元开始访问矩阵所能访问的 单元格的最大数量
。 返回一个表示可访问单元格最大数量的整数。

示例 1:
输入:mat = [[3,1],[3,4]] 输出:2 解释:上图展示了从第 1 行、第 2 列的单元格开始,可以访问 2个单元格。可以证明,无论从哪个单元格开始,最多只能访问 2 个单元格,因此答案是 2 。
示例 2:
输入:mat = [[1,1],[1,1]] 输出:1 解释:由于目标单元格必须严格大于当前单元格,在本示例中只能访问 1个单元格。 示例 3:

实例 3: 输入:mat = [[3,1,6],[-9,5,7]] 输出:4 解释:上图展示了从第 2 行、第 1 列的单元格开始,可以访问4 个单元格。可以证明,无论从哪个单元格开始,最多只能访问 4 个单元格,因此答案是 4 。

数据范围:
m == mat.length n == mat[i].length
1 <= m, n <= 105 1 <= m * n
<= 105
-105 <= mat[i][j] <= 105

考虑最暴力的做法,枚举矩阵的每个坐标。以当前这个坐标为终点,暴搜所有可以到达当前坐标的路径,记录路径长度。保留最大值即可。
上述的思路可以进一步抽象,可以将所有枚举的坐标看作数的根节点,到这个结点的所有路径可以看作是树的路径。
在这里插入图片描述

而我们要做的就是找出所有树中的最长路径。
额…我上面的图是以每个元素的起点为根节点画的,如果需要以终点为根节点的可以自己画一下。

class Solution {
public:
    int maxIncreasingCells(vector<vector<int>>& mat) {
        int m = mat.size(), n = mat[0].size();
        int res = 0;
        function<int(int, int)> dfs = [&] (int x, int y) -> int {
            int cnt = 0;
            for (int i = 0; i < m; i ++ ) 
                if (mat[i][y] < mat[x][y]) cnt = max(cnt, dfs(i, y));
            for (int i = 0; i < n; i ++ )
                if (mat[x][i] < mat[x][y]) cnt = max(cnt, dfs(x, i));
            return cnt + 1;
        };

        for (int i = 0; i < m; i ++ ) {
            for (int j = 0; j < n; j ++ )
                res = max(res, dfs(i, j));
        }
        return res;
    }
};

但是发现没有,这里进行了大量的重复运算。
我们试着简单分析一下最坏时间复杂度。
假设存在某个点 (x1, y1) 到 (x2, y2) 的路径长度为m * n,也就是可以按照题目条件遍历整个矩阵 (需要注意一下,这里的遍历并不是顺序遍历,它是可以直接跳到满足条件的下一个点的)。每次递归最多搜索 m + n 次。那么搜索这个点的最长路径的时间复杂度是 m * n * (n + m),因为搜索的路径是严格单调递增的。所以它一定存在一个结点 (x3, y3) 到 (x2, y2) 的路径长度是 m * n - 1 … 以此类推,我们可以得出一个序列:
mn(n + m) + (mn - 1) (n + m) + (mn - 2) (n + m) + … + 2 (n + m) + n + m
用等差数列的求和公式最后可以得到时间复杂度为:O((m * n ^ 2 + m * n) / 2 * (m + n))
这还只是搜索每个结点的最长路径的时间复杂度,但是每个结点往往有多条路径,时间复杂度无法想象。

我们可以使用记忆化搜索,用一个二维数组存下已经搜索完成的路径。
代码如下:

class Solution {
public:
    int maxIncreasingCells(vector<vector<int>>& mat) {
        int m = mat.size(), n = mat[0].size();
        int res = 0;
        vector<vector<int>> memo(m, vector<int>(n, -1));
        function<int(int, int)> dfs = [&] (int x, int y) -> int {
            int cnt = 0;
            if (memo[x][y] != -1) return memo[x][y];
            for (int i = 0; i < m; i ++ ) 
                if (mat[i][y] < mat[x][y]) cnt = max(cnt, dfs(i, y));
            for (int i = 0; i < n; i ++ )
                if (mat[x][i] < mat[x][y]) cnt = max(cnt, dfs(x, i));
            memo[x][y] = cnt + 1;
            return memo[x][y];
        };

        for (int i = 0; i < m; i ++ ) {
            for (int j = 0; j < n; j ++ )
                res = max(res, dfs(i, j));
        }
        return res;
    }
};

当然,还是没过,hh。
分析一下时间复杂度。 首先每个结点最多被访问一次,因为在下一次访问会被直接返回。然后每个结点在第一次被访问的时候最坏循环 m + n 次。 一共访问 mn 个结点,那么时间复杂度为 O(mn(m + n))。
hh,好像还是有点高。

仔细思考一下,题目告诉我们了,搜索的路径是严格单调递增的,也就是说对于点(x, y),在它前面的点一定是严格小于它的,那么我们可以对所有结点进行排序,从最小的结点开始枚举。我们定义 dp[i, j] 表示移动到 (i, j) 的最大步数。也就是从某个结点到当前结点的最长路径。对于 dp[i, j] 我们可以直接搜索其在同一行,列上的所有小于它的点的最大值。即当前结点的最优路径一定是由小于它的前一个结点的最优路径所转移过来的。
最优子结构,重叠子问题,这不妥妥的动态规划吗?
于是我们将从后往前的递归,转换为从前往后递推。

我们用一个map存储所有的坐标。(map会基于 key 进行自动排序)

typedef pair<int, int> PII;
class Solution {
public:
    int maxIncreasingCells(vector<vector<int>>& mat) {
        int m = mat.size(), n = mat[0].size();
        map<int, vector<PII>> table;
        vector<vector<int>> dp(m, vector<int>(n, 0));

        for (int i = 0; i < m; i ++ )
            for (int j = 0; j < n; j ++ )
                table[mat[i][j]].emplace_back(make_pair(i, j));

        int res = 0;
        for (auto &[_, pos] : table) {
            for (auto &[x, y] : pos) {
                int maxn = 0;
                for (int i = 0; i < m; i ++ ) 
                    if (mat[i][y] < mat[x][y]) 
                        maxn = max(maxn, dp[i][y]);

                for (int i = 0; i < n; i ++ ) 
                    if (mat[x][i] < mat[x][y])
                        maxn = max(maxn, dp[x][i]);     
                dp[x][y] = maxn + 1;
                res = max(res, dp[x][y]);
            }
        }
        return res;
        
    }
};

依旧超时,因为这并没有做到时间复杂度上的优化,因为每个点还是要搜索 m + n 次。时间复杂度依旧是O(mn(m + n))

考虑一下?
我们需要对于每个点都枚举 m + n 次吗,我们有必要枚举大于 mat[i, j] 的点吗?
答案是完全没有必要,在计算 dp[i, j]的时候,我们只需要用到对应行列上所有小于 mat[i, j] 的点中的最大值。
我们定义一个 row 和 col 数组用来维护当前已经
计算过的点在其对应行列上的最大值。这样每次可以用 O(1) 的时间复杂度在找到计算当前dp[i, j] 所需要的最优子结构的值。
代码如下:

typedef pair<int, int> PII;
class Solution {
public:
    int maxIncreasingCells(vector<vector<int>>& mat) {
        int m = mat.size(), n = mat[0].size();
        map<int, vector<PII>> table;
        for (int i = 0; i < m; i ++ ) 
            for (int j = 0; j < n; j ++ ) 
                table[mat[i][j]].emplace_back(i, j);

        vector<int> row(m), col(n);
        for (auto& [_, pos] : table) {
            vector<int> res;
            for (auto& [x, y] : pos) {
                res.emplace_back(max(row[x], col[y]) + 1);
            }
            for (int i = 0; i < pos.size(); i ++ ) {
                auto& [x, y] = pos[i];
                row[x] = max(row[x], res[i]);
                col[y] = max(col[y], res[i]); 
            }
        }
        return ranges::max(row);
    }
};
  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值