动态规划和贪心算法

让我们通过下面的两个算法来深刻的理解一下动态规划和贪心算法吧。

最短路径问题

​ 从上到下找到最短路径〈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。

为什么可以使用贪心算法

  1. 局部最优解:

在每一步中,我们选择当前最低的买入价格和当前价格之间的利润,这样的选择保证了在每一步都可以获取最大的利润。

  1. 全局最优解:

通过遍历整个数组,最终得到的 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,因为无法进行买卖。

实现步骤

  1. 初始化一个变量 maxProfit 来存储总利润。
  2. 遍历 prices 数组,从第二天开始(索引 1),比较当前价格与前一天的价格。
  3. 如果当前价格高于前一天的价格,计算利润并累加到 maxProfit 中。
  4. 返回 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;
    }

动态规划(Dynamic Programming, DP)和贪心算法(Greedy Algorithm)都是求解最优化问题的两种常用算法策略,它们在某些情况下都能达到最优解,但也有一些关键的区别。 **相同点:** 1. **目标都是求解最优解**:两者都旨在找到问题的全局最优解,即在给定限制条件下找到最佳解决方案。 2. **局部最优不一定等于全局最优**:在执行过程中,都会依赖于当前的最佳选择,但不保证局部最优就是全局最优。 **不同点:** 1. **决策过程**: - **贪心算法**通常在每一步选择中都采取当前看起来最优的解决方案,而并不考虑这些决策的长期影响。这种策略基于“每一步都做出最好的选择”的假设,但在复杂问题中,这并不总是正确的。 - **动态规划**则是建立在对所有可能状态的考虑基础上,它会保存并利用前面计算出来的子问题结果,以避免重复劳动,确保最终得到全局最优解。 2. **适用场景**: - **贪心算法**适用于满足“贪心选择性质”(每一步选择都是局部最优的,并且整体上也是最优的)的问题,如霍夫曼编码、最小生成树等。 - **动态规划**更常用于那些具有重叠子问题和最优子结构的问题,比如背包问题、最长公共子序列、最短路径问题等。 3. **存储需求**: - 贪心算法通常只需要处理当前的状态,不需要额外的存储空间。 - 动态规划可能需要存储中间的结果,特别是在使用记忆化搜索或自底向上的策略时,需要一个表格或数组来存储所有状态。 4. **效率**: - 贪心算法往往有较好的时间复杂度,因为它不需要回溯或者大量计算。 - 动态规划的效率取决于状态的数量和依赖关系,复杂问题可能需要较高的空间复杂度和较长的时间。 **总结**:动态规划贪心算法各有其适用范围和优势,选择哪种方法取决于问题的具体特性。如果问题满足贪心选择性质且容易实现,贪心算法可能是首选;否则,动态规划更能够保证找到全局最优解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值