文章目录
让我们通过下面的两个算法来深刻的理解一下动态规划和贪心算法吧。
最短路径问题
从上到下找到最短路径〈n个数字之和最小, n为矩阵的行数),可以从第一行中的任何元素开始,只能往下层走,同时只能走向相邻的节点,找到从第一行到最后一行的最短路径。
例如下面的示例一:
第一排⒉只能走向第二排的 7、3,
第二排的7可以走向,第三排的6、2、9
示例
示例一:
Input:
[5,8,1,2],
[4,1,7,3],
[3,6,2,9]
输出
4
示例二:
思路与步骤
对于这种最短路径问题,我们大都可以使用动态规划的思想来实现:
- 初始化:
创建一个与输入矩阵相同大小的动态规划(DP)矩阵 dp,用来存储到达每个元素的最小路径和。
将第一行的元素直接复制到 dp 矩阵的第一行,因为从第一行开始的路径和就是它们本身。
- 动态规划填充:
从第二行开始,遍历每个元素 matrix[i][j]。
对于每个元素,计算到达这个元素的最小路径和:
正上方:dp[i-1][j]
左上方(如果存在):dp[i-1][j-1](当 j > 0 时)
右上方(如果存在):dp[i-1][j+1](当 j < cols - 1 时,cols 是每行的元素个数)
取这三个值中的最小值,并加上当前元素的值,更新 dp[i][j]。
结果计算:
- 遍历 dp 矩阵
遍历dp矩阵最后一行,找到其中的最小值,这个值就是从第一行到最后一行的最短路径和。
下面我们按照这个思路对示例一进行下操作
[5,8,1,2],
[4,1,7,3],
[3,6,2,9]
第一行的 dp 矩阵为 [5, 8, 1, 2]。
第二行的计算:
dp[1][0] = 4 + min(5, 8) = 4 + 5 = 9
dp[1][1] = 1 + min(5, 8, 1) = 1 + 1 = 2
dp[1][2] = 7 + min(1, 8, 2) = 7 + 1 = 8
dp[1][3] = 3 + min(2, 1) = 3 + 1 = 4
第三行的计算依此类推,最后得到 dp 矩阵。
[5,8,1,2],
[9,2,8,4],
[5,8,4,13]
非常简单,最后得到结果为4。
复杂度分析
- 时间复杂度
该算法的时间复杂度为 𝑂(𝑚×𝑛),其中 𝑚是矩阵的行数,𝑛是列数。每个元素都只被计算一次。
- 空间复杂度
空间复杂度为 𝑂(𝑚×𝑛),用于存储 DP 矩阵。如果只需要当前和上一行的值,可以优化为 𝑂(𝑛)。这个思路利用了动态规划的思想,通过逐步构建解决方案,从而有效地找到最优路径。
Java代码实现
public class MinPathSum {
public static void main(String[] args) {
int[][] matrix = {
{5, 8, 1, 2},
{4, 1, 7, 3},
{3, 6, 2, 9}
};
int result = minPathSum(matrix);
System.out.println("最短路径和: " + result);
}
public static int minPathSum(int[][] matrix) {
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return 0;
}
int rows = matrix.length;
int cols = matrix[0].length;
// 创建一个与输入矩阵相同大小的 dp 矩阵
int[][] dp = new int[rows][cols];
// 初始化第一行
for (int j = 0; j < cols; j++) {
dp[0][j] = matrix[0][j];
}
// 填充 dp 矩阵
for (int i = 1; i < rows; i++) {
for (int j = 0; j < cols; j++) {
// 取上一行的相邻元素的最小路径和
int minPath = dp[i - 1][j]; // 正上方
if (j > 0) {
minPath = Math.min(minPath, dp[i - 1][j - 1]); // 左上方
}
if (j < cols - 1) {
minPath = Math.min(minPath, dp[i - 1][j + 1]); // 右上方
}
dp[i][j] = matrix[i][j] + minPath;
}
}
// 返回最后一行的最小值
int minSum = dp[rows - 1][0];
for (int j = 1; j < cols; j++) {
minSum = Math.min(minSum, dp[rows - 1][j]);
}
return minSum;
}
}
代码说明:
- 输入矩阵:
在 main 方法中定义了一个示例矩阵。
- 动态规划:
minPathSum 方法实现了动态规划的逻辑,计算到达每个元素的最小路径和。
- 输出结果:
程序运行后将输出从上到下的最短路径和。
购买股票的最佳时机的三个问题
题目描述(一)
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。
设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
示例说明
示例 1:
输入: prices = [7, 1, 5, 3, 6, 4]
输出: 最大利润: 5(在第2天以1买入,第5天以6卖出,利润为6-1=5)
示例 2:
输入: prices = [7, 6, 4, 3, 1]
输出: 最大利润: 0(因为价格不断下降,无法获利)
思路
求最优解的问题,大多可以使用动态规划和贪心算法来解决,这道题目使用了贪心算法,主要是因为我们需要在每一天的价格中找出最低的买入价格,并在之后的日子中找出最高的卖出价格,从而获得最大利润。
1. 初始化变量:
- min_price: 用于记录迄今为止的最低买入价格,初始值设为一个很大的数。
- max_profit: 用于记录最大利润,初始值设为0。
2.遍历价格数组:
- 对于每一天的价格:
- 如果当前价格小于 min_price,更新 min_price。
- 计算当前价格卖出时的利润,即 当前价格 - min_price。
- 如果这个利润大于 max_profit,则更新 max_profit。
3. 返回结果:
最后返回 max_profit就是最大利润,如果没有利润,则返回初始化时候的0。
为什么可以使用贪心算法
- 局部最优解:
在每一步中,我们选择当前最低的买入价格和当前价格之间的利润,这样的选择保证了在每一步都可以获取最大的利润。
- 全局最优解:
通过遍历整个数组,最终得到的 max_profit 是全局最优解,因为我们考虑了所有可能的买入和卖出组合,计算了每天会获得的最大利润(局部最优解),而max_profit 是每天的最大利润中最大的那个(所有局部最优解中的最优选择),自然也就是最终结果。
Java代码实现
求解方法
public class StockProfit {
public static int maxProfit(int[] prices) {
int minPrice = Integer.MAX_VALUE; // 初始化为最大整数
int maxProfit = 0; // 初始化最大利润为0
for (int price : prices) {
if (price < minPrice) {
minPrice = price; // 更新最低价格
} else if (price - minPrice > maxProfit) {
maxProfit = price - minPrice; // 更新最大利润
}
}
return maxProfit;
}
方法调用
public static void main(String[] args) {
// 示例1
int[] prices1 = {7, 1, 5, 3, 6, 4};
System.out.println("最大利润: " + maxProfit(prices1)); // 输出: 5
// 示例2
int[] prices2 = {7, 6, 4, 3, 1};
System.out.println("最大利润: " + maxProfit(prices2)); // 输出: 0
}
}
题目描述(二)
给你一个整数数组 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。
最大总利润为 4 + 3 = 7 。
示例 2
输入: prices = [1, 2, 3, 4, 5]
输出: 最大利润 = 4
示例 3
输入: prices = [7, 6, 4, 3, 1]
输出: 最大利润 = 0
问题思路
1.状态定义:
- 我们使用动态规划来维护两个状态:
- dp[i][0]: 第 i 天结束时不持有股票的最大利润。
- dp[i][1]: 第 i 天结束时持有股票的最大利润。
2.初始状态:
- 在第 0 天:
- dp[0][0] = 0: 不持有股票,利润为 0。
- dp[0][1] = -prices[0]: 持有股票,利润为负的股票价格(即购买股票的成本)。
3.状态转移:
- 对于每一天 i(从 1 到 n-1),我们可以进行如下状态转移:
- 不持有股票:
- 选择不持有股票,保持之前的状态:dp[i][0] = dp[i-1][0]
- 选择在第 i 天出售股票,增加今天的价格:dp[i][0] = max(dp[i][0], dp[i-1][1] + prices[i])
- 持有股票:
- 选择持有股票,保持之前的状态:dp[i][1] = dp[i-1][1]
- 选择在第 i 天购买股票,减少今天的价格:dp[i][1] = max(dp[i][1], dp[i-1][0] - prices[i])
- 不持有股票:
状态转移方程
综上所述,我们可以写出以下状态转移方程:
不持有股票的状态转移方程:
𝑑𝑝[𝑖][0]=max(𝑑𝑝[𝑖−1][0],𝑑𝑝[𝑖−1][1]+𝑝𝑟𝑖𝑐𝑒𝑠[𝑖])
- 这里,dp[i][0] 是在第 i 天结束时不持有股票的最大利润,可以通过不变或者卖出股票获得。
持有股票的状态转移方程:
𝑑𝑝[𝑖][1]=max(𝑑𝑝[𝑖−1][1],𝑑𝑝[𝑖−1][0]−𝑝𝑟𝑖𝑐𝑒𝑠[𝑖])
- 这里,dp[i][1] 是在第 i 天结束时持有股票的最大利润,可以通过不变或者买入股票获得。
最终结果
在所有天数结束后,我们只关心不持有股票的状态,即 dp[n-1][0],这就是我们能获得的最大利润。
总结
通过动态规划的方式,我们将问题分解为多个子问题,并利用状态转移方程来计算每一天的最大利润。这样,我们能够高效地找到最大利润,而不需要穷举所有可能的买卖组合。
示例的dp矩阵
示例一:
[7, 1, 5, 3, 6, 4]
示例一的dp矩阵
0 1
+---+---+
0 | 0 | -7|
+---+---+
1 | 0 | -1|
+---+---+
2 | 4 | -1|
+---+---+
3 | 4 | 1 |
+---+---+
4 | 5 | 1 |
+---+---+
5 | 7 | 1 |
+---+---+
示例二:
[1, 2, 3, 4, 5]
示例二的dp矩阵
0 1
+---+---+
0 | 0 | -1|
+---+---+
1 | 1 | -1|
+---+---+
2 | 2 | -1|
+---+---+
3 | 3 | -1|
+---+---+
4 | 4 | -1|
+---+---+
示例三:
[7, 6, 4, 3, 1]
示例三的dp矩阵
0 1
+---+---+
0 | 0 | -7|
+---+---+
1 | 0 | -6|
+---+---+
2 | 0 | -4|
+---+---+
3 | 0 | -3|
+---+---+
4 | 0 | -1|
+---+---+
Java代码实现
public class MaxProfit {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int n = prices.length;
int[][] dp = new int[n][2];
// 初始状态
dp[0][0] = 0; // 第 0 天不持有股票
dp[0][1] = -prices[0]; // 第 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];
}
public static void main(String[] args) {
MaxProfit solution = new MaxProfit();
int[] prices = {7, 1, 5, 3, 6, 4};
System.out.println("最大利润: " + solution.maxProfit(prices)); // 输出: 7
}
}
使用贪心算法实现股票购买
这两个题目对比我们能发现贪心算法比动态规划更好理解,而且代码书写更简单,那么这个问题能否使用贪心算法呢?
答案可以的,
使用贪心算法来解决这个股票买卖问题的思路是:在每一天,我们都可以选择在价格低时买入,并在价格高时卖出。我们希望通过这种方式来获得最大利润。
思路
贪心策略:
-
遍历 prices 数组,寻找每次价格的变化。
-
当发现当前价格高于前一天的价格时,意味着可以通过在前一天买入并在当前天卖出获得利润。
利润计算: -
每次价格上涨时,计算利润并累加到总利润中。
边界条件: -
如果输入数组为空,返回 0。
-
如果价格数组长度小于 2,返回 0,因为无法进行买卖。
实现步骤
- 初始化一个变量 maxProfit 来存储总利润。
- 遍历 prices 数组,从第二天开始(索引 1),比较当前价格与前一天的价格。
- 如果当前价格高于前一天的价格,计算利润并累加到 maxProfit 中。
- 返回 maxProfit。
示例
输入 prices = [7, 1, 5, 3, 6, 4]:
- 第一天(价格 7),没有买入。
- 第二天(价格 1),买入,当前利润 0。
- 第三天(价格 5),卖出,利润增加 5 - 1 = 4。
- 第四天(价格 3),没有买入。
- 第五天(价格 6),卖出,利润增加 6 - 3 = 3。
- 第六天(价格 4),没有买入。
- 最终利润为 4 + 3 = 7。
代码实现
public int maxProfit(int[] prices) {
int len = prices.length;
if (len < 2) {
return 0;
}
int res = 0;
for (int i = 1; i < len; i++) {
int diff = prices[i] - prices[i - 1];
if (diff > 0) {
res += diff;
}
}
return res;
}