不要纠结,干就完事了,熟练度很重要!!!多练习,多总结!!!
最长递增子序列
LeetCode 300. 最长递增子序列
解题思路
dp[i]表示以nums[i]这个数结尾的最长递增子序列的长度。
我们已经知道了dp[0…4]的所有结果,我们如何通过这些已知结果推出dp[5]呢?
nums[5]前面有哪些元素小于nums[5]?这个好算,用 for 循环比较一波就能把这些元素找出来。
for (int i = 0; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
// 寻找 nums[0..j-1] 中比 nums[i] 小的元素
if (nums[i] > nums[j]) {
// 把 nums[i] 接在后面,即可形成长度为 dp[j] + 1,
// 且以 nums[i] 为结尾的递增子序列
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
}
代码实现
class Solution {
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
Arrays.fill(dp, 1);
for(int i = 0;i < nums.length;i++){
for(int j = 0;j < i;j++){
if(nums[i] > nums[j]){
dp[i] = Math.max(dp[i], dp[j]+1);
}
}
}
int res = 0;
for(int i = 0; i < dp.length;i++){
res = Math.max(res, dp[i]);
}
return res;
}
}
LeetCode 354. 俄罗斯套娃信封问题
解题思路
这道题目其实是最长递增子序列的一个变种,因为每次合法的嵌套是大的套小的,相当于在二维平面中找一个最长递增的子序列,其长度就是最多能嵌套的信封个数。
先对宽度w进行升序排序,如果遇到w相同的情况,则按照高度h降序排序;之后把所有的h作为一个数组,在这个数组上计算 LIS 的长度就是答案。
对宽度w从小到大排序,确保了w这个维度可以互相嵌套,所以我们只需要专注高度h这个维度能够互相嵌套即可。
其次,两个w相同的信封不能相互包含,所以对于宽度w相同的信封,对高度h进行降序排序,保证 LIS 中不存在多个w相同的信封。
代码实现
class Solution {
public int maxEnvelopes(int[][] envelopes) {
Arrays.sort(envelopes, new Comparator<int[]>(){
public int compare(int[] a, int[] b){
return a[0] == b[0]?b[1]-a[1]:a[0]-b[0];
}
});
int[] nums = new int[envelopes.length];
for(int i = 0;i < envelopes.length;i++){
nums[i] = envelopes[i][1];
}
return lengthOfLIS(nums);
}
public int lengthOfLIS(int[] nums){
int[] dp = new int[nums.length];
Arrays.fill(dp, 1);
for(int i = 0;i < nums.length;i++){
for(int j = 0;j < i;j++){
if(nums[i]>nums[j]){
dp[i] = Math.max(dp[i], dp[j]+1);
}
}
}
int res = 0;
for(int i = 0; i < nums.length;i++){
res = Math.max(dp[i], res);
}
return res;
}
}
LeetCode 673. 最长递增子序列的个数
解题思路
代码实现
class Solution {
public int findNumberOfLIS(int[] nums) {
if (nums.length <= 1) return nums.length;
int[] dp = new int[nums.length];
for(int i = 0; i < dp.length; i++) dp[i] = 1;
int[] count = new int[nums.length];
for(int i = 0; i < count.length; i++) count[i] = 1;
int maxCount = 0;
for (int i = 1; i < nums.length; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
if (dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
count[i] = count[j];
} else if (dp[j] + 1 == dp[i]) {
count[i] += count[j];
}
}
maxCount = Math.max(maxCount, dp[i]);
}
}
int result = 0;
for (int i = 0; i < nums.length; i++) {
if (maxCount == dp[i]) result += count[i];
}
return result;
}
}
最大子数组和
LeetCode 53. 最大子数组和
解题思路
以nums[i]为结尾的「最大子数组和」为dp[i]。
dp[i]有两种「选择」,要么与前面的相邻子数组连接,形成一个和更大的子数组;要么不与前面的子数组连接,自成一派,自己作为一个子数组。
// 要么自成一派,要么和前面的子数组合并
dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);
代码实现
class Solution {
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
dp[0] = nums[0];
for(int i = 1;i < nums.length;i++){
dp[i] = Math.max(nums[i], dp[i-1]+nums[i]);
}
int res = Integer.MIN_VALUE;
for(int c:dp){
res = Math.max(res, c);
}
return res;
}
}
最大编辑距离
LeetCode 72. 编辑距离
解题思路
if s1[i] == s2[j]:
return dp(i - 1, j - 1) # 啥都不做
# 解释:
# 本来就相等,不需要任何操作
# s1[0..i] 和 s2[0..j] 的最小编辑距离等于
# s1[0..i-1] 和 s2[0..j-1] 的最小编辑距离
# 也就是说 dp(i, j) 等于 dp(i-1, j-1)
dp(i, j - 1) + 1, # 插入
# 解释:
# 我直接在 s1[i] 插入一个和 s2[j] 一样的字符
# 那么 s2[j] 就被匹配了,前移 j,继续跟 i 对比
# 别忘了操作数加一
dp(i - 1, j) + 1, # 删除
# 解释:
# 我直接把 s[i] 这个字符删掉
# 前移 i,继续跟 j 对比
# 操作数加一
dp(i - 1, j - 1) + 1 # 替换
# 解释:
# 我直接把 s1[i] 替换成 s2[j],这样它俩就匹配了
# 同时前移 i,j 继续对比
# 操作数加一
代码实现
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 = 1; i < m+1;i++){
dp[i][0] = i;
}
for(int j = 1;j < n+1;j++){
dp[0][j] = j;
}
for(int i = 1;i < m+1;i++){
for(int j = 1;j < n+1;j++){
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][j-1]+1, Math.min(dp[i-1][j]+1, dp[i-1][j-1]+1));
}
}
}
return dp[m][n];
}
}
最长公共子序列
LeetCode 1143. 最长公共子序列
解题思路
定义:s1[0…i-1] 和 s2[0…j-1] 的 lcs 长度为 dp[i][j]
目标:s1[0…m-1] 和 s2[0…n-1] 的 lcs 长度,即 dp[m][n]
base case: dp[0][…] = dp[…][0] = 0
代码实现
- 自底向上的迭代的动态规划思路:
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];
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-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}
}
- 自顶向下带备忘录的动态规划思路:
// 备忘录,消除重叠子问题
int[][] memo;
/* 主函数 */
int longestCommonSubsequence(String s1, String s2) {
int m = s1.length(), n = s2.length();
// 备忘录值为 -1 代表未曾计算
memo = new int[m][n];
for (int[] row : memo)
Arrays.fill(row, -1);
// 计算 s1[0..] 和 s2[0..] 的 lcs 长度
return dp(s1, 0, s2, 0);
}
// 定义:计算 s1[i..] 和 s2[j..] 的最长公共子序列长度
int dp(String s1, int i, String s2, int j) {
// base case
if (i == s1.length() || j == s2.length()) {
return 0;
}
// 如果之前计算过,则直接返回备忘录中的答案
if (memo[i][j] != -1) {
return memo[i][j];
}
// 根据 s1[i] 和 s2[j] 的情况做选择
if (s1.charAt(i) == s2.charAt(j)) {
// s1[i] 和 s2[j] 必然在 lcs 中
memo[i][j] = 1 + dp(s1, i + 1, s2, j + 1);
} else {
// s1[i] 和 s2[j] 至少有一个不在 lcs 中
memo[i][j] = Math.max(
dp(s1, i + 1, s2, j),
dp(s1, i, s2, j + 1)
);
}
return memo[i][j];
}
LeetCode 583. 两个字符串的删除操作
解题思路
思路参考《LeetCode 1143. 最长公共子序列》,本题要求两个字符串相同操作最小步数,上一题可以求出两个字符串的最大公共长度,那么两个字符串的删除各自多余长度,操作数之和即为本题答案。
代码实现
class Solution {
public int minDistance(String word1, String word2) {
int length = lengthCommonStr(word1, word2);
int m = word1.length(), n = word2.length();
return m-length+n-length;
}
public int lengthCommonStr(String word1, String word2){
int m = word1.length(), n = word2.length();
int[][] dp = new int[m+1][n+1];
for(int i = 1; i <= m;i++){
for(int j = 1;j <= n;j++){
if(word1.charAt(i-1) == word2.charAt(j-1)){
dp[i][j] = dp[i-1][j-1]+1;
}else{
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[m][n];
}
}
LeetCode 712. 两个字符串的最小ASCII删除和
解题思路
思路参考《LeetCode 1143. 最长公共子序列》自顶向下递归方法,只不过在每一步匹配时,要将不匹配的字符算在总和res中。
代码实现
class Solution {
int[][] memo;
public int minimumDeleteSum(String s1, String s2) {
int m = s1.length(), n = s2.length();
memo = new int[m][n];
for(int[] arr:memo){
Arrays.fill(arr, -1);
}
return dp(s1, 0, s2, 0);
}
public int dp(String s1, int i, String s2, int j){
int res = 0;
if(i == s1.length()){
for(; j < s2.length();j++){
res += s2.charAt(j);
}
return res;
}
if(j == s2.length()){
for(;i < s1.length();i++){
res+=s1.charAt(i);
}
return res;
}
if(memo[i][j] != -1){
return memo[i][j];
}
if(s1.charAt(i) == s2.charAt(j)){
memo[i][j] = dp(s1, i+1, s2,j+1);
}else{
memo[i][j] = Math.min(s1.charAt(i)+dp(s1, i+1,s2,j), s2.charAt(j)+dp(s1,i,s2,j+1));
}
return memo[i][j];
}
}
正则匹配
LeetCode 10. 正则表达式匹配
解题思路
dp函数的定义如下:
若dp(s,i,p,j) = true,则表示s[i…]可以匹配p[j…];若dp(s,i,p,j) = false,则表示s[i…]无法匹配p[j…]。
bool dp(string& s, int i, string& p, int j) {
if (s[i] == p[j] || p[j] == '.') {
// 匹配
if (j < p.size() - 1 && p[j + 1] == '*') {
// 1.1 通配符匹配 0 次或多次
return dp(s, i, p, j + 2)
|| dp(s, i + 1, p, j);
} else {
// 1.2 常规匹配 1 次
return dp(s, i + 1, p, j + 1);
}
} else {
// 不匹配
if (j < p.size() - 1 && p[j + 1] == '*') {
// 2.1 通配符匹配 0 次
return dp(s, i, p, j + 2);
} else {
// 2.2 无法继续匹配
return false;
}
}
}
- 一个 base case 是j == p.size()时,按照dp函数的定义,这意味着模式串p已经被匹配完了,那么应该看看文本串s匹配到哪里了,如果s也恰好被匹配完:
if (j == p.size()) {
return i == s.size();
}
- 另一个 base case 是i == s.size()时,此时并不能根据j是否等于p.size()来判断是否完成匹配,只要p[j…]能够匹配空串,就可以算完成匹配。比如说s = “a”, p = "ab* c* ",当i走到s末尾的时候,j并没有走到p的末尾,但是p依然可以匹配s。
int m = s.size(), n = p.size();
if (i == s.size()) {
// 如果能匹配空串,一定是字符和 * 成对儿出现
if ((n - j) % 2 == 1) {
return false;
}
// 检查是否为 x*y*z* 这种形式
for (; j + 1 < p.size(); j += 2) {
if (p[j + 1] != '*') {
return false;
}
}
return true;
}
代码实现
class Solution {
Map<String, Boolean> memo;
public boolean isMatch(String s, String p) {
memo = new HashMap<>();
return dp(s, 0, p, 0);
}
public boolean dp(String s1, int i, String s2, int j){
int m = s1.length(), n = s2.length();
if(j == n){
return i == m;
}
if(i == m){
if((n-j)%2==1){
return false;
}
for(;j+1<n;j+=2){
if(s2.charAt(j+1) != '*'){
return false;
}
}
return true;
}
String key = i+"_"+j;
if(memo.containsKey(key)){
return memo.get(key);
}
boolean res = false;
if(s1.charAt(i) == s2.charAt(j) || s2.charAt(j) == '.'){
if(j < s2.length()-1 && s2.charAt(j+1) == '*'){
res = dp(s1, i, s2, j+2)||dp(s1, i+1, s2,j);
}else{
res = dp(s1, i+1, s2, j+1);
}
}else{
if(j < s2.length()-1 && s2.charAt(j+1) == '*'){
res = dp(s1, i, s2,j+2);
}else{
res = false;
}
}
memo.put(key, res);
return res;
}
}
通用考察
LeetCode 279. 完全平方数
解题思路
代码实现
class Solution {
public int numSquares(int n) {
int[] f = new int[n + 1];
for (int i = 1; i <= n; i++) {
int minn = Integer.MAX_VALUE;
for (int j = 1; j * j <= i; j++) {
minn = Math.min(minn, f[i - j * j]);
}
f[i] = minn + 1;
}
return f[n];
}
}
LeetCode 238. 除自身以外数组的乘积
代码实现
class Solution {
public int[] productExceptSelf(int[] nums) {
int n = nums.length;
int[] dp = new int[n]; // 用来存储左侧所有元素的乘积
int[] pd = new int[n]; // 用来存储右侧所有元素的乘积
// 初始化
dp[0] = 1;
pd[n - 1] = 1;
// 计算dp数组(左侧所有元素的乘积)
for (int i = 1; i < n; i++) {
dp[i] = dp[i - 1] * nums[i - 1];
}
// 计算pd数组(右侧所有元素的乘积)
for (int i = n - 2; i >= 0; i--) {
pd[i] = pd[i + 1] * nums[i + 1];
}
// 构造最终结果
int[] result = new int[n];
for (int i = 0; i < n; i++) {
result[i] = dp[i] * pd[i];
}
return result;
}
}
LeetCode 221. 最大正方形
解题思路
代码实现
class Solution {
public int maximalSquare(char[][] matrix) {
int maxSide = 0;
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return maxSide;
}
int rows = matrix.length, columns = matrix[0].length;
int[][] dp = new int[rows][columns];
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (matrix[i][j] == '1') {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
maxSide = Math.max(maxSide, dp[i][j]);
}
}
}
int maxSquare = maxSide * maxSide;
return maxSquare;
}
}
LeetCode 152. 乘积最大子数组
解题思路
代码实现
class Solution {
public int maxProduct(int[] nums) {
int length = nums.length;
int[] maxF = new int[length];
int[] minF = new int[length];
System.arraycopy(nums, 0, maxF, 0, length);
System.arraycopy(nums, 0, minF, 0, length);
for (int i = 1; i < length; ++i) {
maxF[i] = Math.max(maxF[i - 1] * nums[i], Math.max(nums[i], minF[i - 1] * nums[i]));
minF[i] = Math.min(minF[i - 1] * nums[i], Math.min(nums[i], maxF[i - 1] * nums[i]));
}
int ans = maxF[0];
for (int i = 1; i < length; ++i) {
ans = Math.max(ans, maxF[i]);
}
return ans;
}
}
LeetCode 139. 单词拆分
解题思路
代码实现
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordDictSet = new HashSet(wordDict);
boolean[] dp = new boolean[s.length() + 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()];
}
}
LeetCode 128. 最长连续序列
代码实现
class Solution {
public int longestConsecutive(int[] nums) {
Set<Integer> num_set = new HashSet<Integer>();
for (int num : nums) {
num_set.add(num);
}
int longestStreak = 0;
for (int num : num_set) {
if (!num_set.contains(num - 1)) {
int currentNum = num;
int currentStreak = 1;
while (num_set.contains(currentNum + 1)) {
currentNum += 1;
currentStreak += 1;
}
longestStreak = Math.max(longestStreak, currentStreak);
}
}
return longestStreak;
}
}
LeetCode 32. 最长有效括号
解题思路
情况一:
情况二:
代码实现
class Solution {
public int longestValidParentheses(String s) {
int maxLen=0;
int[] dp=new int[s.length()];
for(int i=1;i<s.length();i++){
if(s.charAt(i)==')'){
if(s.charAt(i-1)=='('){
dp[i]=(i>=2?dp[i-2]:0)+2;
}else if((i-dp[i-1])>0&&s.charAt(i-dp[i-1]-1)=='('){
dp[i]=dp[i-1]+((i-dp[i-1])>=2?dp[i-dp[i-1]-2]:0)+2;
}
maxLen=Math.max(maxLen,dp[i]);
}
}
return maxLen;
}
}
LeetCode 718. 最长重复子数组
解题思路
代码实现
class Solution {
public int findLength(int[] A, int[] B) {
int n = A.length;
int m = B.length;
int[][] dp = new int[n + 1][m + 1]; // dp[i][j]表示A的前i项与B的前j项的最长重复子数组长度
int ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (A[i - 1] == B[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
ans = Math.max(ans,dp[i][j]);
}
}
}
return ans;
}
}
LeetCode 91. 解码方法
解题思路
代码实现
class Solution {
public int numDecodings(String s) {
int n = s.length();
s = " " + s;
char[] cs = s.toCharArray();
int[] f = new int[n + 1];
f[0] = 1;
for (int i = 1; i <= n; i++) {
// a : 代表「当前位置」单独形成 item
// b : 代表「当前位置」与「前一位置」共同形成 item
int a = cs[i] - '0', b = (cs[i - 1] - '0') * 10 + (cs[i] - '0');
// 如果 a 属于有效值,那么 f[i] 可以由 f[i - 1] 转移过来
if (1 <= a && a <= 9) f[i] = f[i - 1];
// 如果 b 属于有效值,那么 f[i] 可以由 f[i - 2] 或者 f[i - 1] & f[i - 2] 转移过来
if (10 <= b && b <= 26) f[i] += f[i - 2];
}
return f[n];
}
}
背包问题
0-1背包
给你一个可装载重量为W的背包和N个物品,每个物品有重量和价值两个属性。其中第i个物品的重量为wt[i],价值为val[i],现在让你用这个背包装物品,最多能装的价值是多少?
解题思路
- 第一步要明确两点,「状态」和「选择」。状态有两个,就是「背包的容量」和「可选择的物品」。选择就是「装进背包」或者「不装进背包」嘛。
or 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
- 第二步要明确dp数组的定义。
dp[i][w]的定义如下:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值是dp[i][w]。最终答案就是dp[N][W]。base case 就是dp[0][…] = dp[…][0] = 0,因为没有物品或者背包没有空间的时候,能装的最大价值就是 0。
int dp[N+1][W+1]
dp[0][..] = 0
dp[..][0] = 0
for i in [1..N]:
for w in [1..W]:
dp[i][w] = max(
把物品 i 装进背包,
不把物品 i 装进背包
)
return dp[N][W]
- 第三步,根据「选择」,思考状态转移的逻辑。
- 不装入第i个物品:最大价值dp[i][w]应该等于dp[i-1][w]。
- 装入第i个物品,dp[i][w]应该等于dp[i-1][w-wt[i-1]] + val[i-1]。
(由于i是从 1 开始的,所以对val和wt的取值是i-1)
for i in [1..N]:
for w in [1..W]:
dp[i][w] = max(
dp[i-1][w],
dp[i-1][w - wt[i-1]] + val[i-1]
)
return dp[N][W]
代码实现
int knapsack(int W, int N, vector<int>& wt, vector<int>& val) {
// vector 全填入 0,base case 已初始化
vector<vector<int>> dp(N + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= N; i++) {
for (int w = 1; w <= W; w++) {
if (w - wt[i-1] < 0) {
// 当前背包容量装不下,只能选择不装入背包
dp[i][w] = dp[i - 1][w];
} else {
// 装入或者不装入背包,择优
dp[i][w] = max(dp[i - 1][w - wt[i-1]] + val[i-1],
dp[i - 1][w]);
}
}
}
return dp[N][W];
}
LeetCode 416. 分割等和子集(0-1背包变体)
解题思路
可以先对集合求和,得出sum,把问题转化为背包问题:
给一个可装载重量为sum/2的背包和N个物品,每个物品的重量为nums[i]。现在让你装物品,是否存在一种装法,能够恰好将背包装满?
-
第一步要明确两点,「状态」和「选择」。状态就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。
-
第二步要明确dp数组的定义。dp[i][j] = x表示,对于前i个物品,当前背包的容量为j时,若x为true,则说明可以恰好将背包装满,若x为false,则说明不能恰好将背包装满。
最终答案dp[N][sum/2],base case 就是dp[…][0] = true和dp[0][…] = false,因为背包没有空间的时候,就相当于装满了,而当没有物品可选择的时候,肯定没办法装满背包。 -
第三步,根据「选择」,思考状态转移的逻辑。
-
如果不把nums[i]算入子集,或者说你不把这第i个物品装入背包,那么是否能够恰好装满背包,取决于上一个状态dp[i-1][j],继承之前的结果。
-
如果把nums[i]算入子集,或者说你把这第i个物品装入了背包,那么是否能够恰好装满背包,取决于状态dp[i - 1][j-nums[i-1]]。
-
代码实现
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
int sum = 0;
for(int c:nums){
sum+=c;
}
if(sum%2 != 0){
return false;
}
sum = sum/2;
boolean[][] dp = new boolean[n+1][sum+1];
for(int i = 1; i <= n;i++){
dp[i][0] = true;
}
for(int i = 1;i <= n;i++){
for(int j = 1;j <= sum;j++){
if(j-nums[i-1]<0){
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = dp[i-1][j] | dp[i-1][j-nums[i-1]];
}
}
}
return dp[n][sum];
}
}
LeetCode 322. 零钱兑换
代码实现
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];
}
}
LeetCode 518. 零钱兑换 II(完全背包)
解题思路
把这个问题转化为背包问题的描述形式:
有一个背包,最大容量为amount,有一系列物品coins,每个物品的重量为coins[i],每个物品的数量无限。请问有多少种方法,能够把背包恰好装满?每个物品的数量是无限的,这也就是传说中的「完全背包问题」,没啥高大上的,无非就是状态转移方程有一点变化而已。
- 第一步要明确两点,「状态」和「选择」。就是「背包的容量」和「可选择的物品」,选择就是「装进背包」或者「不装进背包」。
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 计算(选择1,选择2...)
- 第二步要明确dp数组的定义。base case 为dp[0][…] = 0, dp[…][0] = 1。因为如果不使用任何硬币面值,就无法凑出任何金额;如果凑出的目标金额为 0,那么“无为而治”就是唯一的一种凑法。最终想得到的答案就是dp[N][amount],其中N为coins数组的大小。
int dp[N+1][amount+1]
dp[0][..] = 0
dp[..][0] = 1
for i in [1..N]:
for j in [1..amount]:
把物品 i 装进背包,
不把物品 i 装进背包
return dp[N][amount]
- 第三步,根据「选择」,思考状态转移的逻辑。
- 不把这第i个物品装入背包,也就是说你不使用coins[i]这个面值的硬币,那么凑出面额j的方法数dp[i][j]应该等于dp[i-1][j],继承之前的结果。
- 把这第i个物品装入了背包,也就是说你使用coins[i]这个面值的硬币,那么dp[i][j]应该等于dp[i][j-coins[i-1]]。(0-1背包与完全背包此处i的状态有不同)
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= amount; j++) {
if (j - coins[i-1] >= 0)
dp[i][j] = dp[i - 1][j]
+ dp[i][j-coins[i-1]];
return dp[N][W]
代码实现
class Solution {
public int change(int amount, int[] coins) {
int n = coins.length;
int[][] dp = new int[n+1][amount+1];
for(int i = 1;i <= n;i++){
dp[i][0] = 1;
}
for(int i = 1;i <= n;i++){
for(int j = 1; j<= amount;j++){
if(j - coins[i-1] < 0){
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = dp[i-1][j]+dp[i][j-coins[i-1]];
}
}
}
return dp[n][amount];
}
}
class Solution {
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
}
买卖股票问题
具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。听起来抽象,你只要记住「状态」和「选择」两个词就行,下面实操一下就很容易明白了。
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
dp[i][k][0 or 1]
0 <= i <= n - 1, 1 <= k <= K
n 为天数,大 K 为交易数的上限,0 和 1 代表是否持有股票。
此问题共 n × K × 2 种状态,全部穷举就能搞定。
for 0 <= i < n:
for 1 <= k <= K:
for s in {0, 1}:
dp[i][k][s] = max(buy, sell, rest)
想求的最终答案是dp[n - 1][K][0],即最后一天,最多允许K次交易,最多获得多少利润。
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 今天选择 rest, 今天选择 sell )
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 今天选择 rest, 今天选择 buy )
这个解释应该很清楚了,如果buy,就要从利润中减去prices[i],如果sell,就要给利润增加prices[i]。今天的最大利润就是这两种可能选择中较大的那个。
注意k的限制,在选择buy的时候相当于开启了一次交易,那么对于昨天来说,交易次数的上限k应该减小 1。
(不能认为在sell的时候给k减小 1 和在buy的时候给k减小 1 是等效的,因为交易是从buy开始,如果buy的选择不改变交易次数k的约束,会出现交易次数超出限制的的错误。)
LeetCode 121. 买卖股票的最佳时机
解题思路
dp[i][1][0] = max(dp[i-1][1][0], dp[i-1][1][1] + prices[i])
dp[i][1][1] = max(dp[i-1][1][1], dp[i-1][0][0] - prices[i])
= max(dp[i-1][1][1], -prices[i])
解释:k = 0 的 base case,所以 dp[i-1][0][0] = 0。
现在发现 k 都是 1,不会改变,即 k 对状态转移已经没有影响了。
可以进行进一步化简去掉所有 k:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])
代码实现
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
for(int i = 0;i < n;i++){
if(i-1 == -1){
dp[i][0] = 0;
dp[i][1] = -prices[i];
continue;
}
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[n-1][0];
}
}
LeetCode 122. 买卖股票的最佳时机 II
解题思路
如果k为正无穷,那么就可以认为k和k - 1是一样的:
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])
= max(dp[i-1][k][1], dp[i-1][k][0] - prices[i])
我们发现数组中的 k 已经不会改变了,也就是说不需要记录 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])
代码实现
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
for(int i = 0; i < n;i++){
if(i-1 == -1){
dp[i][0] = 0;
dp[i][1] = -prices[i];
continue;
}
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];
}
}
LeetCode 123. 买卖股票的最佳时机 III
解题思路
k = 2时,由于没有消掉k的影响,所以必须要对k进行穷举:
为什么从大到小遍历k也可以正确提交呢?
因为你注意看,dp[i][k]不会依赖dp[i][k - 1],而是依赖dp[i - 1][k - 1],对于dp[i - 1][…],都是已经计算出来的。所以不管你是k = max_k, k–,还是k = 1, k++,都是可以得出正确答案的。
那为什么我使用k = max_k, k–的方式呢?因为这样符合语义。
你买股票,初始的「状态」是什么?应该是从第 0 天开始,而且还没有进行过买卖,所以最大交易次数限制k应该是max_k;而随着「状态」的推移,你会进行交易,那么交易次数上限k应该不断减少,这样一想,k = max_k, k–的方式是比较合乎实际场景的。
代码实现
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int max_k = 2;
int[][][] dp = new int[n][max_k+1][2];
for(int i = 0;i < n;i++){
for(int k = max_k;k >= 1;k--){
if(i-1 == -1){
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[n-1][max_k][0];
}
}
LeetCode 188. 买卖股票的最佳时机 IV
解题思路
一次交易由买入和卖出构成,至少需要两天。所以说有效的限制k应该不超过n/2,如果超过,就没有约束作用了,相当于k = +infinity。这种情况是之前解决过的。
代码实现
class Solution {
public int maxProfit(int k, int[] prices) {
int n = prices.length;
if(k > n/2){
return maxProfitInfK(prices);
}
int[][][] dp = new int[n][k+1][2];
for(int i = 0;i < n;i++){
for(int j = k;j >= 1;j--){
if(i-1 == -1){
dp[i][j][0] = 0;
dp[i][j][1] = -prices[i];
continue;
}
dp[i][j][0] = Math.max(dp[i-1][j][0], dp[i-1][j][1]+prices[i]);
dp[i][j][1] = Math.max(dp[i-1][j][1], dp[i-1][j-1][0]-prices[i]);
}
}
return dp[n-1][k][0];
}
public int maxProfitInfK(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
for(int i = 0;i < n;i++){
if(i-1 == -1){
dp[i][0] = 0;
dp[i][1] = -prices[i];
continue;
}
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];
}
}
LeetCode 309. 最佳买卖股票时机含冷冻期
解题思路
每次sell之后要等一天才能继续交易:
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])
解释:第 i 天选择 buy 的时候,要从 i-2 的状态转移,而不是 i-1 。
代码实现
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][2];
for(int i = 0; i < n;i++){
if(i-1 == -1){
dp[i][0] = 0;
dp[i][1] = -prices[i];
continue;
}
if(i-2 == -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]);
continue;
}
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-2][0]-prices[i]);
}
return dp[n-1][0];
}
}
LeetCode 714. 买卖股票的最佳时机含手续费
解题思路
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] - fee)
解释:相当于买入股票的价格升高了。
在第一个式子里减也是一样的,相当于卖出股票的价格减小了。
代码实现
class Solution {
public int maxProfit(int[] prices, int fee) {
int n = prices.length;
int[][] dp = new int[n][2];
for(int i = 0; i < n;i++){
if(i-1 == -1){
dp[i][0] = 0;
dp[i][1] = -prices[i]-fee;
continue;
}
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]-fee);
}
return dp[n-1][0];
}
}
打家劫舍系列
LeetCode 198. 打家劫舍
解题思路
解决动态规划问题就是找「状态」和「选择」,仅此而已。
假想你就是这个专业强盗,从左到右走过这一排房子,在每间房子前都有两种选择:抢或者不抢。
- 如果你抢了这间房子,那么你肯定不能抢相邻的下一间房子了,只能从下下间房子开始做选择。
- 如果你不抢这间房子,那么你可以走到下一间房子前,继续做选择。
- 当你走过了最后一间房子后,你就没得抢了,能抢到的钱显然是 0(base case)。
代码实现
class Solution {
public int rob(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
if(n == 1){
return nums[0];
}
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];
}
}
LeetCode 213. 打家劫舍 II
解题思路
这些房子不是一排,而是围成了一个圈。也就是说,现在第一间房子和最后一间房子也相当于是相邻的,不能同时抢。比如说输入数组nums=[2,3,2],算法返回的结果应该是 3 而不是 4,因为开头和结尾不能同时被抢。
只要比较情况二和情况三就行了,因为这两种情况对于房子的选择余地比情况一大呀,房子里的钱数都是非负数,所以选择余地大,最优决策结果肯定不会小。
代码实现
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[] dp1 = new int[n];
int[] dp2 = new int[n];
dp1[0] = nums[0];
dp1[1] = Math.max(nums[0], nums[1]);
for(int i = 2;i < n-1;i++){
dp1[i] = Math.max(dp1[i-1], dp1[i-2]+nums[i]);
}
dp2[1] = nums[1];
dp2[2] = Math.max(nums[1], nums[2]);
for(int j = 3;j < n;j++){
dp2[j] = Math.max(dp2[j-1], dp2[j-2]+nums[j]);
}
return Math.max(dp1[n-2], dp2[n-1]);
}
}
LeetCode 337. 打家劫舍 III
解题思路
动态规划本质思路都是在根据找“状态”,再“选择”,本题也是如此,和数组类似,二叉树有多个节点,每个节点都可以做出选择是否选择“盗窃”,再根据不能盗窃相邻节点的要求,去比对当前节点 偷 与 不偷 的最大值。
代码实现
class Solution {
Map<TreeNode, Integer> memo = new HashMap<>();
public int rob(TreeNode root) {
if(root == null){
return 0;
}
if(memo.containsKey(root)){
return memo.get(root);
}
int do_it = root.val+(root.left != null? rob(root.left.left)+rob(root.left.right):0)+(root.right != null? rob(root.right.left)+rob(root.right.right):0);
int not_do = rob(root.left)+rob(root.right);
int res = Math.max(do_it, not_do);
memo.put(root, res);
return res;
}
}
博弈问题
LeetCode 877. 石子游戏
解题思路
博弈类问题的套路都差不多,其核心思路是在二维 dp 的基础上使用元组分别存储两个人的博弈结果。
把「石头游戏」改的更具有一般性:
你和你的朋友面前有一排石头堆,用一个数组piles表示,piles[i]表示第i堆石子有多少个。你们轮流拿石头,一次拿一堆,但是只能拿走最左边或者最右边的石头堆。所有石头被拿完后,谁拥有的石头多,谁获胜。
对 dp 数组含义的解释:
- dp[i][j].fir = x表示,对于piles[i…j]这部分石头堆,先手能获得的最高分数为x。
- dp[i][j].sec = y表示,对于piles[i…j]这部分石头堆,后手能获得的最高分数为y。
dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec)
dp[i][j].fir = max( 选择最左边的石头堆 , 选择最右边的石头堆 )
# 解释:我作为先手,面对 piles[i...j] 时,有两种选择:
# 要么我选择最左边的那一堆石头,然后面对 piles[i+1...j]
# 但是此时轮到对方,相当于我变成了后手;
# 要么我选择最右边的那一堆石头,然后面对 piles[i...j-1]
# 但是此时轮到对方,相当于我变成了后手。
if 先手选择左边:
dp[i][j].sec = dp[i+1][j].fir
if 先手选择右边:
dp[i][j].sec = dp[i][j-1].fir
# 解释:我作为后手,要等先手先选择,有两种情况:
# 如果先手选择了最左边那堆,给我剩下了 piles[i+1...j]
# 此时轮到我,我变成了先手;
# 如果先手选择了最右边那堆,给我剩下了 piles[i...j-1]
# 此时轮到我,我变成了先手。
根据 dp 数组的定义,我们也可以找出 base case,也就是最简单的情况:
dp[i][j].fir = piles[i]
dp[i][j].sec = 0
其中 0 <= i == j < n
# 解释:i 和 j 相等就是说面前只有一堆石头 piles[i]
# 那么显然先手的得分为 piles[i]
# 后手没有石头拿了,得分为 0
代码实现
class Solution {
public boolean stoneGame(int[] piles) {
int n = piles.length;
Pair[][] dp = new Pair[n][n];
for(int i = 0;i < n;i++){
for(int j = 0;j < n;j++){
dp[i][j] = new Pair(0,0);
if(i == j){
dp[i][j].fir = piles[i];
dp[i][j].sec = 0;
}
}
}
for(int i = n-2;i >=0;i--){
for(int j = i+1;j < n;j++){
int left = piles[i]+dp[i+1][j].sec;
int right = piles[j]+dp[i][j-1].sec;
if(left > right){
dp[i][j].fir = left;
dp[i][j].sec = dp[i+1][j].fir;
}else{
dp[i][j].fir = right;
dp[i][j].sec = dp[i][j-1].fir;
}
}
}
Pair res = dp[0][n-1];
return res.fir>res.sec;
}
}
class Pair{
int fir,sec;
Pair(int fir, int sec){
this.fir = fir;
this.sec = sec;
}
}
路径和
LeetCode 64. 最小路径和
解题思路
dp函数的定义如下:
从左上角位置(0, 0)走到位置(i, j)的最小路径和为dp(grid, i, j)。
dp[i][j]依然取决于dp[i-1][j]和dp[i][j-1]
代码实现
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length, n = grid[0].length;
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
for(int i = 1;i < m;i++){
dp[i][0] = dp[i-1][0]+grid[i][0];
}
for(int j = 1;j < n;j++){
dp[0][j] = dp[0][j-1]+grid[0][j];
}
for(int i = 1;i < m;i++){
for(int j = 1;j < n;j++){
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1])+grid[i][j];
}
}
return dp[m-1][n-1];
}
}
LeetCode 63. 不同路径 II
解题思路
注意障碍物的情况,dp值为0
代码实现
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m=obstacleGrid.length;
int n=obstacleGrid[0].length;
int[][] dp=new int[m][n];
if (obstacleGrid[0][0]!=1){
dp[0][0]=1;
}
for (int i=1;i<m;i++){
dp[i][0]=obstacleGrid[i][0]==1?0:dp[i-1][0];
}
for (int j=1;j<n;j++){
dp[0][j]=obstacleGrid[0][j]==1?0:dp[0][j-1];
}
for (int i=1;i<m;i++){
for (int j=1;j<n;j++){
dp[i][j]=obstacleGrid[i][j]==1?0:(dp[i-1][j]+dp[i][j-1]);
}
}
return dp[m-1][n-1];
}
}
DP玩游戏
LeetCode 174. 地下城游戏
解题思路
dp函数的定义:
从grid[i][j]到达终点(右下角)所需的最少生命值是dp(grid, i, j)。
base case,想求dp(0, 0),那就应该试图通过dp(i, j+1)和dp(i+1, j)推导出dp(i, j),这样才能不断逼近 base case,正确进行状态转移。
代码实现
class Solution {
int[][] memo;
public int calculateMinimumHP(int[][] dungeon) {
int m = dungeon.length, n = dungeon[0].length;
memo = new int[m][n];
for(int[] arr:memo){
Arrays.fill(arr, -1);
}
return dp(dungeon, 0, 0);
}
int dp(int[][] dungeon, int i, int j){
int m = dungeon.length, n = dungeon[0].length;
if(i == m-1 && j == n-1){
return dungeon[i][j] >= 0? 1:-dungeon[i][j]+1;
}
if(i == m || j == n){
return Integer.MAX_VALUE;
}
if(memo[i][j] != -1){
return memo[i][j];
}
int res = Math.min(dp(dungeon, i, j+1), dp(dungeon, i+1, j))-dungeon[i][j];
memo[i][j] = (res <= 0 ?1:res);
return memo[i][j];
}
}
LeetCode 514. 自由之路
解题思路
dp函数的定义如下:
当圆盘指针指向ring[i]时,输入字符串key[j…]至少需要dp(ring, i, key, j)次操作。
根据这个定义,题目其实就是想计算dp(ring, 0, key, 0)的值
int dp(string& ring, int i, string& key, int j) {
// base case 完成输入
if (j == key.size()) return 0;
// 做选择
int res = INT_MAX;
for (int k : [字符 key[j] 在 ring 中的所有索引]) {
res = min(
把 i 顺时针转到 k 的代价,
把 i 逆时针转到 k 的代价
);
}
return res;
}
// 字符 -> 索引列表
unordered_map<char, vector<int>> charToIndex;
// 备忘录
vector<vector<int>> memo;
/* 主函数 */
int findRotateSteps(string ring, string key) {
int m = ring.size();
int n = key.size();
// 备忘录全部初始化为 0
memo.resize(m, vector<int>(n, 0));
// 记录圆环上字符到索引的映射
for (int i = 0; i < ring.size(); i++) {
charToIndex[ring[i]].push_back(i);
}
// 圆盘指针最初指向 12 点钟方向,
// 从第一个字符开始输入 key
return dp(ring, 0, key, 0);
}
// 计算圆盘指针在 ring[i],输入 key[j..] 的最少操作数
int dp(string& ring, int i, string& key, int j) {
// base case 完成输入
if (j == key.size()) return 0;
// 查找备忘录,避免重叠子问题
if (memo[i][j] != 0) return memo[i][j];
int n = ring.size();
// 做选择
int res = INT_MAX;
// ring 上可能有多个字符 key[j]
for (int k : charToIndex[key[j]]) {
// 拨动指针的次数
int delta = abs(k - i);
// 选择顺时针还是逆时针
delta = min(delta, n - delta);
// 将指针拨到 ring[k],继续输入 key[j+1..]
int subProblem = dp(ring, k, key, j + 1);
// 选择「整体」操作次数最少的
res = min(res, 1 + delta + subProblem);
// PS:加一是因为按动按钮也是一次操作
}
// 将结果存入备忘录
memo[i][j] = res;
return res;
}
代码实现
class Solution {
Map<Character, List<Integer>> charIndex;
int[][] memo;
public int findRotateSteps(String ring, String key) {
int m = ring.length();
int n = key.length();
memo = new int[m][n];
charIndex = new HashMap<>();
for(int i = 0;i < ring.length();i++){
char ch = ring.charAt(i);
List<Integer> list;
if(charIndex.containsKey(ch)){
list = charIndex.get(ch);
}else {
list = new ArrayList<>();
}
list.add(i);
charIndex.put(ch, list);
}
return dp(ring, 0, key, 0);
}
public int dp(String ring, int i, String key, int j){
if(j == key.length()){
return 0;
}
if(memo[i][j] != 0){
return memo[i][j];
}
int res = Integer.MAX_VALUE;
for(int k : charIndex.get(key.charAt(j))){
int delta = Math.abs(k - i);
delta = Math.min(delta, ring.length() - delta);
int subNum = dp(ring, k, key, j+1);
res = Math.min(res, delta+subNum+1);
}
memo[i][j] = res;
return res;
}
}
旅行
LeetCode 787. K 站中转内最便宜的航班
解题思路
让你求src到dst权重最小的一条路径,同时要满足,这条路径最多不能超过K + 1条边(经过K个节点相当于经过K + 1条边。
从起点src出发,k步之内(一步就是一条边)到达节点s的最小路径权重为dp(s, k)。
s1, s2是指向dst的相邻节点,我只要能够在K - 1步之内从src到达s1, s2,那我就可以在K步之内从src到达dst。
也就是如下关系式:
dp(dst, k) = min(
dp(s1, k - 1) + w1,
dp(s2, k - 1) + w2
)
代码实现
class Solution {
Map<Integer, List<int[]>> indegree;
int src,dst;
int[][] memo;
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
k++;
this.src = src;
this.dst = dst;
indegree = new HashMap<>();
memo = new int[n][k+1];
for(int[] f:flights){
int from = f[0];
int to = f[1];
int price = f[2];
indegree.putIfAbsent(to, new ArrayList<>());
indegree.get(to).add(new int[]{from, price});
}
return dp(dst, k);
}
public int dp(int s, int k){
if(src == s){
return 0;
}
if(k == 0){
return -1;
}
if(memo[s][k] != 0){
return memo[s][k];
}
int res = Integer.MAX_VALUE;
if(indegree.containsKey(s)){
for(int[] f:indegree.get(s)){
int from = f[0];
int price = f[1];
int sub = dp(from, k-1);
if(sub != -1){
res = Math.min(res, sub+price);
}
}
}
res = (res == Integer.MAX_VALUE ? -1:res);
memo[s][k] = res;
return res;
}
}
总结
本题来源于Leetcode中 归属于动态规划类型题目。
同许多在算法道路上不断前行的人一样,不断练习,修炼自己!
如有博客中存在的疑问或者建议,可以在下方留言一起交流,感谢各位!
觉得本博客有用的客官,可以给个点赞+收藏哦! 嘿嘿
喜欢本系列博客的可以关注下,以后除了会继续更新面试手撕代码文章外,还会出其他系列的文章!