DP
5. 最长回文子串(中等)
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
示例 1:
输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
示例 2:
输入: "cbbd"
输出: "bb"
方法:DP
用 P(i,j) 表示字符串 s的第 i到 j个字母组成的串(s[i:j])是否为回文串:
动态规划的状态转移方程:
也就是说,只有 s[i+1:j-1] 是回文串,并且 s的第 i 和 j个字母相同时,s[i:j] 才会是回文串。
动态规划的边界条件:
所有 P(i,j)=true 中 j-i+1(即子串长度)的最大值。
注意:在状态转移方程中,我们是从长度较短的字符串向长度较长的字符串进行转移的,因此一定要注意动态规划的循环顺序。
class Solution:
def longestPalindrome(self, s: str) -> str:
size = len(s)
if size < 2: # 无字符或单字符直接返回其本身
return s
dp = [[False] * size for i in range(size)] # 初始化状态矩阵
max_length = 1 # 初始化最大长度
start = 0 # 初始化最大回文起点
for i in range(size): # 初始化对角线(单字符为回文)
dp[i][i] = True
for j in range(1, size): # 终点从s[1]开始
for i in range(0, j): # 起点从s[0]开始,到s[j]结束
if s[i] == s[j]:
if j-i < 3:
dp[i][j] = True
else:
dp[i][j] = dp[i+1][j-1]
else:
dp[i][j] = False
if dp[i][j]:
cur_length = j-i+1
if cur_length > max_length:
max_length = cur_length
start = i
return s[start:start+max_length]
300. 最长上升子序列(中等)
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101]
,
它的长度是 4
。
说明:
- 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
- 你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
方法:DP
按照动态规划定义状态的套路,我们有两种常见的定义状态的方式:
(1)dp[i] : 以 i 结尾(一定包括 i)所能形成的最长上升子序列长度, 答案是 max(dp[i]),其中 i = 0,1,2, ..., n - 1
(2)dp[i] : 以 i 结尾(可能包括 i)所能形成的最长上升子序列长度,答案是 dp[-1] (-1 表示最后一个元素)
第二种定义方式由于无需比较不同的 dp[i] 就可以获得答案,因此更加方便。但是想了下,状态转移方程会很不好写,因为 dp[i] 的末尾数字(最大的)可能是 任意 j < i 的位置。
第一种定义方式虽然需要比较不同的 dp[i] 从而获得结果,但是我们可以在循环的时候顺便得出,对复杂度不会有影响,只是代码多了一点而已。因此我们选择第一种建模方式。
解题思路:
状态定义:
dp[i] 的值代表 nums 前 i个数字的最长子序列长度。
转移方程: 设 j∈[0,i),考虑每轮计算新 dp[i] 时,遍历 [0,i) 列表区间,做以下判断:
(1)当 nums[i] > nums[j] 时: nums[i] 可以接在 nums[j] 之后(此题要求严格递增),此情况下最长上升子序列长度为 dp[j] + 1;
(2)当 nums[i] <= nums[j] 时: nums[i]无法接在 nums[j] 之后,此情况上升子序列不成立,跳过。1
上述所有 1. 情况 下计算出的 dp[j] + 1的最大值,为直到 i 的最长上升子序列长度(即 dp[i])。实现方式为遍历 j 时,每轮执行 dp[i] = max(dp[i], dp[j] + 1)。
转移方程: dp[i] = max(dp[i], dp[j] + 1) for j in [0, i)。
初始状态:
dp[i]所有元素置 1,含义是每个元素都至少可以单独成为子序列,此时长度都为 1。
返回值:
返回 dp列表最大值,即可得到全局最长上升子序列长度。
复杂度分析:
时间复杂度 O(N^2): 遍历计算 dp 列表需 O(N),计算每个 dp[i] 需O(N)。
空间复杂度 O(N): dp列表占用线性大小额外空间。
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if not nums:
return 0
n = len(nums)
dp = [1] * n
for i in range(n):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
方法2:二分查找
处理这些扑克牌要遵循以下规则:
只能把点数⼩的牌压到点数⽐它⼤的牌上。如果当前牌点数较⼤没有可以放置的堆,则新建⼀个堆,把这张牌放进去。如果当前牌有多个堆可供选择,则选择最左边的堆放置。
为什么遇到多个可选择堆的时候要放到最左边的堆上呢?因为这样可以保证牌堆顶的牌有序(2, 4, 7, 8, Q),证明略。
每次处理⼀张扑克牌找⼀个合适的牌堆顶来放,牌堆顶的牌有序,这就能⽤到⼆分查找了:⽤⼆分查找来搜索当前牌应放置的位置。
最后整个算法流程为:
设当前已求出的最长上升子序列的长度为 len(初始时为 1),从前往后遍历数组 nums,在遍历到 nums[i] 时:
(1)如果 nums[i] > d[len] ,则直接加入到 d 数组末尾,并更新 len=len+1;
(2)否则,在 d 数组中二分查找,找到第一个比 nums[i] 小的数d[k] ,并更新 d[k+1]=nums[i]。
以输入序列 [0,8,4,12,2] 为例:
第一步插入 0,d=[0]
第二步插入 8,d = [0, 8]
第三步插入 4,d = [0, 4]
第四步插入 12,d = [0, 4, 12]
第五步插入 2,d = [0,2,12]。
最终得到最大递增子序列长度为 3。
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
tails = [0] * len(nums)
res = 0
for num in nums:
i, j = 0, res
while i < j:
mid = (i + j) // 2
if tails[mid] < num:
i = mid + 1
else:
j = mid
tails[i] = num
if j == res:
res += 1
return res
一维动态规划
一、问题描述:给定n,找到不同的将n写成1,3,4相加的方法
例如:n=5,答案是6
5 = 1 + 1 + 1 + 1 + 1
= 1 + 1 + 3
= 1 + 3 + 1
= 3 + 1 + 1
= 1 + 4
= 4 + 1
方法:DP
问题分析:
f(0) = f(1) = f(2) = 1, f(3) = 2
for(i = 4; i <= n; i++)
f(i) = f(i-1) + f(i-3) + f(i-4)
代码:
def coin(n):
dp = [0] * (n + 1)
dp[0] = dp[1] = dp[2] = 1
dp[3] = 2
for i in range(4, n+1):
dp[i] = dp[i-1] + dp[i-3] + dp[i-4]
return dp[n]
测试:
coin(5)
leetcode198打家劫舍
假如你是一个职业抢劫犯,你打算洗劫一个街道。每一个房子里有一定数量的钱,限制你的唯一条件是相邻的房子的安保系统是相连的,如果你抢劫相邻的房子,那么安保系统就会惊动警察。
给定一个非负整数的列表代表每个房子当中的钱,计算在不惊动警察的情况下,你可以抢劫到最多的钱。
分析:
8 3 5 7 6 9 2 8 35 (不能连续相加,找加和最大)
Y 8 3 13 15 19 24 21 32 59
N 0 8 8 13 15 19 24 24 32
Y:代表加上当前的数,所能达到的最大值
N:代表不加当前的数,所能达到的最大值
Y: Y(i) = N(i-1) + a(i)
N: N(i) = max(Y(i-1), N(i-1))
Y(0) = a(0), N(0) = 0
最大:max(Y(i),N(i))
代码1:
引入了两个空间,时间复杂度O(n),空间复杂度O(n)
def rob(nums):
n = len(nums)
# 生成两行n+1的0,第一列都为0,代表初始值
# 规定第二行不取当前值所达到的最大值,第一行取当前值所达到的最大值
dp = [[0 for _ in range(n+1)] for _ in range(2)]
for i in range(1, n+1):
dp[0][i] = dp[1][i-1] + nums[i-1]
dp[1][i] = max(dp[0][i-1], dp[1][i-1])
return max(dp[0][n], dp[1][n])
测试:
nums = [2, 7, 9, 3, 1]
rob(nums)
代码2:
对1进行优化,每次只用到了当前值的前面的两个数字,不用引入空间。
时间复杂度O(n),空间复杂度O(1)
def rob(nums):
n = len(nums)
# 初始值
yes, no = 0, 0
for i in nums:
yes, no = no + i, max(yes, no)
return max(yes, no)
测试:
nums = [2, 7, 9, 3, 1]
rob(nums)
入室抢劫2
该街道的所有房子是圆形排列的。也就是说第一家和最后一家也是邻居。安保系统设置同上问题。
分析:
在上面的情况下,考虑圆环,则分成两种情况:选择起点(就不能选择终点);不选择起点。
def rob_round(nums):
def rob(nums):
yes, no = 0, 0
for i in nums:
yes, no = no + i, max(yes, no)
return max(yes, no)
return max(rob(nums[:-1]), rob(nums[1:]))
测试:
nums = [2, 7, 9, 3, 1]
rob_round(nums)
91. 解码方法(中等)
一段包含着A-Z的短信用以下方式进行编码:
'A'—> 1
'B'—> 2
……
'Z'—> 26
给定一段编码的短信,计算解码的方式。
分析:
没有限制条件时:f(n) = f(n-1) + f(n-2)
条件范围10-26:f(n-2)可以取到
条件范围:
方法:DP
代码:
class Solution:
def numDecodings(self, s: str) -> int:
if not s or s[0] == '0':
return 0
n = len(s)
dp = [0] * (n+1)
dp[0], dp[1] = 1, 1
for i in range(2, n+1):
if '10' <= s[i-2:i] <= '26':
dp[i] += dp[i-2]
if '0' < s[i-1] <= '9':
dp[i] += dp[i-1]
return dp[-1]
测试:
numDecodings('122230789')
53. 最大子序和
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
进阶:
如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的分治法求解。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
ans = nums[0]
for i in range(1, len(nums)):
nums[i] = max(nums[i]+nums[i-1], nums[i])
ans = max(ans, nums[i])
return ans
152. 乘积最大子数组(中等)
找到array中的连续子序列,该子序列的乘积最大。
- 遍历数组时计算当前最大值,不断更新
- 令imax为当前最大值,则当前最大值为 imax = max(imax * nums[i], nums[i])
- 由于存在负数,那么会导致最大的变最小的,最小的变最大的。因此还需要维护当前最小值imin,imin = min(imin * nums[i], nums[i])
- 当负数出现时则imax与imin进行交换再进行下一步计算
时间复杂度:O(n)
class Solution:
def maxProduct(self, nums: List[int]) -> int:
imax = imin = result = nums[0]
for i in range(1, len(nums)):
imax, imin = max(imax*nums[i], imin*nums[i], nums[i]), \
min(imax*nums[i], imin*nums[i], nums[i])
result = max(result, imax)
return result
代码:
def maxProduct(nums):
if len(nums) == 0:
return 0
maximum = minimum = result = nums[0]
for i in range(1, len(nums)):
maximum, minimum = max(maximum * nums[i], minimum * nums[i], nums[i]), \
min(maximum * nums[i], minimum * nums[i], nums[i])
result = max(result, maximum)
return result
测试:
nums = [2, 3, -2, 4]
maxProduct(nums)
643. 子数组最大平均数 I
给定 n
个整数,找出平均数最大且长度为 k
的连续子数组,并输出该最大平均数。
示例 1:
输入: [1,12,-5,-6,50,3], k = 4
输出: 12.75
解释: 最大平均数 (12-5-6+50)/4 = 51/4 = 12.75
注意:
1 <= k
<= n
<= 30,000。
所给数据范围 [-10,000,10,000]。
方法:滑动窗口
假设我们已经索引从 i到 i+k 子数组和为 x。要知道索引从 i+1 到 i+k+1子数组和,只需要从 x 减去 sum[i],加上 sum[i+k+1] 即可。 根据此方法可以获得长度为 k 的子数组最大平均值。
class Solution:
def findMaxAverage(self, nums: List[int], k: int) -> float:
ksum = sum(nums[:k])
res = ksum
for i in range(k, len(nums)):
ksum += nums[i]-nums[i-k]
res = max(res, ksum)
return res/k
买卖股票问题
分析:
每天都有3种【选择】:买入(buy)、卖出(sell)、无操作(rest)。但并不是每天都可以任意选择这3种选择,sell必须在buy之后,buy必须在sell之后。rest操作,一种是buy之后的rest(持有了股票),一种是sell之后的rest(没有持有股票)。
有3个【状态】,第1个是天数,第2个是允许交易的最大次数,第3个是当前的持有状态(1表示持有股票,0表示没有持有)
121. 买卖股票的最佳时机(简单)
(找到一个数组中相差最大的数,小的在前面,大的在后面)
给定一个数组,表示每天的股票价格。
你可以进行一次交易(先买再卖),问如何能得到最大利润。
8 1 3 4 7 3 4
最大利润为6,买1卖7
分析:
记录一个列表中的最小值,不断更新
记录列表中的最大利润,不断更新
初始化:将第一个数作为最小值,将0设为最大利润
for循环,如果后面的数比记录的最小值小,更新最小值;如果更新完最小值,最大利润比0大,更新最大利润
代码:
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if len(prices) < 2:
return 0
min_price = prices[0]
max_profit = 0
for price in prices:
max_profit = max(price-min_price, max_profit)
min_price = min(price, min_price)
return max_profit
测试:
prices = [7,1,5,3,6,4]
maxProfit(prices)
122. 买卖股票的最佳时机 II(简单)
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
示例 3:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
提示:
1 <= prices.length <= 3 * 10 ^ 4
0 <= prices[i] <= 10 ^ 4
分析:
考虑到【不能同时参与多笔交易】,因此每天交易结束后只可能手里有一支股票或者没有股票的状态。
定义状态dp[i][0]表示第i天交易完后手里没有股票的最大利润,dp[i][1]表示第i天交易完后手里持有一支股票的最大利润(i从0开始)。
考虑dp[i][0]的转移方程,如果
解题思路:
股票买卖策略:
单独交易日: 设今天价格p1、明天价格p2,则今天买入、明天卖出可赚取金额 p2−p1(负值代表亏损)。
连续上涨交易日: 设此上涨交易日股票价格分别为 p1, p2, ... , pn,则第一天买最后一天卖收益最大,即 pn−p1;等价于每天都买卖,即 pn - p1=(p2 - p1)+(p3 - p2)+...+(pn - p_n-1)
连续下降交易日: 则不买卖收益最大,即不会亏钱。
算法流程:
遍历整个股票交易日价格列表 price,策略是所有上涨交易日都买卖(赚到所有利润),所有下降交易日都不买卖(永不亏钱)。
设 tmp 为第 i-1 日买入与第 i 日卖出赚取的利润,即 tmp = prices[i] - prices[i - 1] ;
当该天利润为正 tmp > 0,则将利润加入总利润 profit;当利润为 0或为负,则直接跳过;
遍历完成后,返回总利润 profit。
复杂度分析:
时间复杂度 O(N): 只需遍历一次price;
空间复杂度 O(1): 变量使用常数额外空间。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
profit = 0
n = len(prices)
for i in range(1, n):
temp = prices[i] - prices[i-1]
if temp > 0:
profit += temp
return profit
123. 买卖股票的最佳时机 III(困难)
给定一个数组,表示每天的股票价格。
你可以进行两次的交易(先买再卖,在再次买入时,必须将之前的股票卖出),问如何得到最大利润。
输入:prices = [2, 4, 6, 1, 3, 8, 3]
输出:11([2, 6]、[1, 8]是两次进行买入卖出的时机)
方法1:DP
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices:
return 0
n = len(prices)
dp = [[[0]*2 for _ in range(3)] for _ in range(n)]
for i in range(0, n):
for k in range(2, 0, -1):
if i - 1 == -1:
dp[i-1][k][0], dp[i-1][k][1] = 0, float('-inf')
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
return dp[n-1][2][0]
方法2
分析:
首先构建前半部分的利润表,方式和121.买股票的最佳时机(仅一次交易)中的方法一样,更新最小买入价格,然后求每天卖出的最大利润(图中 前半部分的最大利润表)
然后构建后半部分的利润表,为了将构建两个利润表在一次遍历中完成,我们采用从后往前填充后半部分最大利润表。(图中 后半部分最大利润)
方法:从后往前更新最大卖出价格 max, 计算在每天买入的最大利润,这样就可以算出后半部分的利润
最后遍历一遍,求两个利润表和的最大值,就是我们所求的至多两次交易的最大利润。