题目描述

1178. 猜字谜 难度:困难-中等

外国友人仿照中国字谜设计了一个英文版猜字谜小游戏,请你来猜猜看吧。

字谜的迷面 puzzle 按字符串形式给出,如果一个单词 word 符合下面两个条件,那么它就可以算作谜底:

  • 单词 word 中包含谜面 puzzle 的第一个字母。
  • 单词 word 中的每一个字母都可以在谜面 puzzle 中找到。

  例如,如果字谜的谜面是 “abcdefg”,那么可以作为谜底的单词有 “faced”, “cabbage”, 和 “baggage”;而 “beefed”(不含字母 “a”)以及 “based”(其中的 “s” 没有出现在谜面中)都不能作为谜底。
返回一个答案数组 answer,数组中的每个元素 answer[i] 是在给出的单词列表 words 中可以作为字谜迷面 puzzles[i] 所对应的谜底的单词数目。

示例:

输入:
words = ["aaaa","asas","able","ability","actt","actor","access"],
puzzles = ["aboveyz","abrodyz","abslute","absoryz","actresz","gaswxyz"]
输出:[1,1,3,2,4,0]
解释:
1 个单词可以作为 "aboveyz" 的谜底 : "aaaa"
1 个单词可以作为 "abrodyz" 的谜底 : "aaaa"
3 个单词可以作为 "abslute" 的谜底 : "aaaa", "asas", "able"
2 个单词可以作为 "absoryz" 的谜底 : "aaaa", "asas"
4 个单词可以作为 "actresz" 的谜底 : "aaaa", "asas", "actt", "access"
没有单词可以作为 "gaswxyz" 的谜底,因为列表中的单词都不含字母 \'g\'。

提示:

1 <= words.length <= 10^5
4 <= words[i].length <= 50
1 <= puzzles.length <= 10^4
puzzles[i].length == 7
words[i][j], puzzles[i][j] 都是小写英文字母。
每个 puzzles[i] 所包含的字符都不重复。

 题解

初步解法

  鉴于words数组、puzzles数组的长度分别为10^5、10^4,且每个字符串还有自身的长度,因此暴力解法肯定会超时。

  最近在学Java HashSet的实现原理,为了保证不可重复性,并且避免待添加元素与已添加元素的一个个比较,Java采用了哈希函数。其实本题可以借鉴其思想,避免一个个查找。

  那么如何设计哈希映射呢,我们注意到,words数组、puzzles数组都是小写英文字母,小写英文字母总共只有26个,那么何不用26位二进制位串表示每个单词?一旦这样表示,我们可以将二进制位串作为HashMap的key。

  暴力解法是对每个puzzle字符串,按照规则比较words中每一个字符串以考察其是否符合条件。以上我们将二进制位串作为HashMap的key,但是,如何利用key查找呢?我们转化一下思路,原本我们是看每一个key是否满足条件,现在,我们穷举每个puzzle字符串满足条件的子串,再考察其是否在HashMap的key中,如此我们巧妙地利用了构造的HashMap。(为什么这么想?puzzles的每个字符串长度固定,只有7位,所有满足条件的子串只有64个,求其子串的复杂度可以忽略。)

  现在还有一个问题待解决,如何求子串?对于每个puzzle字符串,除去首字母必须出现外,其余6个字母出现与否可以由6位二进制位决定,某一位为1代表字符串的某位出现,其组合共有64种。

代码:

 1 class Solution {
 2     public List<Integer> findNumOfValidWords(String[] words, String[] puzzles) {
 3         HashMap<Integer,Integer> map = new HashMap<>();
 4         for(int i=0;i<words.length;i++){//遍历word数组,将word数组压缩归类,HashMap键为26位压缩二进制数,值为出现的次数。
 5             int key = 0;
 6             for(int j=0;j<words[i].length();j++){
 7                 key |= 1 << (\'z\'-words[i].charAt(j));//a对应1<<25  z对应1<<0
 8             }
 9             if(Integer.bitCount(key)<=7){
10                 map.put(key,map.getOrDefault(key,0)+1);
11             }
12         }
13         List<Integer> ans = new ArrayList(puzzles.length);
14         for(int i=0;i<puzzles.length;i++){//遍历谜面数组,给出解
15             int bitAdd = 0;//6位位串,决定6个字符出现与否,bitAdd高位对应puzzle字符串的低地址字符。
16             int ansI = 0;
17             for(;bitAdd<64;bitAdd++){//64种可能
18                 int key = 1 << (\'z\'-puzzles[i].charAt(0));//首字母必须出现
19                 for(int j=1,k=6;j<=32;j=j<<1,k--){
20                     key |= (j&bitAdd)==0?0:(1 << (\'z\'-puzzles[i].charAt(k)));//取bitAdd的某一位,如果是0,不加(加0),如果是1,或上对应位。
21                 }
22                 ansI += map.getOrDefault(key,0);
23             }
24             ans.add(ansI);
25         }
26         return ans;
27     }
28 }

初步解法的稍加改进

除了上面的求二进制子集的方法外,还有另外一种方法,先给出整个26位位串,直接求其26位的子集(当然,由于至多只有6位不为0 [不包括首字母,因为首字母必须出现] ,复杂度并没有增加,反而减少,因其避免了子集的每个元素一位一位的形成过程。)。方法二比方法一减少复杂度的部分就在于方法一64个子集元素的每一位要通过循环生成,而方法二是先给出64个子集元素的第一个的所有位,然后依次求另外的子集元素。

 1 class Solution {
 2     public List<Integer> findNumOfValidWords(String[] words, String[] puzzles) {
 3         HashMap<Integer,Integer> map = new HashMap<>();
 4         for(int i=0;i<words.length;i++){//遍历word数组,将word数组压缩归类,HashMap键为26位压缩二进制数,值为出现的次数。
 5             int key = 0;
 6             for(int j=0;j<words[i].length();j++){
 7                 key |= 1 << (\'z\'-words[i].charAt(j));
 8             }
 9             if(Integer.bitCount(key)<=7){
10                 map.put(key,map.getOrDefault(key,0)+1);
11             }
12         }
13         List<Integer> ans = new ArrayList(puzzles.length);
14         for(int i=0;i<puzzles.length;i++){//遍历谜面数组,给出解
15             int ansI = 0;
16             int mask = 0;
17             for(int j=1;j<puzzles[i].length();j++){//求出除首字母的26位二进制位串
18                 mask |= 1 << (\'z\'-puzzles[i].charAt(j));
19             }
20             int sub = mask;//包括本身
21             do{
22                 ansI += map.getOrDefault( sub|(1 << (\'z\'-puzzles[i].charAt(0))) ,0);//sub是不包括首字母的那一位的子位串,由于首字母必须存在,因此每个sub都要或上首字母的那一位
23                 sub = (sub - 1)&mask;
24             }while(sub!=mask);//包括0
25             ans.add(ansI);
26         }
27         return ans;
28     }
29 }

看完题解后的另解

总结

 本题中,我们学会了一种利用二进制状态压缩构造的哈希映射方法、求二进制位串子集的两种解法,另解中复习了字典树和回溯法。

 这道题之所以是Hard,是因为考察的都是违反人性”直觉”的东西:

  • 状态压缩:对一个单词出现过哪些字母,不能采用我们直观中的 map/set 进行记录,而要利用一个长度为 26 的二进制数来记录,对于某个字母需要计算在二进制数中的哪一位,如果出现过用 1 表示,没出现过用 0 表示
  • 正难则反:不能从 words 数组出发,去检查有哪些 word 符合要求;而要反过来从 puzzle 出发,去枚举当前 puzzle 所有合法的 word,再去确定这些合法的 word 在真实的 words 数组中出现了多少次。

大家要尽量去理解这种思路的合理性,当这种思路也形成意识的时候,这种题也就不难了。

 

版权声明:本文为hickey2048原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/WhiteThornZzf/p/14456944.html