目录
如果数组的数值可以取负数,是不能使用双指针来求最优解的,就是因为不满足单调性,这种题目其实比较难的是一种抽象问题的能力,有的题目需要把问题做一个转化,首先需要判断是否满足单调性,如果满足,就需要把问题转化为一个可以使用双指针去解决的一个滑动窗口问题。
1 算法模版
1.1 最长上升子数组
n = int(input())
w = list(map(int, input().split()))
res = 0 # 最大长度
l = 0
while l < n:
r = l
# 如果下一个元素大于当前元素,右边界向右移动
while r + 1 < n and w[r + 1] > w[r]:
r += 1
# 更新最大长度
res = max(res, r - l + 1)
l = r + 1
print(res)
区别于 最长递增子序列
- 子数组一定是连续的
- 子序列不一定连续
1.2 最大值/最大长度
这一类题目,也是首先定义两个指针
l
,
r
l,r
l,r,指向数组的起始位置,然后
r
r
r 指针先移动,移动到某一个区间,不满足条件的时候(比如题目要求区间和不大于某一个正整数,或者区间中元素
x
x
x 的个数
≤
k
\le k
≤k),
l
l
l 指针右移,直到满足条件。
然后满足条件的时候更新最大值/最大长度。下面提供模版代码
def query(nums):
n = len(nums)
res = 0 # res为最大值/最大长度
l = r = 0 # 定义两个指针l,r
while r < n:
# ...省略代码,需要维护区间相关信息,例如区间和,或者区间某个元素数量
while not check(): # 当前区间不满足条件
l += 1 # 移动左指针
res = max(res, cur) # 更新最大值/最大长度
r += 1
return res
# 这是一个示例的check函数,你需要根据实际情况来实现它
def check():
# ...省略代码
return True
1.3 最小值/最小长度
这一类题目,也是首先定义两个指针 l , r l,r l,r,指向数组的起始位置,然后 r r r 指针先移动,移动到某一个区间,满足条件的时候(比如题目要求区间和大于某一个正整数,或者区间中元素 x x x 的个数 ≥ k \ge k ≥k), l l l 指针右移,直到不满足条件。然后满足条件的时候更新最小值/最小长度。下面提供模版代码
def query(nums):
n = len(nums)
res = 10**9 # res为最小值/最小长度
l = r = 0 # 定义两个指针l,r
while r < n:
# ...省略代码,需要维护区间相关信息,例如区间和,或者区间某个元素数量
while check(): # 当前区间满足条件
res = min(res, cur) # 更新最小值/最小长度
l += 1 # 移动左指针
r += 1
return res
# 这是一个示例的check函数,你需要根据实际情况来实现它
def check():
# ...省略代码
return True
1.4 方案数
这一类题目,一般是要求计算出满足题目条件的子数组的方案数,这个条件可能是:子数组和 ≤ t a r g e t \le target ≤target,子数组乘积 ≤ k \le k ≤k 等等,这种问题,首先我们考虑一个暴力求解的方式,那就是枚举所有可能的子数组,我们可以分别枚举子数组的左右两个端点,整体时间复杂度为 O ( n 2 ) O(n^2) O(n2),代码如下所示
res = 0 # 方案数
for l in range(n):
sum_val = 0
for r in range(l, n):
sum_val += w[r]
if sum_val <= target:
res += 1
print(res)
我们可以考虑一个优化的做法,首先这个做法的前提是子数组满足单调性,就是以 l l l开头的子数组,子数组长度越长,区间和越大/区间元素累乘结果越大。然后我们可以定义两个双指针 l , r l,r l,r,分别指向数组的起始位置,当 l , r l,r l,r指向的区间不满足题目条件时, l l l指针右移,直到满足条件为止。当 [ l , r ] [l,r] [l,r]区间满足条件时,由上述的单调性我们可知,区间 [ r , r ] , [ r − 1 , r ] , . . . , [ l , r ] [r,r],[r-1,r],...,[l,r] [r,r],[r−1,r],...,[l,r]都是满足条件的,因此一共有 r − l + 1 r-l+1 r−l+1个区间满足条件,直接对方案数进行累加即可,然后继续移动 r r r指针,直到移动到数组末尾。
def check(): # 这是一个示例的check函数,你需要根据实际情况来实现它
# ...省略代码
return True
def query(nums):
n = len(nums)
res = 0 # res为方案数
l = r = 0 # 定义两个指针l,r
# ...省略代码,需要维护区间相关信息,例如区间和,或者区间某个元素数量
while r < n:
while not check(): # 当前区间不满足条件
l += 1 # 移动左指针
res += r - l + 1 # 更新方案数
r += 1
return res
2 判断子序列
本题是一个多指针题型的一个模板题,可以使用两个指针分别指向字符串 s s s 和 t t t 的起始位置,如果当前位置有 s [ l ] = t [ r ] s[l]=t[r] s[l]=t[r],则 l + + , r + + l++,r++ l++,r++,如果不相等,则 r + + r++ r++,如果最终l指向字符串 s s s 的末尾位置,则说明字符串 s s s 是 t t t 的子序列。
def isSubsequence(s, t):
l, r = 0, 0
n, m = len(s), len(t)
while l < n and r < m:
if s[l] == t[r]:
l += 1
r += 1
return l == n
3 运动员和训练师的最大匹配数
排序+双指针,对于一个运动员 p l a y e r [ i ] player[i] player[i] 来说,他应该尽可能匹配比他的数值大,且尽可能接近他的训练师 t r a i n e r s [ i ] trainers[i] trainers[i],如果匹配的 t r a i n e r s [ i ] trainers[i] trainers[i] 过大,会导致其他运动员可能无法匹配,因此我们可以直接把这两个数组排序,然后使用双指针模拟即可,和上面一道判断子序列的模板题的代码基本上一模一样,就是多了一个排序的代码。
class Solution:
def matchPlayersAndTrainers(self, players, trainers):
players.sort()
trainers.sort()
n = len(players)
m = len(trainers)
l = 0
r = 0
res = 0
while l < n and r < m:
if players[l] <= trainers[r]:
l += 1
res += 1
r += 1
return res
4 骑车路线
参考上述最长上升子数组的模版即可。
while True:
try:
n = int(input())
p = list(map(int, input().split()))
res = 0
l = 0
while l < n:
r = l
while r + 1 < n and p[r] <= p[r + 1]:
r += 1
res = max(res, p[r] - p[l])
l = r + 1
print(res)
except:
break
5 删掉一个元素以后全为 1 的最长子数组
首先观察数组,有这样一个特点:元素的值不是0就是1,那么则说明,该子数组的区间和是非递减的(对于从某一个位置开始的区间,区间长度越大,区间和越大),因此是 满足单调性 的(如果本题数组元素有负数,则不可以使用双指针算法求解),这里求解的是最大长度,可以使用第二个模版:最大值/最大长度。
class Solution:
def longestSubarray(self, nums: List[int]) -> int:
# 题目转化为在原数组nums中,返回一个最长的且只包含1个0的非空子数组的长度
# 滑动窗口方法:窗口内最多只有一个0
n=len(nums)
left,right=0,0
res=0
cnt=0
while right<n:
if nums[right]==0:
cnt+=1
while cnt>1:
if nums[left]==0:
cnt-=1
left+=1
res=max(res,right-left)
right+=1
return res
6 将 x 减到 0 的最小操作数
本题可以从最左边和最右边选择一个数,将其对 x x x作差,这样去思考的话,对应的区间是不连续的,但是我们反过来思考:最终剩下的区间一定是一段连续的区间。我们定义数组的元素总和为 s u m sum sum,那么移除若干个元素之后剩下的区间和则为 s u m − x sum-x sum−x。那么就把问题转换为:求一个最长的连续区间(因为要使得删除的元素最小化,则剩下的区间长度一定是最大的),使得其满足区间和等于 s u m − x sum-x sum−x,我们利用上述模版即可,和上一道LeetCode题基本上一模一样的思路,代码也差别不大。
class Solution:
def minOperations(self, nums: List[int], x: int) -> int:
total=sum(nums)
target=total-x #将问题转化为区间和为target的最大长度
if target<0: #一定无解
return -1
n=len(nums)
res=-1
l,r,sum_val=0,0,0 #定义左指针、右指针、区间和
while r<n:
sum_val+=nums[r]
while sum_val>target:
sum_val-=nums[l]
l+=1
if sum_val==target: #更新最大值
res=max(res,r-l+1)
r+=1
if res==-1:
return res
return n-res
7 长度最小的子数组
首先注意到,数组所有的元素都是正整数,这说明该子数组的区间和是非递减的(对于从某一个位置开始的区间,区间长度越大,区间和越大),因此是满足单调性的(如果本题数组元素有负数,则不可以使用双指针算法求解),这里求解的是最小长度,因此可以使用上述双指针的第三个模版求解。
class Solution:
def minSubArrayLen(self, target: int, nums: List[int]) -> int:
inf=float("inf")
res=inf
n=len(nums)
l,r,s=0,0,0
while r<n:
s+=nums[r]
while s>=target:
res=min(res,r-l+1)
s-=nums[l]
l+=1
r+=1
return 0 if res==inf else res
8 三值字符串
同上。
def solve():
s = input().strip()
n = len(s)
cnts = [0, 0, 0] # 分别统计1,2,3字符的个数
res = n+1 # 初始化最小值,注意不能写成res = n,特例s=123
l = r = 0
while r < n:
cnts[ord(s[r]) - ord('1')] += 1
while cnts[0] > 0 and cnts[1] > 0 and cnts[2] > 0: # 区间同时包含字符1,2,3
res = min(res, r - l + 1)
cnts[ord(s[l]) - ord('1')] -= 1
l += 1
r += 1
print(0 if res == n+1 else res)
T = int(input().strip())
for _ in range(T):
solve()
9 乘积小于 K 的子数组
双指针求方案数,可以参考双指针算法模版的第四个模版。
class Solution:
def numSubarrayProductLessThanK(self, nums, k):
n = len(nums)
res = 0
if k <= 1:
return 0
l,r=0,0
total = 1
while r < n:
total *= nums[r]
while total >= k: # 当前区间不满足条件,l右移
total /= nums[l]
l += 1
res += r - l + 1 # [l,r]区间有r-l+1个子区间都满足题意
r+=1
return res
10 统计得分小于 K 的子数组数目
同上。
class Solution:
def countSubarrays(self, nums: List[int], k: int) -> int:
l,r=0,0
res=0
sum_val=0
n=len(nums)
while r<n:
sum_val+=nums[r]
while sum_val*(r-l+1)>=k:
sum_val-=nums[l]
l+=1
res+=r-l+1
r+=1
return res
11 数组的删除方案
首先我们考虑,删除区间的长度越短,则剩余数字的乘积越大,乘积越大,则说明其末尾0的个数越多(至少不会减少),因此是符合单调性的,可以考虑使用双指针算法求解。末尾0的个数是什么呢?这个很重要,其实就是一个数字中因子 10 10 10的个数,例如300有2个0,300可以看成 300 = 3 × 1 0 2 300=3\times 10^2 300=3×102,但是由于10不是质数,它有两个质因子2和5,因此问题就转换为一个数字中因子2和因子5个数的最小值,比如 60 = 2 2 × 3 × 5 1 60=2^2\times 3\times 5^1 60=22×3×51,因此60的末尾0为 m i n ( 1 , 2 ) = 1 min(1,2)=1 min(1,2)=1。
n, k = list(map(int, input().split()))
# 统计a[i]的因子2的数量
a2 = [0] * n
# 统计a[i]的因子5的数量
a5 = [0] * n
a = list(map(int, input().split()))
# 计算每个数的因子2和因子5的数量
for i in range(n):
while a[i] % 2 == 0:
a[i] //= 2
a2[i] += 1
while a[i] % 5 == 0:
a[i] //= 5
a5[i] += 1
cnt2 = sum(a2) # 因子2的总数量
cnt5 = sum(a5) # 因子5的总数量
l,r = 0,0
ans = 0
# 使用滑动窗口计算满足条件的子数组数量
while r<n:
cnt2 -= a2[r]
cnt5 -= a5[r]
while l <= r and min(cnt2, cnt5) < k: #当前区间不满足条件(因子2和因子5的数量都小于k)
cnt2 += a2[l] # 移动左指针,增加因子2的数量
cnt5 += a5[l] # 移动左指针,增加因子5的数量
l += 1
ans += r - l + 1
r+=1
print(ans)