双指针学习指南
前置芝士
尺取法
快指针先动,慢指针跟行,一伸一缩。
两个线段获得的最多奖品
[problem description]
在 X轴 上有一些奖品。给你一个整数数组 prizePositions
,它按照 非递减 顺序排列,其中 prizePositions[i]
是第 i
件奖品的位置。数轴上一个位置可能会有多件奖品。再给你一个整数 k
。
你可以选择两个端点为整数的线段。每个线段的长度都必须是 k
。你可以获得位置在任一线段上的所有奖品(包括线段的两个端点)。注意,两个线段可能会有相交。
- 比方说
k = 2
,你可以选择线段[1, 3]
和[2, 4]
,你可以获得满足1 <= prizePositions[i] <= 3
或者2 <= prizePositions[i] <= 4
的所有奖品 i 。
请你返回在选择两个最优线段的前提下,可以获得的 最多 奖品数目。
示例 1:
输入:prizePositions = [1,1,2,2,3,3,5], k = 2 输出:7 解释:这个例子中,你可以选择线段 [1, 3] 和 [3, 5] ,获得 7 个奖品。
示例 2:
[3, 3]
[4, 4] ,获得 2 个奖品。
提示:
1 <= prizePositions.length <= 10<sup>5</sup>
1 <= prizePositions[i] <= 10<sup>9</sup>
0 <= k <= 10<sup>9</sup>
prizePositions
有序非递减。
[solved]
我们可以强制让第二条线段的右端点恰好落在奖品上,设第二条线段右端点在 prizePositions[right] 时,左端点最远覆盖了 prizePositions[left],我们需要知道在 prizePositions[left]左侧的第一条线段最多可以覆盖多少个奖品。
那么,先想想只有一条线段要怎么做。
使用双指针,设线段右端点在 prizePositions[right] 时,左端点最远覆盖了 prizePositions[left],那么当前覆盖的奖品个数为 right−left+1。
同时,用一个数组 pre[right+1] 记录线段右端点不超过 prizePositions[right]时最多可以覆盖多少个奖品。下标错开一位是为了方便下面计算。
初始 pre[0]=0。根据 pre的定义,有pre[right+1]=max(pre[right],right−left+1)
回到第二条线段的计算,根据开头说的,此时最多可以覆盖的奖品数为right−left+1+pre[left]。
这里 pre[left]表示第一条线段右端点不超过 prizePositions[left−1]时最多可以覆盖多少个奖品。
遍历过程中取上式的最大值,即为答案。
由于我们遍历了所有的奖品作为第二条线段的右端点,且通过 pre[left] 保证第一条线段与第二条线段没有任何交点,且第一条线段覆盖了第二条线段左侧的最多奖品。那么这样遍历后,算出的答案就一定是所有情况中的最大值。
def maximizeWin(self, prizePositions: List[int], k: int) -> int:
pre = [0] * (len(prizePositions) + 1)
ans = left = 0
for right, p in enumerate(prizePositions):
while p - prizePositions[left] > k:
left += 1
ans = max(ans, right - left + 1 + pre[left])
pre[right + 1] = max(pre[right], right - left + 1)
return ans
最小得分子序列(前后缀分解)
[problem description]
给你两个字符串 s
和 t
。
你可以从字符串 t
中删除任意数目的字符。
如果没有从字符串 t
中删除字符,那么得分为 0
,否则:
- 令
left
为删除字符中的最小下标。 - 令
right
为删除字符中的最大下标。
字符串的得分为 right - left + 1
。
请你返回使 t
成为 s
子序列的最小得分,注:空串是任何字符串的子序列。
一个字符串的 子序列 是从原字符串中删除一些字符后(也可以一个也不删除),剩余字符不改变顺序得到的字符串。(比方说 "ace"
是 "abcde" 的子序列,但是 "aec"
不是)。
示例 1:
输入:s = "abacaba", t = "bzaa" 输出:1 解释:这个例子中,我们删除下标 1 处的字符 "z" (下标从 0 开始)。 字符串 t 变为 "baa" ,它是字符串 "abacaba" 的子序列,得分为 1 - 1 + 1 = 1 。 1 是能得到的最小得分。
示例 2:
输入:s = "cde", t = "xyz" 输出:3 解释:这个例子中,我们将下标为 0, 1 和 2 处的字符 "x" ,"y" 和 "z" 删除(下标从 0 开始)。 字符串变成 "" ,它是字符串 "cde" 的子序列,得分为 2 - 0 + 1 = 3 。 3 是能得到的最小得分。
1 <= s.length, t.length <= 10^5
s
和 t
都只包含小写英文字母。
[solved]
(1)如果对于t的一个子区间,删掉了其中的几个字符,满足t是s的子序列,那么此时我们把整个子区间都删了,也一定满足t是s的子序列,且答案不变 。
(2)所以问题转换为,删除t的一个子区间,使t的前缀后缀组成的串满足是s的子序列,求删除的子区间的最小长度。
(3)预处理,[定义]
- dp1[i] 表示s的前i个字符所能匹配到t的前缀的最后一个字符的位置。
- dp2[i] 表示s的后i个字符所能匹配到t的后缀的第一个字符的位置。
(4)得到这两个数组后,答案就是dp2[i]-dp1[i-1]-1的最小值。其中要注意如果出现dp2[i]小于dp[i-1],那么说明整个t必然是s的子序列,所以不用删除。对于s来说,两个不相交的子序列加起来一定还是s的子序列 ,dp1[i]和dp2[i+1]就相当与si前缀的子序列和si+1的后缀的子序列,这两个组起来一定还是s的子序列,所以直接把他们能匹配到的t的前缀和后缀中间删了就行了。
class Solution {
public:
int minimumScore(string s, string t) {
int n=s.length(),m=t.length(),suf[n+1];
suf[n]=m;
for(int i=n-1,j=m-1;i>=0;i--){
if(j>=0&&s[i]==t[j]) --j;
suf[i]=j+1;
// cout<<suf[i]<<" ";
}
cout<<endl;
//pre[i] 为 s[:i]对应的t的最长前缀的结束下标。
// suf[i]:s[i:]对应的t的最长后缀的开始下标
//那么删除的子串就是从 pre[i]+1到 suf[i]−1这段,答案就是 suf[i]−pre[i]−1的最小值。
//可以先计算 suf,然后一边计算 pre,一边更新最小值,所以 pre可以省略。
int res=suf[0];
if(res==0) return 0;
for(int i=0,j=0;i<n;i++){
if(s[i]==t[j]) res=min(res,suf[i+1]-++j);
}
return res;
}
};
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】