算法 - 动态规划(二)

动态规划 

动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,
然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。

1、买卖股票的最好时机(一)(简单)

题目描述:

假设你有一个数组prices,长度为n,其中prices[i]是股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益。
1.你可以买入一次股票和卖出一次股票,并非每天都可以买入或卖出一次,总共只能买入和卖出一次,且买入必须在卖出的前面的某一天。
2.如果不能获取到任何利润,请返回0。
3.假设买入卖出均无手续费。                       要求时间复杂度O(n)

示例:

输入:[8,9,2,5,4,7,1]
返回值:5
说明:在第3天(股票价格 = 2)的时候买入,在第6天(股票价格 = 7)的时候卖出,最大利润 = 7-2 = 5 ,不能选择在第2天买入,第3天卖出,这样就亏损7了;同时,你也不能在买入前卖出股票。 


输入:[2,4,1]                         输入:[3,2,1]
返回值:2                              返回值:0

解题思路:

思路1:最笨的方法就是除了最后一天不买,前面所有天依次买然后依次计算所有天的利润,最后再比较大小。两个for语句搞定,但是不满足时间复杂度。

思路2:利用贪心思想

如果我们在某一天卖出了股票,那么要想收益最高,一定是它前面价格最低的那天买入的股票才可以。因此我们可以利用贪心思想解决,每次都将每日收入与最低价格相减维护最大值。
具体做法:

  • step 1:首先排除数组为空的特殊情况。
  • step 2:将第一天看成价格最低,后续遍历的时候遇到价格更低则更新价格最低。
  • step 3:每次都比较最大收益与当日价格减去价格最低的值,选取最大值作为最大收益。
def max_profit(prices):
    # 初始最大收益设为0
    res = 0
    if len(prices) == 0:
        return res
    # 初始最低股票价格
    _min = prices[0]
    for i in range(1, len(prices)):
        # 如果当日价格更低则更新最低价格
        _min = min(_min, prices[i])
        # 维护最大值
        res = max(res, prices[i] - _min)
    return res

思路三:利用动态规划

对于每天有到此为止的最大收益和是否持股两个状态,因此我们可以用动态规划。
具体做法:

step 1:用dp[i][0]表示第i天不持股到该天为止的最大收益,dp[i][1]表示第i天持股,到该天为止的最大收益。
step 2:(初始状态) 第一天不持股,则总收益为0,dp[0][0]=0;第一天持股,则总收益为买股票的花费,此时为负数,dp[0][1]=−prices[0]。
step 3:(状态转移) 对于之后的每一天,如果当天不持股,有可能是前面的若干天中卖掉了或是还没买, 因此到此为止的总收益和前一天相同,也有可能是当天才卖掉,我们选择较大的状态dp[i][0]=max(dp[i−1][0],dp[i−1][1]+prices[i])
step 4:如果当天持股,有可能是前面若干天中买了股票,当天还没卖,因此收益与前一天相同,也有可能是当天买入, 此时收益为负的股价,同样是选取最大值:dp[i][1]=max(dp[i−1][1],−prices[i])。

def max_profit(prices):
    """
    :param prices: list
    :return:
    """
    # dp[i][0]表示某一天不持股到该天为止的最大收益,dp[i][1]表示某天持股,到该天为止的最大收益
    # 全初始化为0
    dp = [[0] * 2 for i in range(len(prices))]
    # 第一天不持股,总收益为0
    dp[0][0] = 0
    # 第一天持股,总收益负该天的股价
    dp[0][1] = -prices[0]
 
    for i in range(1, len(prices)):
        # 更新不持股收益:(不持股:(1)已经卖了  (2)还没买)
        # (1)已经卖了:如果是当天卖的收益就是当天的价格和之前收益的价格只差,如果是之前就卖了,那么收益与前一天相同
        # (2)还没买:那么收益还是与前一天相同 dp[i - 1][0]
        dp[i][0] = max(dp[i - 1][0], prices[i] + dp[i - 1][1])
        # 更新持股收益(持股:(1)已经买了  (2)当天买)
        # (1)已经买了:那么收益就是之前买的价格 dp[i - 1][1]
        # (2)当天买:收益肯定就是当天买的价格的负数 -prices[i]
        dp[i][1] = max(dp[i - 1][1], -prices[i])
 
    # 最后一天不持股,返回到该天为止的最大收益
    return dp[len(prices)-1][0]

2、买卖股票的最好时机(二)(中等)

题目描述:

假设你有一个数组prices,长度为n,其中prices[i]是某只股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益。

1. 你可以多次买卖该只股票,但是再次购买前必须卖出之前的股票
2. 如果不能获取收益,请返回0
3. 假设买入卖出均无手续费

示例:

输入:[8,9,2,5,4,7,1]

返回值:7

说明:在第1天(股票价格=8)买入,第2天(股票价格=9)卖出,获利9-8=1 在第3天(股票价格=2)买入,第4天(股票价格=5)卖出,获利5-2=3 在第5天(股票价格=4)买入,第6天(股票价格=7)卖出,获利7-4=3 总获利1+3+3=7,返回7

输入:[5,4,3,2,1]

返回值:0

说明:由于每天股票都在跌,因此不进行任何交易最优。最大收益为0。

输入:[1,2,3,4,5]

返回值:4

说明:第一天买进,最后一天卖出最优。中间的当天买进当天卖出不影响最终结果。最大收益为4。

输入:[8,9,2,3,4,7,1]

返回值:6

解题思路:

这道题与买卖股票的最好时机(一)的区别在于可以多次买入卖出。 但是对于每天还是有到此为止的最大收益和是否持股两个状态,因此我们照样可以用动态规划。

具体做法:

step 1:用dp[i][0]表示第i天不持股到该天为止的最大收益,dp[i][1]表示第i天持股,到该天为止的最大收益。
step 2:(初始状态) 第一天不持股,则总收益为0,dp[0][0]=0;第一天持股,则总收益为买股票的花费,此时为负数,dp[0][1]=−prices[0].
step 3:(状态转移) 对于之后的每一天,如果当天不持股,有可能是前面的若干天中卖掉了或是还没买,因此到此为止的总收益和前一天相同,也有可能是当天卖掉股票,我们选择较大的状态dp[i][0]=max(dp[i−1][0], dp[i−1][1]+prices[i]);
step4:如果当天持股,可能是前几天买入的还没卖,因此收益与前一天相同,也有可能是当天买入,减去买入的花费,同样是选取最大值:dp[i][1]=max(dp[i−1][1], dp[i−1][0]−prices[i])。

def max_profit(prices):
    """
    :param prices: list
    :return:
    """
    # dp[i][0]表示某一天持股到该天为止的最大收益,dp[i][1]表示某天不持股,到该天为止的最大收益
    dp = [[0] * 2 for _ in range(len(prices))]
    dp[0][0] = -prices[0]
    dp[0][1] = 0
    for i in range(1, len(prices)):
        # 第i天持股:1). 可能是前几天买入的还没卖,今天不买;因此收益与前一天持股最大收益相同。
        #          2). 也有可能是前几天卖了,现在又买;因此收益是前一天不持股的最大收益 - 今天买入的价格
        dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i])
        # 第i天不持股:1). 可能是前几天卖了,或者前面还没买;因此收益与前一天不持股最大收益相同。
        #            2). 也有可能是前几天买了,今天卖掉;因此收益是前一天持股的最大收益 + 今天卖掉的价格
        dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i])

    return dp[len(prices)-1][1]

3、零钱兑换(一)(中等)

题目描述:

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。你可以认为每种硬币的数量是无限的。
要求:时间复杂度 O(数组大小 × amount)

示例:

输入:coins = [1, 2, 5], amount = 11                   输入:coins = [3, 5], amount = 2

输出:3                                                               输出:-1

解释:11 = 5 + 5 + 1

输入:coins = [5, 2, 3], amount = 20                  输入:coins = [5, 2, 3], amount = 9

输出:4                                                               输出:3

解题思路:

用dp[i]表示要凑出i元钱需要最小的货币数,实际上可以求出从1到amount之间所有数所需要的最小货币数。 因为后面的数 可以由 前面已经求出来的最小货币数 + 1 得来。

step 1:可以用dp[i]表示要凑出i元钱需要最小的货币数。
step 2:一开始都设置为最大值amount+1,因此货币最小1元,即货币数不会超过amount。
step 3:初始化dp[0]=0。
step 4:后续遍历1元到amount元,枚举每种面值的货币都可能组成的情况,取每次的最小值即  可,转移方程为dp[i]=min(dp[i], dp[i − coins中每个货币值] + 1)
step 5:最后比较dp[amount]的值是否超过amount,如果超过说明无解,否则返回即可。 

def min_money(coins, amount):
    """
    :param coins: list
    :param amount: int
    :return:
    """
    if min(coins) > amount:
        return -1
    dp = [(amount + 1) for _ in range(amount + 1)]
    dp[0] = 0
    for i in range(1, amount+1):
        for coin in coins:
            # coin比目标值都大所以不可能由coin组成,不比较
            if coin <= dp[i]:
                dp[i] = min(dp[i], dp[i-coin] + 1)

    if dp[amount] > amount:
        return -1
    else:
        return dp[amount]

4、零钱兑换(二)(中等) 

题目描述:

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回0。 假设每一种面额的硬币有无限个。

示例:

输入:amount = 5, coins = [1, 2, 5]                   输入:amount = 3, coins = [2]

输出:4                                                              输出:0

0解释:有四种方式可以凑成总金额:               解释:只用面额2 的硬币不能凑成总金额3

5=5

5=2+2+1                                   输入:amount = 0, coins = [1, 2, 5]

5=2+1+1+1                               输出:1

5=1+1+1+1+1                           解释:什么都不装可以凑成总金额0,所以也算一种组合数

解题思路:

这是一道典型的背包问题,一看到钱币数量不限,就知道这是一个完全背包。
用一个一维数组记录:dp[1]表示凑出1需要的硬币组合数....dp[amount]表示凑出amount需要的硬币组合数
对于例子:coins=[1, 2, 5],amount=5,初始化dp=[0, 0, 0, 0, 0, 0]
首先dp[0]赋值为1(列表下标从0开始),确定递推公式。
i-coins中每个货币值,如:i-coins[1]表示dp[i-coins[1]]和coins[1]是组合成dp[i]的一类(可能不止一种)组合方式
例如:dp[j],j 为5,
已经有一个1(coins[i]) 的话,有 dp[4]种方法 凑成 dp[5]。
已经有一个2(coins[i]) 的话,有 dp[3]种方法 凑成 dp[5]。
已经有一个3(coins[i]) 的话,有 dp[2]中方法 凑成 dp[5]
已经有一个4(coins[i]) 的话,有 dp[1]中方法 凑成 dp[5]
已经有一个5(coins[i]) 的话,有 dp[0]中方法 凑成 dp[5]
所以递推公式:dp[i] += dp[i - coins中每个货币值]

对于例子amount = 5, coins = [1, 2, 5],给一个一维数组 [1, 0, 0, 0, 0, 0]
遍历coins中每一个硬币[1, 2, 5]
如果使用1:显然用1能满足每一组的组合更新数组 [1, 1, 1, 1, 1, 1]
如果使用2:把能用2组合成的硬币对应dp更新 [1, 1, 2, 2, 3, 3]
同理如果使用5 [1, 1, 2, 2, 3, 4]

def change(amount, coins):
    """
    :param amount: int 总数
    :param coins: list 硬币面额
    :return:
    """
    dp = [0] * (amount + 1)
    # 初始化dp数组,表示金额为0时只有一种情况,也就是什么都不装
    dp[0] = 1
    # 遍历物品(硬币)
    for coin in coins:
        # 遍历背包
        for i in range(coin, amount + 1):
            dp[i] += dp[i - coin]

    return dp[amount]

 奥赛题:12个乒乓球,有一个质量不同的,有一个天平,称三次把那个球找出来,并判断它是轻了还是重了。

5、单词拆分(一)(中等) 

题目描述:

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
          注意,你可以重复使用字典中的单词。

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

解题思路:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值