3228. 将 1 移动到末尾的最大操作次数

题目

给你一个 二进制字符串 s

你可以对这个字符串执行 任意次 下述操作:

  • 选择字符串中的任一下标 i( i + 1 < s.length ),该下标满足 s[i] == '1' 且 s[i + 1] == '0'
  • 将字符 s[i] 向 右移 直到它到达字符串的末端或另一个 '1'。例如,对于 s = "010010",如果我们选择 i = 1,结果字符串将会是 s = "0**001**10"

返回你能执行的 最大 操作次数。

示例 1:

输入: s = "1001101"

输出: 4

解释:

可以执行以下操作:

  • 选择下标 i = 0。结果字符串为 s = "**001**1101"
  • 选择下标 i = 4。结果字符串为 s = "0011**01**1"
  • 选择下标 i = 3。结果字符串为 s = "001**01**11"
  • 选择下标 i = 2。结果字符串为 s = "00**01**111"

示例 2:

输入: s = "00111"

输出: 0

提示:

  • 1 <= s.length <= 10^5
  • s[i] 为 '0' 或 '1'

解题思路

题目描述的操作是:“选择一个 1,将其向右移动,直到遇到字符串末尾或另一个 1”。

这个操作隐含了两个关键信息:

  1. 跨越成本:虽然题目说是“移动”,但实际上是一个 1 跳过了一段连续的 0。无论这段 0 有多长(比如 0 还是 00000),对于这一个 1 来说,这只是一次操作。
  2. 聚类效应(关键):为了让操作次数最大化,我们不应该把右边的 1 先移走,而应该先把左边的 1 尽可能地往右移,让它们和右边的 1 聚在一起。
    • 为什么?因为当 $N$ 个 1 聚在一起时,如果它们面前出现了一个“空隙”(一段 0),这 $N$ 个 1 每一个都能执行一次跨越操作。这比它们分散开来单独跨越赚得更多。

结论:这道题本质上是在计算**“雪球”滚过“坑”的总代价**。

  • 雪球:当前累积的 1 的数量。
  • :一段连续的 0
  • 规则:每遇到一个“坑”,当前手里所有的 1 都要交一次“过路费”(操作数 +1)。

我们不需要真的去移动字符串数组(那会涉及大量的内存写操作),只需要遍历统计。

逻辑流:

  1. 我们从左到右扫描字符串。
  2. 我们需要一个变量 count 来记录当前“雪球”里有多少个 1
  3. 我们需要一个变量 ans 来记录总操作数。
  4. 状态转移
    • 遇到 1count++。雪球变大了,但此时不产生操作数(因为还没遇到坑)。
    • 遇到 0:意味着出现了“坑”。此时,所有累积的 1 都可以跨越这个坑。
    • 去重细节:连续的多个 0 视作同一个坑(同一个 gap)。只有从 1 进入 0 的那个瞬间,才意味着我们要支付过路费。

与其写复杂的 while 循环去跳过连续的 0,不如只关注状态跳变的瞬间。

我们关注的是模式 10

  • 条件s[i] == '1's[i+1] == '0'
  • 意义:这标志着一段 1 的结束和一段 0 的开始。
  • 动作
    1. 因为 s[i] == '1',先把这个 1 加入雪球 (count++)。
    2. 因为 s[i+1] == '0',说明前方立刻就是悬崖(坑)。此时,手里所有的 1(包括刚才加进去的那个)都要执行一次操作。于是 ans += count

如果后面还有连续的 0(例如 100)?

  • i 走到第一个 0 时,s[i] 不等于 1,逻辑直接跳过。这完美地实现了“连续 0 不重复计算”。

具体代码

func maxOperations(s string) int {

    count := 0
    ans := 0
    for i := range s[:len(s)-1] {
        if s[i] == '1' {
            count++
            if s[i + 1] == '0' {
                ans += count
            }
        }
    }
    return ans

}