LeetCode 2306. 公司命名【哈希表,字符串】2305

本文属于「征服LeetCode」系列文章之一,这一系列正式开始于2021/08/12。由于LeetCode上部分题目有锁,本系列将至少持续到刷完所有无锁题之日为止;由于LeetCode还在不断地创建新题,本系列的终止日期可能是永远。在这一系列刷题文章中,我不仅会讲解多种解题思路及其优化,还会用多种编程语言实现题解,涉及到通用解法时更将归纳总结出相应的算法模板。

为了方便在PC上运行调试、分享代码文件,我还建立了相关的仓库:https://github.com/memcpy0/LeetCode-Conquest。在这一仓库中,你不仅可以看到LeetCode原题链接、题解代码、题解文章链接、同类题目归纳、通用解法总结等,还可以看到原题出现频率和相关企业等重要信息。如果有其他优选题解,还可以一同分享给他人。

由于本系列文章的内容随时可能发生更新变动,欢迎关注和收藏征服LeetCode系列文章目录一文以作备忘。

给你一个字符串数组 ideas 表示在公司命名过程中使用的名字列表。公司命名流程如下:

  1. 从 ideas 中选择 2 个 不同 名字,称为 ideaA 和 ideaB 。
  2. 交换 ideaA 和 ideaB 的首字母。
  3. 如果得到的两个新名字  不在 ideas 中,那么 ideaA ideaB串联 ideaA 和 ideaB ,中间用一个空格分隔)是一个有效的公司名字。
  4. 否则,不是一个有效的名字。

返回 不同 且有效的公司名字的数目。

示例 1:

输入:ideas = ["coffee","donuts","time","toffee"]
输出:6
解释:下面列出一些有效的选择方案:
- ("coffee", "donuts"):对应的公司名字是 "doffee conuts"- ("donuts", "coffee"):对应的公司名字是 "conuts doffee"- ("donuts", "time"):对应的公司名字是 "tonuts dime"- ("donuts", "toffee"):对应的公司名字是 "tonuts doffee"- ("time", "donuts"):对应的公司名字是 "dime tonuts"- ("toffee", "donuts"):对应的公司名字是 "doffee tonuts" 。
因此,总共有 6 个不同的公司名字。

下面列出一些无效的选择方案:
- ("coffee", "time"):在原数组中存在交换后形成的名字 "toffee"- ("time", "toffee"):在原数组中存在交换后形成的两个名字。
- ("coffee", "toffee"):在原数组中存在交换后形成的两个名字。

示例 2:

输入:ideas = ["lack","back"]
输出:0
解释:不存在有效的选择方案。因此,返回 0

提示:

  • 2 <= ideas.length <= 5 * 10^4
  • 1 <= ideas[i].length <= 10
  • ideas[i] 由小写英文字母组成
  • ideas 中的所有字符串 互不相同

分析

什么样的一对字符串无法交换首字母?

示例 1 中的 c o f f e e coffee coffee t i m e time time ,虽然这样两个字符串完全不一样,但如果交换了 c o f f e e coffee coffee t i m e time time 的首字母,会得到字符串 t o f f e e toffee toffee ,它在数组 i d e a s ideas ideas 中,不符合题目要求。

又例如 i d e a s = [ a a , a b , a c , b c , b d , b e ] ideas=[aa,ab,ac,bc,bd,be] ideas=[aa,ab,ac,bc,bd,be] ,将其分成两组:

  • 第一组: a a , a b , a c aa,ab,ac aa,ab,ac
  • 第二组: b c , b d , b e bc,bd,be bc,bd,be

其中每一组内的字符串是不能交换首字母的,因为交换后字符串不变,必然在 i d e a s ideas ideas 中。

考虑交换第一组的字符串和第二组的字符串,哪些是可以交换首字母的,哪些是不能交换首字母的?

  • 第一组的 a c ac ac 无法和第二组的任何字符串交换,因为交换后会得到 b c bc bc ,它在 i d e a s ideas ideas 中。
  • 第二组的 b c bc bc 无法和第一组的任何字符串交换,因为交换后会得到 a c ac ac ,它在 i d e a s ideas ideas 中。
  • 其余字符串对可以交换首字母。

上面的分析立刻引出了如下方法。

方法一:按照首字母分组,存储后缀

按照首字母,把 i d e a s ideas ideas 分成(至多) 26 26 26 组字符串。

例如 i d e a s = [ a a , a b , a c , b c , b d , b e ] ideas=[aa,ab,ac,bc,bd,be] ideas=[aa,ab,ac,bc,bd,be] 分成如下两组(只记录去掉首字母后的字符串):

  • A A A 组(集合): a , b , c {a,b,c} a,b,c
  • B B B 组(集合): c , d , e {c,d,e} c,d,e

分组后:

  • A A A 中选一个不等于 c c c 的字符串,这有 2 2 2 种选法。
  • B B B 中选一个不等于 c c c 的字符串,这有 2 2 2 种选法。

考虑两个字符串的先后顺序(谁在左谁在右),有 2 2 2 种方法。根据乘法原理,有 2 × 2 × 2 = 8 2×2×2=8 2×2×2=8 对符合要求的字符串。

由于无法选交集中的字符串,一般地,从 A A A B B B 中可以选出
2 ⋅ ( ∣ A ∣ − ∣ A ∩ B ∣ ) ⋅ ( ∣ B ∣ − ∣ A ∩ B ∣ ) 2⋅(∣A∣−∣A∩B∣)⋅(∣B∣−∣A∩B∣) 2(AAB)(BAB)
对符合要求的字符串。其中 ∣ S ∣ ∣S∣ S 表示集合 S S S 的大小。

枚举所有组对,计算上式,累加到答案中。

class Solution:
    def distinctNames(self, ideas: List[str]) -> int:
        groups = defaultdict(set)
        for s in ideas:
            groups[s[0]].add(s[1:])  # 按照首字母分组

        ans = 0
        for a, b in combinations(groups.values(), 2):  # 枚举所有组对
            m = len(a & b)  # 交集的大小
            ans += (len(a) - m) * (len(b) - m)
        return ans * 2  # 乘 2 放到最后
use std::collections::HashSet;

impl Solution {
    pub fn distinct_names(ideas: Vec<String>) -> i64 {
        let mut groups = vec![HashSet::new(); 26];
        for s in ideas { // 按首字母分组,保存后缀
            groups[(s.as_bytes()[0] - b'a') as usize].insert(s[1..].to_string());
        }
        let mut ans = 0i64;
        for a in 1..26 { // 枚举所有组对
            for b in 0..a {
                let m = groups[a].iter().filter(|&s| groups[b].contains(s)).count();
                ans += (groups[a].len() - m) as i64 * (groups[b].len() - m) as i64;
            }
        }
        ans * 2 // 乘2放到最后
    }
}
class Solution {
public:
    long long distinctNames(vector<string>& ideas) {
        unordered_set<string> groups[26];
        for (auto &s : ideas) {
            groups[s[0] - 'a'].insert(s.substr(1)); // 按首字母分组,保存后缀
        }
        long long ans = 0;
        for (int a = 1; a < 26; ++a) { // 枚举所有组对
            for (int b = 0; b < a; ++b) {
                int m = 0; // 交集大小
                for (auto &s : groups[a]) m += groups[b].count(s);
                ans += (long long) (groups[a].size() - m) * (groups[b].size() - m);
            }
        }
        return ans * 2; // 乘2放到最后
    }
};
class Solution {
    public long distinctNames(String[] ideas) {
        Set<String>[] groups = new HashSet[26];
        Arrays.setAll(groups, i -> new HashSet<>());
        for (String s : ideas) {
            groups[s.charAt(0) - 'a'].add(s.substring(1)); // 按照首字母分组
        }

        long ans = 0;
        for (int a = 1; a < 26; a++) { // 枚举所有组对
            for (int b = 0; b < a; b++) {
                int m = 0; // 交集的大小
                for (String s : groups[a]) {
                    if (groups[b].contains(s)) {
                        m++;
                    }
                }
                ans += (long) (groups[a].size() - m) * (groups[b].size() - m);
            }
        }
        return ans * 2; // 乘 2 放到最后
    }
}
func distinctNames(ideas []string) (ans int64) {
    group := [26]map[string]bool{}
    for i := range group {
        group[i] = map[string]bool{}
    }
    for _, s := range ideas {
        group[s[0]-'a'][s[1:]] = true // 按照首字母分组
    }

    for i, a := range group { // 枚举所有组对
        for _, b := range group[:i] {
            m := 0 // 交集的大小
            for s := range a {
                if b[s] {
                    m++
                }
            }
            ans += int64(len(a)-m) * int64(len(b)-m)
        }
    }
    return ans * 2 // 乘 2 放到最后
}
var distinctNames = function(ideas) {
    const groups = Array.from({ length: 26 }, () => new Set());
    for (const s of ideas) {
        groups[s.charCodeAt(0) - 'a'.charCodeAt(0)].add(s.slice(1)); // 按照首字母分组
    }

    let ans = 0;
    for (let a = 1; a < 26; a++) { // 枚举所有组对
        for (let b = 0; b < a; b++) {
            let m = 0; // 交集的大小
            for (const s of groups[a]) {
                if (groups[b].has(s)) {
                    m++;
                }
            }
            ans += (groups[a].size - m) * (groups[b].size - m);
        }
    }
    return ans * 2; // 乘 2 放到最后
};

复杂度分析

  • 时间复杂度: O ( n m ∣ Σ ∣ ) O(nm|Σ|) O(nm∣Σ∣) ,其中 n n n i d e a s ideas ideas 的长度, m ≤ 10 m≤10 m10 为单个字符串的长度, ∣ Σ ∣ = 26 |Σ|=26 ∣Σ∣=26 是字符集合的大小。注意枚举组对的逻辑看上去是 O ( n m ∣ Σ ∣ 2 ) O(nm |Σ|^2) O(nm∣Σ2) 的,但去掉第二层 O ( ∣ Σ ∣ ) O(|Σ|) O(∣Σ∣) 的循环后,剩余循环相当于把 i d e a s ideas ideas 遍历了一遍,是 O ( n m ) O(nm) O(nm) ,所以总的时间复杂度是 O ( n m ∣ Σ ∣ ) O(nm |Σ|) O(nm∣Σ∣)
  • 空间复杂度: O ( n m + ∣ Σ ∣ ) O(nm+ |Σ|) O(nm+∣Σ∣)

方法二:按照后缀分组,计算交集

公式还是同样的:
2 ⋅ ( ∣ A ∣ − ∣ A ∩ B ∣ ) ⋅ ( ∣ B ∣ − ∣ A ∩ B ∣ ) 2⋅(∣A∣−∣A∩B∣)⋅(∣B∣−∣A∩B∣) 2(AAB)(BAB)
下文把去掉首字母后的剩余部分称作后缀。横看成岭侧成峰,换一个角度计算交集大小 ∣ A ∩ B ∣ ∣A∩B∣ AB

在遍历 i d e a s = [ a a , a b , a c , b c , b d , b e ] ideas=[aa,ab,ac,bc,bd,be] ideas=[aa,ab,ac,bc,bd,be] 的过程中,当我们遍历到 b c bc bc 时,发现之前遍历过一个后缀也为 c c c 的字符串 a c ac ac ,这就对交集大小 ∣ A ∩ B ∣ ∣A∩B∣ AB 产生了 1 1 1 的贡献,也就是交集大小 ∣ A ∩ B ∣ ∣A∩B∣ AB 增加 1 1 1

具体来说,在遍历 i d e a s ideas ideas 的过程中,维护如下信息:

  1. (所有首字母开头的单词的)集合大小 s i z e [ a ] size[a] size[a] 。遍历到 s = i d e a s [ i ] s=ideas[i] s=ideas[i] 时,把 s i z e [ s [ 0 ] ] size[s[0]] size[s[0]] 加一。
  2. 交集大小 i n t e r s e c t i o n [ a ] [ b ] intersection[a][b] intersection[a][b] 。遍历到 s = i d e a s [ i ] s=ideas[i] s=ideas[i] 时,设 b = s [ 0 ] b=s[0] b=s[0] ,把 i n t e r s e c t i o n [ a ] [ b ] intersection[a][b] intersection[a][b] i n t e r s e c t i o n [ b ] [ a ] intersection[b][a] intersection[b][a] 加一,其中 a a a 是和 s s s 同后缀其他字符串的首字母
  3. 为了快速知道前面有哪些字符串和 s s s 有着相同的后缀,用一个哈希表 g r o u p s groups groups 维护, k e y key key 是后缀, v a l u e value value 是后缀对应的首字母列表。注意题目保证所有字符串互不相同。

代码实现时,可以用把哈希表中维护的首字母列表压缩成一个二进制数。

class Solution:
    def distinctNames(self, ideas: List[str]) -> int:
        size = [0] * 26 # 各个集合的大小
        intersections = [[0] * 26 for _ in range(26)] # 交集大小
        groups = defaultdict(list) # 后缀 -> 首字母列表
        for s in ideas:
            b = ord(s[0]) - ord('a')
            size[b] += 1 # 增加集合大小
            g = groups[s[1:]]
            for a in g: # a是和s有着相同后缀的字符串的首字母
                intersections[a][b] += 1 # 增加交集大小
                intersections[b][a] += 1
            g.append(b)
        
        ans = 0
        for a in range(1, 26): # 枚举所有组对
            for b in range(a):
                m = intersections[a][b]
                ans += (size[a] - m) * (size[b] - m)
        return ans * 2 # 乘2放到最后
use std::collections::HashMap;

impl Solution {
    pub fn distinct_names(ideas: Vec<String>) -> i64 {
        let mut size = vec![0; 26]; // 集合大小
        let mut intersection = vec![vec![0; 26]; 26]; // 交集大小
        let mut groups = HashMap::new(); // 后缀 -> 首字母列表
        for s in ideas {
            let b = (s.as_bytes()[0] - b'a') as usize;
            size[b] += 1; // 增加集合大小
            let suffix = &s[1..];
            let mask = *groups.get(suffix).unwrap_or(&0);
            groups.insert(suffix.to_string(), mask | 1 << b); // 把b加到mask中
            for a in 0..26 { // a 是和 s 有着相同后缀的首字母
                if (mask >> a & 1) > 0 { // a 在 mask 中
                    intersection[b][a] += 1; // 增加交集大小
                    intersection[a][b] += 1; // 增加交集大小
                }
            }
        }

        let mut ans = 0i64;
        for a in 1..26 { // 枚举所有组对
            for b in 0..a {
                let m = intersection[a][b];
                ans += (size[a] - m) as i64 * (size[b] - m) as i64;
            }
        }
        ans * 2 // 乘 2 放到最后
    }
}
/**
 * @param {string[]} ideas
 * @return {number}
 */
var distinctNames = function(ideas) {
    const size = Array(26).fill(0); // 集合大小
    const intersection = Array.from({ length: 26 }, () => Array(26).fill(0)); // 交集大小
    const groups = {}; // 后缀 -> 首字母
    for (let s of ideas) {
        const b = s.charCodeAt(0) - 'a'.charCodeAt(0);
        size[b]++; // 增加集合大小
        const suffix = s.slice(1); // 后缀
        const mask = groups[suffix] ?? 0;
        groups[suffix] = mask | 1 << b; // 把 b 加到 mask 中
        for (let a = 0; a < 26; a++) { // a 是和 s 有着相同后缀的字符串的首字母
            if (mask >> a & 1) { // a 在 mask 中
                intersection[b][a]++; // 增加交集大小
                intersection[a][b]++;
            }
        }
    }

    let ans = 0;
    for (let a = 1; a < 26; a++) { // 枚举所有组对
        for (let b = 0; b < a; b++) {
            const m = intersection[a][b];
            ans += (size[a] - m) * (size[b] - m);
        }
    }
    return ans * 2; // 乘 2 放到最后
};
func distinctNames(ideas []string) (ans int64) {
    size := [26]int{} // 集合大小
    intersection := [26][26]int{} // 交集大小
    groups := map[string]int{} // 后缀 -> 首字母
    for _, s := range ideas {
        b := s[0] - 'a'
        size[b]++ // 增加集合大小
        suffix := s[1:]
        mask := groups[suffix]
        groups[suffix] = mask | 1<<b // 把 b 加到 mask 中
        for a := 0; a < 26; a++ { // a 是和 s 有着相同后缀的字符串的首字母
            if mask>>a&1 > 0 { // a 在 mask 中
                intersection[b][a]++ // 增加交集大小
                intersection[a][b]++
            }
        }
    }

    for a := 1; a < 26; a++ { // 枚举所有组对
        for b := 0; b < a; b++ {
            m := intersection[a][b]
            ans += int64(size[a]-m) * int64(size[b]-m)
        }
    }
    return ans * 2 // 乘 2 放到最后
}
class Solution {
public:
    long long distinctNames(vector<string>& ideas) {
        int size[26]{}; // 集合大小
        int intersection[26][26]{}; // 交集大小
        unordered_map<string, int> groups; // 后缀 -> 首字母
        for (auto &s : ideas) {
            int b = s[0] - 'a';
            size[b]++; // 增加集合大小
            auto suffix = s.substr(1);
            int mask = groups[suffix];
            groups[suffix] = mask | 1 << b; // 把 b 加到 mask 中
            for (int a = 0; a < 26; ++a) { // a 是和 s 有着相同后缀的字符串的首字母
                if (mask >> a & 1) { // a 在 mask 中
                    intersection[b][a]++; // 增加交集大小
                    intersection[a][b]++;
                }
            }
        }

        long long ans = 0;
        for (int a = 1; a < 26; a++) { // 枚举所有组对
            for (int b = 0; b < a; b++) {
                int m = intersection[a][b];
                ans += (long long) (size[a] - m) * (size[b] - m);
            }
        }
        return ans * 2; // 乘 2 放到最后
    }
};
class Solution {
    public long distinctNames(String[] ideas) {
        int[] size = new int[26]; // 集合大小
        int[][] intersection = new int[26][26]; // 交集大小
        Map<String, Integer> groups = new HashMap<>(); // 后缀 -> 首字母
        for (String s : ideas) {
            int b = s.charAt(0) - 'a';
            size[b]++; // 增加集合大小
            String suffix = s.substring(1);
            int mask = groups.getOrDefault(suffix, 0);
            groups.put(suffix, mask | 1 << b); // 把 b 加到 mask 中
            for (int a = 0; a < 26; a++) { // a 是和 s 有着相同后缀的字符串的首字母
                if ((mask >> a & 1) > 0) { // a 在 mask 中
                    intersection[b][a]++; // 增加交集大小
                    intersection[a][b]++;
                }
            }
        }

        long ans = 0;
        for (int a = 1; a < 26; a++) { // 枚举所有组对
            for (int b = 0; b < a; b++) {
                int m = intersection[a][b];
                ans += (long) (size[a] - m) * (size[b] - m);
            }
        }
        return ans * 2; // 乘 2 放到最后
    }
}

复杂度分析:

  • 时间复杂度: O ( n ( m + ∣ Σ ∣ ) + ∣ Σ ∣ 2 ) O(n(m+ |Σ|)+ |Σ|^2) O(n(m+∣Σ∣)+∣Σ2) ,其中 n n n i d e a s ideas ideas 的长度, m ≤ 10 m≤10 m10 为单个字符串的长度, ∣ Σ ∣ = 26 |Σ|=26 ∣Σ∣=26 是字符集合的大小。
  • 空间复杂度: O ( n m + ∣ Σ ∣ 2 ) O(nm+|Σ|^2) O(nm+∣Σ2)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

memcpy0

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值