双指针学习指南

前置芝士

尺取法

快指针先动,慢指针跟行,一伸一缩。

两个线段获得的最多奖品

[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:

输入:prizePositions = [1,2,3,4], k = 0
输出: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]

给你两个字符串 st

你可以从字符串 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

st 都只包含小写英文字母。

[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;
    }
};
posted @ 2023-10-18 00:04  White_Sheep  阅读(1)  评论(0编辑  收藏  举报