leetcode
leetcode 1901 ~ 1950
字符串分组

字符串分组

难度:

标签:

题目描述

代码结果

运行时间: 649 ms, 内存: 47.2 MB


import java.util.*;
import java.util.stream.*;

// 思路:使用并查集(Union-Find)结合Java Stream API来解决分组问题。首先初始化每个字符串的唯一ID,然后通过遍历字符串,将关联的字符串合并在同一个组中。最后统计每个组的大小,并找到最大的组大小。
public class Solution {
    public int[] groupStrings(String[] words) {
        int n = words.length;
        UnionFind uf = new UnionFind(n);
        Map<String, Integer> map = new HashMap<>();
        IntStream.range(0, n).forEach(i -> {
            char[] arr = words[i].toCharArray();
            Arrays.sort(arr);
            String sorted = new String(arr);
            if (!map.containsKey(sorted)) {
                map.put(sorted, i);
            } else {
                uf.union(i, map.get(sorted));
            }
            IntStream.range(0, arr.length).forEach(j -> {
                char temp = arr[j];
                arr[j] = '#'; // 替换一个字母
                String modified = new String(arr);
                if (map.containsKey(modified)) {
                    uf.union(i, map.get(modified));
                }
                arr[j] = temp; // 复原字母
            });
        });
        int[] count = new int[n];
        IntStream.range(0, n).forEach(i -> count[uf.find(i)]++);
        int groups = (int) IntStream.of(count).filter(size -> size > 0).count();
        int maxSize = IntStream.of(count).max().orElse(0);
        return new int[]{groups, maxSize};
    }

    class UnionFind {
        int[] parent, rank;

        public UnionFind(int n) {
            parent = new int[n];
            rank = new int[n];
            IntStream.range(0, n).forEach(i -> parent[i] = i);
        }

        public int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }

        public void union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX != rootY) {
                if (rank[rootX] > rank[rootY]) {
                    parent[rootY] = rootX;
                } else if (rank[rootX] < rank[rootY]) {
                    parent[rootX] = rootY;
                } else {
                    parent[rootY] = rootX;
                    rank[rootX]++;
                }
            }
        }
    }
}

解释

方法:

这个题解使用了位掩码和并查集来将具有一定关联的字符串分组。首先,每个字符串被转换成一个整数位掩码,其中位掩码的第 i 位表示字符 'a'+i 是否出现在字符串中。这使得比较两个字符串是否只有一个字符的差异变得容易和快速。题解中先将字符串按长度排序,以便尽可能地减少比较次数。然后,使用一个字典来记录已经遇到的位掩码及其对应字符串的索引。对于每个位掩码,我们不仅记录它本身,还生成26个可能的通过增加或删除单个字母得到的位掩码,并尝试将当前字符串与这些位掩码所代表的字符串联合。这样可以确保所有只差一个字符的字符串都会被放入同一个组。最后,使用并查集来确定组成员,并统计最大组的大小。

时间复杂度:

O(n)

空间复杂度:

O(n)

代码细节讲解

🦆
在使用位掩码表示字符串时,为什么只考虑字符是否出现,而不是字符出现的次数?
在这个题目中,我们的主要目的是判断字符串之间是否可以通过增加、删除或替换一个字符来互相转换,而不是完全一致的匹配。使用位掩码来表示每个字符是否出现(而非出现次数),可以快速比较两个字符串的组成差异。这种方法简化了逻辑,并有效地支持了只通过单一字符变化达到的分组需求,因为字符的具体出现次数在这种变化检测中不是必要的。
🦆
如何确保通过增加、删除或替换一个字母得到的位掩码都能正确地反映两个字符串之间的关系?
题解中生成位掩码的变化主要通过位操作实现。对于每个字符的位,我们尝试将其翻转(即从1变为0或从0变为1),代表删除或添加一个字符。这样的位操作确保了可以覆盖所有可能通过单一字符变化形成的位掩码。通过这种方法,我们可以确保所有只差一个字符的变化都被检测到,并正确地将相关字符串进行分组。
🦆
题解中提到按字符串长度排序可以减少比较次数,请问这是基于什么原理?会不会有例外情况导致效果不显著?
按字符串长度排序的主要目的是尽量减少不必要的比较。理论上,长度差异较大的字符串不太可能通过单一字符的增加、删除或替换来匹配,因此先比较长度相近的字符串可以提高效率。然而,如果输入数据中大部分字符串长度接近,这种排序可能不会显著减少比较次数,因为几乎每个字符串都需要与其他字符串进行比较。
🦆
并查集在此题解中如何处理字符串分组,特别是如何确保一个组内的任一字符串与其他组的字符串都不关联?
并查集在这个题解中用于管理和追踪字符串分组。每次当两个字符串确定只通过一个字符的增加、删除或替换可以互相转换时,这两个字符串就通过并查集的union操作合并到同一个组。并查集通过维护每个节点的父节点来形成一个树状结构,从而确保一旦两个字符串被合并到同一个组,它们将始终保持在同一组内。通过对所有字符串执行find操作,我们可以更新它们的根节点,最终得到一个准确的分组结果,确保每个组内的字符串都是相互关联的,而与其他组的字符串无关。

相关问题