记录一下leetcode:1178猜字谜

7 篇文章 0 订阅
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;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值