Task10:动态规划

1.视频题目

1.1 最大子数组和

1.1.1 描述

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入:nums = [1]
输出:1

示例 3:

输入:nums = [5,4,-1,7,8]
输出:23

提示:

1 <= nums.length <= 105
-104 <= nums[i] <= 104

进阶:如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。

1.1.2 代码
index012345678
value-21-34-121-54

使用上面的例子来进行说明,按照动态规划的思路,从最后一个开始

在这里插入图片描述
定义opt(i)i结尾连续子数组的最大的和,也就是结尾确定,但是开头不确定

对于index8的位置来说,index(8)结尾连续子数组的最大的和,有两种情况:

其一是,之前的最优解,加上arr8之后,比arr8更大,那结果就是之前的最优解加上arr8

其二是,之前的最优解,加上arr8之后,比arr8更小,那结果就是arr8

我们来试图整理一下,为什么要这么做,看能不能推一下大概的思路:

如果按最直观、最简单的想法,应该是从左边开始,连加求得连续之和

但是这么做的话,势必是要求出每一个连续的子数组的和

我之前还想过,能不能连加到和变小就停止,也就是截断在不能继续增加和的元素上

就好像一个序列2,3,-1,7,-6,-4,如果按照上面的思路,应该截断到-1,因为连加到此处开始变小

但是我们从全局的视角来看的话,如果继续加到7,数字之和更大,所以说要求所有子数组的情况

而且这么做的话,有些浪费,例如从2开始要计算到7,其实此时已经是最优的情况了

但是因为要计算所有的情况,所以说从37也要进行计算,但是这个计算是无效的

这是从左端开始思考的一个特点,也就是说,一旦发现某个点是最优序列的左端点之后

再选取这个点右边的其他点作为新的左端点的操作都是无效的,因为这会使得序列变短

所以或者我们可以从右边思考一下最优序列的情况,如果固定最优序列的右端点应该怎么做

那就是说,看看左边已经有的序列,加上这个端点后,是不是会比端点大

如果是的话,就拼接左部分,成为新的序列,如果不是就只只留原端点,作为最优值

也就是把从左到右扩张,改为了从右到左扩张,这样子就能利用到之前计算出的最优解

所以说,如果是从左到右的话,最优序列左端点之后的新的左端点,都是无效的

而从右到左扩张的话,每次只要决定加不加上前一次的结果就好,也对下次计算有用

其实我感觉这里的思路转换,其实还不是那么地顺滑,就是说,如何想到要固定右端点?

如果说固定右端点有什么好处,感觉也能说出来不少,但是总感觉没有触及问题核心

也就是,到底是为了解决什么东西,从而让我们想到了右端点的优点,进而决定固定右端点

暂时想不到,就这样吧,总之是固定右端点,思路改为从右往左进行扩张

啊,我好像想到了一点什么,就是,从右往左方便扩张,简而言之就是越加越大,不会变小

其实就还是上面说过的,如果是看左端点,那最优序列左端点之后的新的左端点,都是无效的

但如果是右端点,那么最优序列的情况下,新的右端点只会使得之前的序列变长,而不会无效

还有一个就是,固定右端点之后,它是一定要考虑左边相邻的点的情况的,也就是扩张不扩张

这个是针对紧紧相邻的左边的点的,所以说要看上一次,也就是左边这个点当时的情况

如果当时左边这个点,也选择了向左扩张,那么现在这个点面临的就是加不加左序列

如果当时左边这个点,没有选择向左扩张,那么现在这个点面临的就是加不加左边点

总之都是能用到上一次计算的结果的,这个地方的思路给我的感觉还挺动态规划

而且右端点应该是可以很好地解决之前那个例子,也就是上面2,3,-1,7,-6,-4的情况

我们先从点7开始看,他的选择是加不加左边的点或者序列,我们再退一步到-1的情况

那其实也就是,对于7来说,它是要加-1,还是要-12,3一起加?当然是选更大的

或许这也就是为什么,以右端点开始,每个都将扩张之后的结果与自身相比,看扩不扩张

因为这是要给下一次的计算递结果,也就是方便下一次计算能够加上一个更大的数值

反正,顺滑一点的思路,大概是,嗯,因为是动态规划,所以倒着来,所以右向左,固定右端点

但是这是在默认该题必须使用动态规划的前提下,如果是一般情况,那就是左端不行就试试右端

class Solution:
    def maxSubArray(self,nums):
        pre,result = 0, nums[0]
        for el in nums:
            pre = max(pre + el,el) 
            # 选择是否向左扩张
            result = max(result,pre)
			# 只求最大,不用dp数组了
        return  result
1.1.3 总结

反正,顺滑一点的思路,大概是,嗯,因为是动态规划,所以倒着来,所以右向左,固定右端点

但是这是在默认该题必须使用动态规划的前提下,如果是一般情况,那就是左端不行就试试右端

1.2 最长公共子序列

1.2.1 描述

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。

示例 2:

输入:text1 = “abc”, text2 = “abc”
输出:3
解释:最长公共子序列是 “abc” ,它的长度为 3 。

示例 3:

输入:text1 = “abc”, text2 = “def”
输出:0
解释:两个字符串没有公共子序列,返回 0 。

提示:

1 <= text1.length, text2.length <= 1000
text1 和 text2 仅由小写英文字符组成。

1.2.2 代码

先说个小问题吧,我在Python建二维数组的时候,出了点问题,查博客1后发现:

matrix = [array] * 3
……
也就是说matrix = [array] * 3操作中,只是创建3个指向array的引用,所以一旦array改变,matrix中3个list也会随之改变。
那如何才能在python中创建一个二维数组呢?
例如创建一个3*3的数组
 
方法1 直接定义
 
matrix = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
 
方法2 间接定义
 
matrix = [[0 for i in range(3)] for i in range(3)]

之后就要来好好说说这个思路了,这个涉及字符比较的操作,我感觉是填表的那个方向

在这里插入图片描述

此时text1=abcdetext2=acem,长度分别为54,于是新建56列的dp数组

对于dp数组当中的值,例如dp[i][j]当中的值,为text2[0:i]text1[0:j]的最长公共子序列

对于dp数组的下标x,对应两个字符串当中的第x个字符的情况,在字符数组的下标为x-1

如果对应的字符相等,也就是说text1[j]==text2[i]dp数组的对应位置应该是dp[i+1][j+1]

这个位置的数字,需要在其左上方的基础上+1,因其代表着text1[j-1]text2[i-1]的最优解

如果不相等,那就要比较左方和上方的数字,将其中较大的那一个填入当前位置

其实也就是看text1[j-1]text2[i]text1[j]text2[i-1]的哪个最优解更大

我一开始这里还写错了,当不相等的时候我选择让其等于左方或者上方的数字

后来发现两种都不行,因为左方和上方其实是两种情况,需要比较后取优

class Solution:
    def longestCommonSubsequence(self, text1: str, text2: str) -> int:
        lenT1 = len(text1)
        lenT2 = len(text2)
        dp = [ [0 for i in range(lenT1+1)] for j in range(lenT2+1) ]
        for i in range(lenT2):
        # 遍历所有行
            for j in range(lenT1):
            # 遍历所有列
                if text2[i] == text1[j] :
                    dp[i+1][j+1] = dp[i][j] + 1
                    # 等于左上方+1
                else:
                # 如果不是字符串的第一列
                    dp[i+1][j+1] = max(dp[i+1][j], dp[i][j+1])
                    # 等于左方或者上方最大的一个
        return  dp[lenT2][lenT1]
1.2.3 总结

字符比较就是填表

2. 作业题目

2.1 买卖股票的最佳时机

2.1.1 描述

给定一个数组 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 <= 105
0 <= prices[i] <= 104

2.1.2 代码

这道题我一开始还是想着动态规划的角度,然后想着去联系第一题,如何从右向左推导什么的

然后就发现,好像不那么的动态规划,我没想到什么动态规划方向的思路

我感觉,只需要用当前值,减去当前遇到的最小值,然后维护一个最大值即可

class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        lo = 10**5
        hi = -1
        for i in prices:
            lo = min(lo,i)
            hi = max(hi,i-lo)
        return hi

后来看了其他题解,发现了动态规划的角度2,状态转移方程如下:

b u y = m a x ( b u y , − p r i c e [ i ] ) \Large buy=max(buy,-price[i]) buy=max(buy,price[i])
s e l l = m a x ( s e l l , b u y + p r i c e [ i ] ) \Large sell=max(sell,buy+price[i]) sell=max(sell,buy+price[i])

其实也就是维护了一个最大值和最小值,只不过将价格变为负数,然后同样维护一个最大值

2.1.3 总结

当前值减去当前遇到的最小值,然后维护一个最大值即可

2.2 买卖股票的最佳时机 II

2.2.1 描述

给定一个数组 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 。

示例 2:

输入: prices = [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

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

提示:

1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104

2.2.2 代码

其实这里我想到的是区间调度问题,就是从买到卖是一段区间,然后收益是权重

然后发现大的区间可以拆解成为小的区间,只需要累加权重为正的区间就好

class Solution:
    def maxProfit(self, prices):
        """
        :type prices: List[int]
        :rtype: int
        """
        result = 0
        for i in range(len(prices)-1):
            if prices[i] < prices[i+1]:
                result += prices[i+1] - prices[i]

        return result

后来看了其他题解,发现了动态规划的角度3,状态转移方程如下:

b u y = m a x ( b u y , s e l l − p r i c e [ i ] ) \Large buy=max(buy,sell-price[i]) buy=max(buy,sellprice[i])
s e l l = m a x ( s e l l , b u y + p r i c e [ i ] ) \Large sell=max(sell,buy+price[i]) sell=max(sell,buy+price[i])

此处的 s e l l sell sell累计了之前的收益,然后 b u y buy buy也是

由于每次计算两者都相互引用,所以不会出现先买两次,再卖两次的情况

2.2.3 总结

大的区间可以拆解成为小的区间,只需要累加权重为正的区间就好

也可以参考其他博客,例如这个4,还有这个5

2.3 最长回文子序列

2.3.1 描述

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入:s = “bbbab”
输出:4
解释:一个可能的最长回文子序列为 “bbbb” 。

示例 2:

输入:s = “cbbd”
输出:2
解释:一个可能的最长回文子序列为 “bb” 。

提示:

1 <= s.length <= 1000
s 仅由小写英文字母组成

2.3.2 代码

字符比较,那就直接打表,然后我联想到上面那个最长公共子序列

首先是,回文串,意味着正反相等

然后还允许删除字符,这一点感觉像之前的子序列

所以说直接将字符串逆转,然后求两串的公共子序列,然后过了

我感觉有一定的道理,并不是一个巧合

因为两者的长度是相等的,所以dp数组是一个方阵

所以说,匹配的最终的结果,也是在长度相等的情况下的
在这里插入图片描述
为什么要强调两个字符串长度相等呢?看一看dp[2][4],此时的串为bbbaba,没有回文串

但是,两个串的最长公共子序列的长度是2,也就是ba,在两个串长度不相等的情况下

所以说,只有在逆序的情况下,也就是长度相等的情况下,才会出现回文串

其实上面强调的倒也不是长度问题,如果两个串长度相等,公共子序列也不一定是回文串

关键还是,两个串,互为逆序,这样才能保证匹配到的相等的字符,在串前后都出现

也就是这两个相等的字符,一个代表前面出现,而另一个代表从后面出现,也就是回文

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        text1 = s
        text2 = s[::-1]
        lenT1 = len(text1)
        lenT2 = len(text2)
        dp = [ [0 for i in range(lenT1+1)] for j in range(lenT2+1) ]
        for i in range(lenT2):
        # 遍历所有行
            for j in range(lenT1):
            # 遍历所有列
                if text2[i] == text1[j] :
                    dp[i+1][j+1] = dp[i][j] + 1
                    # 等于左上方+1
                else:
                # 如果不是字符串的第一列
                    dp[i+1][j+1] = max(dp[i+1][j], dp[i][j+1])
                    # 等于左方或者上方最大的一个
        return  dp[lenT2][lenT1]

题解的思路不太一样,倒着从对角线开始

i代表行号,j代表列号,ji的下一位直到末尾

大意是长度为1的串自然回文长度为1

然后如果两边加上相等的字符,长度+2

否则就是要判断左方和下方哪个更大

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        n = len(s)
        dp = [[0] * n for _ in range(n)]
        for i in range(n - 1, -1, -1):
            dp[i][i] = 1
            for j in range(i + 1, n):
                if s[i] == s[j]:
                    dp[i][j] = dp[i + 1][j - 1] + 2
                else:
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
        return dp[0][n - 1]

填表如下:

在这里插入图片描述
还有点绕,暂时没想到一个合理的推导思路去解释

2.3.3 总结

两个串互为逆序,这样才能保证最长公共子序列是最长回文串


  1. Python的二维数组操作 ↩︎

  2. Leetcode 121:买卖股票的最佳时机(最详细的解法!!!) ↩︎

  3. Leetcode 122:买卖股票的最佳时机II(最详细的解法!!!) ↩︎

  4. 一个方法团灭 LeetCode 股票买卖问题 | labuladong 的算法笔记 ↩︎

  5. 【LeetCode】“买卖股票的最佳时机“ 系列题解&总结_leetcode best time to sell stock-CSDN博客 ↩︎

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值