leetcode總結——動態規劃

取個程式猿的名字發表於2020-02-17

動態規劃:
楊輝三角
最長迴文字串
最大乘積子陣列
01矩陣中最大的1正方形
機器人走路:
字元編碼

股票投資問題
三角路徑和:
打家劫舍
粉刷房子

平方陣列合
紙幣問題
樹形打家劫舍

楊輝三角:
給定一個非負索引 k,其中 k ≤ 33,返回楊輝三角的第 k 行。
在楊輝三角中,每個數是它左上方和右上方的數的和。
示例:
輸入: 3
輸出: [1,3,3,1]

解答一:

class Solution {
    public List<Integer> getRow(int rowIndex) {
        int max=33,row=1;
        Integer[] k = new Integer[rowIndex + 1];
        int num=0,num1=0,num2=0;//作為輔助空間
        for(int i=0;i<=rowIndex;i++,row++){
            for(int j=0;j<=i;j++){
                if(j==0||j==i)
                {
                    k[j]=1;
                    num2=1;
                }
                else
                {   
                    num1=k[j];
                    k[j]=num2+k[j];
                    num2=num1;
                }   
            }
        }
        List<Integer> res = Arrays.asList(k);
        return res;  
    }
}

注意:
1:類似於動態規劃問題 第i層的k[j] 等於第i-1層的k[j-1]+k[j]。所以如果只用一個陣列對其進行運算,需要儲存i-1層中j-1的值
num2為計算 第i層的k[j] 時儲存的 第i-1層的k[j-1] 的值【k[j]還未改變】
num1為儲存的第i層的k[j]的值,是為了方便之後的計算
2: Integer[] k = new Integer[rowIndex + 1] 轉化為 List res = Arrays.asList(k);
陣列轉化為連結串列的方法

  public List<Integer> getRow(int rowIndex) {
        Integer[] dp = new Integer[rowIndex + 1];
        Arrays.fill(dp,1);
        for(int i = 2;i < dp.length;i++){
            for(int j = i - 1;j > 0;j--)
                dp[j] = dp[j] + dp[j - 1];
        }
        List<Integer> res = Arrays.asList(dp);
        return res;
    }

注意:此為逆序運算,則不用儲存值

 public List<Integer> getRow(int rowIndex) {
        List<Integer> result = new ArrayList<>();
        if (rowIndex == 0) {
            result.add(1);
            return result;
        }
        if (rowIndex == 1) {
            result.add(1);
            result.add(1);
            return result;
        }
        result.add(1);
        result.add(1);
        for (int i = 1; i < rowIndex; i++) {
            result.add(1);
            for (int j = 0; j < i; j++) {
                result.add(result.get(0) + result.get(1));
                result.remove(0);
            }
            result.add(1);
            result.remove(0);
        }
        return result;
    }

注意:特殊的解法

最長迴文字串

題目四:

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

示例 1:

輸入: “babad”
輸出: “bab”
注意: “aba” 也是一個有效答案。
示例 2:

輸入: “cbbd”
輸出: “bb”

class Solution {
    public String longestPalindrome(String s) {
        int len=s.length();
        if(len==0)return "";
        int dp[][]=new int [len][len];
        int maxlen=0;
        String str=s.substring(0,1);//如果不存在長度>1的迴文子串,最長迴文串為1(首字元)
        for(int i=1;i<len;i++){
            for(int j=0;j<i;j++){
                if((s.charAt(i)==s.charAt(j))&&((i-j)<=2||dp[j+1][i-1]==1)){
                    dp[j][i]=1;
                    if(i-j+1>=maxlen){
                        maxlen=i-j+1;
                        str=s.substring(j,i+1) ;    
                    }
                }
            }
        }
        return str;
    }
}

最大乘積子陣列

給定一個整數陣列 nums ,找出一個序列中乘積最大的連續子序列(該序列至少包含一個數)。
示例 1:
輸入: [2,3,-2,4]
輸出: 6
解釋: 子陣列 [2,3] 有最大乘積 6。

示例 2:
輸入: [-2,0,-1]
輸出: 0
解釋: 結果不能為 2, 因為 [-2,-1] 不是子陣列。

解析:類似於最大連續序列和的思想,只不過既要維護最小值也要維護最大值

import java.lang.*;
import java.util.*;
class Solution {
    public int maxProduct(int[] nums) {
        int len=nums.length;if(len==0)return 0;
        int min[]=new int [len];
        int max[]=new int [len];
        min[0]=nums[0];
        max[0]=nums[0];
        int max1=nums[0];
        for(int i=1;i<len;i++){
            int num1=min[i-1]*nums[i];
            int num2=max[i-1]*nums[i];
            min[i]=Math.min(Math.min(num1,num2),nums[i]);
            max[i]=Math.max(Math.max(num1,num2),nums[i]);
            max1=Math.max(max1,max[i]);
        }
        return max1;
    }
}

01矩陣中最大的1正方形:
在一個由 0 和 1 組成的二維矩陣內,找到只包含 1 的最大正方形,並返回其面積。
示例:
輸入:
1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0
輸出: 4

#動態規劃 dp[i][j]=Math.min(Math.min(dp[i-1][j-1],dp[i-1][j]),dp[i][j-1])+1;
並且技巧是 new int [matrix.length+1][matrix[0].length+1];多了一維度矩陣,在判斷(1,n)(n,1)時不用單獨分析

class Solution {
    public int maximalSquare(char[][] matrix) {
        if(matrix.length==0)return 0;
        int [][]dp=new int [matrix.length+1][matrix[0].length+1];
        int max=0;
        for(int i=1;i<=matrix.length;i++){
            for(int j=1;j<=matrix[0].length;j++){
                if(matrix[i-1][j-1]=='1'){
                    dp[i][j]=Math.min(Math.min(dp[i-1][j-1],dp[i-1][j]),dp[i][j-1])+1;
                    max=Math.max(max,dp[i][j]);
                }
            }
        }
        return max*max;
    }
}

機器人走路:
一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記為“Start” )。
機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記為“Finish”)。
問總共有多少條不同的路徑?

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

  1. 向右 -> 向右 -> 向下
  2. 向右 -> 向下 -> 向右
  3. 向下 -> 向右 -> 向右
    示例 2:
    輸入: m = 7, n = 3
    輸出: 28

解析:其實就是C(m-1)(m+n-2)的一個排列計算,但是因為會越界,所以當數字就沒有辦法實現

考慮動態規劃

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

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

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

示例:

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

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

字元編碼
一條包含字母 A-Z 的訊息通過以下方式進行了編碼:
‘A’ -> 1
‘B’ -> 2

‘Z’ -> 26
給定一個只包含數字的非空字串,請計算解碼方法的總數。

示例 1:
輸入: “12”
輸出: 2
解釋: 它可以解碼為 “AB”(1 2)或者 “L”(12)。

示例 2:
輸入: “226”
輸出: 3
解釋: 它可以解碼為 “BZ” (2 26), “VF” (22 6), 或者 “BBF” (2 2 6) 。

示例 3:
輸入:“01”
輸出:0
解釋:0無法解碼
public class test0511 {
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.numDecodings(“101”));
}
}

class Solution {
public int numDecodings(String s) {
if(s.length() == 0 || s.charAt(0) ==‘0’ ){
return 0;
}
if(s.length() == 1 ){
return 1;
}
int dp1 = 1;//相當於dp[i-2]
int dp2 = 1;//相當於dp[i-1]
int result = 0;
for(int i = 1;i<s.length();i++){
int i1 = (s.charAt(i - 1) - 48) * 10 + (s.charAt(i) - 48);
//不能連續出現0
if(i1 == 0){
return 0;
}
if(i1 >=10 && i1 <=26 ){
result = dp1;
}
if(s.charAt(i) != ‘0’){
result += dp2;
}
dp1 = dp2;
dp2 = result;
result = 0;
}
return dp2;
}
}

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

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

自頂向下的最小路徑和為 11(即,2 + 3 + 5 + 1 = 11)。

由底向上【妙啊!】
public int minimumTotal(List<List< Integer>> triangle) {
int row = triangle.size();
int[] minlen = new int[row+1];
for (int level = row-1;level>=0;level–){
for (int i = 0;i<=level;i++){ //第i行有i+1個數字
minlen[i] = Math.min(minlen[i], minlen[i+1]) + triangle.get(level).get(i);
}
}
return minlen[0];
}

股票投資問題:
這是要一個系列的動態規劃問題
dp[i][k][j]:其中i代表第幾天【0-N】,k代表可以買賣股票的次數[0-K],j代表當天是否持有股票[0,1] dp為最大的利潤
因為動態規劃肯定是轉移方程,所以我們有
dp[i][k][0]=max{dp[i-1][k][0],dp[i-1][k][1]+prices[i]}
dp[i][k][1]=max{dp[i-1][k][1],dp[i-1][k][0]-prices[i]}

再看看基礎情況
1:如果沒有買賣次數,肯定值為0:dp[i][0][j]=0
2:在第1天【I=0】的時候:
如果選擇購買股票,則,
dp[0][k][1]=-prices[i]
如果選擇不購買股票,則
dp[0][k][0]=0
接下來就解答題目:

題目一:k=1

dp[i][1][0]=max{dp[i-1][1][0],dp[i-1][0][1]+prices[i]}
dp[i][1][1]=max{dp[i-1][1][1],dp[i-1][1][0]-prices[i]}
因為dp[i-1][0][0]=0,又因為上述式中k全為1,可簡化為:
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
得到答案:

class Solution {
    public int maxProfit(int[] prices) {
        int [][]dp=new int [prices.length][2];
        if(prices.length==0)return 0;
        dp[0][0]=0;
        dp[0][1]=-prices[0];
        for(int i=1;i<prices.length;i++){
             dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] + prices[i]);
             dp[i][1] = Math.max(dp[i-1][1], -prices[i]);
        }
        return dp[prices.length-1][0];
    }
}

發現dp僅僅和相鄰的狀態有關,再簡化:

int maxProfit_k_1(int[] prices) {
    int n = prices.length;
    int dp_i_0 = 0, dp_i_1 =-prices[0];
    for (int i = 1; i < n; i++) {
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        dp_i_1 = Math.max(dp_i_1, -prices[i]);
    }
    return dp_i_0;
}

題目二:k=n

因為k為無限大,可以直接不考慮好難想到
得到方程:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
又發現同樣也是,僅僅和相鄰狀態有關,再簡化:只不過這裡dp[i]兩個都和dp[i-1]有關,利用temp儲存其中一個值即可!!!!!
int temp = dp_i_0;
dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
dp_i_1 = Math.max(dp_i_1, temp - prices[i]);

int maxProfit_k_inf(int[] prices) {
    int n = prices.length;
    int dp_i_0 = 0, dp_i_1 =-prices[0];
    for (int i = 1; i < n; i++) {
        int temp = dp_i_0;
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        dp_i_1 = Math.max(dp_i_1, temp - prices[i]);
    }
    return dp_i_0;
}

題目三:k=n且具有一天的冷凍期,即當天如果賣出股票,第二天無法買入

dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-2][0] - prices[i])
原因在於dp[i-1][0]=max(dp[i-2][0], dp[i-2][1] + prices[i])又因為有冷凍期,所以不可能在I=i-2的時候賣出股票,所以dp[i-1][0]=dp[i-2][0]

class Solution {
    public int maxProfit(int[] prices) {
    int n = prices.length;
    if(prices.length==0)return 0;
    int dp_i_0 = 0, dp_i_1 =-prices[0];
    int dp_pre_0 =0;
    for (int i = 1; i < n; i++) {
        int temp = dp_i_0;
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]);
        dp_i_1 = Math.max(dp_i_1, dp_pre_0 - prices[i]);
        dp_pre_0 = temp;
    }
    return dp_i_0;
}
}

題目四:每一次賣出交手續費fee

dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]-fee);
dp_i_1 = Math.max(dp_i_1, temp - prices[i]);
}

class Solution {
int maxProfit(int[] prices, int fee) {
    int n = prices.length;
    int dp_i_0 = 0, dp_i_1 =-prices[0];
    for (int i = 1; i < n; i++) {
        int temp = dp_i_0;
        dp_i_0 = Math.max(dp_i_0, dp_i_1 + prices[i]-fee);
        dp_i_1 = Math.max(dp_i_1, temp - prices[i]);
    }
    return dp_i_0;
}
}

題目五:k=2

注意此時是一個三維陣列了,所以應該有兩個for迴圈
聯想三維矩陣

class Solution {
    public int maxProfit(int[] prices) {
        int max_k = 2;
        int[][][] dp = new int[prices.length][max_k + 1][2];
        if(prices.length==0)return 0;
        for (int i=0;i <prices.length; i++) {
            for (int k = max_k; k >= 1; k--) {
                if (i - 1 == -1) { 
                    /* 處理 base case */
                    dp[i][k][0] = 0;
                    dp[i][k][1] = -prices[i];
                    continue;
            }
            dp[i][k][0]=Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
            dp[i][k][1]=Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
            }
       }
        return dp[prices.length - 1][max_k][0];
    }
}

或者:

class Solution {
    public int maxProfit(int[] prices) {
        int max_k = 2;
        int[][][] dp = new int[prices.length][max_k + 1][2];
        if(prices.length==0)return 0;
        for (int k = 1; k <=max_k; k++) {
            for (int i=0;i <prices.length; i++) {
                if (i - 1 == -1) { 
                    /* 處理 base case */
                    dp[i][k][0] = 0;
                    dp[i][k][1] = -prices[i];
                    continue;
                }
            dp[i][k][0]=Math.max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
            dp[i][k][1]=Math.max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);
            }
       }
        return dp[prices.length - 1][max_k][0];
    }
}

題目六:k任意次數

注意若k過大,我們設定的三維陣列也巨大,可能導致記憶體溢位。但是實際上最大的交換次數是n/2次,所以如果超過2/n,就轉換為問題題目二

class Solution {
    public int maxProfit(int k, int[] prices) {
    int n = prices.length;
    if (k > n / 2) 
        return maxProfit(prices);//轉換為題目二
    int[][][] dp = new int[n][k + 1][2];
    for (int i = 0; i < n; i++) 
        for (int k = k; k >= 1; k--) {
            if (i - 1 == -1) { 
                dp[i][k][0] = 0;
                dp[i][k][1] = -prices[i];
                continue;
            }
            dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i]);
            dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i]);     
        }
    return dp[n - 1][k][0];
}
}

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

給定一個代表每個房屋存放金額的非負整數陣列,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。
示例 1:
輸入: [2,3,2]
輸出: 3
解釋: 你不能先偷竊 1 號房屋(金額 = 2),然後偷竊 3 號房屋(金額 = 2), 因為他們是相鄰的。

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

環形陣列,將第一個和最後一個單獨討論
第一個偷:最後一個不偷
第一個不偷:最後一個偷
最後一個不偷
所以分為兩次動態規劃,第一次[1:]且第一次不偷:第二次[:n-1]且最後一次不偷

class Solution {
public int rob(int[] nums) {
if(nums.length == 0) return 0;
if(nums.length == 1) return nums[0];
return Math.max(myRob(Arrays.copyOfRange(nums, 0, nums.length - 1)),
myRob(Arrays.copyOfRange(nums, 1, nums.length)));
}
private int myRob(int[] nums) {
int a0=0, a1=0, tmp;
for(int i=0;i<nums.length;i++) {
tmp=a0;
a0=Math.max(a0,a1);
a1=tmp+nums[i];
}
return Math.max(a0,a1);
}
}

如果首尾不相鄰:
dp[i]=max(dp[i-1],dp[i-2]+nums[i])
原理同上
如果沒偷:dp[i]=dp[i-1]
如果偷了:dp[i]=dp[i-1]+nums[i] 又因為dp[i-1]=dp[i-2]

樹形打家劫舍
在上次打劫完一條街道之後和一圈房屋後,小偷又發現了一個新的可行竊的地區。這個地區只有一個入口,我們稱之為“根”。 除了“根”之外,每棟房子有且只有一個“父“房子與之相連。一番偵察之後,聰明的小偷意識到“這個地方的所有房屋的排列類似於一棵二叉樹”。 如果兩個直接相連的房子在同一天晚上被打劫,房屋將自動報警。
計算在不觸動警報的情況下,小偷一晚能夠盜取的最高金額。
示例 1:
輸入: [3,2,3,null,3,null,1]

     3
    / \
   2   3
    \   \ 
     3   1

輸出: 7
解釋: 小偷一晚能夠盜取的最高金額 = 3 + 3 + 1 = 7.
示例 2:

輸入: [3,4,5,1,3,null,1]

     3
    / \
   4   5
  / \   \ 
 1   3   1

輸出: 9
解釋: 小偷一晚能夠盜取的最高金額 = 4 + 5 = 9.

public int rob(TreeNode root) {
    if (root == null) return 0;

    int money = root.val;
    if (root.left != null) {
        money += (rob(root.left.left) + rob(root.left.right));
    }

    if (root.right != null) {
        money += (rob(root.right.left) + rob(root.right.right));
    }

    return Math.max(money, rob(root.left) + rob(root.right));
}

粉刷房子:
假如有一排房子,共 n 個,每個房子可以被粉刷成紅色、藍色或者綠色這三種顏色中的一種,你需要粉刷所有的房子並且使其相鄰的兩個房子顏色不能相同。

當然,因為市場上不同顏色油漆的價格不同,所以房子粉刷成不同顏色的花費成本也是不同的。每個房子粉刷成不同顏色的花費是以一個 n x 3 的矩陣來表示的。

例如,costs[0][0] 表示第 0 號房子粉刷成紅色的成本花費;costs[1][2] 表示第 1 號房子粉刷成綠色的花費,以此類推。請你計算出粉刷完所有房子最少的花費成本。
注意:
所有花費均為正整數。
示例:
輸入:
[[17,2,17],
[16,16,5],
[14,3,19]]
輸出: 10
解釋: 將 0 號房子粉刷成藍色,1 號房子粉刷成綠色,2 號房子粉刷成藍色。
最少花費: 2 + 5 + 3 = 10。

class Solution {
    public int minCost(int[][] costs) {
        if(costs.length==0)return 0;
        int dp[]=new int [costs[0].length];
        int tmp[]=new int [costs[0].length];
        dp[0]=costs[0][0];
        dp[1]=costs[0][1];
        dp[2]=costs[0][2];
        for(int i=0;i<costs.length;i++){
            dp[0]=Math.min(tmp[1],tmp[2])+costs[i][0];
            dp[1]=Math.min(tmp[0],tmp[2])+costs[i][1];
            dp[2]=Math.min(tmp[0],tmp[1])+costs[i][2];
            for(int j=0;j<costs[0].length;j++){
                tmp[j]=dp[j];
            }
        }
        return Math.min(Math.min(dp[0],dp[1]),dp[2]);
    }
}

平方陣列合:
給定正整數 n,找到若干個完全平方數(比如 1, 4, 9, 16, …)使得它們的和等於 n。你需要讓組成和的完全平方數的個數最少。
示例 1:
輸入: n = 12
輸出: 3
解釋: 12 = 4 + 4 + 4.
示例 2:
輸入: n = 13
輸出: 2
解釋: 13 = 4 + 9.

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n + 1]; // 預設初始化值都為0
        for (int i = 1; i <= n; i++) {
            dp[i] = i; // 最壞的情況就是每次+1
            for (int j = 1; i - j * j >= 0; j++) { 
                dp[i] = Math.min(dp[i], dp[i - j * j] + 1); // 動態轉移方程
            }
        }
        return dp[n];
    }
}

紙幣問題
給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函式來計算可以湊成總金額所需的最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回 -1。
示例 1:
輸入: coins = [1, 2, 5], amount = 11
輸出: 3
解釋: 11 = 5 + 5 + 1
示例 2:
輸入: coins = [2], amount = 3
輸出: -1
說明:
你可以認為每種硬幣的數量是無限的。

public class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = amount + 1;             
        int[] dp = new int[amount + 1];  
        Arrays.fill(dp, max);  
        dp[0] = 0;   
        for (int i = 1; i <= amount; i++) {
            for (int j = 0; j < coins.length; j++) {
                if (coins[j] <= i) {
                    dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
                }
            }
        }
        return dp[amount] > amount ? -1 : dp[amount];
    }
}

相關文章