7.1 算法解释
动态规划就是把一个复杂的问题划分成若干个子问题,依次划分,一层一层求解
7.2 基本动态规划:一维
题目代号: 70 爬楼梯
题目描述:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
测试用例:
就两种情况
最后一步走1个台阶 f(n-1)
最后一步走2个台阶 f(n-2)
所以 f(n) = f(n-1) + f(n-2)
我的分析:
最后迈一步有两种情况,看是迈1个台阶,还是迈2个台阶了
代码:
class Solution {
public int climbStairs(int n) {
if(n == 0){
return 1;
}
if(n == 1){
return 1;
}
int[] f = new int[n+1];
f[0] = 1;f[1] = 1;
for(int i = 2;i <= n;i++){
f[i] = f[i-1] + f[i-2];
}
return f[n];
}
}
用数组存,一步一步递推容易理解,那我们为了节省空间,现在就使用字符来存数组的内容
class Solution {
public int climbStairs(int n) {
if(n == 0){
return 1;
}
if(n == 1){
return 1;
}
int first = 1,second = 1,res = 0;
for(int i = 2;i <= n;i++){
res = first + second ;
first = second;
second = res;
}
return res ;
}
}
题目代号: 198 打家劫舍
题目描述:
假如你是一个劫匪,并且决定抢劫一条街上的房子,每个房子内的钱财数量各不相同。如果你抢了两栋相邻的房子,则会触发警报机关。求在不触发机关的情况下最多可以抢劫多少钱
测试用例:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
偷窃到的最高金额 = 2 + 9 + 1 = 12 。
我的分析:
我们现在只考虑编号为第 i 间的房子,其实第 i 间房子无非就两种情况,一种是这件房子不抢,那么第 i-1 间房子肯定要抢了;另一种是这件房子抢,那么第 i-2 间房子肯定抢
代码:
class Solution {
public int rob(int[] nums) {
int length = nums.length;//这个数组最大下标的就是length-1
if(length == 0 || nums == null){
return 0;//里面没有元素,那就肯定返回是0了
}
if(length == 1){
return nums[0];//既然只有一间屋子,那就肯定抢这间了
}
int[] res = new int[length];
res[0] = nums[0];
res[1] = Math.max(nums[0],nums[1]);
for(int i = 2;i < length;i++){//每一步都要决策,看看抢哪个,也就是所谓的动态规划
res[i] = Math.max(res[i-2] + nums[i],res[i-1]);
}
return res[length-1];//最大的下标就是length-1
}
}
其实就跟咱们上面70题是一样的,我们大多数就是以数组来存储数据,依次推进的,但我们也可以用字符来暂代替数据,然后依次来推进
class Solution {
public int rob(int[] nums) {
int length = nums.length;
if(length == 0 || nums == null){
return 0;
}
if(length == 1){
return nums[0];
}
int first = nums[0],second = Math.max(nums[0],nums[1]);//这里面second就是最终结果,用不到res,因为它每次变化要靠num[i]
for (int i = 2;i < length;i++){
int rr = second;//rr就是一个中转站
second = Math.max(first+nums[i],second);
first = rr;
}
return second;
}
}
题目代号: 413 等差数列划分
题目描述:
给定一个数组,求这个数组中连续且等差的子数组一共有多少个。
测试用例:
输入是一个一维数组,输出是满足等差条件的连续字数组个数。
Input: nums = [1,2,3,4]
Output: 3
在这个样例中,等差数列有 [1,2,3]、[2,3,4] 和 [1,2,3,4]。
我的分析:
我们在动态规划中,通常是以res[i] 结尾的
但这个题不用,不光要考虑加入num[i] 这一位时直接满足的 if 情况,
还有跟前面的结合带来的变化
当然最后结果要加入 前面本身1 2 3 4有多少种满足的
代码:
class Solution {
public int numberOfArithmeticSlices(int[] nums) {
//这个题以1 2 3 4 5为例
int[] res = new int[nums.length];
int sum = 0;
for(int i = 2;i < nums.length;i++){
if(nums[i]-nums[i-1] == nums[i-1]-nums[i-2]){
res[i] = 1 + res[i-1];//比如现在把5添加进去,那么1代表的是if这种情况,res[i-1]代表的是加入5带来的变化
sum += res[i];//本身的sum是本来1 2 3 4形成的一个结果
}
}
return sum;
}
}
7.3 基本动态规划:二维
题目代号: 64 最小路径和
题目描述:
给定一个 m × n 大小的非负整数矩阵,求从左上角开始到右下角结束的、经过的数字的和最
小的路径。每次只能向右或者向下移动。
测试用例:
Input:
[[1,3,1],
[1,5,1],
[4,2,1]]
Output: 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 < dp.length;i++){
for(int j = 0;j < dp[0].length;j++){
if(i == 0 && j == 0) dp[i][j] = grid[i][j];
else if(i == 0) 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],dp[i-1][j]) + grid[i][j];
}
}
return dp[dp.length-1][dp[0].length-1];
}
}
题目代号: 542 01矩阵
题目描述:
给定一个由 0 和 1 组成的二维矩阵,求每个位置到最近的 0 的距离
测试用例:
输入:
[[0,0,0],
[0,1,0],
[1,1,1]]
输出:
[[0,0,0],
[0,1,0],
[1,2,1]]
我的分析:
一般来说,因为这道题涉及到四个方向上的最近搜索,所以很多人的第一反应可能会是广度优先搜索。但是对于一个大小 O(mn) 的二维数组,对每个位置进行四向搜索,最坏情况的时间复杂度(即全是 1)会达到恐怖的 O(m2n2)。
一种办法是使用一个 dp 数组做 memoization,使得广度优先搜索不会重复遍历相同位置;
另一种更简单的方法是,我们从左上到右下进行一次动态搜索,再从右下到左上进行一次动态搜索。两次动态搜索即可完成四个方向上的查找
代码:
public int[][] updateMatrix(int[][] mat) {
int row = mat.length,column = mat[0].length;
int[][] dp = new int[row][column];
for(int i = 0;i < row;i++){
Arrays.fill(dp[i],Integer.MAX_VALUE/2);//每一行都是一个数组
}
for(int i = 0;i < row;i++){
for(int j = 0;j < column;j++){
if(mat[i][j] == 0) dp[i][j] = 0;
else {
if(j > 0) dp[i][j] = Math.min(dp[i][j],dp[i][j-1]+1);//左边过来
if(i > 0) dp[i][j] = Math.min(dp[i][j],dp[i-1][j]+1);//从上面下来
}
}
}
for(int i = row-1;i >= 0;i--){
for(int j = column-1;j >= 0;j--){
if(mat[i][j] != 0){
if(j < column-1) dp[i][j] = Math.min(dp[i][j],dp[i][j+1]+1);//右边过来
if(i < row-1) dp[i][j] = Math.min(dp[i][j],dp[i+1][j]+1);//从下面上来
}
}
}
return dp;
}
题目代号: 221 最大正方形
题目描述:
给定一个二维的 0-1 矩阵,求全由 1 构成的最大正方形面积。
测试用例:
Input:
[[“1”,“0”,“1”,“0”,“0”],
[“1”,“0”,“1”,“1”,“1”],
[“1”,“1”,“1”,“1”,“1”],
[“1”,“0”,“0”,“1”,“0”]]
Output: 4
我的分析:
dp对于本题,则表示以 (i, j) 为右下角的全由 1 构成的最大正方形面积。
咱们来研究dp[i][j]看看是不是符合条件:
1、当dp[i-1][j-1]、dp[i][j-1] 和 dp[i-1][j]三个值的最小值为k-1
2、matrix[i][j] == 1的时候
这两个条件同时满足才可以,所以我们就找这三个值中的最小值,让它 + 1,这样不就符合了嘛
代码:
public int maximalSquare(char[][] matrix) {
if(matrix.length == 0 || matrix[0].length == 0){
return 0;
}
int row = matrix.length,column = matrix[0].length,bian = 0;
int[][] dp = new int[row][column];
for(int i = 0;i < row;i++){
for(int j = 0;j < column;j++){
if(matrix[i][j] == '1'){
//当 dp[i-1][j-1]、dp[i][j-1] 和 dp[i-1][j]三个值的最小值为k-1,matrix[i][j] == 1的时候
// dp[i][j]肯定就是这三个值中最小值 + 1喽
if(i == 0 || j == 0){
dp[i][j] = 1;
}else {
dp[i][j] = Math.min(dp[i-1][j-1],Math.min(dp[i-1][j],dp[i][j-1])) + 1;
}
}
bian = Math.max(bian,dp[i][j]);
}
}
return bian * bian;
}
7.4 分割类型题
题目代号: 279 完全平方数
题目描述:
给定一个正整数,求其最少可以由几个完全平方数相加构成。
测试用例:
Input: n = 13
Output: 2
在这个样例中,13 的最少构成方法为 4+9。
我的分析:
其实就是一层一层的,我们先找符合 i 的
再找符合 i - j1 * j1的
再找 i - j1 * j1 - j2 * j2的
依此类推
dp[i] 表示数字 i 最少可以由几个完全平方数相加构成。在本题中,位置 i 只依赖 i - k*k 的位置,如 i - 1、i - 4、i - 9 等等,才能满足完全平方分割的条件。因此 dp[i] 可以取的最小值即为 1 + min(dp[i-1], dp[i-4], dp[i-9] · · ·)
代码:
public int numSquares(int n) {
int[] dp = new int[n+1];//默认里面是0
for(int i = 1;i <= n;i++){
int minn = Integer.MAX_VALUE;
for(int j = 1;j * j <= i;j++){
minn = Math.min(minn,dp[i-j*j]);
}
dp[i] = minn + 1;
}
return dp[n];
}
题目代号: 91 解码方法
题目描述:
已知字母 A-Z 可以表示成数字 1-26。给定一个数字串,求有多少种不同的字符串等价于这个数字串。
测试用例:
Input: “226”
Output: 3
在这个样例中,有三种解码方式:BZ(2 26)、VF(22 6) 或 BBF(2 2 6)
我的分析:
代码:
public int numDecodings(String s) {
/*
就很像青蛙跳台的案例,就看最后一步是一个字符,还是两个字符了
*/
int n = s.length();//看一下这个字符串有多长
int[] f = new int[n + 1];
f[0] = 1;
for (int i = 1; i <= n; ++i) {//依次遍历这个字符串长度
if (s.charAt(i - 1) != '0') {//为啥又是0——n-1呢?
f[i] += f[i - 1];
}
if (i > 1 && s.charAt(i - 2) != '0' && ((s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0') <= 26)) {
f[i] += f[i - 2];
}
}
return f[n];
}
题目代号: 139 单词拆分
题目描述:
给定一个字符串和一个字符串集合,求是否存在一种分割方式,使得原字符串分割后的子字符串都可以在集合内找到。
测试用例:
Input: s = “applepenapple”, wordDict = [“apple”, “pen”]
Output: true
我的分析:
类似于完全平方数分割问题,这道题的分割条件由集合内的字符串决定,因此在考虑每个分割位置时,需要遍历字符串集合,以确定当前位置是否可以成功分割。注意对于位置 0,需要初始化值为真
代码:
/*
Input: s = "applepenapple", wordDict = ["apple", "pen"]
Output: true
*/
/*
类似于完全平方数分割问题,这道题的分割条件由集合内的字符串决定,因此在考虑每个分
割位置时,需要遍历字符串集合,以确定当前位置是否可以成功分割。注意对于位置 0,需要初
始化值为真。
*/
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordDictSet = new HashSet(wordDict);//集合中的字符串放进set集合中
boolean[] dp = new boolean[s.length() + 1];//表示字符串 ss 前 ii 个字符组成的字符串 s[0..i-1] 是否能被空格拆分成若干个字典中出现的单词。
dp[0] = true;
for (int i = 1; i <= s.length(); i++) {//一位一位的来遍历字符串,走在前面的指针
for (int j = 0; j < i; j++) {//走在后面的指针
if (dp[j] && wordDictSet.contains(s.substring(j, i))) {
dp[i] = true;
break;
}
}
}
return dp[s.length()];//这个表示是否能拆分的数组和字符串长度相等[true true true]
}
7.5 子序列问题
题目代号: 300 最长子序列
题目描述:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度
测试用例:
Input: [10,9,2,5,3,7,101,18]
Output: 4
在这个样例中,最长递增子序列之一是 [2,3,7,18]。
我的分析:
dp[i] 可以表示以 i 结尾的、最长子序列长度。对于每一个位置 i,如果其之前的某个位置 j 所对应的数字小于位置 i 所对应的数字,则我们可以获得一个以 i 结尾的、长度为 dp[j] + 1 的子序列
代码:
public int lengthOfLIS(int[] nums) {
int length = nums.length,max_sublength = 0;
int[] dp = new int[length];
for(int i = 0;i < length;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);//j是前面的,而i是后面的
}
}
max_sublength = Math.max(max_sublength,dp[i]);
}
return max_sublength;
}
题目代号: 1143 最长公共子序列
题目描述:
给定两个字符串,求它们最长的公共子序列长度。
测试用例:
Input: text1 = “abcde”, text2 = “ace”
Output: 3
在这个样例中,最长公共子序列是“ace”。
我的分析:
对于子序列问题,第二种动态规划方法是,定义一个 dp 数组,其中 dp[i] 表示到位置 i 为止的子序列的性质,并不必须以 i 结尾。这样 dp 数组的最后一位结果即为题目所求,不需要再对每个位置进行统计。
在本题中,我们可以建立一个二维数组 dp,其中 dp[i][j] 表示到第一个字符串位置 i 为止、到第二个字符串位置 j 为止、最长的公共子序列长度。这样一来我们就可以很方便地分情况讨论这两个位置对应的字母相同与不同的情况了。
代码:
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(),n = text2.length();
int[][] dp = new int[m+1][n+1];
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];
}
7.6 背包问题
有 N 个物品和容量为 W 的背包,每个物品都有自己的体积 w 和价值 v,求拿哪些物品可以使得背包所装下物品的总价值最大。如果限定每种物品只能选择 0 个或 1 个,则问题称为 0-1 背包问题;如果不限定每种物品的数量,则问题称为完全背包问题。
每个物品都有选和不选两种情况,一个物品一个物品的思考,容量一点点随之扩大
0-1背包问题:
dp[i][j] 表示前 i 件物品体积不超过 j 的情况下能达到的最大价值;
在我们遍历到第 i 件物品时,在当前背包总容量为 j 的情况下,如果我们不将物品 i 放入背包,那么 dp[i][j]= dp[i-1][j],即前 i 个物品的最大价值等于只取前 i-1 个物品时的最大价值;
如果我们将物品 i 放入背包,假设第 i 件物品体积为 w,价值为 v,那么我们得到 dp[i][j] = dp[i-1][j-w] + v。
dp[i][j]=Math.max(dp[i−1][j],dp[i-1][j−w[i]]+v[i])
0-1背包问题优化:逆序
这里可以发现我们永远只依赖于上一排 i = 1 的信息,之前算过的其他物品都不需要再使用。因此我们可以去掉 dp 矩阵的第一个维度,在考虑物品 i 时变成 dp[j]= max(dp[j], dp[j-w] + v)。
for (int i = 1; i <= n; i++)
for (int j = V; j >= w[i]; j--)
f[j] = max(f[j], f[j - w[i]] + v[i]);
完全背包问题:
个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。
dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v),其与 0-1 背包问题的差别仅仅是把状态转移方程中的第二个 i-1 变成了 i。
dp[i][j] = max(dp[i-1][j], dp[i][j-w] + v)
完全背包优化:顺序
for (int i = 1; i <= n; i++)
for (int j = w[i]; j <= V; j++)
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
题目代号: 416 分割等和子集
题目描述:
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
测试用例:
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
我的分析:
这个题没有价值,价值也是重量=总和
初始值要设置成true
结果 = 这个数不要 || 这个数要
dp[j] = dp[j] || dp[j-nums[i-1]];
代码:
public boolean canPartition(int[] nums) {
int length = nums.length;//数组的长度
int sum = 0;//记录总和
for(int num : nums){
sum += num;
}
//以下情况如果不符合,直接就不能平分:总和是奇数
if(sum % 2 == 1) return false;
int target = sum/2;
//行:前 i 件物品(数组的长度),列:前 i 件物品的总体积(总和)
boolean[] dp = new boolean[target+1];
//先填写0行,当0件物品时候,肯定能够填满整个背包
dp[0] = true;
for(int i = 1;i <= length;i++){
for(int j = target;j >= nums[i-1];j--){
dp[j] = dp[j] || dp[j-nums[i-1]];
}
}
return dp[target];
}
题目代号: 474 一和零
题目描述:
给定 m 个数字 0 和 n 个数字 1,以及一些由 0-1 构成的字符串,求利用这些数字最多可以构成多少个给定的字符串,字符串只可以构成一次。
测试用例:
Input: Array = {“10”, “0001”, “111001”, “1”, “0”}, m = 5, n = 3
Output: 4
在这个样例中,我们可以用 5 个 0 和 3 个 1 构成 [“10”, “0001”, “1”, “0”]
我的分析:
public int findMaxForm(String[] strs, int m, int n) {
int length = strs.length;
//dp[i][j][k] 表示在前 ii 个字符串中,使用 jj 个 00 和 kk 个 11 的情况下最多可以得到的字符串数量。
int[][][] dp = new int[length + 1][m + 1][n + 1];
for (int i = 1; i <= length; i++) {
int[] zerosOnes = getZerosOnes(strs[i - 1]);
int zeros = zerosOnes[0], ones = zerosOnes[1];//zeros数的是这个字符串0的个数,ones数的是这个字符串1的个数
for (int j = 0; j <= m; j++) {
for (int k = 0; k <= n; k++) {
if (j >= zeros && k >= ones) {//背包空间够这个字符串的大小,字符串可以放进去
dp[i][j][k] = Math.max(dp[i-1][j][k], dp[i - 1][j - zeros][k - ones] + 1);
}
dp[i][j][k] = dp[i - 1][j][k];//这个字符串不符合,不放进去,那就等于上一个字符串放进去,占的背包最大体积
}
}
}
return dp[length][m][n];
}
public int[] getZerosOnes(String str) {//每一个字符串进来都要数清楚0的个数和1的个数
int[] zerosOnes = new int[2];
int length = str.length();
for (int i = 0; i < length; i++) {
zerosOnes[str.charAt(i) - '0']++;
}
return zerosOnes;
}
题目代号: 494 目标和
题目描述:
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
测试用例:
输入: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
我的分析:
正数和 - 负数和 = target
正数和 + 负数和 = sum
正数和 x = (target + sum)/2;
这道题就转变为从一个数组中挑选元素出来,使其和为x的方案有几个
代码:
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for(int num : nums){
sum += num;
}
//正数和 - 负数和 = target
//正数和 + 负数和 = sum
// 正数和 x = (target + sum)/2;
if(target > sum || (target+sum)%2 == 1) return 0;
int x = (target+sum)/2;
//这道题就转变为从一个数组中挑选元素出来,使其和为x的方案有几个
int[] dp = new int[x+1];
dp[0]=1;//定义 dp[0] = 1 表示只有当不选取任何元素时,元素之和才为 0,因此只有 1 种方案
//dp[i][j]代表前i个数,元素和为x的方案是多少个
for(int i = 0;i < nums.length;i++){
for (int j = x;j >= nums[i];j--){
//原本应该是这样的dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]];不选+选
dp[j] = dp[j] + dp[j-nums[i]];
}
}
return dp[x];
}
完全背包问题
题目代号: 322 零钱兑换
题目描述:
给定一些硬币的面额,求最少可以用多少颗硬币组成给定的金额
测试用例:
Input: coins = [1, 2, 5], amount = 11
Output: 3
在这个样例中,最少的组合方法是 11 = 5 + 5 + 1。
我的分析:
dp[i][j]从前i枚硬币中,达到金额是j,所需的硬币数量
代码:
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];//前i个银币,达到总和是amount的硬币数量
// for (int t = 0; t <= dp.length; t++) {
// dp[t] = Integer.MAX_VALUE;
// }
Arrays.fill(dp,amount+2);//我们把 dp 数组初始化为 amount + 2 而不是-1 的原因是,在动态规划过程中有求
//最小值的操作,如果初始化成-1 则会导致结果始终为-1。
dp[0] = 0;//达到总和是0的硬币数量是0
for (int i = 0; i < coins.length; i++) {
for (int j = 1; j <= amount; j++) {
if (j >= coins[i]) {
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
//不要这个硬币+要这个硬币
}
}
}
return dp[amount] == amount+2 ? -1:dp[amount];//。在动态规划完成后,若结果仍然是amount+2,则说明不存在满足条
//件的组合方法,返回-1。
}
题目代号: 377 组合总和IV
题目描述:
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
测试用例:
输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。
我的分析:
肯定还是从i个数中,依次拿出来满足总和是j,的组合数
但有个问题是这个题涉及到排列组合,所以内外环要调换,外环是从1到target遍历,内环是nums数组的遍历
代码:
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];//前i个元素中,和达到target的方案个数
dp[0] = 1;//定义 dp[0] = 1 表示只有当不选取任何元素时,元素之和才为 0,因此只有 1 种方案。
for (int i = 1; i <= target; i++) {//这个内外层不能调换顺序,只有外层循环是从1到target的值,
//遍历数组nums的值,这样在计算dp[i]的值时,nums中的每个小于等于i的元素都可能作为元素之和等于
//i的排列的最后一个元素
for (int num : nums) {
if (num <= i) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
题目代号: 518 零钱兑换 II
题目描述:
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
测试用例:
输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
我的分析:
这个题简直就是说曹操曹操到,上一题377我纠结半天,没想到这个题就给我解惑了,如果像这道题这样,就正常求方案数,我们就外环遍历数组中元素,内环遍历目标数值就好了
如果像377,方案中数组换位置也算多种情况,涉及到排列组合的话,那就外环遍历目标数值,内环是数组元素
代码:
public int change(int amount, int[] coins) {
int[] dp = new int[amount+1];
dp[0] = 1;
for(int i = 0;i < coins.length;i++){
for(int j = 1;j <= amount;j++){
if(j >= coins[i]){
dp[j] = dp[j] + dp[j - coins[i]];
}
}
}
return dp[amount];
}
7.7 字符串编辑
题目代号: 72 编辑距离
题目描述:
给定两个字符串,已知你可以删除、替换和插入任意字符串的任意字符,求最少编辑几步可以将两个字符串变成相同。
测试用例:
Input: word1 = “horse”, word2 = “ros”
Output: 3
在这个样例中,一种最优编辑方法是(1)horse -> rorse (2)rorse -> rose(3)rose -> ros。
我的分析:
类似于题目 1143,我们使用一个二维数组 dp[i][j],表示将第一个字符串到位置 i 为止,和第二个字符串到位置 j 为止,最多需要几步编辑。
当第 i 位和第 j 位对应的字符相同时,dp[i][j] 等 于 dp[i-1][j-1];
当二者对应的字符不同时,修改的消耗是 dp[i-1][j-1]+1,
插入 i 位置/删除 j 位置的消耗是 dp[i][j-1] + 1,
插入 j 位置/删除 i 位置的消耗是 dp[i-1][j] + 1。
代码:
public int minDistance(String word1, String word2) {
int m = word1.length(),n = word2.length();
int[][] dp = new int[m+1][n+1];//表示将第一个字符串到位置 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){//其中一个字符串都没动,你想去吧,肯定都不相同啊,那步数肯定就是懂得5那个字符串走的路程了
dp[i][j] = i;
}else {
//dp[i][j] = Math.min(Math.min(dp[i-1][j-1]+0,dp[i-1][j-1]+1),Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1));
dp[i][j] = Math.min(dp[i-1][j-1] + ((word1.charAt(i-1) == word2.charAt(j-1))? 0: 1),Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1));
}
}
}
return dp[m][n];
}
题目代号: 650 只有两个键的键盘
题目描述:
给定一个字母 A,已知你可以每次选择复制全部字符,或者粘贴之前复制的字符,求最少需要几次操作可以把字符串延展到指定长度。
测试用例:
Input: 3
Output: 3
在这个样例中,一种最优的操作方法是先复制一次,再粘贴两次。
我的分析:
比如n = 10 ,那就主要找它的素数2 和 5,然后求 5 的素数,依次往下求解
代码:
public int minSteps(int n) {
int ans = 0, d = 2;//当n进来的时候,就看它的最大素数是几了,咱们从2开始看
while (n > 1) {
while (n % d == 0) {
ans += d;
n /= d;//满足的话就往下迭代
}
d++;//不满足的话,就按位叠加
}
return ans;
}
题目代号: 10 正则表达式匹配
题目描述:
测试用例:
我的分析:
咱们先来看第二个字符:
是 * 号(第一个字符相同,第一个字符不相同)
不是 * 号
代码:
public boolean isMatch(String s, String p) {
if(s == null || p == null){
return false;
}
int i = 0;int j = 0;
return match(s,0,p,0);
}
private boolean match(String s, int i,String p,int j){
if(j == p.length() && i == s.length()){//两个指针一步一步挪动呗
return true;
}
if(i < s.length() && j == p.length()){
return false;
}
if(j+1 < p.length() && p.charAt(j+1) == '*'){//如果第二个字符是*
if((i < s.length() && s.charAt(i) == p.charAt(j)) || (i < s.length() && p.charAt(j) == '.')){//第一个字符相等:有三种情况
//分别代表匹配0个、1个、多个
//例如s = a p = a*
return match(s,i,p,j+2) || match(s,i+1,p,j+2) || match(s,i+1,p,j);
}else {//第一个字符不等
//例如 s = a p = b*a
return match(s,i,p,j+2);
}
}else {//第二个字符不是*
if((i < s.length() && s.charAt(i) == p.charAt(j)) || (i < s.length() && p.charAt(j) == '.')){
return match(s,i+1,p,j+1);
}
}
return false;
}
7.8 股票交易
股票类问题主要要思考以下几个点:
1.dp函数的定义是什么?有几个状态?
2.初始值如何确定?
3.状态方程又是什么?
持有不持有的区分:
第i天买入算持有
第i天卖出算不持有
题目代号: 121 买卖股票的最佳时间:限定交易次数 k=1
题目描述:
给定一段时间内每天的股票价格,已知你只可以买卖各一次,求最大的收益。
测试用例:
Input: [7,1,5,3,6,4]
Output: 5
在这个样例中,最大的利润为在第二天价格为 1 时买入,在第五天价格为 6 时卖出
我的分析:
既然咱们现在要最大值 - 最小值
那就用minnum来记录前 i 个数的最小值
用当前值 - minnum 来记录最大利润
代码:
public int maxProfit(int[] prices) {
int n = prices.length;
int minnum = prices[0];//记录最小值
int sell = 0;//记录卖出的最大值
for (int i = 1; i < n; ++i) {
minnum = Math.min(minnum, prices[i]);//找前i个元素中最小的
sell = Math.max(sell, prices[i] - minnum);
}
return sell;
}
我的分析:
状态:
天数
股票持有状态
dp[i][j]代表第i天,持有股票状态为j,能获得得最大利润
初始值:
dp[0][0] = 0
dp[0][1] = -prices[0]
状态方程:
第i天不持有:
第i-1天不持有:dp[i][0] = dp[i-1][0]
第i-1天持有:dp[i][0] = dp[i-1][1]+prices[i]
dp[i][0]=Math.max(dp[i-1][0], dp[i-1][1]+prices[i])
第i天持有:
第i-1天不持有:dp[i][1] = -prices[i]
第i-1天持有:dp[i][1]=dp[i-1][1]
dp[i][1]=Math.max(-prices[i],dp[i-1][1])
代码:
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = 0;dp[0][1]=-prices[0];
for(int i = 1;i < n;i++){
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1]=Math.max(-prices[i],dp[i-1][1]);
}
return dp[n-1][0];
}
题目代号: 122 买卖股票得最佳时机II : 交易次数无限制
题目描述:
给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
测试用例:
示例 1:
输入: prices = [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: prices = [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: prices = [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
我的分析:
状态:
天数
股票持有状态
dp[i][j]代表第i天,持有股票状态为j,能获得得最大利润
初始值:
dp[0][0] = 0
dp[0][1] = -prices[0]
状态方程:
第i天不持有:
dp[i][0]=Math.max(dp[i-1][0], dp[i-1][1]+prices[i])
第i天持有:
dp[i][1]=Math.max(dp[i-1][1], dp[i-1][0]-prices[i])
代码:
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = 0;dp[0][1]=-prices[0];
for(int i = 1;i < n;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], dp[i-1][0]-prices[i]);
}
return dp[n-1][0];
}
题目代号: 123. 买卖股票的最佳时机 III : 交易次数限定为2
题目描述:
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
测试用例:
示例 1:
输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
示例 2:
输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入:prices = [7,6,4,3,1]
输出:0
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。
我的分析:
主要看第二次买的时候,要以第一次卖的时刻赚的钱为基准,先降下去,然后再升上来
代码:
class Solution {
public static int maxProfit(int[] prices){
int buy1 = prices[0],sell1 = 0;//buy1就是数组里面的最小值,sell1是当前遍历值与买的值的差值
int buy2 = prices[0],sell2 = 0;
for(int i = 1;i < prices.length;i++){
buy1 = Math.min(buy1,prices[i]);
sell1 = Math.max(sell1,prices[i] - buy1);
buy2 = Math.min(buy2,prices[i] - sell1);
//这个地方我给你好好说说,好家伙,我终于明白了,
// 既然你第一次的买卖已经结束了,那就知道自己挣了多少钱了,
// 那我第二次买的时候以第一次的基础降下去,但我第二次卖的时候又升上去了,所以还是得到了总的
sell2 = Math.max(sell2,prices[i] - buy2);
}
return sell2;
}
}
我的分析:
跟188题思路一样,只需要将k固定为2即可
代码:
public static int maxProfit(int[] prices){
int K = 2;
int n=prices.length;
if(n<=1) return 0;
//因为一次交易至少涉及两天,所以如果k大于总天数的一半,就直接取天数一半即可,多余的交易次数是无意义的
K=Math.min(K,n/2);
/*1、dp定义:dp[i][j][k]代表 第i天交易了k次时的最大利润,其中j代表当天是否持有股票,0不持有,1持有*/
int[][][] dp=new int[n][2][K+1];
for(int k=0;k<=K;k++){
dp[0][0][k]=0;//2、初始值 : 不持有股票时
dp[0][1][k]=-prices[0];//2、初始值 : 持有股票时
}
/*3、状态方程:
dp[i][0][k],当天不持有股票时,看前一天的股票持有情况
dp[i][1][k],当天持有股票时,看前一天的股票持有情况*/
for(int i=1;i<n;i++){
for(int k=1;k<=K;k++){
dp[i][0][k]=Math.max(dp[i-1][0][k],dp[i-1][1][k]+prices[i]);
dp[i][1][k]=Math.max(dp[i-1][1][k],dp[i-1][0][k-1]-prices[i]);
}
}
return dp[n-1][0][K];
}
题目代号: 188 买卖股票的最佳时间 IV :限定交易次数 k = ?
题目描述:
给定一段时间内每天的股票价格,已知你只可以买卖各 k 次,且每次只能拥有一支股票,求最大的收益。
测试用例:
Input: [3,2,6,5,0,3], k = 2
Output: 7
在这个样例中,最大的利润为在第二天价格为 2 时买入,在第三天价格为 6 时卖出;再在第五天价格为 0 时买入,在第六天价格为 3 时卖出。
我的分析:
本题中存在三种状态:天数、是否持有股票的状态、交易次数
而对于每种状态,我们又有不同的选择:
- 天数由prices确定
- 是否持有股票的状态我们可以用0/1表示,0代表不持有股票,1代表持有股票
- 交易次数由k确定
dp[i][j][k],其存储的内容是在第i天,我们持有股票的状态为j,已经进行了k次交易时能够获取的最大利润
初始值:
1.不持有股票时:dp[0][0][k]=0
2.持有股票时:dp[0][1][k]=-prices[0]
状态方程:
第i天不持有股票时:
1.如果第i-1天也不持有股票,那就代表状态不变,第i天的状态=第i-1天的状态
2.如果第i-1天持有股票,说明我们在第i天把股票卖了,既然卖股票赚钱了,利润就要多prices[i]
dp[i][0][k] = max(dp[i-1][0][k] , dp[i-1][1][k]+prices[i])
第i天持有股票时:
1.如果第i-1天也持有股票,那就代表状态不变,即dp[i][1][k] = dp[i-1][1][k]
2.如果第i-1天不持有股票,说明我们在第i天买入股票,既然买股票要花钱,利润就要少price[i]
买入股票的同时,当天的交易次数要在前一天的基础上+1
dp[i][1][k] = max(dp[i-1][1][k] , dp[i-1][0][k-1]-prices[i])
代码:
public int maxProfit(int K, int[] prices) {//这里悄咪咪把小k换成了大K,便于后续索引赋值
int n=prices.length;
if(n<=1) return 0;
//因为一次交易至少涉及两天,所以如果k大于总天数的一半,就直接取天数一半即可,多余的交易次数是无意义的
K=Math.min(K,n/2);
/*1、dp定义:dp[i][j][k]代表 第i天交易了k次时的最大利润,其中j代表当天是否持有股票,0不持有,1持有*/
int[][][] dp=new int[n][2][K+1];
for(int k=0;k<=K;k++){
dp[0][0][k]=0;//2、初始值 : 不持有股票时
dp[0][1][k]=-prices[0];//2、初始值 : 持有股票时
}
/*3、状态方程:
dp[i][0][k],当天不持有股票时,看前一天的股票持有情况
dp[i][1][k],当天持有股票时,看前一天的股票持有情况*/
for(int i=1;i<n;i++){
for(int k=1;k<=K;k++){
dp[i][0][k]=Math.max(dp[i-1][0][k],dp[i-1][1][k]+prices[i]);
dp[i][1][k]=Math.max(dp[i-1][1][k],dp[i-1][0][k-1]-prices[i]);
}
}
return dp[n-1][0][K];
}
题目代号: 309 最佳买卖股票时机含冷冻期
题目描述:
给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
测试用例:
输入: [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
我的分析:
冷冻期就是:卖出股票后,你无法在第二天买入股票,那又有何难
直接设置三个持有状态:
dp[i][0]:持有股票
dp[i][1]:不持有股票,处于冷冻期
dp[i][2]:不持有股票,不处于冷冻期
初始值:
dp[0][0]= -prices[0]
dp[0][1]=不可能
dp[0][2]=0
状态方程:
dp[i][0]=max(dp[i-1][0] , dp[i-1][2]-prices[i]) //当天持有股票,前一天不可能是冷冻期,也就没有dp[i-1][1]
dp[i][1]=dp[i-1][0]+prices[i] //当天是冷冻期,只可能是前一天持有股票,然后今天卖出股票了
dp[i][2]=max(dp[i-1][1],dp[i-1][2]) //当天是非冷冻期,前一天不可能持有股票
代码:
public int maxProfit(int[] prices) {
if (prices.length == 0) {
return 0;
}
int n = prices.length;
// f[i][0]: 手上持有股票的最大收益
// f[i][1]: 手上不持有股票,并且处于冷冻期中的累计最大收益
// f[i][2]: 手上不持有股票,并且不在冷冻期中的累计最大收益
int[][] dp = new int[n][3];
dp[0][0] = -prices[0];
for (int i = 1; i < n; ++i) {
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][2]-prices[i]); //当天持有股票,前一天不可能是冷冻期,也就没有dp[i-1][1]
dp[i][1]=dp[i-1][0]+prices[i]; //当天是冷冻期,只可能是前一天持有股票,然后今天卖出股票了
dp[i][2]=Math.max(dp[i-1][1],dp[i-1][2]); //当天是非冷冻期,前一天不可能持有股票
}
return Math.max(dp[n - 1][1], dp[n - 1][2]);
}
题目代号: 714 买卖股票的最佳时机
题目描述:
给定一个整数数组 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
我的分析:
状态:
天数
股票持有状态
dp[i][j]代表第i天,持有股票状态为j,能获得得最大利润
初始值:
dp[0][0] = 0
dp[0][1] = -prices[0]
状态方程:
第i天不持有:
dp[i][0]=Math.max(dp[i-1][0], dp[i-1][1]+prices[i]-fee)
第i天持有:
dp[i][1]=Math.max(dp[i-1][1], dp[i-1][0]-prices[i])
代码:
public int maxProfit(int[] prices,int fee) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = 0;dp[0][1]=-prices[0];
for(int i = 1;i < n;i++){
dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]+prices[i]-fee);
dp[i][1]=Math.max(dp[i-1][1], dp[i-1][0]-prices[i]);
}
return dp[n-1][0];
}