https://leetcode-cn.com/problems/longest-increasing-path-in-a-matrix/solution/ju-zhen-zhong-de-zui-chang-di-zeng-lu-jing-by-leet/
给定一个整数矩阵,找出最长递增路径的长度。
对于每个单元格,你可以往上,下,左,右四个方向移动。 你不能在对角线方向上移动或移动到边界外(即不允许环绕)。
方法一:朴素的优先深度搜索
直觉
深度优先搜索可以找到任何单元格开始的最长递增路径.我们可以对全部单元格进行深度优先搜索
算法
每个单元格可以看作图G中的一个定点.若相邻两细胞的值满足 a < b,则存在有向边(a,b):
问题转换成:寻找有向图G中的最长路径
很显然我们可以使用深度优先搜索或广度优先搜索从根开始访问所有的连接细胞.在搜索期间更新路径的最大长度,并在搜索完成后得到答案.一般而言,在深度优先搜索中,我们可以使用集合visited来避免重复访问
class Solution4{
private static final int[][] dir = {{0,1},{1,0},{0,-1},{-1,0}};
private static int m;
private static int n;
public static int longestIncreasingPath(int[][] matrix) {
if (matrix.length == 0){
return 0;
}
m = matrix.length;
n = matrix[0].length;
int ans = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
ans = Math.max(ans,def(matrix,i,j));
}
}
return ans;
}
private static int def(int[][] matrix,int i,int j){
int ans = 0;
for (int[] d : dir){
int x = i + d[0];
int y = j + d[1];
if (x >= 0 && y >= 0 && x < m && y < n && matrix[x][y] > matrix[i][j]){
ans = Math.max(ans,def(matrix,x,y));
}
}
return ++ans;
}
public static void main(String[] args) {
int[][] arr = {
{3,4,5},
{3,3,6},
{1,2,1}
};
System.out.println(longestIncreasingPath(arr));
}
}
时间复杂度 :O(2^{m+n})。对每个有效递增路径均进行搜索
空间复杂度 : O(mn)。 对于每次深度优先搜索,系统栈需要 O(h) 空间,其中 h 为递归的最深深度.
方法二:记忆化深度优先搜索
直觉:
将递归的结果存储下来,这样每个子问题只需要计算一次
算法
从上面的分析中,我们知道在淳朴的深度优先搜索方法中有许多重复的计算
一个优化途径是我们可以用一个集合来避免一次深度优先搜索中的重复访问.该优化可以将一次深度优先搜索的时间复杂度优化到 O(mn)
下面介绍一个更有力的优化方法,记忆化
在计算中,记忆化是一种优化技术,它通过存储"昂贵"的函数调用的结果,在相同的输入再次出现时返回缓存的结果,以此加快程序速度
在本问题中,我们多次递归调用 dfs(x,y).但是,如果我们已经知道四个相邻单元格的结果,就只需要常数时间.在搜索过程中,如果未计算过单元格的结果,我们会计算并将其缓存;否则,直接从缓存中取
public class Solution {
private static final int[][] dirs = {{0,1},{1,0},{0,-1},{-1,0}};
private static int m,n;
public static int longestIncreasingPath(int[][] matrix) {
if (matrix.length == 0){
return 0;
}
m = matrix.length;
n = matrix[0].length;
int[][] cache = new int[m][n];//这个就是缓存??
int ans = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
ans = Math.max(ans,dfs(matrix,i,j,cache));
}
}
return ans;
}
public static int dfs(int[][] matrix,int i,int j,int[][] cache){
if (cache[i][j] != 0){
return cache[i][j];
}
for (int[] d : dirs){
int x = i + d[0];
int y = j + d[1];
if (0 <= x && x < m && 0 <= y && y < n && matrix[x][y] > matrix[i][j]){
cache[i][j] = Math.max(cache[i][j],dfs(matrix,x,y,cache));
}
}
return ++cache[i][j];
}
}
时间复杂度:
O(mn).每个单元格均计算一次,且只被计算一次.每条边也均计算一次并只计算一次.总时间复杂度为O(V+E).V是顶点总数,E是边总数.本问题中,O(V) = O(mn) ,O(E) = O(4V) = O(mn)
空间复杂度:
O(mn).缓存决定了空间复杂度
方法三:剥洋葱(动态规划)
直觉
每个细胞的结果只与相邻的结果有关,能否使用动态规划呢?
算法
如果我们定义从单元格(i,j)开始的最长递增路径为函数
f(i,j)
则可以写出状态转移函数
f(i,j) = max{f(x,y)|(x,y) is a nei *** or of (i,j) and matrix[x][y] > matrix[i][j] } + 1;
此公式与以前方法使用的公式相同.有了状态转移函数,你可能会觉得可以使用动态规划来推导出所有结果,取他的深度优先搜索!
这听起来很美好,可惜忽略了一件事情:我们没有依赖列表
想要让动态规划有效,如果问题B依赖问题A的结果,就必须确保问题A比问题B先计算.这样的依赖顺序对许多问题十分简单自然.如著名的斐波那契数列:
F(0) = 1,F(1) = 1,F(n) = F(n -1 ) + F(n - 2)
子问题F(n)依赖于F(n -1 ) + F(n - 2).因此,自然顺序就是正确的计算顺序.被依赖者总会先被计算
这种依赖顺序的术语是"拓扑顺序"或"拓扑排序"
对有向无环图的拓扑排序是顶点的一个线性排序,使得对于任何有向边(u,v),顶点u都在顶点v的前面
在本问题中,拓扑顺序并不简单自然.没有矩阵的值,我们无法知道两个邻居A和B的依赖关系.作为预处理,我们必须显示执行拓扑排序.之后我们可以按照存储的拓扑顺序使用状态转移函数动态地解决问题
有多种实现拓扑排序的方法.这里我们使用的是一种被称为"剥洋葱"的方法.其思路是在一个有向无环图中,会有一些不依赖于其他顶点的的顶点,称为"叶子".我们将这些叶子放于一个列表中(他们的内部排序并不重要),然后将他们从图中移出.移除之后就会产生新的叶子,就像一层一层拨开洋葱的心.最后,列表中就会存储有效的拓扑排序.
在本问题中,因为我们想要求出整个图的最长路径,也就是洋葱层的总层数.因此我们可以在剥离的期间计算层数,在不调用动态规划的情况下返回计数.
// Topological Sort Based Solution
// An Alternative Solution
public class Solution {
private static final int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
private int m, n;
public int longestIncreasingPath(int[][] grid) {
int m = grid.length;
if (m == 0) return 0;
int n = grid[0].length;
// padding the matrix with zero as boundaries
// assuming all positive integer, otherwise use INT_MIN as boundaries
int[][] matrix = new int[m + 2][n + 2];
for (int i = 0; i < m; ++i)
System.arraycopy(grid[i], 0, matrix[i + 1], 1, n);
// calculate outdegrees
int[][] outdegree = new int[m + 2][n + 2];
for (int i = 1; i <= m; ++i)
for (int j = 1; j <= n; ++j)
for (int[] d: dir)
if (matrix[i][j] < matrix[i + d[0]][j + d[1]])
outdegree[i][j]++;
// find leaves who have zero out degree as the initial level
n += 2;
m += 2;
List<int[]> leaves = new ArrayList<>();
for (int i = 1; i < m - 1; ++i)
for (int j = 1; j < n - 1; ++j)
if (outdegree[i][j] == 0) leaves.add(new int[]{i, j});
// remove leaves level by level in topological order
int height = 0;
while (!leaves.isEmpty()) {
height++;
List<int[]> newLeaves = new ArrayList<>();
for (int[] node : leaves) {
for (int[] d:dir) {
int x = node[0] + d[0], y = node[1] + d[1];
if (matrix[node[0]][node[1]] > matrix[x][y])
if (--outdegree[x][y] == 0)
newLeaves.add(new int[]{x, y});
}
}
leaves = newLeaves;
}
return height;
}
}
作者:LeetCode
链接:https://leetcode-cn.com/problems/longest-increasing-path-in-a-matrix/solution/ju-zhen-zhong-de-zui-chang-di-zeng-lu-jing-by-leet/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。