给你一个字符串 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 类型的可变字符串对象代替栈,可变字符串的左端为栈底,右端为栈顶,拼接和删除字符都在栈顶操作。
算法逻辑
-
贪心策略:根据
x
和y
的大小关系,决定先执行哪种删除操作。如果x
大于等于y
,先删除尽可能多的 "ab",再删除 "ba";如果x
小于y
,则相反。 -
栈的使用:栈用于模拟删除操作。栈顶代表当前关注的位置,栈底代表字符串的开始。
-
删除 "ab":
- 遍历字符串
s
,对于每个字符:- 如果当前字符是 'b' 并且栈顶字符是 'a',则表示可以删除 "ab",将 'a' 出栈并累加得分
x
。 - 否则,将当前字符压入栈。
- 如果当前字符是 'b' 并且栈顶字符是 'a',则表示可以删除 "ab",将 'a' 出栈并累加得分
- 遍历字符串
-
删除 "ba":
- 再次遍历字符串
s
或剩余的字符串:- 如果当前字符是 'a' 并且栈顶字符是 'b',则可以删除 "ba",将 'b' 出栈并累加得分
y
。 - 否则,将当前字符压入栈。
- 如果当前字符是 'a' 并且栈顶字符是 'b',则可以删除 "ba",将 'b' 出栈并累加得分
- 再次遍历字符串
-
返回结果:两轮操作后,返回累计的得分。
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’,则当前字符为一个最长字串的末尾字符,计算当前最长字串的最大得分。
算法思路
-
理解操作:我们可以想象有两个操作工,一个专门删除 "ab",另一个专门删除 "ba"。他们可以独立工作,但每次操作只能删除一个子字符串。
-
贪心策略:由于我们想要最大化总分,我们应该优先执行得分更高的操作。这意味着如果
x
大于或等于y
,我们先尽可能多地删除 "ab",然后再删除 "ba";如果x
小于y
,则先删除 "ba"。 -
处理最长子串:我们可以将字符串
s
中的每个只包含 'a' 和 'b' 的最长连续子串作为一个单元来考虑。这样做的原因是,对于每个这样的子串,我们可以一次性计算出其最大得分,而不需要在子串之间移动。 -
遍历和计算:从左到右遍历字符串
s
,使用两个计数器count1
和count2
分别记录 'a' 和 'b' 的数量。对于每个字符,根据当前字符和计数器的值,决定是删除 "ab" 还是 "ba"。 -
更新得分:在遍历过程中,每删除一个 "ab" 或 "ba",就更新总分。
-
处理边界:当遇到非 'a' 或 'b' 的字符或到达字符串末尾时,计算当前最长子串的最大得分,并重置计数器。
具体实现
-
初始化:设置得分
points
为 0,根据x
和y
的大小,确定删除 "ab" 和 "ba" 的顺序。 -
遍历字符串:使用一个循环遍历字符串
s
的每个字符。 -
更新计数器:根据当前字符是 'a' 还是 'b',更新相应的计数器。
-
删除操作:如果当前字符是 'b' 并且 'a' 的计数器大于 0,表示可以删除一个 "ab",减少 'a' 的计数并增加得分;否则,如果 'a' 的计数器为 0,则表示只能删除 "ba",增加 'b' 的计数。
-
边界处理:如果当前字符之后是字符串的结尾或者下一个字符不是 'a' 或 'b',表示当前最长子串结束,计算并添加剩余 'a' 和 'b' 可以删除 "ba" 的得分,然后重置计数器。
-
返回结果:遍历结束后,返回计算得到的总分。
为什么使用 points += y * Math.min(count1, count2)?
-
剩余字符:
count1
和count2
分别表示剩余的 'a' 和 'b' 字符的数量。 -
删除 "ba":由于我们已经尽可能多地删除了 "ab",剩余的 'a' 和 'b' 无法配对形成 "ab",但可以形成 "ba"。
-
最小数量:
Math.min(count1, count2)
计算两个计数中较小的一个,这是因为即使 'b' 的数量多于 'a',我们也无法形成更多的 "ba" 序列,因为每个 "ba" 都需要一个 'b' 紧跟着一个 'a'。所以,我们能删除的 "ba" 数量由数量较少的那种字符决定。 -
计算分数:我们将能删除的 "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)。