最大、括号
最大
1.LeetCode-239 滑动窗口的最大值
方法三:分块 + 预处理
分块预处理:将数组从左到右按每 k 个元素分为一组 (最后一组元素可能会不是k个),分块数组 后 预处理产生两个 辅助数组。前缀最大值数组、后缀最大值数组。
前缀最大值数组,每个分块 index 从 0 -> k-1, 前缀block[0], block[0到1], block[0到2],… block[0到k-1]的最大值
后缀最大值数组,每个分块 index 从 k-1 -> 0, 后缀block[k-1], block[k-1到k-2], block[k-1到0]的最大值
使用 i 表示元素索引下标,当 i 从 1 到 n-1, 对应 k 个元素的区间 {num[i], num[i+k-1]} 可能 属于分块数组中的同一块,也可能横跨两个块。那么当前窗口的最大值 前缀最大值 和 后缀最大值其中大的那个。
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
prefix_array = [nums[0]] * n
suffix_array = [nums[-1]] * n
for i in range(1, n): # [1, 2, 3, n-1]
if i % k == 0: # i % k == 0, 刚好是每个分块的起始下标
prefix_array[i] = nums[i]
else:
prefix_array[i] = max(prefix_array[i-1], nums[i])
j = n - 1 - i
if (j+1) % k == 0: # (j + 1) % k == 0, 每个分块的终止下标
suffix_array[j] = nums[j]
else:
suffix_array[j] = max(suffix_array[j + 1], nums[j])
# print(i, j, prefix_array, suffix_array)
res = []
for i in range(0, n - k + 1): # [0, 1, 2, ...., n - k]
res.append(max(suffix_array[i], prefix_array[i+k-1]))
return res
[1,3,-1,-3,5,3,6,7]
1 6 8 [1, 3, 1, 1, 1, 1, 1, 1] [7, 7, 7, 7, 7, 7, 7, 7]
2 5 8 [1, 3, 3, 1, 1, 1, 1, 1] [7, 7, 7, 7, 7, 3, 7, 7]
3 4 8 [1, 3, 3, -3, 1, 1, 1, 1] [7, 7, 7, 7, 5, 3, 7, 7]
4 3 8 [1, 3, 3, -3, 5, 1, 1, 1] [7, 7, 7, 5, 5, 3, 7, 7]
5 2 8 [1, 3, 3, -3, 5, 5, 1, 1] [7, 7, -1, 5, 5, 3, 7, 7]
6 1 8 [1, 3, 3, -3, 5, 5, 6, 1] [7, 3, -1, 5, 5, 3, 7, 7]
7 0 8 [1, 3, 3, -3, 5, 5, 6, 7] [3, 3, -1, 5, 5, 3, 7, 7]
[1, 3, 3, -3, 5, 5, 6, 7] [3, 3, -1, 5, 5, 3, 7, 7]
2.LeetCode-53 最大和 连续子数组
# 动态规划数组的定义: dp[i] 最大和, 这个子数组是以 nums[i] 结尾的,dp[i] 只与 dp[i-1] 有关
# 状态转移方程: dp[i] = max(nums[i], nums[i] + dp[i-1])
# key1: 用 nums[i] 更新 dp[i] 时,nums[i] 可以成为已有数组的尾部,也可以成为单元素数组,这两种情况 nums[i] 都是作为子数组的结尾
# key2: 如果某一块数组的和为负数,这一块不会作为和最大连续子数组的结尾(多元素数组/单元素数组),因为去掉这个部分,后半段加和会更大。 空间约减,将空间复杂度降为o(1)
class Solution:
def maxSubArray0(self, nums: List[int]) -> int:
"""
@note 暴力法-遍历(n-1)*n/2个子数组,找出最大和
求和次数减少:用空间换时间,已经算过的和可以用二维数组存起来,如下
"""
n = len(nums)
dp = [[0] * n for _ in range(n)]
res = nums[0] # len(nums) >= 1 的假设
for i in range(n):
sub_arr_sum[i][i] = nums[i]
res = max(res, nums[i])
for i in range(n-2, -1, -1):
for j in range(i+1, n):
sub_arr_sum[i][j] = sub_arr_sum[i][j-1] + nums[j]
res = max(res, sub_arr_sum[i][j])
return res
def maxSubArray(self, nums: List[int]) -> int:
"""
@note 一维 dp
# [-2, 1, -3, 4, -1, 2, 1]
# i = 0, dp[0] = 0
# i = 1, dp[1] = max(1, 1) = 1
# i = 2, dp[2] = max(-3, 1+-3) = -2
# i = 3, dp[3] = max(4, 4+-2) = 4
# i = 4, dp[4] = max(-1, 4 -1) = 3
# i = 5, dp[5] = max(2, 2 + 3) = 5
"""
n = len(nums)
dp = [0] * n
dp[0] = nums[0]
res = dp[0]
for i in range(1, n):
dp[i] = max(nums[i], nums[i] + dp[i-1])
res = max(res, dp[i])
return res
def maxSubArray2(self, nums: List[int]) -> int:
"""
@note 空间约减后,空间复杂度为0(1)
dp 非负数 最大子数组和,如果为0,说明nums[i+1]更新dp数组时会重新开辟一个新的子数组
"""
res = float("-INF")
dp = 0
for val in nums:
dp += val # [-1] 先加,更新答案,确定是否归零
res = max(dp, res)
dp = max(dp, 0)
return res
3.LeetCode-152 乘积最大的子数组。
给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
直接思路:
f
m
a
x
(
i
)
f_{max}(i)
fmax(i)表示以第i个元素结尾的乘积最大的子数组的积,状态转移方程为
f
m
a
x
(
i
)
=
max
i
=
1
n
{
f
(
i
−
1
)
∗
a
i
,
a
i
}
f_{max}(i)=\max_{i=1}^n\{f(i-1)*a_i,a_i\}
fmax(i)=i=1maxn{f(i−1)∗ai,ai}
即,
f
m
a
x
(
i
)
f_{max}(i)
fmax(i)可以考虑nums[i]加入前面
f
m
a
x
(
i
−
1
)
f_{max}(i-1)
fmax(i−1)对应的一段,或者自成一段,这两种情况取最大值,求出所有的f_i之后,选取一个最大的作为结果。
核心问题:当前位置的最优解不一定是由前一个位置的最优解得到。因为存在正负数
如果当前的数是负数的话,我们希望以num[i-1]为结尾的某一个段的积也是一个负数,负负可以为正。
如果当前数为正,我们希望以num[i-1]为结尾的某一个段的积也是一个正数,正的越多,乘积完越大。
所以再维护一个
f
m
i
n
(
i
)
f_{min}(i)
fmin(i)表示以第i个元素结尾的乘积最小的子数组的积:
f
m
a
x
(
i
)
=
max
{
f
m
a
x
(
i
−
1
)
∗
a
i
,
f
m
i
n
(
i
−
1
)
∗
a
i
,
a
i
}
f
m
i
n
(
i
)
=
min
{
f
m
a
x
(
i
−
1
)
∗
a
i
,
f
m
i
n
(
i
−
1
)
∗
a
i
,
a
i
}
f_{max}(i)=\max\{f_{max}(i-1)*a_i,f_{min}(i-1)*a_i,a_i\}\\ f_{min}(i)=\min\{f_{max}(i-1)*a_i,f_{min}(i-1)*a_i,a_i\}
fmax(i)=max{fmax(i−1)∗ai,fmin(i−1)∗ai,ai}fmin(i)=min{fmax(i−1)∗ai,fmin(i−1)∗ai,ai}
def maxProduct(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
pre_max, pre_min = 1, 1
res = float("-INF")
for val in nums:
cur_max = max(pre_max * val, pre_min * val, val)
cur_min = min(pre_max * val, pre_min * val, val)
pre_max, pre_min = cur_max, cur_min
res = max(res, cur_max)
return res
4.剑指 Offer 14- I. 剪绳子为k个整数段,使各个段成绩最大
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
DP
dp[i]: 长度为i 绳子至少剪了一次的最长长度
d
p
[
i
]
=
m
a
x
(
d
p
[
j
]
∗
(
i
−
j
)
,
j
∗
(
i
−
j
)
,
d
p
[
i
]
)
,
j
∈
[
1
,
i
−
1
]
dp[i] = max(dp[j]*(i-j),j*(i-j),dp[i]),j\in[1,i-1]
dp[i]=max(dp[j]∗(i−j),j∗(i−j),dp[i]),j∈[1,i−1]
class Solution(object):
def cuttingRope(self, n):
"""
:type n: int
:rtype: int
"""
dp = [0]*(n+1)
dp[1] = 1
for i in range(2,n+1):
for j in range(1,i):
dp[i]= max(dp[i],dp[j]*(i-j),j*(i-j))
return dp[-1]
n^2复杂度的DP
数学推导
通过数学不等式推到,可以得到,当每段长度为3时乘积最大。所以尽可能分为三段,最后一段依据具体情况判断:
class Solution(object):
def cuttingRope(self, n):
"""
:type n: int
:rtype: int
"""
if n<=3:
return n-1
s, mod = n //3, n % 3
if mod == 0:
res = 3**s
elif mod == 1:
res = 3**(s-1)*4
else:
res = 3**s*2
return res%(10**9+7)
5.盛最多水的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。
class Solution:
def maxArea1(self, height: List[int]) -> int:
"""
@note 暴力法,遍历所有可以盛水的容器, 49 / 61 个通过的测试用例
"""
n = len(height)
vol_arrary = [[0] * n for _ in range(n)]
res = 0
for i in range(0, n):
for j in range(i+1, n):
w = j - i
h = min(height[i], height[j])
vol_arrary[i][j] = w * h
res = max(res, vol_arrary[i][j])
return res
def maxArea2(self, height: List[int]) -> int:
"""
@note 动态规划-key 在左右边界往里缩的时候,让短板边长,才有使盛水量变多的可能
array[i][j] = w * h, h = min(height[i], height[j])
每次移动短板,有效成水高度h 有变大的可能, 面积可能会变大
每次移动高板,有效成水高度h, 不变/变小 面积一定变小
"""
n = len(height)
left, right = 0, n-1
res = 0
while(left < right):
vol = (right - left) * min(height[left], height[right])
res = max(res, vol)
if height[left] <= height[right]:
left += 1
else:
right -= 1
return res
括号
1.LeetCode-22 括号生成–各种括号排列组合
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
输入:n = 3
输出:[
"((()))",
"(()())",
"(())()",
"()(())",
"()()()"
]
回溯法
def generateParenthesis(self, n):
res = []
def back_track(s,left,right):
if len(s) == 2*n:
res.append(s)
return
if left < n:
back_track(s+"(",left+1,right)
if right< left: # 保证不会生成不合理的括号 ,必须要有配对的左括号已经存在
back_track(s+")",left,right+1)
back_track("",0,0)
return res
2.LeetCode-20 有效括号(是否)–堆栈
给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串,判断字符串是否有效。
有效字符串需满足:左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
输入: "()[]{}"
输出: true
机理:合理的右括号,总能找到对应的左括号。多出左括号或者右括号都是不对的。多对括号复合,拿掉一对合理的括号,并不改变括号复合的合理性。
堆栈:栈顶匹配。左括号入栈,配对右括号,弹出对应的左括号;不配对右括号,入栈。遍历完字符串,查看栈是否为空,空则有效,非空,无效。
def isValid(self, s):
dit={")":"(","]":"[","}":"{"}
stack=[]
for char in s:
if char in dit: # char为右括号
left=stack.pop() if stack else "#"
if dit[char]!=left:
return False
else: # char 为左括号入栈
stack.append(char)
return True if len(stack) == 0 else False
多括号行为,单括号可以直接用计数法
3.LeetCode-32 最长有效括号(长度)–dp
给定一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度。
输入: "(()"
输出: 2
解释: 最长有效括号子串为 "()"
dp[i] 表示以下标 i字符结尾的最长有效括号的长度,(以s[i]结尾能构成的有效字符串的长度)依据s[i] 与之前的括号的配对情况,更行dp数组。
显然有效的子串一定以)结尾,因此我们可以知道以(结尾的子串对应的dp 值必定为 0,我们只需要求解 )在dp 数组中对应位置的值。
# 1.s[i]==")" and s[i-1]=="(" dp[i] = dp[i-2]+2
# 2.s[i]==")" and s[i-1]==")" dp[i] = dp[i-1] + dp[i-dp[i-1]-2]+2 下标的合理性
def longestValidParentheses(self, s):
n=len(s)
if n<2:
return 0
dp=[0]*n
res=0
for i in range(1,n):
if i==1:
if s[i]==")" and s[i-1]=="(":
dp[1]=2
else:
if s[i]==")" and s[i-1]=="(":
dp[i]=dp[i-2]+2
if s[i]==")" and s[i-1]==")":
if i-1-dp[i-1]>=0 and s[i-1-dp[i-1]]=="(":
dp[i]=dp[i-1]+2 # index 有效性没有验证
if i-2-dp[i-1]>=0:
dp[i]+=dp[i-2-dp[i-1]]
res=max(res,dp[i])
return res
4.LeetCode-301删除无效括号 --多种删除方式
删除最小数量的无效括号,使得输入的字符串有效,返回所有可能的结果。
说明: 输入可能包含了除 ( 和 ) 以外的字符。
输入: "()())()"
输出: ["()()()", "(())()"]
考虑所有的删除情况,采用广度优先,第一层为原字符串表达式,第二层为删除一个字符,第三层为删除两个字符的情况,不断广度优先遍历,直至找到第一个有效的删除数量,即为最少数量
DFS:要求删除的括号最少,每次删除一个,观察删除后的字符串是否合法,如果已经合法,不用继续删除。
BFS:本层level和下一层level 之间的关系:本层level每个元素都拿出来,列举删除一个括号后的所有可能,添加到下一层level 中。
解决重复性问题:吧level 中的list换成set
检查括号是否合法:堆栈法
用filter(func, param) 可以得到param中所有符合条件的元素。
class Solution(object):
def removeInvalidParentheses(self, s):
"""
:type s: str
:rtype: List[str]
"""
def is_valid(string):
count = 0
for char in string:
if char == "(":
count += 1
elif char == ")":
count -= 1
if count < 0: # 中途中计数器如果小于0说明,不明多余右括号出现
return False
return count == 0
# BFS
level = {s} # 用set避免重复
while True:
valid = list(filter(isValid, level)) # 判断同一层的所有删除结果时候存在有效备选
if valid: return valid
# 下一层level
next_level = set()
for item in level:
for i in range(len(item)):
if item[i] in "()": # 如果item[i]这个char是个括号就删了,如果不是括号就留着
next_level.add(item[:i]+item[i+1:])
level = next_level