[每日一题] 113. 矩阵中的最长递增路径(数组、动态规划、多方法)

1. 题目来源

链接:矩阵中的最长递增路径
来源:LeetCode

2. 题目说明

给定一个整数矩阵,找出最长递增路径的长度。

对于每个单元格,你可以往上,下,左,右四个方向移动。 你不能在对角线方向上移动或移动到边界外(即不允许环绕)。

示例1:

输入: nums =
[
[9,9,4],
[6,6,8],
[2,1,1]
]
输出: 4
解释: 最长递增路径为 [1, 2, 6, 9]。

示例2:

输入: nums =
[
[3,4,5],
[3,2,6],
[2,2,1]
]
输出: 4
解释: 最长递增路径是 [3, 4, 5, 6]。注意不允许在对角线方向上移动。

3. 题目解析

方法一:普通动态规划、递归解法

本题该解法采用递归和dp解法,采用dp的原因是为了提高效率,避免重复运算。

需要维护一个二维动态数组dp,其中dp[i][j]表示数组中以(i,j)为起点的最长递增路径的长度,初始将dp数组都赋为0,当用递归调用时,遇到某个位置(x, y), 如果dp[x][y]不为0的话,直接返回dp[x][y]即可,不需要重复计算。

需要以数组中每个位置都为起点调用递归来做,比较找出最大值。在以一个位置为起点用dfs搜索时,对其四个相邻位置进行判断,如果相邻位置的值大于上一个位置,则对相邻位置继续调用递归,并更新一个最大值,搜素完成后返回即可, 参见代码如下:

由于代码执行较为冗杂,各类递归情况均考虑了一遍,故执行用时较高,为168 ms

class Solution {
public:
    vector<vector<int>> dirs = {{0, -1}, {-1, 0}, {0, 1}, {1, 0}};
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        if (matrix.empty() || matrix[0].empty()) return 0;
        int res = 1, m = matrix.size(), n = matrix[0].size();
        vector<vector<int>> dp(m, vector<int>(n, 0));
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                res = max(res, dfs(matrix, dp, i, j));
            }
        }
        return res;
    }
    int dfs(vector<vector<int>> &matrix, vector<vector<int>> &dp, int i, int j) {
        if (dp[i][j]) return dp[i][j];
        int mx = 1, m = matrix.size(), n = matrix[0].size();
        for (auto a : dirs) {
            int x = i + a[0], y = j + a[1];
            if (x < 0 || x >= m || y < 0 || y >= n || matrix[x][y] <= matrix[i][j]) continue;
            int len = 1 + dfs(matrix, dp, x, y);
            mx = max(mx, len);
        }
        dp[i][j] = mx;
        return mx;
    }
};
方法二:bfs解法

下面再来看一种bfs的解法,需要用queue来辅助遍历,还是需要dp数组来减少重复运算。

遍历数组中的每个数字,跟上面的解法一样,把每个遍历到的点都当作bfs遍历的起始点,需要优化的是:

  • 如果当前点的dp值大于0了,说明当前点已经计算过了,直接跳过。
  • 否则就新建一个queue,然后把当前点的坐标加进去,再用一个变量cnt,初始化为1,表示当前点为起点的递增长度,然后进入while循环,然后cnt自增1,这里先自增1没有关系,因为只有当周围有合法的点时候才会用cnt来更新。

由于当前结点周围四个相邻点距当前点距离都一样,所以采用类似二叉树层序遍历的方式,先求出当前queue的长度,然后遍历跟长度相同的次数,取出queue中的首元素,对周围四个点进行遍历,计算出相邻点的坐标后,要进行合法性检查,横纵坐标不能越界,且相邻点的值要大于当前点的值,并且相邻点点dp值要小于cnt,才有更新的必要。用cnt来更新dp[x][y],并用cnt来更新结果res,然后把相邻点排入queue中继续循环即可,参见代码如下:

慢的鬼一样???执行用时为 2108 ms

class Solution {
public:
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        if (matrix.empty() || matrix[0].empty()) return 0;
        int m = matrix.size(), n = matrix[0].size(), res = 1;
        vector<vector<int>> dirs{{0,-1},{-1,0},{0,1},{1,0}};
        vector<vector<int>> dp(m, vector<int>(n, 0));
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j ) {
                if (dp[i][j] > 0) continue;
                queue<pair<int, int>> q{{{i, j}}};
                int cnt = 1;
                while (!q.empty()) {
                    ++cnt;
                    int len = q.size();
                    for (int k = 0; k < len; ++k) {
                        auto t = q.front(); q.pop();
                        for (auto dir : dirs) {
                            int x = t.first + dir[0], y = t.second + dir[1];
                            if (x < 0 || x >= m || y < 0 || y >= n || 
                                matrix[x][y] <= matrix[t.first][t.second] || cnt <= dp[x][y]) continue;
                            dp[x][y] = cnt;
                            res = max(res, cnt);
                            q.push({x, y});
                        }
                    }
                }
            }
        }
        return res;
    }
};
方法三:动态规划、高效率版
  1. 将所有的坐标按照对应位置的值升序排序
  2. 依次处理上一步中得到的坐标列表,从附近比当前点矮的点DP值取最大加1得到当前点的DP值

时间复杂度分析:

第一步的时间复杂度是 O(log(mn))
第二步的时间复杂度是 O(mn)

经过升序预处理之后效率确实高,执行用时为 52 ms

class Point {
public:
    int x, y, v;
    Point(int ix, int iy, int iv) {
        x = ix;
        y = iy;
        v = iv;
    }
};

class Solution {
public:
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        if (matrix.size() == 0)
            return 0;
        const int rows = matrix.size();
        const int cols = matrix[0].size();
        vector<vector<int>> dp(rows, vector<int>(cols, 1));
        vector<Point> vp;
        vp.reserve(rows * cols);
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                vp.push_back(Point(i, j, matrix[i][j]));
            }
        }
        // 采用lambda表达式,根据读入数据依次创建坐标点,并根据升序排序
        sort(vp.begin(), vp.end(), [](const Point &lhs, const Point &rhs){
            return lhs.v < rhs.v;
        });
        int ret = 1;
        for (const auto &p : vp) {
            auto x = p.x;
            auto y = p.y;
            auto v = p.v;
            // 寻找附近比当前矮的点的最高DP值
            if (x > 0 && matrix[x - 1][y] < v && dp[x - 1][y] + 1 > dp[x][y])
                dp[x][y] = dp[x - 1][y] + 1;
            if (x < rows - 1 && matrix[x + 1][y] < v && dp[x + 1][y] + 1 > dp[x][y])
                dp[x][y] = dp[x + 1][y] + 1;
            if (y > 0 && matrix[x][y - 1] < v && dp[x][y - 1] + 1 > dp[x][y])
                dp[x][y] = dp[x][y - 1] + 1;
            if (y < cols - 1 && matrix[x][y + 1] < v && dp[x][y + 1] + 1 > dp[x][y])
                dp[x][y] = dp[x][y + 1] + 1;
            if (dp[x][y] > ret)
                ret = dp[x][y];
        }
        return ret;
    }
};

在对矩阵进行排序时,采用了lambda表达式的方法,确实便捷好用。

方法四:拓扑排序、记忆化搜索,暂时能力不足,仅贴上dalao思路和代码
  1. 找到所有的的洼地点(即 拓扑排序中入度为0的点), 加入队列
  2. 宽搜,如果搜到下一个点的路径值比当前小,则加入队列

时间复杂度分析:

上述两个步骤的时间复杂度均为O(mn)

执行用时为 76 ms

class Solution {
public:
    int longestIncreasingPath(vector<vector<int>>& matrix) {
        queue<int> qx, qy;
        const int rows = matrix.size();
        if (!rows)
            return 0;
        const int cols = matrix[0].size();
        const vector<int> dx = {1, -1, 0, 0};
        const vector<int> dy = {0, 0, 1, -1};
        vector<vector<int>> in(rows, vector<int>(cols, 0));
        vector<vector<int>> dp(rows, vector<int>(cols, 1));
        for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++)
        for (int k = 0; k < 4; k++){
            int nx = i + dx[k];
            int ny = j + dy[k];
            if (nx >= 0 && nx < rows && ny >= 0 && ny < cols && matrix[i][j] < matrix[nx][ny]) {
                in[nx][ny]++;
            }
        }
        for (int i = 0; i < rows; i++)
        for (int j = 0; j < cols; j++) {
            if (!in[i][j]) {
                qx.push(i);
                qy.push(j);
            }
        }
        int ret = 0;
        while (!qx.empty()) {
            int i = qx.front();
            int j = qy.front();
            qx.pop();
            qy.pop();
            if (dp[i][j] > ret)
                ret = dp[i][j];
            int nv = dp[i][j] + 1;
            for (int k = 0; k < 4; k++) {
                int nx = i + dx[k];
                int ny = j + dy[k];                
                if (nx >= 0 && nx < rows && ny >= 0 && ny < cols && matrix[i][j] < matrix[nx][ny]) {
                    if (dp[nx][ny] < nv)
                        dp[nx][ny] = nv;
                    in[nx][ny]--;
                    if (!in[nx][ny]) {
                        qx.push(nx);
                        qy.push(ny);
                    }
                }
            }
        }
        return ret;
    }
};

关于拓扑排序有一个经典的LeetCode155次周赛压轴题很有意思1203. 项目管理,近期会将拓扑排序知识详细更新,也会将那道周赛题也会详细更新。

发布了209 篇原创文章 · 获赞 42 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/yl_puyu/article/details/104101935