696. 计数二进制子串

题目

给定一个字符串 s,统计并返回具有相同数量 0 和 1 的非空(连续)子字符串的数量,并且这些子字符串中的所有 0 和所有 1 都是成组连续的。

重复出现(不同位置)的子串也要统计它们出现的次数。

示例 1:

输入:s = "00110011" 输出:6 解释:6 个子串满足具有相同数量的连续 1 和 0 :"0011"、"01"、"1100"、"10"、"0011" 和 "01" 。 注意,一些重复出现的子串(不同位置)要统计它们出现的次数。 另外,"00110011" 不是有效的子串,因为所有的 0(还有 1 )没有组合在一起。

示例 2:

输入:s = "10101" 输出:4 解释:有 4 个子串:"10"、"01"、"10"、"01" ,具有相同数量的连续 1 和 0 。

提示:

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

解题思路

这道题的核心意思:在给定的字符串中,找出一组连续的 0 和一组连续的 1 挨在一起的子串,并且这组 0 和这组 1 的数量必须完全相等。

我们可以把题目要求拆解成三个必须同时满足的条件:

  1. 连续子串:必须是原字符串中连在一起的一段。
  2. 0和1数量相等:子串里有多少个 0,就必须有多少个 1
  3. 成组连续(最关键的一点):子串里的 0 必须全部挨在一起,1 也必须全部挨在一起。也就是说,只能是 00...11... 或者 11...00... 的形式。
    • 有效:"01", "10", "0011", "111000"
    • 无效:"0101"(0和1交替出现,没有各自抱团),"001100"(0被1隔开了)

这道题如果用双指针去暴力匹配会比较低效。既然要求 01 各自成组,我们其实可以把原字符串按字符的连续长度进行压缩统计

比如 s = "00110011"

  • 连续的 0 有 2 个
  • 连续的 1 有 2 个
  • 连续的 0 有 2 个
  • 连续的 1 有 2 个压缩成长度数组就是:[2, 2, 2, 2]

规律就在这里:任意两个相邻的组(比如有 $u$ 个 0 和相邻的 $v$ 个 1),它们能构成的有效子串数量,恰好就是 min(u, v)

比如长度数组里相邻的两个 2min(2, 2) = 2,说明这一段可以贡献 2 个有效子串。我们只需要把所有相邻数字的最小值加起来即可。

具体代码

func countBinarySubstrings(s string) int {
    if len(s) <= 1 {
        return 0
    }

    ans := 0
    prevLen := 0
    currLen := 1

    for i := 1; i < len(s); i++ {
        // 如果当前字符和前一个字符相同,当前组的长度加 1
        if s[i] == s[i-1] {
            currLen++
        } else {
            // 如果不同,说明遇到边界了(比如从 0 变成了 1)
            // 此时上一组和当前组的最小长度,就是它们能组成的有效子串数量
            if prevLen < currLen {
                ans += prevLen
            } else {
                ans += currLen
            }
            // 滚动更新:当前组变成上一组,新的一组长度初始化为 1
            prevLen = currLen
            currLen = 1
        }
    }

    // 遍历结束后,不要忘记加上最后两组计算出的有效子串
    if prevLen < currLen {
        ans += prevLen
    } else {
        ans += currLen
    }

    return ans
}