LeetCode 1717. 删除子字符串的最大得分

1717. 删除子字符串的最大得分

给你一个字符串 s 和两个整数 x 和 y 。你可以执行下面两种操作任意次。

  • 删除子字符串 "ab" 并得到 x 分。
    • 比方说,从 "cabxbae" 删除 ab ,得到 "cxbae" 。
  • 删除子字符串"ba" 并得到 y 分。
    • 比方说,从 "cabxbae" 删除 ba ,得到 "cabxe" 。

请返回对 s 字符串执行上面操作若干次能得到的最大得分。

示例 1:

输入:s = "cdbcbbaaabab", x = 4, y = 5
输出:19
解释:
- 删除 "cdbcbbaaabab" 中加粗的 "ba" ,得到 s = "cdbcbbaaab" ,加 5 分。
- 删除 "cdbcbbaaab" 中加粗的 "ab" ,得到 s = "cdbcbbaa" ,加 4 分。
- 删除 "cdbcbbaa" 中加粗的 "ba" ,得到 s = "cdbcba" ,加 5 分。
- 删除 "cdbcba" 中加粗的 "ba" ,得到 s = "cdbc" ,加 5 分。
总得分为 5 + 4 + 5 + 5 = 19 。

示例 2:

输入:s = "aabbaaxybbaabb", x = 5, y = 4
输出:20

提示:

  • 1 <= s.length <= 105
  • 1 <= x, y <= 104
  • s 只包含小写英文字母。

提示 1

Note that it is always more optimal to take one type of substring before another


提示 2

You can use a stack to handle erasures

解法1:贪心 + 栈

为了得到最大得分,应使用贪心策略,做法如下。

  • 当 x != y 时,应首先执行得分高的删除操作直到无法继续删除,然后执行得分低的删除操作直到无法继续删除。
  • 当 x=y 时,可以按任意顺序执行两种操作。

为方便处理,将 x=y 和 x>y 归入同一种情况,因此有 x≥y 和 x<y 的两种情况,贪心策略分别如下。

  • 当 x≥y 时,首先删除字符串中的所有 “ab",然后删除字符串中的所有 “ba"。
  • 当 x<y 时,首先删除字符串中的所有 “ba",然后删除字符串中的所有 “ab"。

贪心策略的正确性说明如下:

每次操作无论是删除 “ab" 还是删除 “ba",都会减少一个 ‘a’ 和一个 ‘b’,且剩余字符的相对顺序不变。如果被删除的 “ab" 前后都有字母且前后的字母都是 ‘a’ 或 ‘b’,则删除 “ab" 之后,其前后的字母变成相邻,可能有以下情况:

  • 如果前后的字母是 “aa" 或 “bb",则在删除 “ab" 之前的 4 个相邻字符是 “aaba" 或 “babb",无论是删除 “ab" 还是删除 “ba" 都会剩余两个相同字母,无法一次删除两个相同字母,因此在删除 “ab" 的前后,总删除次数不变。
  • 如果前后的字母是 “ab",则在删除 “ab" 之前的 4 个相邻字符是 “aabb",在删除 “ab" 之后可以一次删除前后的字母 “ab",使用 2 次操作删除 4 个字符,因此在删除 “ab" 的前后,总删除次数不变。
  • 如果前后的字母是 “ba",则在删除 “ab" 之前的 4 个相邻字符是 “baba",在删除 “ab" 之后可以一次删除前后的字母 “ba",使用 2 次操作删除 4 个字符,另一种使用 2 次操作删除 4 个字符的方法是每次删除一个 “ba",因此在删除 “ab" 的前后,总删除次数不变。

因此每次删除任意位置的 “ab" 或 “ba" 之后,总删除次数都不变。为了得到最大得分,应将得分高的删除操作次数最大化。

计算最大得分时,可以使用栈模拟删除操作,做法如下。

  • 删除 “ab" 的过程中,对于每个遍历到的字符,执行如下操作:
  • 如果当前字符是 ‘b’ 且栈顶字符是 ‘a’,则需要删除当前的 “ab",将栈顶字符 ‘a’ 出栈,将得分加 x。
  • 否则,将当前字符入栈。

  • 删除 “ba" 的过程中,对于每个遍历到的字符,执行如下操作:
  • 如果当前字符是 ‘a’ 且栈顶字符是 ‘b’,则需要删除当前的 “ba",将栈顶字符 ‘b’ 出栈,将得分加 y。
  • 否则,将当前字符入栈。

执行两轮删除操作之后,即可得到最大得分。

实现方面,可以使用 StringBuffer 或 StringBuilder 类型的可变字符串对象代替栈,可变字符串的左端为栈底,右端为栈顶,拼接和删除字符都在栈顶操作。

算法逻辑
  1. 贪心策略:根据 xy 的大小关系,决定先执行哪种删除操作。如果 x 大于等于 y,先删除尽可能多的 "ab",再删除 "ba";如果 x 小于 y,则相反。

  2. 栈的使用:栈用于模拟删除操作。栈顶代表当前关注的位置,栈底代表字符串的开始。

  3. 删除 "ab"

    • 遍历字符串 s,对于每个字符:
      • 如果当前字符是 'b' 并且栈顶字符是 'a',则表示可以删除 "ab",将 'a' 出栈并累加得分 x
      • 否则,将当前字符压入栈。
  4. 删除 "ba"

    • 再次遍历字符串 s 或剩余的字符串:
      • 如果当前字符是 'a' 并且栈顶字符是 'b',则可以删除 "ba",将 'b' 出栈并累加得分 y
      • 否则,将当前字符压入栈。
  5. 返回结果:两轮操作后,返回累计的得分。

Java版:

class Solution {
    int points = 0;

    public int maximumGain(String s, int x, int y) {
        if (x >= y) {
            s = removeab(s, x);
            s = removeba(s, y);
        } else {
            s = removeba(s, y);
            s = removeab(s, x);
        }
        return points;
    }

    private String removeab(String s, int x) {
        StringBuffer stack = new StringBuffer();
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            int m = stack.length();
            if (m > 0 && ch == 'b' && stack.charAt(m - 1) == 'a') {
                points += x;
                stack.deleteCharAt(m - 1);
            } else {
                stack.append(ch);
            }
        }
        return stack.toString();
    }

    private String removeba(String s, int y) {
        StringBuffer stack = new StringBuffer();
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            int m = stack.length();
            if (m > 0 && ch == 'a' && stack.charAt(m - 1) == 'b') {
                points += y;
                stack.deleteCharAt(m - 1);
            } else {
                stack.append(ch);
            }
        }
        return stack.toString();
    }
}

Python3版:

class Solution:
    def maximumGain(self, s: str, x: int, y: int) -> int:
        
        def removeab(s, x, points):
            stack = []
            for ch in s:
                if stack and ch == 'b' and stack[-1] == 'a':
                    points += x
                    stack.pop()
                else:
                    stack.append(ch)
            return [''.join(stack), points] 
        
        def removeba(s, y, points):
            stack = []
            for ch in s:
                if stack and ch == 'a' and stack[-1] == 'b':
                    points += y
                    stack.pop()
                else:
                    stack.append(ch)
            return [''.join(stack), points]
        
        points = 0
        if x >= y:
            s, points = removeab(s, x, points)     
            s, points = removeba(s, y, points)
        else:
            s, points = removeba(s, y, points)
            s, points = removeab(s, x, points)
        return points

复杂度分析

  • 时间复杂度:O(n),其中 n 是字符串 s 的长度。需要遍历字符串 s 两次模拟删除操作,每次遍历的时间是 O(n)。
  • 空间复杂度:O(n),其中 n 是字符串 s 的长度。每次遍历都需要创建一个长度为 O(n) 的 StringBuffer 或 StringBuilder 类型的对象。

解法2:贪心

考虑字符串 s 中的每个只包含 ‘a’ 和 ‘b’ 的最长子串,最长字串的含义如下:

如果字符串 s 中的所有字符都是 ‘a’ 或 ‘b’,则 s 为只包含 ‘a’ 和 ‘b’ 的最长子串;

如果字符串 s 中有不是 ‘a’ 或 ‘b’ 的字符,则以不是 ‘a’ 或 ‘b’ 的字符作为分界将 s 分成多个子串,每个用 s 的边界或者字符边界划分的子串都是只包含 ‘a’ 和 ‘b’ 的最长子串。

对于每个最长子串,从左到右遍历,遍历过程中计算该最长子串的最大得分。如果 x≥y 则先考虑删除 “ab" 后考虑删除 “ba",如果 x<y 则先考虑删除 “ba" 后考虑删除 “ab"。

当 x≥y 时,先考虑删除 “ab":

从左到右遍历的过程中维护 ‘a’ 的个数 count 1 ​ 和 ‘b’ 的个数 count 2 ​ ,对于遍历到的每个字符,执行如下操作。

  • 如果当前字符是 ‘a’,则将 count 1 ​ 加 1。
  • 如果当前字符是 ‘b’ 且 count 1 ​>0,则前面一定存在字符 ‘a’ 可以和当前字符 ‘b’ 组成一个 “ab",删除这个 “ab",将 count 1 ​ 减 1 并将得分加 x。
  • 如果当前字符是 ‘b’ 且 count 1 ​=0,则不能删除 “ab",将 count 2 ​ 加 1。 

遍历过程中,每个 ‘b’ 都会和前面的 ‘a’ 配对并删除。遍历结束之后,剩余的字母一定满足 ‘b’ 在前,‘a’ 在后,此时不能继续删除 “ab",因此删除 “ba",删除 “ba" 的次数为 min(count1, count2 ​ ),将得分加 y×min(count 1 ​ ,count 2 ​ )。此时即可得到当前最长子串的最大得分。 

当 x<y 时,先考虑删除 “ba",后考虑删除 “ab",使用相同的方法计算当前最长子串的最大得分。

上述做法为使用常数空间的贪心策略。由于优先考虑得分高的删除操作,且删除次数最大化,因此上述贪心策略可以得到最大得分。

实现方面,从左到右遍历字符串 s,对于遍历到的每个字符,如果当前字符为字符串的末尾字符或者当前字符的后一个字符不是 ‘a’ 或 ‘b’,则当前字符为一个最长字串的末尾字符,计算当前最长字串的最大得分。

算法思路

  1. 理解操作:我们可以想象有两个操作工,一个专门删除 "ab",另一个专门删除 "ba"。他们可以独立工作,但每次操作只能删除一个子字符串。

  2. 贪心策略:由于我们想要最大化总分,我们应该优先执行得分更高的操作。这意味着如果 x 大于或等于 y,我们先尽可能多地删除 "ab",然后再删除 "ba";如果 x 小于 y,则先删除 "ba"。

  3. 处理最长子串:我们可以将字符串 s 中的每个只包含 'a' 和 'b' 的最长连续子串作为一个单元来考虑。这样做的原因是,对于每个这样的子串,我们可以一次性计算出其最大得分,而不需要在子串之间移动。

  4. 遍历和计算:从左到右遍历字符串 s,使用两个计数器 count1count2 分别记录 'a' 和 'b' 的数量。对于每个字符,根据当前字符和计数器的值,决定是删除 "ab" 还是 "ba"。

  5. 更新得分:在遍历过程中,每删除一个 "ab" 或 "ba",就更新总分。

  6. 处理边界:当遇到非 'a' 或 'b' 的字符或到达字符串末尾时,计算当前最长子串的最大得分,并重置计数器。

具体实现

  1. 初始化:设置得分 points 为 0,根据 xy 的大小,确定删除 "ab" 和 "ba" 的顺序。

  2. 遍历字符串:使用一个循环遍历字符串 s 的每个字符。

  3. 更新计数器:根据当前字符是 'a' 还是 'b',更新相应的计数器。

  4. 删除操作:如果当前字符是 'b' 并且 'a' 的计数器大于 0,表示可以删除一个 "ab",减少 'a' 的计数并增加得分;否则,如果 'a' 的计数器为 0,则表示只能删除 "ba",增加 'b' 的计数。

  5. 边界处理:如果当前字符之后是字符串的结尾或者下一个字符不是 'a' 或 'b',表示当前最长子串结束,计算并添加剩余 'a' 和 'b' 可以删除 "ba" 的得分,然后重置计数器。

  6. 返回结果:遍历结束后,返回计算得到的总分。

为什么使用 points += y * Math.min(count1, count2)?

  1. 剩余字符count1 和 count2 分别表示剩余的 'a' 和 'b' 字符的数量。

  2. 删除 "ba":由于我们已经尽可能多地删除了 "ab",剩余的 'a' 和 'b' 无法配对形成 "ab",但可以形成 "ba"。

  3. 最小数量Math.min(count1, count2) 计算两个计数中较小的一个,这是因为即使 'b' 的数量多于 'a',我们也无法形成更多的 "ba" 序列,因为每个 "ba" 都需要一个 'b' 紧跟着一个 'a'。所以,我们能删除的 "ba" 数量由数量较少的那种字符决定。

  4. 计算分数:我们将能删除的 "ba" 数量乘以删除每个 "ba" 获得的分数 y,然后将这个值加到总分数 points 上。

举例说明

假设我们有以下情况:

  • count1 = 3(剩余3个 'a')
  • count2 = 5(剩余5个 'b')

如果我们删除 "ba",每次操作需要一个 'b' 紧跟一个 'a'。即使我们有5个 'b',但只有3个 'a',所以只能形成3个 "ba"。因此,我们只能获得 3 * y 的分数。

结论

代码行 points += y * Math.min(countA, countB); 是为了计算在当前最长子串的结尾,我们能从剩余的 'a' 和 'b' 字符中通过删除 "ba" 获得的最大分数,并将其加到总分数中。这是基于贪心策略,确保我们获得尽可能多的分数。

Java版:

class Solution {
    public int maximumGain(String s, int x, int y) {
        int points = 0;
        // 根据 x 和 y 的大小,确定删除 "ab" 或 "ba" 的顺序
        char c1 = 'a', c2 = 'b';
        if (x < y) {
            c1 = 'b';
            c2 = 'a';
            int t = x;
            x = y;
            y = t;
        }
        int count1 = 0, count2 = 0;
        int n = s.length();
        for (int i = 0; i < n; i++) {
            char ch = s.charAt(i);
            if (ch == c1) {
                count1++;
            } else if (ch == c2) {
                if (count1 > 0) {
                    count1--;
                    points += x;
                } else {
                    count2++;
                }
            }
            // 检查是否到达最长子串的边界
            char next = i == n - 1 ? 'c' : s.charAt(i + 1);
            if (next != c1 && next != c2) {
                points += y * Math.min(count1, count2);
                // 重置计数器
                count1 = 0;
                count2 = 0;
            }
        }
        return points;
    }
}

Python3版:

class Solution:
    def maximumGain(self, s: str, x: int, y: int) -> int:
        points = 0
        # 根据 x 和 y 的大小,确定删除 "ab" 或 "ba" 的顺序
        c1, c2 = 'a', 'b'
        if x < y:
            c1, c2 = 'b', 'a'
            x, y = y, x
        
        count1, count2 = 0, 0
        for i, ch in enumerate(s):
            if ch == c1:
                count1 += 1
            elif ch == c2:
                if count1 > 0:
                    count1 -= 1
                    points += x
                else:
                    count2 += 1

            # 检查是否到达最长子串的边界
            next_ = 'c' if i == len(s) - 1 else s[i + 1]
            if next_ != c1 and next_ != c2:
                points += y * min(count1, count2)
                # 重置计数器
                count1, count2 = 0, 0
        return points

复杂度分析

  • 时间复杂度:O(n),其中 n 是字符串 s 的长度。需要遍历字符串 s 一次计算最大得分。

  • 空间复杂度:O(1)。

  • 8
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值