目录
121 买卖股票的最佳时机
打家劫舍
62 不同路径
64 最小路径和
53 最大子数组和 (动归 普通数组部分)
152 乘积最大子数组
300 最长递增子序列
1143 最长公共子序列
72 编辑距离
121 买卖股票的最佳时机
给定一个数组 prices
,它的第 i
个元素 prices[i]
表示一支给定股票第 i
天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
class Solution {
public int maxProfit(int[] prices) {
int len = prices.length;
// dp[i][0]代表第i天持有股票的最大收益
// dp[i][1]代表第i天不持有股票的最大收益
int[][] dp = new int[len][2];
//第一天持有股票,只能是第一天购入,收益为-prices
dp[0][0] = -prices[0];
//第一天不持有股票,既没有购买,收益为0
dp[0][1] = 0;
for (int i = 1; i < len; i++) {
//第i天持有,可能是前一天购入,或当天购入
dp[i][0] = Math.max(dp[i - 1][0], -prices[i]);
//第i天不持有,可能是前一天持有,今天售出或前一天也不持有(保持现状)
dp[i][1] = Math.max(dp[i - 1][0] + prices[i], dp[i - 1][1]);
}
//本题中不持有股票状态所得金钱一定比持有股票状态得到的多!
return dp[len - 1][1];
}
}
打家劫舍
如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
class Solution {
public int rob(int[] nums) {
// 考虑下标 i(包括 i)以内的房屋,最多可以偷窃的金额为 dp[i]。
// 如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] 因为求的是能偷的最高金额,因此尽量不走空
// 如果不偷第i房间,那么dp[i] = dp[i - 1] (只是考虑偷i-1不一定真的偷)
// 然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
// dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值
if (nums.length == 1) return nums[0];
int[] dp = new int[nums.length];
dp[0] = nums[0];
dp[1] = Math.max(dp[0], nums[1]);
for (int i = 2; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[nums.length - 1]; //数组下标从0开始
}
}
62 不同路径
一个机器人位于一个 m x n网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
class Solution {
public int uniquePaths(int m, int n) {
// dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
// dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
// dp[i][0],dp[0,j]一定都是1
int[][] dp = new int[m][n];
// 初始化
for (int i = 0; i < m; i++) {
dp[i][0] = 1;
}
for (int i = 0; i < n; i++) {
dp[0][i] = 1;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
}
64 最小路径和
给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
不使用备忘录计数,更好理解
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[][] dp = new int[m][n];
dp[0][0] = grid[0][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++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 填充剩余格子
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];
}
}
53 最大子数组和 (动归 普通数组部分)
子数组是数组中的一个连续部分。
动规解法
class Solution {
public int maxSubArray(int[] nums) {
int n = nums.length;
if (n == 0) return 0;
int[] dp = new int[n];
// base case
// 第一个元素前面没有子数组
dp[0] = nums[0];
// 状态转移方程
for (int i = 1; i < n; i++) {
dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);
}
// 得到 nums 的最大子数组
int res = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
res = Math.max(res, dp[i]);
}
return res;
}
}
152 乘积最大子数组
dp 数组是:dp[i] 记录以 nums[i] 为结尾的「最大子数组和」,从而写出状态转移方程。
这道题和 上一题的最大区别在于,要同时维护「以 nums[i] 结尾的最大子数组」和「以 nums[i] 结尾的最小子数组」,以便适配 nums[i] 可能为负的情况。
class Solution {
public int maxProduct(int[] nums) {
int n = nums.length;
// 定义:以 nums[i] 结尾的子数组,乘积最小为 dp1[i]
int[] dp1 = new int[n];
// 定义:以 nums[i] 结尾的子数组,乘积最大为 dp2[i]
int[] dp2 = new int[n];
// base case
dp1[0] = nums[0];
dp2[0] = nums[0];
// 状态转移方程 需要和 nums[i]比较 以nums[i]为结尾的数组
//单独的 nums[i] 可能作为一个新的子数组。
for (int i = 1; i < n; i++) {
dp1[i] = min(dp1[i - 1] * nums[i], dp2[i - 1] * nums[i], nums[i]);
dp2[i] = max(dp1[i - 1] * nums[i], dp2[i - 1] * nums[i], nums[i]);
}
// 遍历所有子数组的最大乘积,求最大值
int res = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
res = Math.max(res, dp2[i]);
}
return res;
}
int min(int a, int b, int c) {
return Math.min(Math.min(a, b), c);
}
int max(int a, int b, int c) {
return Math.max(Math.max(a, b), c);
}
}
300 最长递增子序列
子数组一定是连续的,而子序列不一定是连续的。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
dp 数组的定义:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。
那么 dp 数组中最大的那个值就是最长的递增子序列长度。
子数组一定是连续的,而子序列不一定是连续的。
class Solution {
public int lengthOfLIS(int[] nums) {
// dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度
int[] dp = new int[nums.length];
// base case:dp 数组全都初始化为 1
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;
}
}
1143 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
- dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]
- 两种情况:text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同
-
- 如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以 dp[i][j] = dp[i - 1][j - 1] + 1;
- 如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
- dp 数组的第0行和第0列用于表示空字符串的情况
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
//dp 数组的第0行和第0列用于表示空字符串的情况
int[][] dp = new int[text1.length() + 1][text2.length() + 1];
for (int i = 1; i <= text1.length(); i++) {
for (int j = 1; j <= text2.length(); j++) {
char char1 = text1.charAt(i - 1);
//当 dp 数组的索引为 i 和 j 时,我们实际上是在处理 text1 的第 i-1 个字符和 text2 的第 j-1 个字符
char char2 = text2.charAt(j - 1);
if (char1 == char2) {
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[text1.length()][text2.length()];
}
}
72 编辑距离
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]。
- if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1];
- if (word1[i - 1] != word2[j - 1]),此时就需要编辑了,如何编辑呢?
- 操作一:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。
即 dp[i][j] = dp[i - 1][j] + 1;
- 操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。
即 dp[i][j] = dp[i][j - 1] + 1;
- 操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增删加元素。
可以回顾一下,if (word1[i - 1] == word2[j - 1])的时候我们的操作 是 dp[i][j] = dp[i - 1][j - 1] 对吧。
那么只需要一次替换的操作,就可以让 word1[i - 1] 和 word2[j - 1] 相同。
所以 dp[i][j] = dp[i - 1][j - 1] + 1;
class Solution {
public int minDistance(String s1, String s2) {
int m = s1.length(), n = s2.length();
int[][] dp = new int[m + 1][n + 1];
// base case 有一个里面没元素
for (int i = 1; i <= m; i++) //对word1里的元素全部做删除操作
dp[i][0] = i;
for (int j = 1; j <= n; j++)
dp[0][j] = j;
// 自底向上求解
for (int i = 1; i <= m; i++)
for (int j = 1; j <= n; j++)
// 因为dp数组有效位从1开始
// 所以当前遍历到的字符串的位置为i-1 | j-1
if (s1.charAt(i - 1) == s2.charAt(j - 1))
dp[i][j] = dp[i - 1][j - 1];
else
dp[i][j] = min(
dp[i - 1][j] + 1,//word1删除
dp[i][j - 1] + 1,//word2删除
dp[i - 1][j - 1] + 1//替换
);
// 储存着整个 s1 和 s2 的最小编辑距离
return dp[m][n];
}
int min(int a, int b, int c) {
return Math.min(a, Math.min(b, c));
}
}