LeetCode 1178. 猜字谜
题目描述


题意分析
对每个谜面,统计单词列表里有多少个单词同时满足两个条件:单词的每一种字母都出现在谜面中,并且单词必须包含谜面的第一个字母。
谜面恰好包含七个不同的小写字母,单词可以重复使用其中某个字母,不要求字母次数一致,也不要求按谜面顺序排列。统计的是单词条目数量,即使多个单词的字母集合相同,也要分别计数。
解法:位掩码 + 子集枚举
核心思路
[!blue]
匹配只依赖出现过哪些字母,可以把每个单词压成二十六位集合掩码:读到字母就按位或上对应位,重复出现不会改变掩码。用哈希表统计每一种集合对应多少个单词,不保存字符顺序或重复次数。
谜面最多提供七种字母,因此单词不同字母超过七种时不可能匹配任何谜面,可以提前跳过。这里限制的是集合大小,不是单词长度;保留下来的相同掩码仍要累加频次。
对一个谜面,首字母必须选择,其他六个字母可以自由选择是否出现在单词集合中。因此固定首字母位,只枚举其余六位的所有子集,再将首位或回去,就恰好得到六十四个可能的合法单词集合。每个满足题意的集合对应其中唯一一个子集,查表求和既不会漏掉,也不会重复计数。
子集枚举从可选位全集
mask开始,用sub = (sub - 1) & mask移到下一个。减一会清掉最低的一个置一位并让更低位成为可选状态,再与全集相与去掉不属于谜面的位,得到下一个更小子集;反复执行就能逐个遍历所有子集。空子集也需要处理,它与固定首位组合后,代表只使用首字母的单词集合。处理零后必须结束,因为继续执行减一并与全集相与,会重新回到全集,导致重复循环。
解题步骤
- 扫描所有单词,用按位或生成字符集合掩码,统计不超过七种字母的掩码频次。
- 对每个谜面单独保存首字母位,用其余六个字母形成可选集合。
- 从可选全集开始枚举子集,将当前子集与首位合并后查频次表,累加对应单词数。
- 当前子集为零时结束;否则通过
(sub - 1) & mask继续枚举。- 将累计数量写入当前谜面的答案。
代码实现
class Solution {
public int[] findNumOfValidWords(String[] words, String[] puzzles) {
Map<Integer, Integer> count = new HashMap<>();
for (String w : words) {
int mask = 0;
for (int i = 0; i < w.length(); i++) {
// 用「或」标记出现过,重复字母不影响集合。
mask |= 1 << (w.charAt(i) - 'a');
}
// 超过 7 个不同字母的单词不可能是任何谜面的子集,直接丢弃。
if (Integer.bitCount(mask) <= 7) {
count.put(mask, count.getOrDefault(mask, 0) + 1);
}
}
int[] res = new int[puzzles.length];
for (int i = 0; i < puzzles.length; i++) {
String p = puzzles[i];
// 只枚举其余六位,再固定首位;否则同一候选会被重复计数。
int first = 1 << (p.charAt(0) - 'a');
int mask = 0;
for (int j = 1; j < p.length(); j++) {
mask |= 1 << (p.charAt(j) - 'a');
}
int total = 0;
int sub = mask;
while (true) {
int cur = sub | first;
total += count.getOrDefault(cur, 0);
// 先处理再判空,保证空集(只含首字母)也被统计。
if (sub == 0) {
break;
}
// 空集已经处理并退出;其他情况下转到下一个子集。
sub = (sub - 1) & mask;
}
res[i] = total;
}
return res;
}
}
import "math/bits"
func findNumOfValidWords(words []string, puzzles []string) []int {
count := make(map[int]int)
for _, w := range words {
mask := 0
for i := 0; i < len(w); i++ {
// 用「或」标记出现过,重复字母不影响集合。
mask |= 1 << (w[i] - 'a')
}
// 超过 7 个不同字母的单词不可能是任何谜面的子集,直接丢弃。
if bits.OnesCount(uint(mask)) <= 7 {
count[mask]++
}
}
res := make([]int, len(puzzles))
for i, p := range puzzles {
// 只枚举其余六位,再固定首位;否则同一候选会被重复计数。
first := 1 << (p[0] - 'a')
mask := 0
for j := 1; j < len(p); j++ {
mask |= 1 << (p[j] - 'a')
}
total := 0
sub := mask
for {
cur := sub | first
total += count[cur]
// 先处理再判空,保证空集(只含首字母)也被统计。
if sub == 0 {
break
}
// 空集已经处理并退出;其他情况下转到下一个子集。
sub = (sub - 1) & mask
}
res[i] = total
}
return res
}
复杂度分析
- 时间复杂度:期望 $O(L + 64P)$,
L为单词总字符数,P为谜面数,每个谜面固定查询六十四个候选集合。- 空间复杂度:$O(U)$ 保存不同有效掩码的单词频次,
U为这些掩码的数量;返回结果另占 $O(P)$。
关键点总结
[!green]
- 字母存在性使用 OR,不应使用表示次数奇偶的 XOR。
- 必选首字母与可选六个字母分开,才能唯一生成所有合法候选集合。
- 哈希表统计同集合的单词总数,不只是记录集合是否出现。
- 空子集有实际含义,先统计它,再结束枚举。
易错点总结
[!yellow]
- 根据单词长度大于七就丢弃,误删只有少量不同字母但含很多重复字符的单词。
- 忽略同掩码的多个单词,会少算题目要求的单词数量。
- 枚举首字母也可以不选,却没有强制检查首字母,会计入缺少必选字母的单词。
- 把首字母放进可选集合后又统一或回,会让不同子集生成相同候选而重复计数。
- 跳过零子集会漏掉只含首字母的集合,处理零之后仍继续循环又会重复枚举。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 318. 最大单词长度乘积 | 中等 | 同样把单词字符集合编码成位掩码,原题判断不相交,本题判断单词是谜面子集且含首字母。 |
| 1239. 串联字符串的最大长度 | 中等 | 同样忽略字符原顺序并按集合关系筛选,本题对固定短谜面枚举子集,原题选择互不重叠的单词集合。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!