【數據結構與算法】動態規划經典題總結[leetcode]


爬樓梯

** 五星 **
LeetCode:爬樓梯

題目描述:

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

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

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

示例:

輸入: 3
輸出: 3
解釋: 有三種方法可以爬到樓頂。
1.  1 階 + 1 階 + 1 階
2.  1 階 + 2 階
3.  2 階 + 1 階

思想:

1.動態規划,時間O(n)
到達某一個台階方法數,等於抵達前兩個台階的方法數之和。

可使用滾動數組來優化,僅保存三個變量,使得空間復雜度為O(1)
2.矩陣快速冪
時間:O(log(n));空間:O(1)
3.斐波那契數列通項公式
時間:O(log(n));空間:O(1)

代碼:

滾動數組,僅保存三個變量

class Solution {
    public int climbStairs(int n) {
        int r1=1,r2=2,res=n;//r1和r2分別表示當前位置的前兩個數
        for(int i = 3;i<=n;++i){
            res = r1 + r2;
            r1 = r2;
            r2 = res;
        }
        return res;
    }
}

自底向上動態規划法

class Solution {
    public int climbStairs(int n) {
        int[] dp = new int[n+1];//存儲每一個台階的方法數
        dp[0]=1;//0號位是多余的,故數組要分配n+1項
        dp[1]=1;
        for(int i = 2;i<=n;++i){
            dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];
    }
}

矩陣快速冪:

    public int climbStairs3(int n) {
        if (n < 2) return 1;
        int[][] q = new int[][]{{1, 1}, {1, 0}};
        q = pow(q, n - 1);
        return q[0][0];
    }

    //計算一個矩陣的n次方
    private int[][] pow(int[][] q, int n) {
        int[][] res = new int[][]{{1, 0}, {0, 1}};
        while (n > 0) {
            if ((n & 1) == 1) res = multiply(res, q);
            n >>= 1;
            q = multiply(q, q);
        }
        return res;
    }

    private int[][] multiply(int[][] a, int[][] b) {
        int[][] res = new int[2][2];
        for (int i = 0; i < 2; ++i) {
            for (int j = 0; j < 2; ++j) {
                res[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j];
            }
        }
        return res;
    }

通項公式:

    public int climbStairs2(int n) {
        double sqrt5 = Math.sqrt(5);
        double res = (Math.pow((1 + sqrt5) / 2, n + 1) - Math.pow((1 - sqrt5) / 2, n + 1)) / sqrt5;
        return (int) res;
    }

補充:快速冪方法求x的k次方

double quickMul(double x, long k){
    double res = 1.0;
    while(k>0){
        if((k&1)==1) res*=x;
        k>>=1;
        x*=x;
    }
    return res;
}

買賣股票的最佳時機

LeetCode:買賣股票的最佳時機

題目描述:

給定一個數組,它的第 i 個元素是一支給定股票第 i 天的價格。

如果你最多只允許完成一筆交易(即買入和賣出一支股票一次),設計一個算法來計算你所能獲取的最大利潤。

注意:你不能在買入股票前賣出股票。

示例:

輸入: [7,1,5,3,6,4]
輸出: 5
解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 5 天(股票價格 = 6)的時候賣出,最大利潤 = 6-1 = 5 。
     注意利潤不能是 7-1 = 6, 因為賣出價格需要大於買入價格。

思想:

dp思想

  • 記錄【今天之前買入的最小值】
  • 計算【今天之前最小值買入,今天賣出的獲利】,也即【今天賣出的最大獲利】
  • 比較【每天的最大獲利】,取最大值即可

代碼:

class Solution {
    public int maxProfit(int[] prices) {
        int L = prices.length;
        if(L == 0){
            return 0;
        }
        //min表示當前位置之前的最小值
        int min = prices[0],maxProfit = 0;
        for(int i=1;i<L;++i){
            //prices[i]-min表示當前位置拋售可獲得的最大利潤
            maxProfit = Math.max(maxProfit,prices[i]-min);
            min = Math.min(prices[i],min);
        }
        return maxProfit;
    }
}

最佳買賣股票時機含冷凍期

** 五星 **
LeetCode:最佳買賣股票時機含冷凍期

題目描述:

給定一個整數數組,其中第 i 個元素代表了第 i 天的股票價格 。​

設計一個算法計算出最大利潤。在滿足以下約束條件下,你可以盡可能地完成更多的交易(多次買賣一支股票):

你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。
賣出股票后,你無法在第二天買入股票 (即冷凍期為 1 天)。

示例:

輸入: [1,2,3,0,2]
輸出: 3 
解釋: 對應的交易狀態為: [買入, 賣出, 冷凍期, 買入, 賣出]

思想:

注意狀態是指每一天結束時的狀態。
三種狀態:無操作(冷凍或不持股),剛賣出,持股。
如果前一天“無操作”,今天可選擇買入或不買入,分別轉化為“持股”和“無操作”;
如果前一天“剛賣出”,今天必須進入冷凍,即轉化為“無操作”狀態;
如果前一天“持股”,今天可選擇賣出或不賣出,分別可轉化為“剛賣出”和“持股”;
這樣狀態轉移方程就可以寫出來了:

dp[i][0] = Math.max(dp[i-1][1],dp[i-1][0]);
dp[i][1] = dp[i-1][2] + prices[i];
dp[i][2] = Math.max(dp[i-1][2],dp[i-1][0] - prices[i]);  

代碼:

  • 二維dp數組方法
class Solution {
    public int maxProfit(int[] prices) {
        int len = prices.length;
        if(len<2) return 0;
        int[][] dp = new int[len][3];
        dp[0][0]=0;dp[0][1]=0;dp[0][2]=-prices[0];
        for(int i=1;i<len;++i){
            dp[i][0] = Math.max(dp[i-1][1],dp[i-1][0]);
            dp[i][1] = dp[i-1][2] + prices[i];
            dp[i][2] = Math.max(dp[i-1][2],dp[i-1][0] - prices[i]);
        }
        return Math.max(dp[len-1][0],dp[len-1][1]);
    }
}
  • 優化存儲空間后的代碼
class Solution {
    public int maxProfit(int[] prices) {
        int len = prices.length;
        if(len<2) return 0;
        int[] state = new int[3];
        int temp;
        state[0]=0;state[1]=0;state[2]=-prices[0];
        for(int i=1;i<len;++i){
            temp = Math.max(state[1],state[0]);
            state[1] = state[2] + prices[i];
            state[2] = Math.max(state[2],state[0] - prices[i]);
            state[0] = temp;
        }
        return Math.max(state[0],state[1]);
    }
}

這題非常難,看別人題解說得再詳細也沒用,還得自己一步一步理解

最長回文子串

LeetCode:最長回文子串

題目描述:

給定一個字符串 s,找到 s 中最長的回文子串。你可以假設 s 的最大長度為 1000。

示例:

輸入: "babad"
輸出: "bab"
注意: "aba" 也是一個有效答案。

思想:

  • 動態規划,用boolean[][] dp記錄每一對字符是否相等;
  • 雙循環遍歷所有子串情況,每次遍歷時,當前子串首尾相等且內層-1字符串dp值為true,則記錄dp值為true;全部遍歷完,取最長,即為最長子串;
  • 臨界條件很復雜,最好在循環之前把長度小於2的情況剔除;條件中有一個i-j<3,因為小於3且首尾相等的子串一定是回文串,不需要再往內層再判斷dp。

代碼:

class Solution {
    public String longestPalindrome(String s) {

        int len = s.length();
        boolean[][] dp = new boolean[len][len];
        int i,j,max=0,m=0,n=0;
        if(len<2) return s;
        for(i=0;i<len;++i){
            for(j=0;j<=i;++j){
                if(s.charAt(i) == s.charAt(j)&&(i-j<3||dp[j+1][i-1])){
                    dp[j][i]=true;
                    if(i-j>max){
                        max = i-j;
                        m=j;n=i;
                    }
                }else{
                    dp[j][i]=false;
                }
            }
        }
        return s.substring(m,n+1);
    }
}

不同路徑

LeetCode:不同路徑

題目描述:

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

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

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

示例:

輸入: m = 3, n = 2
輸出: 3
解釋:
從左上角開始,總共有 3 條路徑可以到達右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右

思想:

方法1:
dp思想,每一個格子的路徑數,等於上面一格+左邊一格的路徑數之和;

注意i=0和j=0時,所有格的dp值為1,直接判斷一下賦值1即可,不用再算,太麻煩。
方法2:
對方法1的優化。可以使用一維dp數組 dp[n] 。
因為每次累加時(假設逐行遍歷),只使用了一行的數據,並沒有涉及到前面幾行,所以考慮縮減為單維dp數組。
方法3:
數學方法。總共需要 m+n-2 步到達終點,需要向下走 m-1 步,仔細想想,這實際上是一個“組合”問題,在 Y=m+n-2 次移動中取 X=m-1 個向下的移動,C(Y,X) 即為結果。
組合公式的代碼實現如下:

for(int i=1;i<m;++i){
    res = res * (Y - X +i) / i;
}

注意:

  1. 有除法,但是不可能出現非整除的情況,結果必是整數。但是如果這樣寫:res *= (Y - X +i) / i 就可能出現錯誤,除不盡。

  2. res * (Y - X +i)可能超出int范圍,所以要用 long 來定義 res。

代碼:

方法1:

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

方法2:

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

方法3:

class Solution {
    public int uniquePaths(int m, int n) {
        int Y = m + n -2;
        int X = m - 1;
        long res = 1;
        for(int i=1;i<m;++i){
            res = res * (Y - X +i) / i;
        }
        return (int) res;
    }
}

不同路徑2

LeetCode:不同路徑 II

題目描述:

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

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

現在考慮網格中有障礙物。那么從左上角到右下角將會有多少條不同的路徑?

示例:

輸入:
[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]
輸出: 2
解釋:
3x3 網格的正中間有一個障礙物。
從左上角到右下角一共有 2 條不同的路徑:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

思想:

跟上一題差不多,加一點判斷條件;

注意:這題i=0和j=0時,dp值不全為1,可能是0,因為前面可能有障礙物。判斷條件需要做一些調整。

代碼:

class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        for(int i=0;i<m;++i){
            for(int j=0;j<n;++j){
                if(obstacleGrid[i][j]==1){
                    obstacleGrid[i][j] = 0;
                }else if(i==0&&j==0){
                    obstacleGrid[i][j] = 1;
                }else{
                    obstacleGrid[i][j] = (i==0?0:obstacleGrid[i-1][j]) + (j==0?0:obstacleGrid[i][j-1]);
                }
            }
        }
        return obstacleGrid[m-1][n-1];
    }
}

最小路徑和

LeetCode:最小路徑和

題目描述:

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

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

示例:

輸入:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
輸出: 7
解釋: 因為路徑 1→3→1→1→1 的總和最小。

思想:

動態規划,可以用原數組作為dp數組

代碼:

class Solution {
    public int minPathSum(int[][] grid) {
        int i=0,j=0;
        for(i=0;i<grid.length;++i){
            for(j=0;j<grid[0].length;++j){
                if(i>0&&j>0){
                    grid[i][j]+= Math.min(grid[i-1][j],grid[i][j-1]);
                }else{
                    grid[i][j]+= (i==0?0:grid[i-1][j]) + (j==0?0:grid[i][j-1]);
                }
            }
        }
        return grid[i-1][j-1];
    }
}

三角形最小路徑和

LeetCode:三角形最小路徑和

題目描述:

給定一個三角形,找出自頂向下的最小路徑和。每一步只能移動到下一行中相鄰的結點上。

如果你可以只使用 O(n) 的額外空間(n 為三角形的總行數)來解決這個問題,那么你的算法會很加分。

示例:

例如給定三角形
[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]
自頂向下的最小路徑和為 11(即,2 + 3 + 5 + 1 = 11)。

思想:

自底向上,修改dp數組

代碼:

第一種方法:在原數組上修改。這樣貌似效率不高。

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        for(int i=triangle.size()-2;i>=0;--i){
            for(int j=0;j<i+1;++j){
                triangle.get(i).set(j,triangle.get(i).get(j)+Math.min(triangle.get(i+1).get(j+1),triangle.get(i+1).get(j)));
            }
        }
        return triangle.get(0).get(0);
    }
}

第二種方法:設置dp數組,修改dp數組;注意這很巧妙,每一次修改都不會影響下次循環的判斷;其次,每次循環,最后一個數都不會修改它,直到最后一輪,加上頂部的數,得到最終結果dp[0]。

class Solution {
    public int minimumTotal(List<List<Integer>> triangle) {
        int len = triangle.size();
        int[] dp=new int[len];
        for(int i=0;i<len;++i){
            dp[i]=triangle.get(len-1).get(i);
        }
        for(int i=len-2;i>=0;--i){
            for(int j=0;j<i+1;++j){
                dp[j] = Math.min(dp[j],dp[j+1]) + triangle.get(i).get(j);
            }
        }
        return dp[0];
    }
}

打家劫舍

LeetCode:打家劫舍

題目描述:

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

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

示例:

輸入:[2,7,9,3,1]
輸出:12
解釋:偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接着偷竊 5 號房屋 (金額 = 1)。
     偷竊到的最高金額 = 2 + 9 + 1 = 12 。

思想:

標准的動態規划,注意以下兩點:

  • 不需要額外的dp數組;
  • 不需要修改dp數組,用幾個變量即可;

代碼:

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

打家劫舍2

五星
LeetCode:打家劫舍2

題目描述:

強盜在環形街區搶劫

示例:

輸入: [1,2,3,1]
輸出: 4
解釋: 你可以先偷竊 1 號房屋(金額 = 1),然后偷竊 3 號房屋(金額 = 3)。
     偷竊到的最高金額 = 1 + 3 = 4 。

思想:

分別計算1n-1和0n-2的結果值,比較哪個大

不偷第一個房子情況下最大值,和不偷最后一個房子情況下最大值

代碼:

class Solution {
    public int rob(int[] nums) {
        if(nums.length == 1) return nums[0];
        return Math.max(rob(nums,0,nums.length-1),rob(nums,1,nums.length-1));
    }
    private int rob(int[] nums,int pos, int len){
        int pre1=0,pre2=0;
        int ret=0;
        for(int i=pos;i<len+pos;++i){
            ret = Math.max(pre1+nums[i], pre2);
            pre1 = pre2;
            pre2 = ret;
        }
        return ret;
    }
}

數組區間和

LeetCode:區域和檢索 - 數組不可變

題目描述:

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

示例:

給定 nums = [-2, 0, 3, -5, 2, -1],求和函數為 sumRange()

sumRange(0, 2) -> 1
sumRange(2, 5) -> -1
sumRange(0, 5) -> -3

思想:

最容易想到的笨方法:每次調用時都執行一次循環,獲得結果。

可以在輸入數組時就修改原數組,使得每個元素值為該元素之前所有元素之和。
這樣,每次調用時,執行nums[j] - nums[i-1] 即可得到結果。

注意:邊界問題,可以把數組長度聲明為n+1,來解決。

代碼:

class NumArray {
    private final int[] arr;
    public NumArray(int[] nums) {
        arr = new int[nums.length + 1];
        for(int i=1;i<arr.length;++i){
            arr[i] = arr[i-1] + nums[i-1];
        }
    }
    
    public int sumRange(int i, int j) {
        return arr[j+1] - arr[i];
    }
}

/**
 * Your NumArray object will be instantiated and called as such:
 * NumArray obj = new NumArray(nums);
 * int param_1 = obj.sumRange(i,j);
 */

數組中等差遞增子區間的個數

** 五星 **
LeetCode:等差數列划分

題目描述:

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

例如,以下數列為等差數列:

1, 3, 5, 7, 9
7, 7, 7, 7
3, -1, -5, -9

以下數列不是等差數列。

1, 1, 2, 5, 7

數組 A 包含 N 個數,且索引從0開始。數組 A 的一個子數組划分為數組 (P, Q),P 與 Q 是整數且滿足 0<=P<Q<N 。

如果滿足以下條件,則稱子數組(P, Q)為等差數組:

元素 A[P], A[p + 1], ..., A[Q - 1], A[Q] 是等差的。並且 P + 1 < Q 。

函數要返回數組 A 中所有為等差數組的子數組個數。

示例:

A = [1, 2, 3, 4]

返回: 3, A 中有三個子等差數組: [1, 2, 3], [2, 3, 4] 以及自身 [1, 2, 3, 4]。

思想:

自己的方法
對於一個等差數組,n-1個元素,若增加一個元素(第n個)依然是等差數組,則子數組數量增加n-2個。找到此規律,就好做了。
dp方法:dp數組記錄每一個元素為右邊界的等差數列數量(注意不是右邊界左邊的數列總數量)

代碼:

  • 方法一:常數空間復雜度
class Solution {
    public int numberOfArithmeticSlices(int[] A) {
        int ret = 0;
        int n=2;
        for(int i=2;i<A.length;++i){
            if(A[i-1]-A[i-2]==A[i]-A[i-1]){
                n++;
            }else{
                n=2;
            }
            if(n>2) ret +=(n-2);
        }
        return ret;
    }
}
  • 方法二:dp
class Solution {
    public int numberOfArithmeticSlices(int[] A) {
        int len = A.length;
        if(len<3) return 0;
        int[] dp = new int[len];
        int sum = 0;
        for(int i=2;i<len;++i){
            if(A[i]-A[i-1]==A[i-1]-A[i-2])
                dp[i] = dp[i-1] + 1;
            sum += dp[i];
        }
        return sum;
    }
}

分割整數的最大乘積

LeetCode:整數拆分

題目描述:

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

示例:

輸入: 10
輸出: 36
解釋: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

思想:

方法一動態規划:對於小於n的每一個狀態i而言,可以拆分成j和i-j,i-j可能拆也可以不拆,分別是j(i-j)和jdp[i-j],取最大值,因此狀態轉移方程為 dp[i] = Math.max(dp[i], Math.max(j*(i-j),j*dp[i-j]))
方法二:找規律。觀察幾個樣例可以發現,數字拆分結果一定是多個 3 再加上若干個1、2。所以使用一個遞歸讓數字不斷減去3,直到小於某個閾值時返回,遞歸過程中計算乘積。

代碼:

方法一:動態規划

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

方法二:規律

class Solution {
    public int integerBreak(int n) {
        if(n<4) return n-1;
        if(n==4) return 4;
        return count(n);
    }
    private int count(int n){
        if(n<5) return n; 
        return count(n-3)*3;
    }
}

按平方數來分割整數

LeetCode:完全平方數

題目描述:

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

示例:

輸入: n = 12
輸出: 3 
解釋: 12 = 4 + 4 + 4.

思想:

這題可以看做一個完全背包問題。1、4、9、16這樣的序列可看做一系列物品體積。背包問題的分析可參見我另一篇博客:背包問題總結梳理

代碼:

class Solution {
    public int numSquares(int n) {
        List<Integer> squareList = generateSquareList(n);
        int[] dp = new int[n+1];
        for(int i=1;i<=n;++i){
            dp[i] = n;
        }
        for(int item : squareList){
            for(int j=item;j<=n;++j){
                dp[j] = Math.min(dp[j],dp[j-item]+1);
            }
        }
        return dp[n];
    }
    private List<Integer> generateSquareList(int n){
        List<Integer> list = new ArrayList<>();
        int add =1;
        for(int i=1;i<=n;i+=add){
            list.add(i);
            add += 2;
        }
        return list;
    }
}

最長上升子序列

LeetCode:最長上升子序列

題目描述:

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

示例:

輸入: [10,9,2,5,3,7,101,18]
輸出: 4 
解釋: 最長的上升子序列是 [2,3,7,101],它的長度是 4。

思想:

自己的思路
遍歷到某個item時,前面已經有了各個長度的上升子序列,需要考慮把item掛在哪個子序列后面。前面的上升子序列(長度為k)的末尾元素與item進行比較,若item更大,則組成新的上升子序列(長度為k+1),否則以同樣的方法比較長度為k-1的上升子序列與item的關系。這樣一來,兩層循環可以完成上述操作。
優化思路
內層循環可以用二分查找來優化。因為這實際上是一個查找的過程,在一串末尾元素中,查找適合掛item的元素,使其對應的長度加一即可。對於“7”而言,前置數組為[10, 9, 2, 5, 3],以每一個元素作為末尾元素的最長上升子序列長度為[1, 1, 1, 2, 2],實際上可以簡化為[2 , 3] 和 [1, 2]的對應關系(2 是長度為1的最小元素,3是長度為2的最小元素),除了2和3之外的其它元素均可不用考慮。使用一個數組tails存儲 [2, 3], 很明顯是有序的,使用二分在tails 中尋找小於“7”的最大元素,下標加一即為以“7”為末尾的最大長度。

代碼:

  • 方法一:普通動態規划
class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp = new int[nums.length+1];
        int pos = 0;
        dp[0] = Integer.MIN_VALUE;
        for(int item : nums){
            for(int i=pos;i>-1;i--){
                if(item>dp[i]){
                    dp[i+1]=item;
                    pos = i==pos?(pos+1):pos;
                    break;
                }
            }
        }
        return pos;
    }
}
  • 方法二:二分查找優化
class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] tails = new int[nums.length+1];
        tails[0] = Integer.MIN_VALUE;
        int res = 0;
        for(int item : nums){
            int index = binarySearch(tails, res, item);
            tails[index+1] = item;
            res = res==index?(res+1):res;
        }
        return res;
    }

    private int binarySearch(int[] tails, int len, int target){
        int low = 0,high = len;
        while(low<=high){
            int mid = low + (high-low)/2;
            if(tails[mid]<target){
                low = mid + 1;
            }else if(tails[mid]>target){
                high = mid - 1;
            }else{
                return mid - 1;
            }
        }
        return high;
    }
}

最長數對鏈

LeetCode:最長數對鏈

題目描述:

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

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

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

示例:

輸入: [[1,2], [2,3], [3,4]]
輸出: 2
解釋: 最長的數對鏈是 [1,2] -> [3,4]

思想:

動態規划
排序按照左邊界來排序,否則會出問題。本題動態規划不是最優方法。
貪心
先按照右邊界排序。假設A的下一個區間是B。若B能掛在A的后面,則數對鏈長度+1,若B不能掛在A的后面,則將B忽略即可,因為后續區間一定是掛在A后面比掛B后面收益要更大。

代碼:

  • 動態規划
class Solution {
    public int findLongestChain(int[][] pairs) {
        Arrays.sort(pairs, (a,b)->(a[0]-b[0]));
        int[] dp = new int[pairs.length];
        int n = 0;
        int max = 0;
        for(int[] item : pairs){
            int val = 1;
            for(int i=n-1;i>=0;i--){
                if(pairs[i][1]<item[0]){
                    val += dp[i];
                    break;
                } 
            }
            dp[n++] = val;
            max = Math.max(val,max);
        }
        return max;
    }
}
  • 貪心
class Solution {
    public int findLongestChain(int[][] pairs) {
        Arrays.sort(pairs,(a,b) -> a[1]-b[1]);
        int preRight = pairs[0][0] - 1;
        int res = 0;
        for(int[] item : pairs){
            if(item[0]>preRight){
                res++;
                preRight = item[1];
            }
        }
        return res;
    }
}

最長公共子序列

五星
LeetCode:最長公共子序列

題目描述:

給定兩個字符串 text1 和 text2,返回這兩個字符串的最長公共子序列的長度。

一個字符串的 子序列 是指這樣一個新的字符串:它是由原字符串在不改變字符的相對順序的情況下刪除某些字符(也可以不刪除任何字符)后組成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。兩個字符串的「公共子序列」是這兩個字符串所共同擁有的子序列。

若這兩個字符串沒有公共子序列,則返回 0。

示例:

輸入:text1 = "abcde", text2 = "ace" 
輸出:3  
解釋:最長公共子序列是 "ace",它的長度為 3。

思想:

像這種兩個字符串的情況,應當考慮二維dp數組,一維無法解決。dp[i][j]表示text1第i位置之前與text2第j位置之前的最長公共子序列長度。
狀態轉移方程分兩種情況:

  • 當text1[i]==text2[j]時:dp[i][j] = dp[i-1][j-1] + 1;
  • 當text1[i]!=text2[j]時:dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);

兩層循環遍歷二維數組,可以保證dp[i-1][j-1]、dp[i-1][j]、dp[i][j-1]都在已經遍歷過的位置。所以可以用此動態規划思想來做。

注意:為了避免臨界溢出,可以把數組長度設為len+1:int[][] dp = new int[len1+1][len2+1];

代碼:

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        char[] arr1 = text1.toCharArray();
        char[] arr2 = text2.toCharArray();
        int len1 = arr1.length,len2 = arr2.length;
        int[][] dp = new int[len1+1][len2+1];
        for(int i=1;i<len1+1;++i){
            for(int j=1;j<len2+1;++j){
                if(arr1[i-1]==arr2[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[len1][len2];
    }
}

分割等和子集

五星
LeetCode:分割等和子集

題目描述:

給定一個只包含正整數的非空數組。是否可以將這個數組分割成兩個子集,使得兩個子集的元素和相等。

注意:

每個數組中的元素不會超過 100
數組的大小不會超過 200

示例:

輸入: [1, 5, 11, 5]

輸出: true

解釋: 數組可以分割成 [1, 5, 5] 和 [11].

思想:

問題看成背包大小為sum/2的01背包問題。

因為只需要從0的位置開始轉化,dp數組可以定義為布爾類型。

代碼:

class Solution {
    public boolean canPartition(int[] nums) {
        int sum = 0;
        for(int item : nums){
            sum +=item;
        }
        if(sum%2!=0) return false;
        sum /= 2;
        boolean[] dp = new boolean[sum+1];
        dp[0] = true;
        for(int item : nums){
            for(int j=sum;j>=item;--j){
                dp[j] = dp[j]||dp[j-item];
            }
        }
        return dp[sum];
    }
}

目標和

五星
LeetCode:目標和

題目描述:

給定一個非負整數數組,a1, a2, ..., an, 和一個目標數,S。現在你有兩個符號 + 和 -。對於數組中的任意一個整數,你都可以從 + 或 -中選擇一個符號添加在前面。

返回可以使最終數組和為目標數 S 的所有添加符號的方法數。

示例:

輸入:nums: [1, 1, 1, 1, 1], S: 3
輸出:5
解釋:

-1+1+1+1+1 = 3
+1-1+1+1+1 = 3
+1+1-1+1+1 = 3
+1+1+1-1+1 = 3
+1+1+1+1-1 = 3

一共有5種方法讓最終目標和為3。

思想:

  • 動態規划-背包
    轉化為總容量為 sum = (S + sum)/2;的01背包問題。這點很難想到,想到了就很容易做。
    把dp[0]標為1,狀態轉移dp[j] = dp[j] + dp[j-item];,最后結果即為方法數量。
  • DFS暴力

代碼:

  • 背包
class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        int len = nums.length;
        int sum =0;
        for(int item : nums){
            sum += item;
        }
        if(S>sum) return 0;
        if((S+sum)%2!=0) return 0;
        sum = (S + sum)/2;
        int[] dp = new int[sum+1];
        dp[0] = 1;
        for(int item : nums){
            for(int j=sum;j>=item;--j){
                dp[j] = dp[j] + dp[j-item];
            }
        }
        return dp[sum];
    }
}
  • DFS
class Solution {
    public int findTargetSumWays(int[] nums, int S) {
        return count(nums, 0, S);
    }
    private int count(int[] nums, int i, int S){
        if(i==nums.length){
            return S==0?1:0;
        }
        return count(nums,i+1,S-nums[i]) + count(nums,i+1,S+nums[i]);
    }
}

一和零

五星
LeetCode:一和零

題目描述:

在計算機界中,我們總是追求用有限的資源獲取最大的收益。

現在,假設你分別支配着 m 個 0 和 n 個 1。另外,還有一個僅包含 0 和 1 字符串的數組。

你的任務是使用給定的 m 個 0 和 n 個 1 ,找到能拼出存在於數組中的字符串的最大數量。每個 0 和 1 至多被使用一次。

注意:

給定 0 和 1 的數量都不會超過 100。
給定字符串數組的長度不會超過 600。

示例:

輸入: Array = {"10", "0001", "111001", "1", "0"}, m = 5, n = 3
輸出: 4

解釋: 總共 4 個字符串可以通過 5 個 0 和 3 個 1 拼出,即 "10","0001","1","0" 。

思想:

二維費用背包問題,計算dp兩層循環

代碼:

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp = new int[m+1][n+1];
        for(String str : strs){
            int n0 = 0;
            int n1 = 0;
            for(int i=0;i<str.length();++i){
                if(str.charAt(i)=='0') n0++;
                else n1++;
            }
            for(int i=m;i>=n0;--i){
                for(int j=n;j>=n1;--j){
                    dp[i][j] = Math.max(dp[i][j],dp[i-n0][j-n1]+1);
                }
            }
        }
        return dp[m][n];
    }
}

零錢兌換

LeetCode:零錢兌換

題目描述:

給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函數來計算可以湊成總金額所需的最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回 -1。

你可以認為每種硬幣的數量是無限的。

示例:

輸入:coins = [1, 2, 5], amount = 11
輸出:3 
解釋:11 = 5 + 5 + 1

思想:

完全背包的變種。因為要求恰好能達到目標容量,所以程序運行到最后時,dp[amount]一定是從dp[0]轉移過來的。所以可以把dp[0]初始化為 -amount-1 ,每次放置硬幣時,價值+1,總價值不可能超過amount。所以最后結果從dp[0]轉化而來的結果一定小於0,從其它項轉化來的都大於等於0。最后的dp[amount]如果大於等於0,則不存在硬幣組合,返回-1,否則返回 dp[amount]+amount+1 即為需要的硬幣數量。

代碼:

public int coinChange(int[] coins, int amount) {
    int[] dp = new int[amount+1];
    dp[0] = -amount-1;
    for(int coin : coins){
        for(int i=coin;i<=amount;++i){
            dp[i] = Math.min(dp[i],dp[i-coin]+1);
        }
    }
    if(dp[amount]>=0) return -1;
    return dp[amount]+amount+1;
}

零錢兌換2

LeetCode:零錢兌換2

題目描述:

給定不同面額的硬幣和一個總金額。寫出函數來計算可以湊成總金額的硬幣組合數。假設每一種面額的硬幣有無限個。

示例:

輸入: amount = 5, coins = [1, 2, 5]
輸出: 4
解釋: 有四種方式可以湊成總金額:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

思想:

基於完全背包的變種。amount相當於總容量,coin相當於每個物品的體積。但是本體的dp[i]的值就不代表容量為i時的總價值了,應該指代物品容量為i時的總組合數,相對應的,狀態轉移方程也需要調整。每次遍歷到容量為 i 且需要放置物品時,不放該物品的組合數為dp[i],放該物品組合數為dp[i-coin],此處的組合數dp[i]應該等於兩者之和。

注意:需要把dp[0]初始化為1,保證在放置第一個物品時,容量i=coin情況下,組合數為1

代碼:

public int change(int amount, int[] coins) {
    int[] dp = new int[amount+1];
    dp[0] = 1;
    for(int coin : coins){
        for(int i=coin;i<=amount;++i){
            dp[i] = dp[i] + dp[i-coin];
        }
    }
    return dp[amount];
}

單詞拆分

五星
LeetCode:單詞拆分

題目描述:

給定一個非空字符串 s 和一個包含非空單詞的列表 wordDict,判定 s 是否可以被空格拆分為一個或多個在字典中出現的單詞。

說明:

  • 拆分時可以重復使用字典中的單詞。
  • 你可以假設字典中沒有重復的單詞。

示例:

輸入: s = "leetcode", wordDict = ["leet", "code"]
輸出: true
解釋: 返回 true 因為 "leetcode" 可以被拆分成 "leet code"。

思想:

不一定非得套着背包問題的模板來做,回歸dp思想。dp[i] 表示s中以 i 為結尾的左半字符串是否可以由字典中的詞組成。於是很自然得想到外層遍歷s每個字符,內層遍歷字典中每個詞 str,比較dp[i] 和減去str長度位置的dp值,對dp數組進行更新。

優化:內層循環若遇到dp[i]值為true了,可以直接break,無需繼續遍歷字典其它詞了。

代碼:

public boolean wordBreak(String s, List<String> wordDict) {
    int len = s.length();
    boolean[] dp = new boolean[len+1];
    dp[0] = true;
    for(int i=1;i<=len;++i){
        for(String item : wordDict){
            if(dp[i]) break;
            int k = i - item.length();
            if(k<0||!item.equals(s.substring(k,i))) continue;
            dp[i] = dp[k]||dp[i];
        }
    }
    return dp[len];
}

組合總和4

LeetCode:組合總和4

題目描述:

給定一個由正整數組成且不存在重復數字的數組,找出和為給定目標正整數的組合的個數。

示例:

nums = [1, 2, 3]
target = 4

所有可能的組合為:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

請注意,順序不同的序列被視作不同的組合。

因此輸出為 7。

思想:

規律:像這種要考慮順序的背包問題,要把對容量的遍歷放在外層,物品的循環放在內層。

換種思路去理解:假設物品1到n,對於每一個容量K而言(K<=target),要從前一步抵達K的位置,有1到n種可能。假設某物品體積為v,對於容量K-v也同樣是遍歷過n個物品,所以應該在內層循環遍歷n個物品,這樣一定枚舉了所有排列情況。

代碼:

public int combinationSum4(int[] nums, int target) {
    int[] dp = new int[target+1];
    dp[0] = 1;
    for(int i=1;i<=target;++i){
        for(int item : nums){
            if(i>=item) dp[i] += dp[i-item];
        }
    }
    return dp[target];
}

買賣股票的最佳時機含手續費

LeetCode:買賣股票的最佳時機含手續費

題目描述:

給定一個整數數組 prices,其中第 i 個元素代表了第 i 天的股票價格 ;非負整數 fee 代表了交易股票的手續費用。

你可以無限次地完成交易,但是你每筆交易都需要付手續費。如果你已經購買了一個股票,在賣出它之前你就不能再繼續購買股票了。

返回獲得利潤的最大值。

注意:這里的一筆交易指買入持有並賣出股票的整個過程,每筆交易你只需要為支付一次手續費。

示例:

輸入: prices = [1, 3, 2, 8, 4, 9], fee = 2
輸出: 8
解釋: 能夠達到的最大利潤:  
在此處買入 prices[0] = 1
在此處賣出 prices[3] = 8
在此處買入 prices[4] = 4
在此處賣出 prices[5] = 9
總利潤: ((8 - 1) - 2) + ((9 - 4) - 2) = 8.

思想:

我最開始想到如下方法:

public int maxProfit(int[] prices, int fee) {
    int[] dp = new int[prices.length];
    int max = 0;
    for(int i=0;i<prices.length;++i){
        dp[i] = max;
        for(int j=0;j<i;++j){
            int profit = prices[i] - prices[j]- fee + dp[j];
            if(profit>max) dp[i] = max = profit;
        }
    }
    System.out.println(Arrays.toString(dp));
    return max;
}

使用dp數組存儲每一天的最大利潤,外層對每一天進行遍歷,內層循環遍歷該天之前的所有天數,第i天與第j天進行買賣交易的利潤加上第j天自身積累的利潤之和,與第i天不進行賣出的當前積累的利潤,二者進行比較取最大值,可以完成動態規划操作。但是,該做法會報超時。實際上,順着這個思路可以優化代碼,將內層循環去掉。

主要理解最核心的一點:第j天結束時的利潤,相當於變相降低了第j天的價格,每天結束時,用當天價格減去當前利潤,得到一個值,這里把它稱為“實際價格”吧。用一個變量minPrice記錄第 i 天以前的最小“實際價格”,在第 i 天只需要跟最小實際價格比較判斷就行,不需要循環遍歷 i 之前的所有元素。用一個變量 maxProfit 記錄最大利潤,這樣甚至都能把 dp 數組省略。

代碼:

public int maxProfit(int[] prices, int fee) {
    int maxProfit = 0;
    int minPrice = 50000;
    for(int i=0;i<prices.length;++i){
        maxProfit = Math.max(prices[i] - minPrice - fee, maxProfit);
        minPrice = Math.min(minPrice,prices[i]-maxProfit);
    }
    return maxProfit;
}

買賣股票的最佳時機3

五星
LeetCode:買賣股票的最佳時機3

題目描述:

給定一個數組,它的第 i 個元素是一支給定的股票在第 i 天的價格。

設計一個算法來計算你所能獲取的最大利潤。你最多可以完成 兩筆 交易。

注意: 你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

示例:

輸入: [3,3,5,0,0,3,1,4]
輸出: 6
解釋: 在第 4 天(股票價格 = 0)的時候買入,在第 6 天(股票價格 = 3)的時候賣出,這筆交易所能獲得利潤 = 3-0 = 3 。
     隨后,在第 7 天(股票價格 = 1)的時候買入,在第 8 天 (股票價格 = 4)的時候賣出,這筆交易所能獲得利潤 = 4-1 = 3 。

思想:

這題是在121的基礎上改進。第一次交易的利潤,相當於抵消一部分第二次購買付出的錢,想明白這一點就很好做了。在循環遍歷數組的過程中,能算出以每一天為截止日期第一次交易的最大利潤,設為profit1,第二次交易的買入價格為數組的每一項減去遍歷到該項時的profit1,可以想象成形成了一個新的價格數組,對這個新數組求第二次交易的最大利潤,即為兩次次交易的最大利潤。

代碼:

class Solution {
    public int maxProfit(int[] prices) {
        if(prices.length==0) return 0;
        int maxProfit1=0;
        int min1 = prices[0];
        int maxProfit2=0;
        int min2 = prices[0];
        for(int item : prices){
            min1=Math.min(min1,item);
            maxProfit1 = Math.max(maxProfit1,item-min1);
            min2=Math.min(min2,item-maxProfit1);
            maxProfit2=Math.max(maxProfit2,item-min2);
        }
        return maxProfit2;
    }
}

買賣股票的最佳時機4

五星
LeetCode:買賣股票的最佳時機4

題目描述:

給定一個整數數組 prices ,它的第 i 個元素 prices[i] 是一支給定的股票在第 i 天的價格。

設計一個算法來計算你所能獲取的最大利潤。你最多可以完成 k 筆交易。

注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

示例:

輸入:k = 2, prices = [3,2,6,5,0,3]
輸出:7
解釋:在第 2 天 (股票價格 = 2) 的時候買入,在第 3 天 (股票價格 = 6) 的時候賣出, 這筆交易所能獲得利潤 = 6-2 = 4 。
     隨后,在第 5 天 (股票價格 = 0) 的時候買入,在第 6 天 (股票價格 = 3) 的時候賣出, 這筆交易所能獲得利潤 = 3-0 = 3 。

思想:

這題是上一題的改進版,由兩次股票交易擴展到k次。於是需要一個數組 int[] min 記錄第j天之前最小價格,和數組 int[] maxProfit 記錄第j天之前的最大利潤。本題的解法上上一題的基礎上,增加了一層for循環遍歷k。除了這一點,還有一點也不太一樣。上一題中,第二次交易的利潤profit2,就是兩筆交易加起來的總利潤,本題中maxProfit[k-1]並不是總利潤,而僅僅是第k次交易的最大利潤,需要將maxProfit[0]~maxProfit[k-1] 全部加起來,才能得到總利潤。所以代碼中才會有這一句 if(i==prices.length-1) res+=maxProfit[j]; 累加得到總利潤

這是為什么呢?
因為上一題中,通過 item-min2 計算第二次交易的利潤,使用的 item 依然是原始價格,而min2是減去第一次利潤值之后取得最小值,因此 item-min2 相當於把兩次交易利潤都囊括進去了。但是,在本題中,每一次交易完,都進行price = price - maxProfit[j];操作。而當前交易使用 price-min[j] 來計算最大利潤,使用的是減過上一次交易利潤的價格,min[j]也是減過上一次交易利潤的最小值,相當於抵消了上一次交易利潤值,所以內層的每一次循環而言,都是對一個全新的prices數組求單次股票交易的最大利潤。所以maxProfit[j]記錄的是第j次交易的最大利潤,最后需要累加求和。

代碼:

class Solution {
    public int maxProfit(int k, int[] prices) {
        int[] min = new int[k];
        int[] maxProfit = new int[k];
        int res = 0;
        Arrays.fill(min,1001);
        for(int i=0;i<prices.length;++i){
            int price = prices[i];
            for(int j=0;j<k;++j){
                min[j] = Math.min(min[j],price);
                maxProfit[j] = Math.max(maxProfit[j],price-min[j]);
                price = price - maxProfit[j];
                if(i==prices.length-1) res+=maxProfit[j];
            }
        }
        return res;
    }
}

只有兩個鍵的鍵盤

五星
LeetCode:只有兩個鍵的鍵盤

題目描述:

最初在一個記事本上只有一個字符 'A'。你每次可以對這個記事本進行兩種操作:
1.Copy All (復制全部) : 你可以復制這個記事本中的所有字符(部分的復制是不允許的)。
2.Paste (粘貼) : 你可以粘貼你上一次復制的字符。
給定一個數字 n 。你需要使用最少的操作次數,在記事本中打印出恰好 n 個 'A'。輸出能夠打印出 n 個 'A' 的最少操作次數。

示例:

輸入: 3
輸出: 3
解釋:
最初, 我們只有一個字符 'A'。
第 1 步, 我們使用 Copy All 操作。
第 2 步, 我們使用 Paste 操作來獲得 'AA'。
第 3 步, 我們使用 Paste 操作來獲得 'AAA'。

思想:

方法一(動態規划):先要找出狀態轉移方程。設dp[k]為n等於k時的最小操作數。若k能被2整除,在k/2基礎上進行copy All+Paste兩次操作一定是最優的,即dp[k]=dp[k/2]+2。若k能被3整除,dp[k]=dp[k/3]+3,即在k/3基礎上進行copy All+Paste+Paste三次操作。於是可以想到,用一個循環在2~k之間逐一試探,找到可以被k整除的 j,dp[k]=dp[k/j]+j。目的是要使增加的操作數盡可能小,所以j一定是從最小開始遍歷,即從2開始。按照直觀的動態規划思想,外層遍歷dp[2]dp[n],內層循環遍歷2n尋找可以被k整除的最小的 j 值,於是有了下方的代碼。

方法二(優化):上述方法中外層遍歷了dp[2]~dp[n]的所有情況,有很多不必要的冗余計算,因為實際上只需要遍歷抵達dp[n]的所有路徑結點即可。那么如何做呢。正向從1向n遍歷不太好做,因為不知道哪個方向能抵達終點,但是如果從終點向起點遍歷就很容易了。於是,也很容易想到可以借助遞歸。

代碼:

方法一(動態規划):

public int minSteps(int n) {
    int[] dp = new int[n+1];
    for(int i=2;i<=n;++i){
        for(int j=2;j<=n;++j){
            if(i%j==0){
                dp[i] = dp[i/j]+j;
                break;
            }
        }
    }
    return dp[n];
}

方法二(優化):

public int minSteps(int n) {
    if(n==1) return 0;
    int j = 2;
    while(n%j!=0){
        j++;
    }
    return minSteps(n/j)+j;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM