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 代码
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
value | -2 | 1 | -3 | 4 | -1 | 2 | 1 | -5 | 4 |
使用上面的例子来进行说明,按照动态规划的思路,从最后一个开始
定义opt(i)
是以i
结尾的连续子数组的最大的和,也就是结尾确定,但是开头不确定
对于index
为8
的位置来说,以index(8)
结尾的连续子数组的最大的和,有两种情况:
其一是,之前的最优解,加上arr8
之后,比arr8
更大,那结果就是之前的最优解加上arr8
其二是,之前的最优解,加上arr8
之后,比arr8
更小,那结果就是arr8
我们来试图整理一下,为什么要这么做,看能不能推一下大概的思路:
如果按最直观、最简单的想法,应该是从左边开始,连加求得连续之和
但是这么做的话,势必是要求出每一个连续的子数组的和
我之前还想过,能不能连加到和变小就停止,也就是截断在不能继续增加和的元素上
就好像一个序列2,3,-1,7,-6,-4
,如果按照上面的思路,应该截断到-1
,因为连加到此处开始变小
但是我们从全局的视角来看的话,如果继续加到7
,数字之和更大,所以说要求所有子数组的情况
而且这么做的话,有些浪费,例如从2
开始要计算到7
,其实此时已经是最优的情况了
但是因为要计算所有的情况,所以说从3
到7
也要进行计算,但是这个计算是无效的
这是从左端开始思考的一个特点,也就是说,一旦发现某个点是最优序列的左端点之后
再选取这个点右边的其他点
作为新的左端点的操作都是无效的,因为这会使得序列变短
所以或者我们可以从右边思考一下最优序列的情况,如果固定最优序列的右端点应该怎么做
那就是说,看看左边已经有的序列,加上这个端点后,是不是会比端点大
如果是的话,就拼接左部分,成为新的序列,如果不是就只只留原端点,作为最优值
也就是把从左到右扩张,改为了从右到左扩张,这样子就能利用到之前计算出的最优解
所以说,如果是从左到右的话,最优序列左端点之后的新的左端点,都是无效的
而从右到左扩张的话,每次只要决定加不加上前一次的结果就好,也对下次计算有用
其实我感觉这里的思路转换,其实还不是那么地顺滑,就是说,如何想到要固定右端点?
如果说固定右端点有什么好处,感觉也能说出来不少,但是总感觉没有触及问题核心
也就是,到底是为了解决什么东西,从而让我们想到了右端点的优点,进而决定固定右端点
暂时想不到,就这样吧,总之是固定右端点,思路改为从右往左进行扩张
啊,我好像想到了一点什么,就是,从右往左方便扩张,简而言之就是越加越大,不会变小
其实就还是上面说过的,如果是看左端点,那最优序列左端点之后的新的左端点,都是无效的
但如果是右端点,那么最优序列的情况下,新的右端点只会使得之前的序列变长,而不会无效
还有一个就是,固定右端点之后,它是一定要考虑左边相邻的点的情况的,也就是扩张不扩张
这个是针对紧紧相邻的左边的点的,所以说要看上一次,也就是左边这个点当时的情况
如果当时左边这个点,也选择了向左扩张,那么现在这个点面临的就是加不加左序列
如果当时左边这个点,没有选择向左扩张,那么现在这个点面临的就是加不加左边点
总之都是能用到上一次计算的结果的,这个地方的思路给我的感觉还挺动态规划
的
而且右端点应该是可以很好地解决之前那个例子,也就是上面2,3,-1,7,-6,-4
的情况
我们先从点7
开始看,他的选择是加不加左边的点或者序列,我们再退一步到-1
的情况
那其实也就是,对于7
来说,它是要加-1
,还是要-1
和2,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=abcde
,text2=acem
,长度分别为5
和4
,于是新建5
行6
列的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,sell−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])
此处的 s e l l sell sell累计了之前的收益,然后 b u y buy buy也是
由于每次计算两者都相互引用,所以不会出现先买两次,再卖两次的情况
2.2.3 总结
大的区间可以拆解成为小的区间,只需要累加权重为正的区间就好
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]
,此时的串为bbba
和ba
,没有回文串
但是,两个串的最长公共子序列的长度是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
代表列号,j
从i
的下一位直到末尾
大意是长度为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 总结
两个串互为逆序,这样才能保证最长公共子序列是最长回文串