import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* leetcode: 1178.猜字谜
* <p>
* 外国友人仿照中国字谜设计了一个英文版猜字谜小游戏,请你来猜猜看吧。
* <p>
* 字谜的迷面 puzzle 按字符串形式给出,如果一个单词 word 符合下面两个条件,那么它就可以算作谜底:
* <p>
* 单词 word 中包含谜面 puzzle 的第一个字母。
* 单词 word 中的每一个字母都可以在谜面 puzzle 中找到。
* 例如,如果字谜的谜面是 "abcdefg",那么可以作为谜底的单词有 "faced", "cabbage", 和 "baggage";而 "beefed"(不含字母 "a")以及 "based"(其中的 "s" 没有出现在谜面中)。
* 返回一个答案数组 answer,数组中的每个元素 answer[i] 是在给出的单词列表 words 中可以作为字谜迷面 puzzles[i] 所对应的谜底的单词数目。
* <p>
* 输入:
* 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'。
*
* @Author: xiaofeifei
* @Date: 2021/2/26 10:49
*/
public class FindNumOfValidWords {
public static void main(String[] args) {
// System.out.println(findNumOfValidWords(new String[]{"aaaa", "asas", "able", "ability", "actt", "actor", "access"}, new String[]{"aboveyz", "abrodyz", "abslute", "absoryz", "actresz", "gaswxyz"}));
System.out.println(findNumOfValidWords1(new String[]{"aaaa", "asas", "able", "ability", "actt", "actor", "access"}, new String[]{"aboveyz", "abrodyz", "abslute", "absoryz", "actresz", "gaswxyz"}));
}
public static boolean contains(String puzzle, Set<String> set) {
// 1. word 中的字母必须包含puzzle的首字母
if (!set.contains(String.valueOf(puzzle.charAt(0)))) {
return false;
}
// 2. word 中的每个字母必须在puzzle中有所包含
for (String s : set) {
if (!puzzle.contains(s)) {
return false;
}
}
return true;
}
/**
* 思路:
* 1. word 中的字母必须包含puzzle的首字母
* 2. word 中的每个字母必须在puzzle中有所包含
* 3. puzzles[i].length == 7 => 表示puzzle的长度固定为7
* 4. 每个 puzzles[i] 所包含的字符都不重复
* <p>
* 对word进行字母去重 然后相互比较
* <p>
* <p>
* 这种方法可以实现 但是不能满足leetcode自身的执行时间限制
*
* @param words
* @param puzzles
* @return
*/
public static List<Integer> findNumOfValidWords(String[] words, String[] puzzles) {
int[] result = new int[puzzles.length];
int puzzleLen = puzzles.length;
List<String> puzzleCharList = Stream.of(puzzles)
.map(puzzle -> String.valueOf(puzzle.charAt(0)))
.collect(Collectors.toList());
List<Set<String>> wordList = Stream.of(words)
.filter(word -> {
for (String puzzleFirst : puzzleCharList) {
if (word.contains(puzzleFirst)) {
return true;
}
}
return false;
})
.map(word -> {
char[] charArr = word.toCharArray();
// 校验是否满足第一个条件
// 去重处理
Set<String> set = new HashSet<>(charArr.length);
for (char ch : charArr) {
set.add(String.valueOf(ch));
}
return set;
})
.collect(Collectors.toList());
// 对puzzles进行排序
for (int p = 0; p < puzzleLen; p++) {
for (Set<String> strings : wordList) {
if (contains(puzzles[p], strings)) {
result[p]++;
}
}
}
ArrayList<Integer> nums = new ArrayList<>(puzzles.length);
for (int num : result) {
nums.add(num);
}
return nums;
}
/**
* leetcode 官方解答:
* <p>
* 因为都是小写字母 所以可以用0 -> 25位来表示 a -> z
* <p>
* a => 97 所以将字符转为整型 再减去a则得到具体的值 然后转换成二进制
*
* @param words
* @param puzzles
* @return
*/
public static List<Integer> findNumOfValidWords1(String[] words, String[] puzzles) {
// 用来记录words的表示集合 方便后续存取
List<Integer> result = new ArrayList<>(puzzles.length);
Map<Integer, Integer> frequency = new HashMap<>();
for (String word : words) {
// mask用来表示word的二进制表示形式比如说abcdefg => 0111 1111
int mask = 0;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
// 比如说ch为c=3 1 << 3 => 0000 1000
// 这样即使字符出现重复 二进制的方式也完美解决了重复问题
mask |= 1 << (ch - 'a');
}
// 获取二进制中1的数量 如果数量大于7 则它一定不会成为谜底 因为谜面本身就只有7位
if (Integer.bitCount(mask) <= 7) {
// 这里很关键哈,key用来记录word的二进制表示,value用来记录word的出现次数
// 也就是是否重复了,相同的word则value就+1
frequency.put(mask, frequency.getOrDefault(mask, 0) + 1);
}
}
// 同理 将puzzles 也转换成二进制表示信息 肯定是7个1 puzzles本身
// 然后再遍历中直接比较
for (String puzzle : puzzles) {
// 记录当前谜面匹配次数
int total = 0;
int mask = 0;
// 这里请注意,这里获取到的mask是6位 不包含最高位
// 因为最高位就是第一个字符,这里获取的是后6位的值
// 一般认为应该是遍历7次 这里只使用6次循环的好处是 降低了执行时间成本
for (int i = 1; i < puzzle.length(); i++) {
mask |= (1 << (puzzle.charAt(i) - 'a'));
}
// 6个1 的puzzle 不包含最高位
int subset = mask;
do {
// firstBit 用于表示puzzle的首字母
int firstBit = 1 << (puzzle.charAt(0) - 'a');
// 获取最后真实的puzzle的二进制
int s = subset | firstBit;
if (frequency.containsKey(s)) {
total += frequency.get(s);
}
// 这里是核心
// 比如说 puzzle 为 0111 1111
// 因为最高位必须要满足 所以只需要比较6位为 0011 1111
// 所以6为的子集为 2^7 - 1 & mask(0011 1111) -> 不包含 -1 & mask(0011 1111)这么多种可能
// 然后加上首字母 组成二进制 判断frequency是否包含 包含则匹配成功 返回
// 不包含则继续获取子集 直到获取的subset 为 0 即(subset - 1) = -1
// -1 & mask = mask 所以当subset == mask时
// 则应该跳出循环 至此 所以子集匹配结束
// 这里的思想很聪明 跳过了 puzzles * words的遍历导致时间复杂度{O{M*N}}过大的问题
// 而是转而利用二进制的本身机制 反向通过puzzle的二进制表示形式来反推其所有满足的子集是否存在于
// words所表示的hashMap中,hashMap本身的散列机制,检索效率很快。
// 因此它的时间复杂度要远远小于直接遍历判断的问题
// (subset - 1) & mask 保证了其结果肯定为mask的子集 结果为负则子集遍历结束
subset = (subset - 1) & mask;
// 所以当subset == mask时会退出
// 这里为什么不判断subset > 0 就好了呢,因此存在首字母
// 因为subset是6个1 ,还有一个首字母 如果word就只包含首字母呢 则当subset为0时
// 也是可以满足条件的,只有当subset = -1时才应该跳出循环
} while (subset != mask);
result.add(total);
}
return result;
}
}