day 19-20 算法:動態規劃,爬樓梯,三角形最小路徑和,乘積最大子序列,最長上升子序列

1. 題目

  1. 爬樓梯:假設需要n階能夠爬到頂樓,每一次只能爬1階或者2階,求問,有多少種不同的方法爬上樓頂? https://leetcode-cn.com/problems/climbing-stairs/description/
  2. 三角形最小路徑和:https://leetcode-cn.com/problems/triangle/description/
  3. 乘積最大子序列:https://leetcode-cn.com/problems/maximum-product-subarray/description/
  4. 最長上升子序列:https://leetcode-cn.com/problems/longest-increasing-subsequence/

2. 基本知識

2.1 動態規劃

動態規劃和分治類似,但他們有異同點:

  1. 都是通過組合子問題的方式來解決原始問題
  2. 動態規劃的子問題是會重合的,而分治的子問題則是不相交的
  3. 不同的子問題中會有一些公共的子問題,動態規劃不會重複計算子問題,而分治則會重複計算,做更多的不必要工作

2.2 動態規劃的核心

使用動態規劃都可以使用遞歸(分治)來完成,在遞歸的基礎上加上記憶化即可去除重複的計算,推導出最優解的方程(當前解和之前已經算出來的解之間的關係)來得出結果。

  1. 遞歸 + 記憶化 -> 遞推(從小到大計算)
  2. 狀態的定義,用數組輔助存儲已計算的值,避免多次計算
  3. 狀態轉移方程:opt[n] = best_of(opt[n-1], opt[n-2],…)
  4. 最優子結構

2.3 簡單的例子

2.3.1 斐波那契數列

計算斐波那契數列的和: 0,1,2,3,5,8…

  1. 遞歸算法

    時間複雜度 O(2n),空間複雜度O(1)

     private int fib(int n) {
         return (n <= 1) ? n : fib(n - 1) + fib(n - 2);
     }
    
  2. 動態規劃

    時間複雜度O(n),空間複雜度O(n)

     private int dpFib(int n) {
         int[] memo = new int[n];
         memo[0] = 0; memo[1] = 1;
         for (int i = 2; i < n; i++) {
             // 從小到大遞推,如果從大到小,又是一堆重複計算
             memo[i] = memo[i - 1] + memo[i - 2];
         }
         return memo[n-1];
     }
    

2.3.2 障礙棋盤路徑

給定一個棋盤,從最左上角到最右下角有多少條路徑,棋盤中有些格子中有石頭無法通過。

  1. 遞歸解法

     private int paths(boolean[][] isStone, int m, int n) {
         if (isStone[m][n]) return 0;
         if (m == 0 || n == 0) return 1;
         return paths(isStone, m - 1, n) + paths(isStone, m, n - 1);
     }
    
  2. 動態規劃

     private int dpPaths(boolean[][] isStone, int m, int n) {
         int[][] opt = new int[m + 1][n + 1];
    
         for (int i = 0; i < m; i++) {
             for (int j = 0; j < n; j++) {
                 // opt[i,j] = opt[i-1,j] + opt[i,j-1]
                 if (i == 0 || j == 0) {
                     opt[i][j] = 1;
                     continue;
                 }
                 if (isStone[i][j]) {
                     //碰到石頭
                     opt[i][j] = 0;
                 } else {
                     //可以通過
                     opt[i][j] = opt[i - 1][j] + opt[i][j - 1];
                 }
             }
         }
    
         return opt[m][n];
     }
    

3. 算法題解

3.1 爬樓梯

假設需要n階能夠爬到頂樓,每一次只能爬1階或者2階,求問,有多少種不同的方法爬上樓頂?

分析:由於每次只能爬1階或者2階,所以,從站在樓頂那一刻倒推,倒推1階,假設有f(n-1)種辦法,倒推2階,假設有f(n-2)種辦法,則,f(n) = f(n-1) + f(n-2),這是一個標準的斐波那契數列的和解法。見 2.3.1 斐波那契數列

3.2 三角形最小路徑

給定一個三角形,找出自頂向下的最小路徑和,每一步只能移動到下一行的相鄰節點上(角標:(i+1,j+1);(i+1,j))
例如:

[
    [2],
    [3,4],
    [6,5,7],
    [4,1,8,3]
]

得到的結果爲:2+3+5+1 = 11

解法1:遞歸
當前節點值加上下面兩個分支的最小值,加起來總和爲最小路徑和。
該解法時間複雜度:O(2n),空間複雜度爲O(1)

private int getResult(int[][] num) {
    return dfsSum(num, 0, 0);
}

 private int dfsSum(int[][] num, int m, int n) {
    if (m >= num.length || n >= num[0].length) return 0;
    int min = Math.min(dfsSum(num, m + 1, n), dfsSum(num, m + 1, n + 1));
    return num[m][n] + min;
}

解法2:動態規劃
最後一行分別賦值給初始最小路徑節點值,然後根據公式,sum[i][j] = Math.min(sum[i+1][j], sum[i+1][j+1]) + num[i][j];層層倒推,得到sum[0][0]即爲當前的解。

private int dpSum(int[][] num) {
    int row = num.length;
    int col = num[num.length - 1].length;
    int[][] sum = new int[row][col];

    for (int i = row-1; i >=0; i--) {
        for (int j = 0; j < num[i].length; j++) {
            if (i == row - 1) {
                // 最後一行
                sum[i][j] = num[i][j];
                continue;
            }
            // 下一行相鄰兩個路徑的最小值加上當前節點值
            sum[i][j] = Math.min(sum[i+1][j], sum[i+1][j+1]) + num[i][j];
        }
    }
    return sum[0][0];
}

上面使用的是二維數組來存放中間計算的值,空間複雜度爲O(n2),下面優化爲O(n)

private int dpSum2(List<List<Integer>> triangle) {
    int row = triangle.size();
    // 初始化一個一維數組,用來輔助存儲單次循環內的路徑和
    int[] dp = new int[row];

    for (int i = 0; i < row; i++) {
        // 將最後一行的值賦值爲初始值
        dp[i] = triangle.get(row - 1).get(i);
    }
    for (int i = row - 2; i >= 0; i--) {
        for (int j = 0; j <= i; j++) {
            // 將路徑和更新
            dp[j] = triangle.get(i).get(j) + Math.min(dp[i], dp[i + 1]);
        }
    }
    return dp[0];
}

3.3 乘積最大子序列

給定一個數組nums,找出一個序列中乘積最大的連續子序列(該序列至少包含一位數)

示例:

輸入:[2,3,-2,4]
輸出:6
解釋:子數組[2,3]有最大乘積

解題思路:
動態規劃兩個核心步驟:

  1. 定義狀態:使用二維數組存儲子乘積的最大和最小值

  2. 定義狀態方程:如果當前數大於0:取最大數*當前數,得最大子乘積;如果當前數小於0,則取最小數,乘以當前數,得最小數。

    private int maxProduct(int[] nums) {
    //第二列爲了表示是最大還是最小值,第一行爲最大值,第二行爲最小值
    int[][] dp = new int[2][2];
    //初始化最大最小值
    dp[0][0] = dp[0][1] = nums[0];

     int max = 0;
     for (int i = 1; i < nums.length; i++) {
         int x = i % 2;
         int y = (i - 1) % 2;
         // 最大值
         dp[x][0] = Math.max(dp[y][0] * nums[i], dp[y][1] * nums[i]);
         // 最小值
         dp[x][1] = Math.min(dp[y][0] * nums[i], dp[y][1] * nums[i]);
         max = Math.max(max, dp[x][0]);
     }
     return max;
    

    }

也可以只定義兩個變量代替數組:

private int maxProduct2(int[] nums) {
    int icMax = nums[0];
    int icMin = nums[0];

    for (int i = 1; i < nums.length; i++) {
        icMax = max(icMax * nums[i], icMax * nums[i], icMax);
        icMin = min(icMax * nums[i], icMax * nums[i], icMin);
    }
    return icMax;
}

3.4 最長上升子序列

給定一個無序的整數數組,找到其中最長上升子序列的長度。

示例:

輸入:[10,9,2,5,3,7,101,18]
輸出:4
解釋:最長子序列:[2,3,7,101]

解法1: 遞歸
從頭開始遞歸,如果當前節點比上一個節點值大,那麼就 + 1,否則繼續遞歸,直到遍歷完成爲止。
該解法時間複雜度:O(2n),空間複雜度O(1)

private int lengthOfLIS(int[] nums) {
    return lengthofLIS(nums, Integer.MIN_VALUE, 0);
}

private int lengthofLIS(int[] nums, int pre, int cur) {

    if (cur == nums.length) return 0;

    int taken = 0;
    if (pre < nums[cur]) {
        //當前節點是有效的上升子序列的一個
        taken = 1 + lengthofLIS(nums, nums[cur], cur + 1);
    }
    // 當前節點不是上升子序列中的節點
    int noTaken = lengthofLIS(nums, pre, cur + 1);
    return Math.max(taken, noTaken);
}

解法2: 動態規劃
動態規劃核心步驟:

  1. 定義狀態:使用一個一位數組來存放當前升序的最大子序列長度

  2. 定義狀態方程:如果當前值和之前的所有元素遍歷對比,當前值較大,且該子序列是前面所有子序列長度的最大值,那麼+1就是當前的最大子序列的長度。

     private int lengthOfLIS(int[] nums) {
         if (nums.length == 0) return 0;
    
         int[] dp = new int[nums.length];
         dp[0] = 1;
    
         int maxas = 1;
         for (int i = 1; i < nums.length; i++) {
             int maxval = 0;
             for (int j = 0; j < i; j ++) {
                 if (nums[i] > nums[j]) {
                     maxval = Math.max(maxval, dp[j]);
                 }
             }
             dp[i] = maxval + 1;
             maxas = Math.max(maxas, dp[i]);
         }
         return maxas;
     } 
    
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章