题目及测试
package pid329;
/*给定一个整数矩阵,找出最长递增路径的长度。
对于每个单元格,你可以往上,下,左,右四个方向移动。 你不能在对角线方向上移动或移动到边界外(即不允许环绕)。
示例 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]。注意不允许在对角线方向上移动。
*/
import java.util.List;
public class main {
public static void main(String[] args) {
int[][] testTable = {{2147483647,1}};
test(testTable);
}
private static void test(int[][] ito) {
Solution solution = new Solution();
int rtn;
long begin = System.currentTimeMillis();
for(int i=0;i<ito.length;i++){
for(int j=0;j<ito[0].length;j++){
System.out.print( ito[i][j]+" ");
}
System.out.println();
}
System.out.println();
//开始时打印数组
rtn = solution.longestIncreasingPath(ito);//执行程序
long end = System.currentTimeMillis();
System.out.println("rtn="+rtn);
System.out.println();
System.out.println("耗时:" + (end - begin) + "ms");
System.out.println("-------------------");
}
}
解法1(应该成功,超时)
朴素的深度优先搜索,
时间复杂度 :O(2^(m+n))。对每个有效递增路径均进行搜索。在最坏情况下,会有 O(2^(m+n))次调用
使用深度优先搜索或广度优先搜索从根开始访问连接的所有细胞。在搜索期间更新路径的最大长度,并在搜索完成后得到答案。
找到从坐标(i,j)出发的递增最大长度,前一个节点大小为prevNum,已访问的节点设置为Integer.Min_VALUE
package pid329;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
class Solution {
public int longestIncreasingPath(int[][] matrix) {
int row=matrix.length;
if(row==0){
return 0;
}
int col=matrix[0].length;
if(col==0){
return 0;
}
int maxPath=0;
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
maxPath=Math.max(maxPath, findLongestPath(matrix, i, j, Integer.MIN_VALUE));
}
}
return maxPath;
}
/** 找到从坐标(i,j)出发的递增最大长度,前一个节点大小为prevNum,已访问的节点为Integer.Min_VALUE
* @param matrix
* @param i
* @param j
* @param prevNum
* @return
*/
public int findLongestPath(int[][] matrix,int i,int j,int prevNum){
// 如果超出边界,返回0
if(i<0||j<0||i>=matrix.length||j>=matrix[0].length){
return 0;
}
int now=matrix[i][j];
// 如果当前节点已经被访问,或者比上一个节点小,返回0
if(now==Integer.MIN_VALUE||now<=prevNum){
return 0;
}
// 表明已经被访问过
matrix[i][j]=Integer.MIN_VALUE;
// maxPath是不包含这个节点,从这个节点出发的递增路径的最大长度
int maxPath=0;
maxPath=Math.max(maxPath, findLongestPath(matrix, i+1, j, now));
maxPath=Math.max(maxPath, findLongestPath(matrix, i-1, j, now));
maxPath=Math.max(maxPath, findLongestPath(matrix, i, j+1, now));
maxPath=Math.max(maxPath, findLongestPath(matrix, i, j-1, now));
matrix[i][j]=now;
// 包含自己
return maxPath+1;
}
}
解法二(别人的)
记忆化深度优先搜索
将递归的结果存储下来,这样每个子问题只需要计算一次。
从上面的分析中,在淳朴的深度优先搜索方法中有许多重复的计算。
一个优化途径是我们可以用一个集合来避免一次深度优先搜索中的重复访问。该优化可以将一次深度优先搜索的时间复杂度优化到 O(mn),总时间复杂度 O(m^2 * n^2)。
下面介绍一个更有力的优化方法,记忆化。
在计算中,记忆化是一种优化技术,它通过存储“昂贵”的函数调用的结果,在相同的输入再次出现时返回缓存的结果,以此加快程序的速度。
在本问题中,我们多次递归调用 dfs(x, y) 。但是,如果我们已经知道四个相邻单元格的结果,就只需要常数时间。在搜索过程中,如果未计算过单元格的结果,我们会计算并将其缓存;否则,直接从缓存中获取之。
即int[ ] [ ] cache 里面存储从(i,j)出发的最长递增序列,如果当前节点小于下一个节点,则更新从这个节点出发的最长递增序列(不包括该节点)
// DFS + Memoization Solution
// Accepted and Recommended
public class Solution {
private static final int[][] dirs = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
private int m, n;
public 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;
}
private 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], 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];
}
}
方法三(别人的)
“剥洋葱”(动态规划)
每个细胞的结果只与相邻的结果相关,能否使用动态规划?
如果我们定义从单元格 (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;
}
}