最美子字符串的数目

标签: 位运算 哈希表 字符串 前缀和

难度: Medium

如果某个字符串中 至多一个 字母出现 奇数 次,则称其为 最美 字符串。

  • 例如,"ccjjc""abab" 都是最美字符串,但 "ab" 不是。

给你一个字符串 word ,该字符串由前十个小写英文字母组成('a''j')。请你返回 word最美非空子字符串 的数目如果同样的子字符串在 word 中出现多次,那么应当对 每次出现 分别计数

子字符串 是字符串中的一个连续字符序列。

 

示例 1:

输入:word = "aba"
输出:4
解释:4 个最美子字符串如下所示:
- "aba" -> "a"
- "aba" -> "b"
- "aba" -> "a"
- "aba" -> "aba"

示例 2:

输入:word = "aabb"
输出:9
解释:9 个最美子字符串如下所示:
- "aabb" -> "a"
- "aabb" -> "aa"
- "aabb" -> "aab"
- "aabb" -> "aabb"
- "aabb" -> "a"
- "aabb" -> "abb"
- "aabb" -> "b"
- "aabb" -> "bb"
- "aabb" -> "b"

示例 3:

输入:word = "he"
输出:2
解释:2 个最美子字符串如下所示:
- "he" -> "h"
- "he" -> "e"

 

提示:

  • 1 <= word.length <= 105
  • word 由从 'a''j' 的小写英文字母组成

Submission

运行时间: 803 ms

内存: 17.0 MB

class Solution:
    def wonderfulSubstrings(self, word: str) -> int:
        @cache
        def two(s):
            return 1 << ord(s) - ord('a')
        res = 0
        a = {}
        a[res] = 1
        for w in word:
            res ^= two(w)
            if res in a:
                a[res] += 1
            else:
                a[res] = 1
        ans = 0
        l = list(a.keys())
        for x in l:
            ans += a[x]*(a[x]-1) // 2
        
        n = len(l)
        for i in range(n-1):
            for j in range(i+1,n):
                if (l[i] ^ l[j]).bit_count() == 1:
                    ans += a[l[i]]*a[l[j]]
        return ans

Explain

这个题解使用了位运算和前缀XOR的概念来高效地解决问题。首先,每个字母可以映射到一个10位的二进制数中的某一位(因为字母'a'到'j'共有10个)。对于字符串中的每个字符,通过一个异或操作更新当前的前缀XOR值。这个前缀XOR在二进制表示中,每位的1表示对应位置的字母出现奇数次。题解中使用了一个字典来存储每个前缀XOR出现的次数。然后,计算所有可能的最美子字符串的数量。只要当前的前缀XOR或者与之前某个前缀XOR的差异正好是一位(即异或结果中只有一个位是1),则表示这之间的子字符串是最美的。总体上,此方法避免了直接检查每个子字符串,而是通过巧妙地利用前缀XOR来统计满足条件的子字符串数量。

时间复杂度: O(n + K^2),其中K是唯一前缀XOR值的数量,最坏情况下K为1024

空间复杂度: O(K),其中K为前缀XOR的不同值的数量,最坏情况下为1024

class Solution:
    def wonderfulSubstrings(self, word: str) -> int:
        @cache
        def two(s):
            return 1 << ord(s) - ord('a')
        res = 0
        a = {}
        a[res] = 1
        for w in word:
            res ^= two(w)
            if res in a:
                a[res] += 1
            else:
                a[res] = 1
        ans = 0
        l = list(a.keys())
        for x in l:
            ans += a[x]*(a[x]-1) // 2
        
        n = len(l)
        for i in range(n-1):
            for j in range(i+1,n):
                if (l[i] ^ l[j]).bit_count() == 1:
                    ans += a[l[i]]*a[l[j]]
        return ans

Explore

每个字母映射到一个10位二进制数的某一位是基于其字母顺序决定的。例如,'a'到'j'分别映射到从第0位到第9位。具体实现是通过`1 << (ord(s) - ord('a'))`,这样'a'映射为1(二进制0000000001),'b'映射为2(二进制0000000010),依此类推。选择异或操作更新前缀XOR值是因为异或操作具有几个关键性质:它可以有效地切换位值(如果某位为1则切换为0,如果为0则切换为1),且对于两次相同的异或操作,原始值会被恢复。这使得异或操作非常适合于跟踪字符出现次数的奇偶性,因为每遇到同一个字母一次,其对应的位就会切换一次。

是的,这个逻辑确实确保了子字符串符合最美条件。当前缀XOR值与之前某个前缀XOR值的差异正好是一位时(即两者的异或结果中只有一个位是1),这表明从上一个前缀到当前前缀之间的子字符串中,只有一个字母的出现次数从偶数变为奇数或从奇数变为偶数。这意味着该子字符串中除了这一个字母可能出现奇数次外,其他所有字母均出现偶数次,从而满足最美子字符串的条件。

为了处理和优化重复的前缀XOR值,可以使用哈希表(或字典)来存储每个唯一前缀XOR值出现的次数。这种方法不仅避免了重复计算,因为我们可以直接从哈希表中获取信息,而不是重新计算每个子字符串,同时也减少了存储需求,因为我们只存储每个唯一的前缀XOR值及其出现次数,而不是每个子字符串的前缀XOR值。此外,可进一步优化内存使用和检索效率,例如通过限制哈希表的大小,或使用更高效的数据结构来处理冲突,从而提高算法的整体性能和效率。