Dynamic Programming选编(2)
labuladong
787. Cheapest Flights Within K Stops
加权有向图中求最短路径
同时要满足,这条路径最多不能超过
K + 1
条边(经过K
个节点相当于经过K + 1
条边)。
- BFS 算法相当于从起始点开始,一步一步向外扩散,那当然是离起点越近的节点越先被遍历到,如果 BFS 遍历的过程中遇到终点,那么走的肯定是最短路径。
- 对于加权图的最短路径来说,普通的队列不管用了,得用优先级队列
PriorityQueue
Dijkstra算法,从某一点到其余点的最短路径
从起点 src
出发,k
步之内(一步就是一条边)到达节点 s
的最小路径权重为 dp(s, k)
10. Regular Expression Matching
1 | if (s[i] == p[j] || p[j] == '.') { |
难点:
通配符匹配多次?
- 前提:模式串*的前一个字符能够跟文本串的末位匹配上
- 匹配多次,即先匹配一次,再继续匹配,模式串指针不动,文本串指针前移
1 | class Solution { |
背包问题选编
https://mp.weixin.qq.com/s/xmgK7SrTnFIM3Owpk-emmg
0-1背包模板
dp[N] [C+1] 解法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Solution {
public int maxValue(int N, int C, int[] weight, int[] value) {
int[][] dp = new int[N][C+1];
// 先处理「考虑第一件物品」的情况
for (int i = 0; i <= C; i++) {
dp[0][i] = i >= weight[0] ? value[0] : 0;
}
// 再处理「考虑其余物品」的情况
for (int i = 1; i < N; i++) {
for (int j = 0; j < C + 1; j++) {
// 不选该物品
int n = dp[i-1][j];
// 选择该物品,前提「剩余容量」大于等于「物品体积」
int y = j >= weight[i] ? dp[i-1][j-weight[i]] + value[i] : 0;
dp[i][j] = Math.max(n, y);
}
}
return dp[N-1][C];
}
}dp[2] [C+1] 解法
滚动数组优化
根据「转移方程」,我们知道计算第
i
行格子只需要第i-1
行中的某些值。也就是计算「某一行」的时候只需要依赖「前一行」。
因此可以用一个只有两行的数组来存储中间结果,根据当前计算的行号是偶数还是奇数来交替使用第 0 行和第 1 行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class Solution {
public int maxValue(int N, int C, int[] weight, int[] value) {
int[][] dp = new int[2][C+1];
// 先处理「考虑第一件物品」的情况
for (int i = 0; i < C + 1; i++) {
dp[0][i] = i >= weight[0] ? weight[0] : 0;
}
// 再处理「考虑其余物品」的情况
for (int i = 1; i < N; i++) {
for (int j = 0; j < C + 1; j++) {
// 不选该物品
int n = dp[(i-1)&1][j];
// 选择该物品
int y = j >= weight[i] ? dp[(i-1)&1][j-weight[i]] + value[i] : 0;
dp[i&1][j] = Math.max(n, y);
}
}
return dp[(N-1)&1][C];
}
}dp[C+1] 解法
不难发现当求解第
i
行格子的值时,不仅是只依赖第i-1
行,还明确只依赖第i-1
行的第c
个格子和第c-weight[i]
个格子(也就是对应着第i
个物品不选和选的两种情况)。换句话说,只依赖于「上一个格子的位置」以及「上一个格子的左边位置」。
dp数组可以只保留代表「剩余容量」的维度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Solution {
public int maxValue(int N, int C, int[] weight, int[] value) {
int[] dp = new int[C + 1];
for (int i = 0; i < N; i++) {
for (int j = C; j >= weight[i]; j--) { //若当前物品的i的weight大于当前容量C,则无需考虑
// 不选该物品
int n = dp[j]; //此时dp[j]相当于dp[i-1][j]
// 选择该物品 由于是从右往左更新,此时左侧的dp仍然是上一行的结果
int y = dp[j-weight[i]] + value[i]; //此时dp[j-weight[i]]相当于dp[i-1][j-weight[i]]
dp[j] = Math.max(n, y);
}
}
return dp[C];
}
}
416. Partition Equal Subset Sum
1 | public boolean canPartition(int[] nums) { |
1 | public boolean canPartition(int[] nums) { |
1049. Last Stone Weight II
the smallest possible weight of the left stone
尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了
所谓不断「合并」&「重放」,本质只是在构造一个折叠的计算表达式,最终的结果仍然可以使用「为原来 stones 数组中的数字添加 +/− 符号,形成的“计算表达式”」所表示。
问题转换为:为 stones中的每个数字添加 +/−,使得形成的「计算表达式」结果绝对值最小。
从 stones数组中选择,凑成总和不超过
$$
\frac{sum}{2}
$$
的最大价值。
1 | public class LeetCode1049 { |
474. Ones and Zeroes
1 | public int findMaxForm(String[] strs, int m, int n) { |
494. Target Sum
1 | //a+b=sum |
完全背包模板
dp[N] [C+1] 解法
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
30class Solution {
public int maxValue(int N, int C, int[] v, int[] w) {
int[][] dp = new int[N][C + 1];
// 先预处理第一件物品
for (int j = 0; j <= C; j++) {
// 显然当只有一件物品的时候,在容量允许的情况下,能选多少件就选多少件
int maxK = j / v[0];
dp[0][j] = maxK * w[0];
}
// 处理剩余物品
for (int i = 1; i < N; i++) {
for (int j = 0; j <= C; j++) {
// 不考虑第 i 件物品的情况(选择 0 件物品 i)
int n = dp[i - 1][j];
// 考虑第 i 件物品的情况
int y = 0;
for (int k = 1 ;; k++) {
if (j < v[i] * k) {
break;
}
y = Math.max(y, dp[i - 1][j - k * v[i]] + k * w[i]);
}
dp[i][j] = Math.max(n, y);
}
}
return dp[N - 1][C];
}
}dp[2] [C+1] 解法
滚动数组优化
根据「转移方程」,我们知道计算第
i
行格子只需要第i-1
行中的某些值。也就是计算「某一行」的时候只需要依赖「前一行」。
因此可以用一个只有两行的数组来存储中间结果,根据当前计算的行号是偶数还是奇数来交替使用第 0 行和第 1 行
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
30class Solution {
public int maxValue(int N, int C, int[] v, int[] w) {
int[][] dp = new int[2][C + 1];
// 先预处理第一件物品
for (int j = 0; j <= C; j++) {
// 显然当我们只有一件物品的时候,在容量允许的情况下,能选多少件就选多少件
int maxK = j / v[0];
dp[0][j] = maxK * w[0];
}
// 处理剩余物品
for (int i = 1; i < N; i++) {
for (int j = 0; j <= C; j++) {
// 不考虑第 i 件物品的情况(选择 0 件物品 i)
int n = dp[(i - 1)&1][j];
// 考虑第 i 件物品的情况
int y = 0;
for (int k = 1 ;; k++) {
if (j < v[i] * k) {
break;
}
y = Math.max(y, dp[(i - 1)&1][j - k * v[i]] + k * w[i]);
}
dp[i&1][j] = Math.max(n, y);
}
}
return dp[(N - 1)&1][C];
}
}dp[C+1] 解法
不难发现当求解第
i
行格子的值时,不仅是只依赖第i-1
行,还明确只依赖第i-1
行的第c
个格子和第c-weight[i]
个格子(也就是对应着第i
个物品不选和选的两种情况)。换句话说,只依赖于「上一个格子的位置」以及「上一个格子的左边位置」。
dp数组可以只保留代表「剩余容量」的维度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Solution {
public int maxValue(int N, int C, int[] v, int[] w) {
int[] dp = new int[C + 1];
for (int i = 0; i < N; i++) {
for (int j = 0; j <= C; j++) {
// 不考虑第 i 件物品的情况(选择 0 件物品 i)
int n = dp[j];
// 考虑第 i 件物品的情况
int y = j - v[i] >= 0 ? dp[j - v[i]] + w[i] : 0;
dp[j] = Math.max(n, y);
}
}
return dp[C];
}
}
279. Perfect Squares
1 | public class LeetCode279 { |
Subarray选编
718. Maximum Length of Repeated Subarray
类似maximum subarray sum,随时比较并更新res
1 | public class LeetCode718 { |
代码随想录
1035. Uncrossed Lines
有意思的题:
直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。
precisely the same as longest common subsequence
!
1 | public class LeetCode1035 { |
115. Distinct Subsequences
backtracking果不其然超时,配得上hard题
若要求返回所有结果,则只能backtracking硬干;但注意到本题只要求返回结果数,考虑用dp优化
1 | public class LeetCode115 { |
1 | public int numDistinct(String s, String t) { |
自选
120. Triangle
1 | public int minimumTotal(List<List<Integer>> triangle) { |
Follow up: Could you do this using only O(n)
extra space, where n
is the total number of rows in the triangle?
1 | public int minimumTotal(List<List<Integer>> triangle) { |
256. Paint House
1 | public int minCost(int[][] costs) { |
518. Coin Change II
1 | public int change(int amount, int[] coins) { |
for each coin:
- not use it
- use it
1 | public int change(int amount, int[] coins) { |
486. Predict the Winner
好题!
注意:
- recurison的返回值不同:
- 由于recursion函数返回值代表player 1与player 2的最大差值,在player 1的轮次与player 2的轮次,目标是不同的
- 轮到player 1时,应该尽可能最大化分数
- 轮到player 2时,应该尽可能最小化分数
Approach #1 Using Recursion [Accepted]
This is done because, looking from Player 1’s perspective, for any move made by Player 1, it tends to leave the remaining subarray in a situation which minimizes the best score possible for Player 2, even if it plays in the best possible manner. But, when the turn passes to Player 1 again, for Player 1 to win, the remaining subarray should be left in a state such that the score obtained from this subarrray is maximum(for Player 1).
This is a general criteria for any arbitrary two player game and is commonly known as the Min-Max algorithm.
1 | public boolean PredictTheWinner(int[] nums) { |
Approach #2 Similar Approach [Accepted]
1 | class Solution { |
Approach #3 Dynamic Programming [Accepted]:
1 | public boolean PredictTheWinner(int[] nums) { |
2420. Find All Good Indices
转化新题为旧题!
longest non-increasing subarray
longest non-decreasing subarray
1 | public List<Integer> goodIndices(int[] nums, int k) { |