动态规划
动态规划做题步骤
1.确定dp数组及下标的含义
比如dp[i] 是第i+1个斐波那契数列的值
2.确定递推公式
比如dp[i] = dp[i-1] + dp[i-2]
3.初始化dp数组
比如dp[0]=1 dp[1]=1
4.确定遍历顺序
比如从前向后遍历
5.举例推导dp数组
比如第5项的值
value 1 1 2 3 5
index 0 1 2 3 4
斐波那契数列
1 | 1. dp[i] 第 i+1项的值 |
1 | public int Fibonacci(int n) { |
跳台阶
1 | 1.确定dp数组及下标的含义 |
1 | public int jumpFloor(int target) { |
- 压缩空间
只用保存之前的结果
1 | public int climbStairs(int n) { |
- 斐波那契数列公式
a1 = 1
a2 = 1
a3 = 2
a4 = 3
an =
a0 = 1
a1 = 1
a2 = 2
an = 把n+1代入通项公式中
1 | public int climbStairs(int n) { |
跳台阶扩展
1 | 1. 确定dp[i] 到第i层楼梯的跳法 |
1 | public int jumpFloorII(int target) { |
- 优化
上面的方法,每次都是从dp[0]累加到dp[i-1],每次都得从头开始计算,可以定义属性作为全局遍历,记录dp[0]累加到dp[i-2]的和
1 | private int sum = 0; |
- 再次优化
对于数列
a1 = 1
a2 = 1
a3 = 1+1 = 2
a4 = 1+1+2=4
a5 = 1+1+2+4=8
…
an = Σai(i=1,2…n-1)
an = a1+a2+a3+…+an-2+an-1 = (a1+a2+a3+…+an-2) + an-1 = 2an-1
所以求第n项不用从0开始累加,它是前一个元素的二倍,竟然是等比数列!
1 | public int jumpFloorII(int target) { |
- 完全背包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30public int jumpFloorII(int target) {
/*
可以把n个台阶当成背包,每次跳的台阶当成物品
可以重复跳某个数列的台阶,也就是物品可以重复,完全背包
求的是装满背包的方式,而不是物品的价值
所以并不是用价值的递推公式 dp[j] = max(dp[j], dp[j-weights[i]]+values[i])
而是用组合数或排列数的递推公式 dp[j] += dp[j-nums[i]] (i=0, 1, 2...)
1. dp[j] 跳到j台阶的方法
2. dp[j] += dp[j-nums[i]] (i=0, 1, 2...)
3. dp[0] = 1
4. 先跳1个台阶,再跳2个台阶 与 先跳2个台阶,再跳1个台阶 是不一样的方式,所以是求排列数
先遍历背包 再遍历物品
5. [1, 2, 3, 4] n=4
j=0 j=1 j=2 j=3 j=4
初始化 1 0 0 0 0
i=1 1 0+1 0+1 0+2 0+4
i=2 1 1 1+1 2+1 4+2
i=3 1 1 2 3+1 6+1
i=4 1 1 2 4 7+1
*/
int[] dp = new int[target+1];
dp[0] = 1;
for (int j=1; j<=target; j++) {
// i<=j 物品不能超过背包容量,不然装不进去
for (int i=1; i<=j; i++) {
dp[j] += dp[j-i];
}
}
return dp[target];
}
连续子数组的最大和
- 暴力
不是按照滑动窗口的尺寸依次遍历,而是根据起始位置分类遍历,从不同的起始位置依次向后累加,也能包含所有尺寸滑动窗口极其所有位置
1 | public int FindGreatestSumOfSubArray(int[] array) { |
- 贪心
思路: 如果前面累加的数是负数,就从0开始重新累加,因为加上负数值肯定变小,不如不要
1 | public int FindGreatestSumOfSubArray(int[] array) { |
- 动态规划
不是很理解,本题dp[i]的含义,不明白为啥是必须包括i的之前最大的和,而且以往就是dp数组的末尾值就是最优解,用贪心好理解
1 | 1. 确定dp数组含义 |
1 | public int FindGreatestSumOfSubArray(int[] array) { |
连续子数组的最大和Ⅱ
- 动态规划dp[i][2]
使用dp[i][1]记录改最大值子序列的开始索引,由于可能存在最大值相等的序列,遍历dp[i][0],找到最大值,使用i+1-dp[i][1]记录长度,找到长度最长时的开始索引startIndex = dp[i][1]
结束索引i+1
1 | 1. dp[i][0] 以num[i] 结尾的之前的子序列最大和 |
1 | import java.util.*; |
- 动态规划dp[i]举例发现dp[i]为负数的时候,就是序列开始索引改变的时候,可以在dp中找到最大值dp[i],该最大值前面最近的负数为dp[j] 子序列索引为[j+1, i+1) 长度i-j
1
2
3举例 [1,-2,3,10,-4,7,2,-5]
dp[i][0] 1 -1 3 13 9 16 18 13
dp[i][1] 0 0 2 2 2 2 2 21
2
3
4
5
6
71. dp[i] 以num[i] 结尾的之前的子序列最大和
2. 由于是num[i] 结尾要么包含num[i-1] 要么不包含num[i-1]
dp[i] = max{dp[i-1]+num[i], num[i]}
3. 初始化 dp[0] = num[0]
4. 从前向后
5. 举例 [1,-2,3,10,-4,7,2,-5]
dp[i] 1 -1 3 13 9 16 18 13
1 | public int[] FindGreatestSumOfSubArray (int[] array) { |
矩形覆盖
1 | 1. dp[i] 能放下i个小木板的矩形,小木板的放法 |
买卖股票的最好时机Ⅰ
- 暴力法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public int maxProfit (int[] prices) {
// write code here
int profit = 0;
for (int i=0; i<prices.length-1; i++) {
// 找到后面几天的最大值减去今天的值
profit = Math.max(this.getMax(prices, i+1)-prices[i], profit);
}
return profit;
}
private int getMax(int[] prices, int startIndex) {
int max = 0;
for (int i=startIndex; i<prices.length; i++) {
if (prices[i] > max) {
max = prices[i];
}
}
return max;
} - 贪心
1
2
3
4
5
6
7
8
9
10
11// 贪心,记录历史最小值,利润是今天减去历史最小值
public int maxProfit (int[] prices) {
// write code here
int min = Integer.MAX_VALUE;
int profit = 0;
for (int i=0; i<prices.length; i++) {
min = Math.min(min, prices[i]);
profit = Math.max(prices[i]-min, profit);
}
return profit;
} - 动态规划
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. dp[i][0] 第i天持有股票,手里的最大现金
dp[i][1] 第i天不持有股票,手里的最大现金
2. dp[i][0] 两种方式得到,第i天买入,继续保持dp[i-1][0]股票
dp[i][0] = max{-prices[i], dp[i-1][0]}
dp[i][1] 两种方式得到,第i天卖出持有的股票,或者继续保持dp[i-1][1]不持有的状态
dp[i][1] = max{dp[i-1][0]+prices[i], dp[i-1][1]}
3. dp[0][0] = -prices[0] 第0天持有股票,就是买入,-prices[0]
dp[0][1] = 0 第0天不持有股票,不买入,0
4. 从前往后
5. 举例
[8,9,2,5,4,7,1]
dp[i][0] -8 -8 -2 -2 -2 -2 -1
dp[i][1] 0 1 1 3 3 5 5
1 | public int maxProfit (int[] prices) { |
买卖股票的最好时机Ⅱ
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/
- 动态规划
1
2
3
4
5
6
7
8
9
10
11
121. dp[i][0] 第i天持有股票,所获得的最大金币
dp[i][1] 第i天不持有股票,所获得的最大金币
2. dp[i][0] 持有股票,保持dp[i-1][0]现状,或者买入dp[i-1][1]-prices[i]
dp[i][0] = max{dp[i-1][0], dp[i-1][1]-prices[i]}
dp[i][1] 不持有股票,保持dp[i-1][1]现状,或者卖出dp[i-1][0] + prices[i]
dp[i][0] = max{dp[i-1][1], dp[i-1][0] + prices[i]}
3. dp[i][0] = -prices[0] dp[i][1] = 0
4. 前后
5. 举例
[1,2,3,4,5]
dp[i][0] -1 -1 -1 -1 -1
dp[i][1] 0 1 2 3 4
1 | public int maxProfit(int[] prices) { |
- 贪心
1
2
3
4
5
6
7
8public int maxProfit(int[] prices) {
int result = 0;
for (int i=1; i<prices.length; i++) {
// 收集正利润
result += Math.max(prices[i] - prices[i-1], 0);
}
return result;
}
买卖股票的最好时机Ⅲ
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iii/
- 单调增区间 ×想错了
想把每个增区间的最大利润找出来,前两个的和就是最大利润,想错了
[1,2,4,2,5,7,2,4,9,0]
这种情况下,写的算法只会在各自的区间内计算最大利润
[1,2,4, 2,5,7, 2,4,9, 0]
但是最大利润是
[1,2,4,2,5,7,2,4,9,0]
1 | public int maxProfit(int[] prices) { |
- 动态规划
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
341. dp[i][j]
j= 0, 1, 2, 3, 4
0 没有操作
1 第一次买入股票
2 第一次卖出股票
3 第二次买入股票
4 第二次卖出股票
dp[i][j]表示第i天状态i手中的最大金币数量
2.
没有操作
dp[i][0] = dp[i-1][0]
第一次买入股票
第一次买入股票可以保持前一天的第一次买入状态dp[i-1][1],或者今天第一次买入dp[i-1][0] - prices[i]
dp[i][1] = max{dp[i-1][1], dp[i-1][0]-prices[i]}
第一次卖出股票
第一次卖出股票可以保持前一天第一次卖出的状态dp[i-1][2],或者今天第一次卖出dp[i-1][1] + prices[i]
dp[i][2] = max{dp[i-1][2], dp[i-1][1]+prices[i]}
第二次买入股票
第一次买入股票可以保持前一天的第二次买入状态dp[i-1][3],或者今天第二次买入dp[i-1][2] - prices[i]
dp[i][3] = max{dp[i-1][3], dp[i-1][2] - prices[i]}
第二次卖出股票
第二次卖出股票可以保持前一天第二次卖出的状态dp[i-1][4],或者今天第二次卖出dp[i-1][3] + prices[i]
dp[i][4] = max{dp[i-1][4], dp[i-1][3]+prices[i]}
3.
初始化 dp[0][0] = 0 dp[0][1] = -prices[0] dp[0][2] = 0 dp[0][3] = -prices[0] dp[0][4] = 0
4. 前往后
5.
[1, 2, 3, 4, 5]
dp[i][0] 0 0 0 0 0
dp[i][1] -1 -1 -1 -1 -1
dp[i][2] 0 1 2 3 4
dp[i][3] -1 -1 -1 -1 -1
dp[i][4] 0 1 2 3 4
1 | public int maxProfit(int[] prices) { |
买卖股票的最好时机Ⅳ
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/
- 动态规划
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
261.
dp[i][0] 第i天没有操作
dp[i][2*k]
奇数次买入股票,偶数次卖出股票
dp[i][j] 第i天买入或卖出股票最大金币数
2.
dp[i][0] = dp[i-1][0]
j 为奇数,买入股票,保持当前状态,或买入
dp[i][j] = max(dp[i-1][j], dp[i-1][j-1] - prices[i])
j 为偶数,卖出股票,保持当前状态,或卖出
dp[i][j] = max(dp[i-1][j], dp[i-1][j-1] + prices[i])
3.
dp[0][0] = 0
j为奇数
dp[0][j] = -prices[0]
j为偶数
dp[0][j] = 0
4. 从前往后
5. k=2
[1, 2, 3, 4, 5]
dp[i][0] 0 0 0 0 0
dp[i][1] -1 -1 -1 -1 -1
dp[i][2] 0 1 2 3 4
dp[i][3] -1 -1 -1 -1 -1
dp[i][4] 0 1 2 3 4
1 | public int maxProfit(int k, int[] prices) { |
买卖股票最好时机冷冻期
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/
- 动态规划
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
381.
dp[i][j]
j = 0, 1, 2, 3
0: 持有股票
1: 卖出无操作状态
2: 今天卖出
3: 冷冻期
dp[i][j] 第i天状态j最大金币
2.
0: 持有股票 a.之前就买了dp[i-1][0]
b.今天才买: 昨天是冷冻期 dp[i-1][3] - prices[i]
昨天是卖出无操作状态 dp[i-1][1] - prices[i]
dp[i][0] = max(dp[i-1][0], dp[i-1][3] - prices[i], dp[i-1][1] - prices[i])
1: 卖出无操作状态 a.昨天是卖出无操作状态 dp[i-1][1]
b.昨天是冷冻期 dp[i-1][3]
dp[i][1] = max(dp[i-1][1], dp[i-1][3])
2: 今天卖出 a.昨天买入 dp[i-1][0] + prices[i]
dp[i][2] = dp[i-1][0] + prices[i]
3: 冷冻期 a.昨天卖出 dp[i-1][2]
dp[i][3] = dp[i-1][2]
3.
dp[0][0] = -prices[0]
dp[0][1] = 0
dp[0][2] = 0
dp[0][3] = 0
4. 后往前
5. [1, 2, 3, 0, 2]
dp[i][0] -1 -1 -1 1 1
dp[i][1] 0 0 0 1 2
dp[i][2] 0 1 2 -1 3
dp[i][3] 0 0 1 2 -1
dp[length-1][1] dp[length-1][2] dp[length-1][3] 的最大值
1 | public int maxProfit(int[] prices) { |
买卖股票最好时机手续费
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/
- 动态规划
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
161.
dp[i][0] 第i天持有股票最大金币
dp[i][1] 第i天不持有股票最大金币
2.
持有股票 昨天就持有dp[i-1][0] 今天买 dp[i-1][1] - prices[i]
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i]);
不持有股票 昨天就没有dp[i-1][1], 今天卖 dp[i-1][0] + prices[i] - fee
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i] - fee);
3.
dp[0][0] = -prices[0]
dp[0][1] = 0
4.
从前往后
5. [1, 3, 2, 8, 4, 9] fee = 2
dp[i][0] -1 -1 -1 -1 1 1
dp[i][1] 0 0 0 5 5 8
1 | public int maxProfit(int[] prices, int fee) { |
- 贪心
在贪心算法:122.买卖股票的最佳时机II中使用贪心策略不用关心具体什么时候买卖,只要收集每天的正利润,最后稳稳的就是最大利润了。
而本题有了手续费,就要关系什么时候买卖了,因为计算所获得利润,需要考虑买卖利润可能不足以手续费的情况。
如果使用贪心策略,就是最低值买,最高值(如果算上手续费还盈利)就卖。
此时无非就是要找到两个点,买入日期,和卖出日期。
买入日期:其实很好想,遇到更低点就记录一下。
卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。
所以我们在做收获利润操作的时候其实有三种情况:
情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润。
情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。
情况三:不作操作,保持原有状态(买入,卖出,不买不卖)
1 | public int maxProfit(int[] prices, int fee) { |
礼物的最大价值
1 | 1. dp[row][column] 到达该格子所获得的最大价值 |
1 | public int maxValue (int[][] grid) { |
最长不含重复字符的字符串
- 双指针
left right指针,右指针逐个向右移动,当[left, right)区间中有值与右指针指向的值重复时,左指针移动到重复值的下一个位置
1 | private int maxLength; |
- 双指针,利用substring与indexOf
上述方法,自定义了查找重复字符的索引,避免重复造轮子,使用String内置的方法indexOf(),但是需要利用substring把字符串切割处理,才能使用
1 | public int lengthOfLongestSubstring (String s) { |
- 双指针利用set
上述方法都是遍历查找string,看是否存在重复的char,可以利用set判断是否重复,这样在没有重复的时候就不用重复查找了
1 | public int lengthOfLongestSubstring (String s) { |
- 动态规划
1
2
3
4
5
6
7
81. dp[i] 索引为i字符结尾的不重复子串的最大长度
2. 索引为i的字符,距离重复字符距离为d
if d<=dp[i-1] dp[i] = d
else dp[i] = dp[i-1] + 1
3. dp[0] = 1
4. 前至后
5. "abcabcbb"
1 2 3 3 3 3 2 1
1 | public int lengthOfLongestSubstring (String s) { |
数字翻译成字符串
- 递归
分类加法
有点类似于爬楼梯,当前可以翻译一个字符,也可以翻译两个字符,把两种情况相加
1 | public int solve (String nums) { |
- 动态规划
1
2
3
4
5
6
7
8
91. dp[i] 包括i为索引的前面的字符串所有的译码方式
2. dp[i] = dp[i-1] + dp[i-2] // 当前数字,可以单独变成英文字母,也可以与前一个一起变成英文字母
dp[i] = dp[i-1] // 当前数字,可以由单独数字变成英文字母
dp[i] = dp[i-2] // 当前数字与前一个字符,两个字符可以变成英文字母
dp[i] = 0 // 一个字符,两个字符都不能翻译 当前为0,dp[i]为0,后续dp[i+1]为0,继而dp[i+2]为0
3. dp[0] = 0 或 1
4. 前至后
5. 123
1 2 3
1 | public int solve (String nums) { |
最长递增序列长度
https://leetcode-cn.com/problems/longest-increasing-subsequence/
1 | /* |
最长等差数列
https://leetcode-cn.com/problems/longest-arithmetic-subsequence/
1 | 1 dp[i][d] 包括i的之前的数组公差为d的子序列长度 |
1 | public int longestArithSeqLength(int[] nums) { |
最长等比子序列
1 | public int longestSeqLength(int[] nums) { |