算法-動態規劃-最小路徑和
1 題目概述
1.1 題目出處
https://leetcode-cn.com/problems/minimum-path-sum/
1.2 題目描述
給定一個無序的整數數組,找到其中最長上升子序列的長度。
示例:
輸入: [10,9,2,5,3,7,101,18]
輸出: 4
解釋: 最長的上升子序列是 [2,3,7,101],它的長度是 4。
說明:
可能會有多種最長上升子序列的組合,你只需要輸出對應的長度即可。
你算法的時間複雜度應該爲 O(n2) 。
進階: 你能將算法的時間複雜度降低到 O(n log n) 嗎?
2 BFS
2.1 思路
從左上角開始,進行BFS,每次拿出隊首元素,然後分別計算該元素的路徑和加上右側和下側元素的數值之和
,看是否分別小於當前右和下側元素的路徑和,如果小於就更新,並且在這些元素未訪問過時推入隊列末尾進行BFS。
2.2 代碼
class Solution {
public int minPathSum(int[][] grid) {
int result = 0;
if(grid == null || grid.length == 0){
return result;
}
int m = grid.length - 1;
int n = grid[0].length - 1;
// 用來存放某節點的最小路徑和
int[][] record = new int[m + 1][n + 1];
for(int i = 0; i < record.length; i++){
for(int j = 0; j < record[0].length; j++){
record[i][j] = -1;
}
}
// 思路 從左上開始,分別計算右側和下側節點的最小路徑和,並推入隊列等待計算
LinkedList<int[]> queue = new LinkedList<>();
record[0][0] = grid[0][0];
int[] first = {0, 0, record[0][0]};
queue.add(first);
while(!queue.isEmpty()){
int[] ele = queue.poll();
if(ele[1] < n){
if(record[ele[0]][ele[1] + 1] == -1){
record[ele[0]][ele[1] + 1] = record[ele[0]][ele[1]] + grid[ele[0]][ele[1] + 1];
int[] right = {ele[0], ele[1] + 1, record[ele[0]][ele[1] + 1]};
queue.add(right);
}else{
record[ele[0]][ele[1] + 1] = Math.min(record[ele[0]][ele[1] + 1], record[ele[0]][ele[1]] + grid[ele[0]][ele[1] + 1]);
}
}
if(ele[0] < m){
if(record[ele[0] + 1][ele[1]] == -1){
record[ele[0] + 1][ele[1]] = record[ele[0]][ele[1]] + grid[ele[0] + 1][ele[1]];
int[] below = {ele[0] + 1, ele[1], record[ele[0] + 1][ele[1]]};
queue.add(below);
}else{
record[ele[0] + 1][ele[1]] = Math.min(record[ele[0] + 1][ele[1]], record[ele[0]][ele[1]] + grid[ele[0] + 1][ele[1]]);
}
}
}
return record[m][n];
}
}
2.3 時間複雜度
O(m*n)
2.4 空間複雜度
O(m*n)
3 DFS
3.1 思路
既然寫了BFS,那爲了練習我們可以來個DFS。
但有個問題,如果採用DFS,先遍歷右側,那可能導致有些節點因爲已經被訪問過而沒有更新更小的值!
也就是說,我們不能像之前那樣不管已經訪問過的節點,需要再次訪問!
3.2 代碼
class Solution {
private int m = 0;
private int n = 0;
public int minPathSum(int[][] grid) {
int result = 0;
if(grid == null || grid.length == 0){
return result;
}
m = grid.length - 1;
n = grid[0].length - 1;
// 用來存放某節點的最小路徑和
int[][] record = new int[m + 1][n + 1];
for(int i = 0; i < record.length; i++){
for(int j = 0; j < record[0].length; j++){
record[i][j] = -1;
}
}
record[0][0] = grid[0][0];
dfs(grid, record, 0, 0);
return record[m][n];
}
private void dfs(int[][] grid, int[][] record, int i, int j){
if(j < n){
if(record[i][j + 1] == -1){
record[i][j + 1] = record[i][j] + grid[i][j + 1];
}else{
record[i][j + 1] = Math.min(record[i][j + 1], record[i][j] + grid[i][j + 1]);
}
dfs(grid, record, i, j+1);
}
if(i < m){
if(record[i + 1][j] == -1){
record[i + 1][j] = record[i][j] + grid[i + 1][j];
}else{
record[i + 1][j] = Math.min(record[i + 1][j], record[i][j] + grid[i + 1][j]);
}
dfs(grid, record, i+1, j);
}
}
}
3.3 時間複雜度
果然,超出了時間限制!
4 DFS-優化
4.1 思路
從右下往左上進行DFS:
record[i][j] = grid[i][j] + Math.min(dfs(grid, record, i - 1, j), dfs(grid, record, i, j - 1));
還是採用record進行記錄,但注意這裏因爲已經是最終的最小路徑和,所以重複訪問時可以直接返回該值,不需要再進行計算!
4.2 代碼
class Solution {
private int m = 0;
private int n = 0;
public int minPathSum(int[][] grid) {
int result = 0;
if(grid == null || grid.length == 0){
return result;
}
m = grid.length - 1;
n = grid[0].length - 1;
// 1.緩存最小路徑和
// 2.標識是否訪問過
Integer[][] record = new Integer[m + 1][n + 1];
// 思路 從右下開始往左和上推
return dfs(grid, record, m, n);
}
private int dfs(int[][] grid, Integer[][] record, int i, int j){
if(i < 0 || j < 0){
// 越界
return Integer.MAX_VALUE;
}
if(i == 0 && j == 0){
return grid[0][0];
}
if(record[i][j] != null){
return record[i][j];
}
record[i][j] = grid[i][j] + Math.min(dfs(grid, record, i - 1, j), dfs(grid, record, i, j - 1));
return record[i][j];
}
}
4.3 時間複雜度
O(m*n)
4.4 空間複雜度
O(m*n)
5 動態規劃1
5.1 思路
前面BFS看起來不錯,但其實好像沒有必要用隊列,而且速度也很慢。我們改成動態規劃試試。
因爲我們遍歷採用的是從左到右,從上到下,所以是可以保證每個節點都是最小路徑和。
動態規劃轉移方程如下:
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j-1]) + grid[i-1][j-1];
其中dp[i][j]表示該節點的最小路徑和。
5.2 代碼
class Solution {
public int minPathSum(int[][] grid) {
int result = 0;
if(grid == null || grid.length == 0){
return result;
}
int m = grid.length;
int n = grid[0].length;
// 用來存放某節點的最小路徑和
int[][] dp = new int[m + 1][n + 1];
// 在外圍虛擬一圈,除了grid[0][0]的左和上爲0外,其他值都爲Integer.MAX_VALUE;
// 這樣做的目的是爲了動態規劃遍歷中不用管邊界值
int[] zeroRow = dp[0];
for(int i = 2; i < zeroRow.length; i++){
zeroRow[i] = Integer.MAX_VALUE;
}
for(int i = 2; i < m + 1; i++){
dp[i][0] = Integer.MAX_VALUE;
}
for(int i = 1; i <= m; i++){
for(int j = 1; j <= n; j++){
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j-1]) + grid[i-1][j-1];
}
}
return dp[m][n];
}
}
5.3 時間複雜度
O(m*n)
這次速度快很多了!
5.4 空間複雜度
O(m*n)
6 動態規劃2
6.1 思路
前面動態規劃1看起來很妙,但還用了額外空間。
仔細觀察,其實按上面方法遍歷,每個元素的初始值只用了一次,那麼可以複用原始數組grid,遍歷過程中將計算的最小路徑和放入裏面。也就是說grid[i][j]最終表示該節點的最小路徑和。
這樣,最後直接返回grid[m-1][n-1]即可
6.2 代碼
class Solution {
public int minPathSum(int[][] grid) {
int result = 0;
if(grid == null || grid.length == 0){
return result;
}
int m = grid.length;
int n = grid[0].length;
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(i + j == 0){
continue;
}else if (i == 0){
grid[i][j] = grid[i][j-1] + grid[i][j];
}else if (j == 0){
grid[i][j] = grid[i - 1][j] + grid[i][j];
}else{
grid[i][j] = Math.min(grid[i - 1][j], grid[i][j-1]) + grid[i][j];
}
}
}
return grid[m-1][n-1];
}
}
6.3 時間複雜度
O(m*n)
6.4 空間複雜度
O(1)
因爲複用原數組grid,沒有額外空間。