1. 程式人生 > 其它 >[leetcode刷題]——動態規劃

[leetcode刷題]——動態規劃

此部落格主要記錄使用動態規劃解題的方法

斐波那契數列

一、 爬樓梯

70. 爬樓梯 (easy)2021-07-25

假設你正在爬樓梯。需要n階你才能到達樓頂。

每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?

注意:給定n是一個正整數。

  第一次見到這個人題的我,這不就遞迴嘛,然後就。。。。。。很快啊 , 超出時間限制

class Solution {
    public int climbStairs(int n) {
        if(n == 1) return 1;
        if(n == 2) return 2;
        return
climbStairs(n - 1) + climbStairs(n - 2); } }

  不使用遞迴,將每一次遍歷使用變數儲存下來。遞迴的時候重複運算太多,此方法解決了這個問題

class Solution {
    public int climbStairs(int n) {
        if(n == 1) return 1;
        if(n == 2) return 2;
        int pre2 = 1, pre1 = 2;
        for(int i = 3; i <= n; i++){
            int cur = pre1 + pre2;
            pre2 
= pre1; pre1 = cur; } return pre1; } }

二、強盜搶劫

198. 打家劫舍 (medium)2021-07-25

你是一個專業的小偷,計劃偷竊沿街的房屋。每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。

給定一個代表每個房屋存放金額的非負整數陣列,計算你 不觸動警報裝置的情況下 ,一夜之內能夠偷竊到的最高金額。

  比上一題稍微複雜一點,但是是一個套路

class Solution {
    
public int rob(int[] nums) { int pre2 = 0, pre1 = 0; for (int i = 0; i < nums.length; i++) { int cur = Math.max(pre2 + nums[i], pre1); pre2 = pre1; pre1 = cur; } return pre1; } }

三、 強盜在環形街區搶劫

213. 打家劫舍Ⅱ (medium)2021-07-25

你是一個專業的小偷,計劃偷竊沿街的房屋,每間房內都藏有一定的現金。這個地方所有的房屋都 圍成一圈 ,這意味著第一個房屋和最後一個房屋是緊挨著的。同時,相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警 。

給定一個代表每個房屋存放金額的非負整數陣列,計算你 在不觸動警報裝置的情況下 ,今晚能夠偷竊到的最高金額。

//核心思想,將環形佇列分成兩個佇列
//一個 0 - n-1, 一個 1 - n, 返回一個最大的
class Solution {
    public int rob(int[] nums) {
        if(nums == null || nums.length == 0){
            return 0;
        }
        int len = nums.length;
        if(len == 1) return nums[0];
        return Math.max(robCalculate(nums,0,len - 2), 
                        robCalculate(nums,1,len - 1));
    }
    public int robCalculate(int[] nums, int first, int last){
        int pre2 = 0;
        int pre1 = 0;
        for(int i = first; i <= last; i++){
            int cur = Math.max(pre2 + nums[i], pre1);
            pre2 = pre1;
            pre1 = cur;
        }
        return pre1;
    }
}

矩陣路徑

一、矩陣的最小路徑和

64. 最小路徑和 (medium)2021-07-25

給定一個包含非負整數的mxn網格grid,請找出一條從左上角到右下角的路徑,使得路徑上的數字總和為最小。

說明:每次只能向下或者向右移動一步。

  慣性思維的我習慣使用回溯演算法,可奈何遞迴的方法太容易溢位了。

  遞迴已死,動態規劃當立

class Solution {
    public int minPathSum(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int[][] path = new int[m][n];
        path[0][0] = grid[0][0];
        for(int i = 1; i < m; i++){
            path[i][0] = path[i - 1][0] + grid[i][0];
        }
        for(int j = 1; j < n; j++){
            path[0][j] = path[0][j - 1] + grid[0][j];
        }
        
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                path[i][j] = Math.min(path[i][j - 1], path[i - 1][j]) 
                    + grid[i][j];
            }
        }
        return path[m - 1][n - 1];
    }   
}

二、矩陣的總路徑數

62. 不同路徑 (medium)2021-07-25

一個機器人位於一個 m x n網格的左上角 (起始點在下圖中標記為 “Start” )。

機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記為 “Finish” )。

問總共有多少條不同的路徑?

  想用數學方法降維打擊,結果我被打擊了,又溢位了。

class Solution {
    public int uniquePaths(int m, int n) {
        return factorial(m + n - 2) /(factorial(m - 1) * factorial(n - 1));
    }
    public int factorial(int k){
        if(k == 0 || k == 1) return 1;
        return factorial(k - 1) * k;
    }
}

  換個方法,超過100%, 嘻嘻

class Solution {
    public int uniquePaths(int m, int n) {
        int[][] path = new int[m][n];
        path[0][0] = 0;
        for(int i = 0; i < m; i++){
            path[i][0] = 1;
        }
        for(int j = 0; j < n; j++){
            path[0][j] = 1;
        }
        for(int i = 1; i < m; i++){
            for(int j = 1; j < n; j++){
                path[i][j] = path[i - 1][j] + path[i][j - 1];
            }
        }
        return path[m - 1][n - 1];
    }
}

陣列區間

一、陣列區間和

303. 區域和檢索 - 陣列不可變 (easy)2021-07-26

給定一個整數陣列 nums,求出陣列從索引i到j(i≤j)範圍內元素的總和,包含i、j兩點。

實現 NumArray 類:

NumArray(int[] nums) 使用陣列 nums 初始化物件
int sumRange(int i, int j) 返回陣列 nums 從索引i到j(i≤j)範圍內元素的總和,包含i、j兩點(也就是 sum(nums[i], nums[i + 1], ... , nums[j]))

class NumArray {
    private int[] nums;

    public NumArray(int[] nums) {
        this.nums = nums;
    }
    
    public int sumRange(int left, int right) {
        int ret = 0;
        for(int i = left; i <= right; i++){
            ret  += nums[i];
        }
        return ret;
    }
}

這個題的題目意思沒有表達清楚,需要高頻多次呼叫sumRange函式

正經答案是這樣的

class NumArray {

    private int[] sums;

    public NumArray(int[] nums) {
        sums = new int[nums.length + 1];
        for (int i = 1; i <= nums.length; i++) {
            sums[i] = sums[i - 1] + nums[i - 1];
        }
    }

    public int sumRange(int i, int j) {
        return sums[j + 1] - sums[i];
    }
}

二、陣列中等差遞增子區間的個數

413. 等差數列的劃分 (medium)2021-07-26

如果一個數列 至少有三個元素 ,並且任意兩個相鄰元素之差相同,則稱該數列為等差數列。

例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差數列。
給你一個整數陣列 nums ,返回陣列 nums 中所有為等差陣列的 子陣列 個數。

子陣列 是陣列中的一個連續序列。

這個題的主要解題思想就是,使用dp 陣列記錄以 nums[n]結尾的等差陣列的個數, 如果nums[n + 1]和前面的依然等差,那麼dp[n+1] = dp[n]+1

/**
動態規劃的題目最重要的是找到他的遞迴函式
*/
class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        if(nums == null || nums.length == 0){
            return 0;
        }
        int n = nums.length;
        int[] dp = new int[n];
        for(int i = 2; i < n; i++){
            if(nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]){
                dp[i] = dp[i - 1] + 1;
            }
        }
        int count = 0;
        for(int d : dp){
            count += d;
        }
        return count;
    }
}

分割整數

一、分割整數的最大乘積

343. 整數拆分 (medium)2021-07-26

給定一個正整數n,將其拆分為至少兩個正整數的和,並使這些整數的乘積最大化。 返回你可以獲得的最大乘積。

  動態規劃最重要的找到遞迴的函式。

  這個題的主要思路,dp[i] 表示整數 i 對應的最大乘積, 那麼 dp[i] = dp[j] * (i - j), j屬於[1, i -1] ,然後遍歷取最大值

  dp[i] 的最大值不一定是 dp[i - j] * j, 還有一種可能是(i- j)* j 。這是多出來的一種組合。

class Solution {
    public int integerBreak(int n) {
        int[] dp = new int[n + 1];
        dp[2] = 1;
        for(int i = 3; i <= n; i++){
            for(int j = 1; j < i; j++){
                dp[i] = Math.max(dp[i], Math.max(dp[i-j] * j, (i-j)*j));
            }
        }
        return dp[n];
    }
}

二、按平方數來分割整數

279. 完全平方數 (medium)2021-07-27

給定正整數n,找到若干個完全平方數(比如1, 4, 9, 16, ...)使得它們的和等於 n。你需要讓組成和的完全平方數的個數最少。

給你一個整數 n ,返回和為 n 的完全平方數的 最少數量 。

完全平方數 是一個整數,其值等於另一個整數的平方;換句話說,其值等於一個整數自乘的積。例如,1、4、9 和 16 都是完全平方數,而 3 和 11 不是。

  找到動態規劃中的遞迴方法尤其重要

class Solution {
    public int numSquares(int n) {
        List<Integer> list = square(n);
        int[] dp = new int[n + 1];
        for(int i = 1; i <= n; i++){
            int min = Integer.MAX_VALUE;
            for(int l : list){
                if(i - l < 0) break; 
                min = Math.min(min, dp[i - l] + 1);
            }
            dp[i] = min;
        }
        return dp[n];    
    }
    //返回小於n 的所有完全平方數
    public List<Integer> square(int n){
        List<Integer> list = new ArrayList<>();
        int diff = 3;
        int gap = 2;
        int i = 1;
        while(true){
            list.add(i);
            i += diff;
            diff += gap;
            if(i > n) break;
        }
        return list;
    }
}

三、 分割整數構成字母字串

91. 解碼方法 (medium)2021-07-27

一條包含字母A-Z 的訊息通過以下對映進行了 編碼 :

'A' -> 1
'B' -> 2
...
'Z' -> 26
要 解碼 已編碼的訊息,所有數字必須基於上述對映的方法,反向映射回字母(可能有多種方法)。例如,"11106" 可以對映為:

"AAJF" ,將訊息分組為 (1 1 10 6)
"KJF" ,將訊息分組為 (11 10 6)
注意,訊息不能分組為 (1 11 06) ,因為 "06" 不能對映為 "F" ,這是由於 "6" 和 "06" 在對映中並不等價。

給你一個只含數字的 非空 字串 s ,請計算並返回 解碼 方法的 總數 。

題目資料保證答案肯定是一個 32 位 的整數。

  這個題沒什麼意思,和演算法關係不大,主要是邊界條件的判斷。

public int numDecodings(String s) {
    if (s == null || s.length() == 0) {
        return 0;
    }
    int n = s.length();
    int[] dp = new int[n + 1];
    dp[0] = 1;
    dp[1] = s.charAt(0) == '0' ? 0 : 1;
    for (int i = 2; i <= n; i++) {
        int one = Integer.valueOf(s.substring(i - 1, i));
        if (one != 0) {
            dp[i] += dp[i - 1];
        }
        if (s.charAt(i - 2) == '0') {
            continue;
        }
        int two = Integer.valueOf(s.substring(i - 2, i));
        if (two <= 26) {
            dp[i] += dp[i - 2];
        }
    }
    return dp[n];
}

最長遞增子序列

一、 最長遞增子序列

300. 最長遞增子序列 (medium)2021-07-27

給你一個整數陣列 nums ,找到其中最長嚴格遞增子序列的長度。

子序列是由陣列派生而來的序列,刪除(或不刪除)陣列中的元素而不改變其餘元素的順序。例如,[3,6,2,7] 是陣列 [0,3,1,6,2,2,7] 的子序列。

  

  傳統的動態規劃套路,但是時間複雜度 O(N*2), 空間複雜度 O(N)。

class Solution {
    public int lengthOfLIS(int[] nums) {
        int len = nums.length;
        int[] dp = new int[len];
        for(int i = 0; i < len; i++){
            dp[i] = 1;
            for(int j = 0; j < i; j++){
                if(nums[j] < nums[i]){
                    dp[i] = Math.max(dp[i] ,dp[j] + 1);
                }
            }
        }
        int max = 0;
        for(int i = 0; i < len; i++){
            max = Math.max(max, dp[i]);
        }
        return max;
    }
}

  有更好的二分查詢的方法,時間複雜度為 O(NlogN)

二、 一組整數對能夠構成的最長鏈

646. 最長數對鏈 (medium)2021-08-01

給出n個數對。在每一個數對中,第一個數字總是比第二個數字小。

現在,我們定義一種跟隨關係,當且僅當b < c時,數對(c, d)才可以跟在(a, b)後面。我們用這種形式來構造一個數對鏈。

給定一個數對集合,找出能夠形成的最長數對鏈的長度。你不需要用到所有的數對,你可以以任何順序選擇其中的一些數對來構造。

  貪心演算法解決

class Solution {
    public int findLongestChain(int[][] pairs) {
        Arrays.sort(pairs, new Comparator<int[]>(){
            @Override
            public int compare(int[] o1, int[] o2){
                return o1[1] - o2[1];
            }
        });
        int len = 1;
        int last = pairs[0][1];
        for(int i = 1; i < pairs.length; i++){
            if(pairs[i][0] > last){
                last = pairs[i][1];
                
                len++;
            }
        }
        return len;
    }
}

  這裡使用動態規劃反而並不是最佳的方案

public int findLongestChain(int[][] pairs) {
    if (pairs == null || pairs.length == 0) {
        return 0;
    }
    Arrays.sort(pairs, (a, b) -> (a[0] - b[0]));
    int n = pairs.length;
    int[] dp = new int[n];
    Arrays.fill(dp, 1);
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (pairs[j][1] < pairs[i][0]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }
    return Arrays.stream(dp).max().orElse(0);
}

三、最長擺動子序列

376. 擺動序列 (medium)2021-08-01

如果連續數字之間的差嚴格地在正數和負數之間交替,則數字序列稱為 擺動序列 。第一個差(如果存在的話)可能是正數或負數。僅有一個元素或者含兩個不等元素的序列也視作擺動序列。

例如,[1, 7, 4, 9, 2, 5] 是一個 擺動序列 ,因為差值 (6, -3, 5, -7, 3)是正負交替出現的。

相反,[1, 4, 7, 2, 5]和[1, 7, 4, 5, 5] 不是擺動序列,第一個序列是因為它的前兩個差值都是正數,第二個序列是因為它的最後一個差值為零。
子序列 可以通過從原始序列中刪除一些(也可以不刪除)元素來獲得,剩下的元素保持其原始順序。

給你一個整數陣列 nums ,返回 nums 中作為 擺動序列 的 最長子序列的長度 。

  這個題並沒有用到動態規劃,使用數學規律時間複雜度達到 O(N)

class Solution {
    public int wiggleMaxLength(int[] nums) {
        if(nums == null || nums.length == 0) return 0;
        int up = 1, down = 1;
        for(int i = 1; i < nums.length; i++){
            if(nums[i] > nums[i - 1]){
                up = down + 1;
            }else if(nums[i] < nums[i - 1]){
                down = up + 1;
            }
        }
        return Math.max(up, down);
    }
}

最長公共子序列

1143. 最長公共子序列 (medium)2021-08-01

給定兩個字串text1 和text2,返回這兩個字串的最長 公共子序列 的長度。如果不存在 公共子序列 ,返回 0 。

一個字串的子序列是指這樣一個新的字串:它是由原字串在不改變字元的相對順序的情況下刪除某些字元(也可以不刪除任何字元)後組成的新字串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
兩個字串的 公共子序列 是這兩個字串所共同擁有的子序列。

  值得注意的是,很容易走向動態規劃就一個dp[] 一維陣列的誤區,這裡用到的是二維陣列。

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int l1 = text1.length();
        int l2 = text2.length();
        int[][] dp = new int[l1  + 1][l2 + 1];
        for(int i = 1; i <= l1; i++){
            for(int j = 1; j <= l2; j++){
                if(text1.charAt(i-1) == text2.charAt(j-1)){
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                }else{
                    dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[l1][l2];
    }
}