力扣做题记录——121.买卖股票的最佳时机&动态规划总结

  这道题和之前做过的70.爬楼梯类似,都是有关动态规划的问题,因此在此总结一下动态规划相关的问题。
为什么要用动态规划?

Those who cannot remember the past are condemned to repeat it.
记不起过去的人注定要重蹈覆辙

  如计算“1+1+1+1”和计算“1+1+1+1+1”时,显然在式1的基础上再加1来计算式2是比逐步计算更快的方法。上面两个式子在计算时非常快。但是,若有从1开始计算1,1+1,……,231-1个1相加的值,逐步相加的运行速度会非常慢。因此,为了加快程序的运行速度,可以使用动态规划算法。
什么是动态规划?
  动态规划(Dynamic Programming,DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划通常适用于有重叠子问题和最优子结构性质的问题。

dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.

  简单来说,动态规划其实就是给定一个问题,我们把他拆成一个个子问题,知道子问题可以被解决。然后将子问题答案保存起来以减少重复运算。再根据子问题答案反推,得出原问题解的一种方法。

一般这些子问题很相似,可以通过函数关系式递推出来。动态规划就致力于解决每个子问题一次,减少重复计算,比如斐波那契数列就是入门级的经典动态规划问题。

动态规划核心思想
  动态规划最核心的思想在于拆分子问题,记住子问题答案,减少重复计算。因为记住子问题的答案需要花费额外的储存空间,所以动态规划算法也被称为“用空间换时间”。
一个例子带你走进动态规划
  在之前的文章70.爬楼梯中,有这样一道题:爬楼梯时可以每次上一阶楼梯,也可以每次上两阶楼梯,爬上一个10阶台阶的楼梯有多少种方法。
  这道题直接上手的时候可能会有些懵,找不到思路,很容易就把自己绕晕。那么如果我们反过来想,按照这个规则,第10阶台阶只能由第9阶或第8阶台阶上来,第9阶台阶只能由第8阶或者第7阶台阶上来,……,第3阶台阶只能由第2阶或第1阶台阶上来。即:

f(10) = f(9) + f(8)
f(9) = f(8) + f(7)
……
f(3) = f(2) + f(1)
即通用公式为f(n) = f(n - 1) + f(n - 2)

  我们得到了通用公式后,就可以用递归方法解决这个问题:

class Solution{
public:
	int numWays(int n){
		if (n == 1) return 1;
		if (n == 2) return 2;
		return numWays(n - 1) + numWays(n - 2);
	}
}

  但是,当我们用递归法提交时,发现运行超时了,这说明我们的程序在遇到较多台阶时运行不够快。那么不够快的原因是什么呢?让我们再来看上面的程序,在计算f(10) = f(9) + f(8)时计算过1次f(8)了,而在计算f(8) = f(7) + f(6)时又计算了一次,所以迭代法中存在大量的重复计算,降低了程序的运行速度。这时候我们从“1+1+1”的例子想到,如果我在之前计算的时候,就把f(n)的值记录下来,这样在之后的计算中不就可以直接使用了吗?
  所以可以用两种动态规划的算法解决这道题:
1. 自顶向下的备忘录法
  一般使用一个数组或者一个哈希表充当备忘录:

  1. 计算f(10) = f(9) + f(8),将f(9)f(8)计算出来存入备忘录中
  2. 计算f(9) = f(8) + f(7),,此时因为f(8)已经计算过了,所以可以不计算,
    ……
    以此类推。

  带备忘录的递归算法中,子问题个数=树节点数=n,解决一个子问题的时间复杂度是O(1),因此带备忘录的递归算法的时间复杂度是O(n)。代码如下:

class Solution{
public:
	unordered_map<int, int> hash;
	int climbStairs(int n) {
        if(n == 1) return 1;
        if(n == 2) return 2;

        auto it = mp.find(n);
        if( it != mp.end() )
            return it->second;
        int sum = climbStairs(n-1) + climbStairs(n-2);
        mp.insert(pair<int, int> (n, sum));
        return sum;
	}
}

自底向上的动态规划
  动态规划和带备忘录的递归解法基本思想是一致的,都可以减少重复计算,时间复杂度相差也不多,但:

  • 带备忘录的递归算法是从f(10)f(1)方向延伸求解,所以也称为自顶向下的解法;
  • 动态规划则是从较小问题的解,由交叠性质逐步决策出较大问题的解,它是从f(1)f(10)方向求解,所以称为自底向上的解法。

  动态规划有几个典型特征:最优子结构、状态转移方程、边界、重叠子问题。在爬台阶问题中:

  • f(n - 1)f(n - 2)称为f(n)的最优子结构;
  • f(n) = f(n - 1) + f(n - 2)称为状态转移方程;
  • f(1) = 1f(2) = 2称为边界;
  • f(10) = f(9) + f(8)f(9) = f(8) + f(7)称为重叠子问题

  动态规划的代码如下:

class Solution {
public:
    int climbStairs(int n) {
        if (n == 1) return 1;
        if (n == 2) return 2;
        long res = 0, a = 1, b = 2;
        for (int i = 3; i <= n; i++)
        {
            res = a + b;
            a = b;
            b = res;
            
        }
        return res;
    }
}

什么样的问题可以使用动态规划

如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来之后,发现存在重叠子问题,就可以考虑使用动态规划。

  比如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等。
动态规划的解题思路

  1. 穷举分析
  2. 确定边界
  3. 找出规律,确定最优子结构
  4. 写出状态转移方程

按照上面的思路,我们来看一下力扣第121题,买卖股票的最佳时机
  给定一个数组prices,它的第i个元素prices[i]表示一支给定股票第i天的价格。你只能选择某一天买入这只股票,并选择在未来的某一个不同的日子卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0
示例1

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例2

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

提示

  • 1 <= prices.length <= 10^5
  • 0 <= prices[i] <= 10^4

思路
  在某一天卖出时,其最大利润应该为当天的股票价格减去前几天中最低的股票价格,即maxProfit = prices[i] - minPrice

  1. 穷举:第一天卖出,最大利润为0;第二天卖出最大利润为-6;第三天卖出,最大利润为4;第四天卖出,最大利润为2;第五天卖出,最大利润为5;第六天卖出最大利润为3。
  2. 边界:利润初始值为0,价格初始值为第一天的价格;
  3. 最优子结构:maxProfit = prices[i] - minPrice
  4. 状态转移方程:每到新的一天,都要判断今天的价格是否是价格新低,然后确定新的最大利润:minPrice = min(minPrice, prices[i])maxProfit = max(maxProfit, (prices[i] - minPrice))

  所以代码如下:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int maxProfit = 0;
        int minPrice = prices[0];
        int n = prices.size();
        for (int i = 0; i < n; i++)
        {
            minPrice = min(minPrice, prices[i]);
            maxProfit = max(maxProfit, (prices[i] - minPrice));
        }
        return maxProfit;
    }
};

  本文题目及部分解答来自力扣,其余参考资料见下文链接。

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock

参考
看一遍就理解:动态规划详解
算法-动态规划 Dynamic Programming–从菜鸟到老鸟

你可能感兴趣的:(刷题记录,leetcode,动态规划,算法)