LeetCode刷题——动态规划相关问题

动态规划问题

动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。”

通俗一点来讲,动态规划和其它遍历算法(如深/广度优先搜索)都是将原问题拆成多个子问
题然后求解,他们之间最本质的区别是,动态规划保存子问题的解,避免重复计算。解决动态规划问题的关键是找到状态转移方程,这样我们可以通过计算和储存子问题的解来求解最终问题。

同时,我们也可以对动态规划进行空间压缩,起到节省空间消耗的效果。

在一些情况下,动态规划可以看成是带有状态记录(memoization)的优先搜索。状态记录的意思为,如果一个子问题在优先搜索时已经计算过一次,我们可以把它的结果储存下来,之后遍历到该子问题的时候可以直接返回储存的结果。动态规划是自下而上的,即先解决子问题,再解决父问题;而用带有状态记录的优先搜索是自上而下的,即从父问题搜索到子问题,若重复搜索到同一个子问题则进行状态记录,防止重复计算。如果题目需求的是最终状态,那么使用动态搜索比较方便;如果题目需要输出所有的路径,那么使用带有状态记录的优先搜索会比较方便。

70. 爬楼梯 (Easy)

问题描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

输入输出样例

示例 1:

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1 阶 + 1 阶
2 阶

示例 2:

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1 阶 + 1 阶 + 1 阶
1 阶 + 2 阶
2 阶 + 1 阶

思路
由于可以跨一步也可以跨两步,则第n层,可以由第n-1层和n-2层到达。所以爬第n层的方法f(n)=f(n-1)+f(n-2)。

代码(未优化)

class Solution {
    public int climbStairs(int n) {
        if(n<2)return n;
        int[] dp = new int[n+1];
        dp[0]=1;
        dp[1]=1;
        for(int i=2;i<=n;i++){
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n];
    }
}

代码(优化)

class Solution {
    public int climbStairs(int n) {
        if(n<2)return n;
        int pre1 = 1;
        int pre2 = 1;
        int result = 0;
        for(int i=1;i<n;i++){ //迭代一次能得到第2个值,所以求n需要迭代n-1次
            result = pre1 + pre2;
            pre1 = pre2;
            pre2 = result;
        }
        return result;
    }
}

198. 打家劫舍 (Easy)

问题描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

输入输出样例

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。

思路
对于i号房屋,有打劫和不打劫两种选择,用dp[i]记录打劫到i号最多偷窃的钱数。如果打劫当前房屋,获得的钱为dp[i-2]+nums[i](打劫之前偷窃的钱+当前这户);如果不打结,获得的钱为dp[i-1](打劫上户之前的钱)。最后选择的钱最多的一种方式。状态转换方程: f ( n ) = m a x ( f ( n − 1 ) , f ( n − 2 ) + n u m s ( n ) ) f(n)=max(f(n-1),f(n-2)+nums(n)) f(n)=max(f(n1),f(n2)+nums(n))

代码

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

代码(优化)

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n==1)return nums[0];
        int pre1 = 0;
        int pre2 = 0;
        int result = 0;
        for(int i=0;i<n;i++){
            result=Math.max(pre1,pre2+nums[i]);
            pre2 = pre1; //注意pre2在后,pre在前
            pre1 = result;
        }
        return result;
    }
}

413. 等差数列 (Medium)

问题描述
如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。

例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。
给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。

子数组 是数组中的一个连续序列。

输入输出样例

示例 1:

输入:nums = [1,2,3,4]
输出:3
解释:nums 中有三个子等差数组:[1, 2, 3]、[2, 3, 4] 和 [1,2,3,4] 自身。

示例 2:

输入:nums = [1]
输出:0

思路
对于一个数组 [1,2,3,4,5,4]
[1,2,3]时有 【1 2 3】
[1,2,3,4]时有 【1 2 3】 【2 3 4】【1 2 3 4】
[1,2,3,4,5]时有 【1 2 3】 【2 3 4】【1 2 3 4】【3 4 5】【2 3 4 5】【1 2 3 4 5】
其中标黄的是新增的数组。它们的规律是dp[i]=dp[i-1]。
则我们可以总结出规律:

  • 当访问的i元素与前面构成递增数列时(即nums[i]-nums[i-1] == nums[i-1]-nums[i-2]时):增加的等差数列数,是前一个+1。
  • 当不构成递增数组时,增加的等差数组数为0

于是可以设状态数组dp用来保存访问i时新增的等差数列的个数,一共有sum(dp)个等差数组。

代码

class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        int n = nums.length;
        if(n<3) return 0;
        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 s = 0;
        for(int d:dp){
            s+=d;
        }
        return s;
    }
}

64. 方阵左上角到右下角最短路径 (Medium)

题目描述
给定一个m× n 大小的非负整数矩阵,求从左上角开始到右下角结束的、经过的数字的和最小的路径。每次只能向右或者向下移动。
输入输出样例
示例 1:

LeetCode刷题——动态规划相关问题_第1张图片
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

示例 2:

输入:grid = [[1,2,3],[4,5,6]]
输出:12

思路
建立一个二维数组dp,每个位置记录从(0,0)出发的最近距离。则dp[i][j]=min(dp[i-1][j],dp[i][j-1])。输出dp[m-1][n-1]即可

代码

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

542. 01 矩阵 (Medium)

问题说明
给定一个由 0 和 1 组成的矩阵 mat ,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。

两个相邻元素间的距离为 1 。

输入输出样例

示例 1:

输入:mat = [[0,0,0],[0,1,0],[0,0,0]]
输出:[[0,0,0],[0,1,0],[0,0,0]]

示例 2:

输入:mat = [[0,0,0],[0,1,0],[1,1,1]]
输出:[[0,0,0],[0,1,0],[1,2,1]]

思路
从左上向右下遍历,如果是 nums[i][j]==0dp[i][j]=0,如果不为零,则dp[i][j]=min(dp[i-1][j],dp[i][j-1])+1
然后从右下向上遍历,如果不为零,则dp[i][j]=min(dp[i-1][j],dp[i][j-1])+1

代码

class Solution {
    public int[][] updateMatrix(int[][] mat) {
        int m = mat.length;
        int n = mat[0].length;
        int[][]dp = new int[m][n];
        for(int i=0;i<m;i++){
            Arrays.fill(dp[i],Integer.MAX_VALUE/2); //先装一个较大的数
        }
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(mat[i][j]==0){
                    dp[i][j]=0;
                }else {
                    if(j>0){
                        dp[i][j] = Math.min(dp[i][j-1]+1,dp[i][j]);
                    }
                    if(i>0){
                        dp[i][j] = Math.min(dp[i-1][j]+1,dp[i][j]);
                    }
                }
            }
        }
        for(int i=m-1;i>=0;i--){
            for(int j=n-1;j>=0;j--){
                if(mat[i][j]==0){
                    dp[i][j]=0;
                }else {
                    if(j+1<n){
                        dp[i][j] = Math.min(dp[i][j+1]+1,dp[i][j]);
                    }
                    if(i+1<m){
                        dp[i][j] = Math.min(dp[i+1][j]+1,dp[i][j]);
                    }
                }
            }
        }
        return dp;
    }
}

221. 最大的正方形面积 (Medium)

问题描述
在一个由 ‘0’ 和 ‘1’ 组成的二维矩阵内,找到只包含 ‘1’ 的最大正方形,并返回其面积。

输入输出样例

输入:matrix = [[“1”,“0”,“1”,“0”,“0”],[“1”,“0”,“1”,“1”,“1”],[“1”,“1”,“1”,“1”,“1”],[“1”,“0”,“0”,“1”,“0”]]
输出:4

思路
这是一个动态规划的题目,观察可以发现:对于当前矩阵,如果ch[i][j]为1,则当前最大正方形的边长dp[i][j]是周围最小正方形边长数+1。于是当前正方形边长的关系可以表示为:dp[i][j] = min(dp[i-1][j],dp[i][j],dp[i][j-1])+1

代码

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

279. 最少的完全平方数 (Medium)

问题描述
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

输入输出样例
示例 1:

输入:n = 12
输出:3
解释:12 = 4 + 4 + 4

示例 2:

输入:n = 13
输出:2
解释:13 = 4 + 9

思路
对于分割类型题,动态规划的状态转移方程通常并不依赖相邻的位置,而是依赖于满足分割
条件的位置。我们定义一个一维矩阵dp,其中dp[i] 表示数字i 最少可以由几个完全平方数相加构成。在本题中,位置i 只依赖i - k^2 的位置,如i - 1、i - 4、i - 9 等等,才能满足完全平方分割的条件。因此dp[i] 可以取的最小值即为1 + min(dp[i-1], dp[i-4], dp[i-9])

代码

class Solution {
    public int numSquares(int n) {
        int[] dp = new int[n+1];
        dp[0] = 0;
        for(int i=1;i<n+1;i++){
            int min = Integer.MAX_VALUE;
            for(int j=1;i-j*j>=0;j++){
                min = Math.min(min,dp[i-j*j]);
            }
            dp[i]=min+1;
        }
        return dp[n];
    }
}

91.数字解码的方式 (Medium)

问题描述
一条包含字母 A-Z 的消息通过以下映射进行了 编码 :

‘A’ -> “1”
‘B’ -> “2”

‘Z’ -> “26”
要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)

输入输出样例
示例 1:

输入:s = “12”
输出:2
解释:它可以解码为 “AB”(1 2)或者 “L”(12)。

示例 2:

输入:s = “226”
输出:3
解释:它可以解码为 “BZ” (2 26), “VF” (22 6), 或者 “BBF” (2 2 6) 。

思路
可以看做是70题上楼的同类题。dp[i]表示前i个字符划分的种类。对于当前字符i,如果它是最一般的情况(不为0,和上一个数字可以组成字母),则它划分有两种情况,一种是单独划分它:此时划分种类dp[i]=dp[i-1],划分的种类并没有新增;另一种是把它和前一个字符划分到一起,可以将它们视作一个字符,此时划分种类dp[i]=dp[i-2],则一共有dp[i-1]+dp[i-2]中。特殊情况,如果当前为0,则只能和前一个划分到一起;如果当前和前一个划分到一起超过26,则只能单独划分。最后返回状态dp[n]

代码

class Solution {
    public int numDecodings(String s) {
        int n = s.length();
        int[] dp = new int[n+1];
        dp[0] = 1;
        for(int i=1;i<=n;i++){
            if(s.charAt(i-1)!='0'){
                dp[i]+=dp[i-1]; 
            }
            if(i>1 && s.charAt(i-2)!='0' &&(s.charAt(i-2)-'0')*10+(s.charAt(i-1)-'0')<=26){
                dp[i]+=dp[i-2];//使用累加的方式,将多种情况加起来,避免了考虑复杂情况。
            }
        }
        return dp[n];
    }
}

139. 是否可以被字典中的字符组合而成 (Medium)

问题描述
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。

注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

输入输出样例
示例 1:

输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以由 “leet” 和 “code” 拼接成。

示例 2:

输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以由 “apple” “pen” “apple” 拼接成。
注意,你可以重复使用字典中的单词。

思路
单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。
拆分时可以重复使用字典中的单词,说明就是一个完全背包!
动规五部曲分析如下:
确定dp数组以及下标的含义
dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词。

确定递推公式
如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。

所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。

dp数组如何初始化
从递归公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递归的根基,dp[0]一定要为true,否则递归下去后面都都是false了。

那么dp[0]有没有意义呢?

dp[0]表示如果字符串为空的话,说明出现在字典里。

但题目中说了“给定一个非空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。

下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。

确定遍历顺序
题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。

举例推导dp[i]

代码

class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        int n = s.length();
        boolean[] dp = new boolean[n+1]; //dp[i]表示前i是否可以拆分成list中的字符串
        dp[0]=true;
        for(int i=1;i<=n;i++){
            for(int start = 0;start<i;start++){
                if(dp[start] && wordDict.contains(s.substring(start,i))){
                    dp[i]=true;
                }
            }
        }
        return dp[n];
    }
}

300. 最长严格递增子序列的长度 (Medium)

问题描述
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

输入输出样例
示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

思路
设dp[i]为以nums[i]结尾的最大递增序列的长度
dp[i]=max(dp[j])+1,其中j是nums前面小于nums[i]的元素索引。
最终返回dp中最大的数。

代码

class Solution {
    public int lengthOfLIS(int[] nums) {
        int n = nums.length;
        int[] dp = new int[n];//dp[i]表示以i字符结尾的最大递增序列长度
        if(n==0)return 0;
        if(n==1)return 1;
        dp[0] = 1;
        int maxLen = 0;
        for(int i=1;i<n;i++){
            int max = 0;
            for(int j=i-1;j>=0;j--){
                if(nums[j]<nums[i]){
                    max = Math.max(max,dp[j]);
                }
            }
            dp[i]=max+1;
            maxLen = Math.max(maxLen,dp[i]);
        }
        return maxLen;
    }
}

1143. 最长公共子序列 (Medium)

问题说明
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

输入输出样例
示例 1:

输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。

示例 2:

输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。

思路
使用dp[i][j]表示text1,text2分别遍历到i,j时最长的公共序列长度
可以观察到当text1[i]==text2[j]时,dp[i][j]=dp[i-1][j-1]+1
不相等时,dp[i][j]=max(dp[i-1][j],dp[i][j-1]),举例子:
LeetCode刷题——动态规划相关问题_第2张图片

代码

class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length();
        int n = text2.length();
        int[][] dp = new int[m+1][n+1]; //dp[i][j]表示text1,text2分别遍历到i,j时最长的公共序列长度
        for(int i=1;i<=m;i++){
            for(int j=1;j<=n;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][j-1],dp[i-1][j]);
                }
            }
        }
        return dp[m][n];
    }
}

416. 分割等和子集

问题描述
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

输入输出样例
示例 1:

输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。

示例 2:

输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。

思路
这是一个01背包问题,关于背包问题,有01背包和完全背包两种。
01背包: 背包中每样物品要么放,要么不放。每个物品有不同的weight和value,求背包容量为W的情况最多能放多少value的物品?一般答题模板:

int[]dp=new int[W+1];
for(int i=1;i<=n;i++){ //遍历每一个物品
	int w = weight[i-1];
	int v = value[i-1];
	for(int j=W;j>=w;j--){ //倒叙遍历物品weight
	//理解为:将能放下w的情况都尝试装上,然后选择v最大的一种情况
		dp[j]=Math.max(dp[j],dp[j-w]+v);
	}
}

完全背包问题: 背包中可以把某样物品放多次,求背包容量为W的情况最多能放多少value的物品?一般答题模板:(其中dp[i]表示不超过i所能放最多的value)

int[]dp=new int[W+1];
for(int i=1;i<=n;i++){ //遍历每一个物品
	int w = weight[i-1];
	int v = value[i-1];
	for(int j=w;j<=W;j++){ //正序遍历物品weight
		dp[j]=Math.max(dp[j],dp[j-w]+v);
	}
}

回到该问题上,可以将该问题转变为:一个大小容量为sum(nums)/2的背包,将数组中的元素往背包里放,各元素的value和weight都是元素本身。如果背包正好能装满(nums[last]!=0)则说明数组可以切割成两个和相等的部分,返回true,否则返回false。

代码

class Solution {
    public boolean canPartition(int[] nums) {
        int s = 0;
        int n = nums.length;
        for(int a:nums){
            s+=a;
        }
        if(s%2!=0)return false; //如果和为奇数,则一定不可能分割成两部分
        int m = s/2;
        int[] dp = new int[m+1];
        for (int i = 1; i <= n; i++) {
            int v = nums[i-1];
            for (int j = m; j >= v ; j--) {
                dp[j]=Math.max(dp[j],dp[j-v]+v);
            }
        }
        return dp[m] == m;
    }
}

474. 一和零 (Medium)

问题描述
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

输入输出样例
示例 1:

输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,“0001”,“1”,“0”} ,因此答案是 4 。
其他满足题意但较小的子集包括 {“0001”,“1”} 和 {“10”,“1”,“0”} 。{“111001”} 不满足题意,因
为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

输入:strs = [“10”, “0”, “1”], m = 1, n = 1
输出:2
解释:最大的子集是 {“0”, “1”} ,所以答案是 2 。

思路
这是一个二维01背包问题,相当于有两个容量分别为m,n的书包,每次放入一个物品时同时要占两个背包的空间,每个物品的value为1,最后求最大的value即为能放最多的物品数量。

代码

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int[][] dp = new int[m+1][n+1];
        for(int i=1;i<=strs.length;i++){
            int w1 = contains_0(strs[i-1]); //0的个数
            int w2 = strs[i-1].length()-w1; //1的个数
            for(int j=m;j>=w1;j--){
                for(int k=n;k>=w2;k--){
                    dp[j][k] = Math.max(dp[j][k],dp[j-w1][k-w2]+1);
                }
            }
        }
        return dp[m][n];
    }

    public int contains_0(String ss){
        int count = 0;
        for (char s:ss.toCharArray()){
            if(s == '0')count++;
        }
        return count;
    }
}

322. 零钱兑换 (Medium)

问题描述
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

输入输出样例
示例 1:

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

示例 2:

输入:coins = [2], amount = 3
输出:-1

思路
这是一个完全背包问题,背包的总容量为硬币总钱数,然后将各种类型的硬币放入背包中,每个硬币的weight为硬币面值,每个硬币value为1。求满足value的最小值即为最少使用的硬币数量。

代码

class Solution {
    public int coinChange(int[] coins, int amount) {
        int n = coins.length;
        int[]dp = new int[amount+1];
        Arrays.fill(dp,Integer.MAX_VALUE/2); //要求最小值,首先初始化为较大的值
        dp[0]=0; //dp[i]表示不大于i的价值(weight)能放入的最小硬币数量(value),显然dp[0]=0
        for(int i=1;i<=n;i++){
            int w = coins[i-1];
            for(int j=w;j<=amount;j++){
                dp[j]=Math.min(dp[j],dp[j-w]+1);
            }
        }
        if(dp[amount]==Integer.MAX_VALUE/2)return -1;
        return dp[amount];
    }
}

121. 买卖股票的最佳时机 (Easy)

问题描述
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

输入输出样例
示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

思路
注意本次只进行一次买卖交易。思路是遍历价格数组,取当前遇到的最小的价格,用当前比最小价格小的价钱买入,更新最大价格。最后返回最大价格。

class Solution {
    public int maxProfit(int[] prices) {
        int n = prices.length;
        int min=Integer.MAX_VALUE;
        int max=0; //保存最大利润
        for(int i=0;i<n;i++){
            if(prices[i]<min){
                min = prices[i];
            }else {
                max = Math.max(max,prices[i]-min);
            }
        }
        return max;
    }
}

213. 打家劫舍 II (Medium)

问题说明
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

输入输出样例
示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

思路
这道题是「198. 打家劫舍」的进阶,和第 198 题的不同之处是,这道题中的房屋是首尾相连的,第一间房屋和最后一间房屋相邻,因此第一间房屋和最后一间房屋不能在同一晚上偷窃。
分两种情况:如果偷窃了第一间房屋,则不能偷窃最后一间房屋,因此偷窃房屋的范围是第一间房屋到最后第二间房屋;如果偷窃了最后一间房屋,则不能偷窃第一间房屋,因此偷窃房屋的范围是第二间房屋到最后一间房屋。然后返回这两种情况抢的最多的。

代码

class Solution {
    public int rob(int[] nums) {
        int n = nums.length;
        if(n==1)return nums[0];
        if(n==2)return Math.max(nums[0],nums[1]);
        //打劫范围:第一间到倒数第二间
        int[] dp = new int[n];
        dp[0]=0;
        dp[1]=nums[0];
        int max =0;
        for(int i=2;i<n;i++){
            dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i-1]);
        }
        max = dp[n-1];
        //打劫范围:第二间到倒数第一间
        dp = new int[n];
        dp[0]=0;
        dp[1]=nums[1];
        for(int i=2;i<n;i++){
            dp[i] = Math.max(dp[i-1],dp[i-2]+nums[i]);
        }
        max = Math.max(max,dp[n-1]);
        return max;
    }
}

53. 最大连续子数组和 (Easy)

描述
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组 是数组中的一个连续部分。

输入输出样例
示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入:nums = [1]
输出:1

思路
动态规划问题。设dp[i]为以nums[i]元素结尾的最大子序列之和。对于当前元素nums[i]可以选择加入前面的序列,也可以选择作为新的序列的开始,取最大的情况。于是dp[i]=max(dp[i-1]+nums[i],nums[i])

代码

class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        int[] dp=new int[n];
        dp[0]=nums[0];
        for(int i=1;i<n;i++){
            dp[i]=Math.max(dp[i-1]+nums[i],nums[i] );
        }
        int max = Integer.MIN_VALUE;
        for(int a:dp){
            max = Math.max(a,max);
        }
        return max;
    }
}

空间优化

class Solution {
    public int maxSubArray(int[] nums) {
        int n = nums.length;
        int pre=nums[0];
        int max = pre;
        int current;
        for(int i=1;i<n;i++){
            current = Math.max(pre+nums[i],nums[i] );
            max = Math.max(max,current);
            pre = current;
        }    
        return max;
    }
}

343. 整数拆分 (Medium)

问题描述
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。
返回 你可以获得的最大乘积 。

输入输出样例
示例 1:

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

思路
设dp[i]为第i个元素分部乘积最大值,对于一个整数num,可以分解为num1和num2两部分,它们的乘积最大值为:
max( num1*dp[num2],num1*num2)

代码

class Solution {
    public int integerBreak(int n) {
        int[] dp = new int[n+1];
        dp[0] = 0;
        for(int i=1;i<=n;i++){
            int max = 0;
            for(int j=i-1;j>=0;j--){//遍历所有num1,num2的情况
                max = Math.max(max,Math.max((i-j)*dp[j],(i-j)*j));
            }
            dp[i] = max;
        }
        return dp[n];
    }
}

72. 编辑距离 (Hard)

问题描述
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

插入一个字符
删除一个字符
替换一个字符

输入输出样例
示例 1:

输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)

示例 2:

输入:word1 = “intention”, word2 = “execution”
输出:5
解释:
intention -> inention (删除 ‘t’)
inention -> enention (将 ‘i’ 替换为 ‘e’)
enention -> exention (将 ‘n’ 替换为 ‘x’)
exention -> exection (将 ‘n’ 替换为 ‘c’)
exection -> execution (插入 ‘u’)

思路
dp[i][j]为分别遍历到字符串1的第i元素,以及遍历到字符串2的第j元素进行最小的变换次数。
首先对dp[i][j]进行初始化,当i==0时,相当于每次把字符串2删除一个字符/把字符串1新增一个字符就能变换相等,于是:dp[i][j]=j;同理,当j==0时:dp[i][j]=i
当两个字符串遍历到相同字符时,不会新增变换次数,于是:dp[i][j]=dp[i-1][j-1]
当遍历到不同字符时,考虑修改i或者修改j的元素的次数dp[i-1][j-1]+1,删除i或者新增j的元素的次数dp[i-1][j]+1,删除j或者新增i的元素的次数dp[i][j-1]+1这几种方式中修改次数最少的方式。于是dp[i][j]=min(dp[i-1][j-1]+1,dp[i-1][j]+1,dp[i][j-1]+1)。最后返回dp[m][n]

代码

class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();
        int[][] dp = new int[m+1][n+1];//dp[i][j]表示分别遍历word1和word2到i,j时变换需要的最小步骤
        for(int i=0;i<=m;i++){
            for(int j=0;j<=n;j++) {
                if (i == 0) {
                    dp[i][j] = j;
                } else if (j == 0) {
                    dp[i][j] = i;
                } else if (word1.charAt(i-1) == word2.charAt(j-1)) {
                    dp[i][j] = dp[i-1][j-1];
                }else { //不相等的时候
                    dp[i][j] = Math.min(dp[i-1][j-1],Math.min(dp[i-1][j],dp[i][j-1]))+1;
                }
            }
        }
        return dp[m][n];
    }
}

583. 两个字符串的删除操作 (Medium)

问题说明
给定两个单词 word1 和 word2 ,返回使得 word1 和 word2 相同所需的最小步数。
每步可以删除任意一个字符串中的一个字符。

输入输出样例
示例 1:

输入: word1 = “sea”, word2 = “eat”
输出: 2
解释: 第一步将 “sea” 变为 “ea” ,第二步将 "eat "变为 “ea”

示例 2:

输入:word1 = “leetcode”, word2 = “etco”
输出:4

思路
本题和上一题的区别在于本题限制操作只能是删除。遇到相同的字符时不需要删除,dp[i][j]=dp[i-1][j-1];遇到不同字符时,只能是从删除text1的i以及text2的j这两种情况,并选择最小的删除方式,即dp[i][j]=min(dp[i-1][j]+1,dp[i][j-1]+1)

代码

class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();
        int[][] dp = new int[m+1][n+1];
        for(int i=0;i<=m;i++){
            for(int j=0;j<=n;j++){
                if(i==0) dp[i][j]=j;
                else if(j==0) dp[i][j]=i;
                else if(word1.charAt(i-1)==word2.charAt(j-1)){
                    dp[i][j]=dp[i-1][j-1];
                }else {
                    dp[i][j]=Math.min(dp[i-1][j],dp[i][j-1])+1;
                }
            }
        }
        return dp[m][n];
    }
}

650. 只有两个键的键盘 (Medium)

问题说明
最初记事本上只有一个字符 ‘A’ 。你每次可以对这个记事本进行两种操作:

Copy All(复制全部):复制这个记事本中的所有字符(不允许仅复制部分字符)。
Paste(粘贴):粘贴 上一次 复制的字符。
给你一个数字 n ,你需要使用最少的操作次数,在记事本上输出 恰好 n 个 ‘A’ 。返回能够打印出 n 个 ‘A’ 的最少操作次数。

输入输出样例
示例 1:

输入:3
输出:3
解释:
最初, 只有一个字符 ‘A’。
第 1 步, 使用 Copy All 操作。
第 2 步, 使用 Paste 操作来获得 ‘AA’。
第 3 步, 使用 Paste 操作来获得 ‘AAA’。

示例 2:

输入:n = 1
输出:0

思路
dp[i]表示i个 ‘A’ 的最少操作次数。对于i个"A",它是由已有的k个“A”通过完全复制拷贝而来的,i是k的整数倍(即k时i的因数)。即dp[i]=dp[k]+1+(i-k)/k。1表示将已有的k个A复制一次,然后拷贝(i-k)/k次。

代码

class Solution {
    public int minSteps(int n) {
        int[] dp = new int[n+1];
        for(int i=2;i<=n;i++){
            int min = Integer.MAX_VALUE;
            for(int j=1;j<=i/2;j++){
                if(i%j==0){//j是i的因数
                    min = Math.min(min,dp[j]+1+(i-j)/j);
                }
            }
            dp[i]=min;
        }
        return dp[n];
    }
}

646. 最长数对链 (Medium)

问题描述
给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。

现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。

给定一个数对集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

输入输出样例
示例:

输入:[[1,2], [2,3], [3,4]]
输出:2
解释:最长的数对链是 [1,2] -> [3,4]

思路
和求最长递增数列的问题有些类似。设dp[i]为遍历到i有序数对时最长的数对链,dp[i]=max(dp[j]+1),其中j是满足数对尾小于当前数对头的有序数对。(前提:现将有序数对集合按照头的大小进行排序)

代码

class Solution {
    public int findLongestChain(int[][] pairs) {
        int m = pairs.length;
        Comparator<int[]> comparator = new Comparator<int[]>() {
            @Override
            public int compare(int[] o1, int[] o2) {
                if(o1[0]!=o2[0])return Integer.compare(o1[0],o2[0]);
                else return Integer.compare(o1[1],o2[1]);
            }
        };
        Arrays.sort(pairs,comparator);
        int[]dp = new int[m];
        dp[0]=1;
        for(int i=1;i<m;i++){
            int max = 0;
            for(int j=i;j>=0;j--){
                if(pairs[j][1]<pairs[i][0]){
                    max = Math.max(max,dp[j]);
                }
            }
            dp[i]=max+1;
        }
        return dp[m-1];
    }
}

376. Wiggle Subsequence (Medium)

问题说明
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。

相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列 的 最长子序列的长度 。

输入输出样例

示例 1:

输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。

示例 2:

输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。

思路
使用up和down两个状态集合来存储状态。
up[i]表示后两位是上摆时的最大子序列长度
down[i]表示后两位是下摆时的最大子序列长度
LeetCode刷题——动态规划相关问题_第3张图片

遍历数组:
如果nums[i]>nums[i-1] up[i]=down[i-1]+1;down[i]=down[i-1];
如果nums[i] down[i]=up[i-1]+1;up[i]=up[i-1];
如果nums[i]=nums[i-1] down[i]=up[i-1]+1;up[i]=up[i-1];
返回max(down[m],up[m])

代码

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

494. Target Sum (Medium)

问题描述
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

输入输出样例
示例 1:

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 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
+1 + 1 + 1 + 1 - 1 = 3

示例 2:

输入:nums = [1], target = 1
输出:1

思路

sum(P) 前面符号为+的集合;sum(N) 前面符号为减号的集合
所以题目可以转化为
 sum(P) - sum(N) = target 
=> sum(nums) + sum(P) - sum(N) = target + sum(nums)
=> 2 * sum(P) = target + sum(nums) 
=> sum(P) = (target + sum(nums)) / 2 
因此题目转化为01背包,也就是能组合成容量为sum(P)的方式有多少种?
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int m = nums.length;
        int sum = 0;
        for(int a:nums){
            sum+=a;
        }
        int M = (target+sum)/2;
        //这两种情况不可能达成tartget,所以放回0种方式
        if(Math.abs(target)>sum || (sum+target)%2 ==1)return 0;
        int[] dp = new int[M+1];
        dp[0] = 1; //表示容量为零是,可以什么都不放,所以方式为1种
        for(int i=1;i<=m;i++){
            int w = nums[i-1];
            for(int j=M;j>=w;j--){
            	//dp[j]表示容量不超过j时装满背包的方式数量
            	/*它可以分别由j-w,j-1-w,...的时候通过装w装满,所以总填装的方式有
            	dp[j]=dp[j-w]+dp[j-1-w]+dp[j-2-w]...+dp[1]+dp[0],类似爬楼梯那道题*/
                dp[j]+=dp[j-w];
            }
        }
        return dp[M];
    }
}

你可能感兴趣的:(刷题,leetcode,算法,动态规划)