LeetCode 472. 连接词(字典树+回溯) / 1995. 统计特殊四元组(标记这个题) / 846. 一手顺子

472. 连接词

2021.12.28 每日一题

题目描述

给你一个 不含重复 单词的字符串数组 words ,请你找出并返回 words 中的所有 连接词 。

连接词 定义为:一个完全由给定数组中的至少两个较短单词组成的字符串。

示例 1:

输入:words = [“cat”,“cats”,“catsdogcats”,“dog”,“dogcatsdog”,“hippopotamuses”,“rat”,“ratcatdogcat”]
输出:[“catsdogcats”,“dogcatsdog”,“ratcatdogcat”]
解释:“catsdogcats” 由 “cats”, “dog” 和 “cats” 组成;
“dogcatsdog” 由 “dog”, “cats” 和 “dog” 组成;
“ratcatdogcat” 由 “rat”, “cat”, “dog” 和 “cat” 组成。

示例 2:

输入:words = [“cat”,“dog”,“catdog”]
输出:[“catdog”]

提示:

1 <= words.length <= 10^4
0 <= words[i].length <= 1000
words[i] 仅由小写字母组成
0 <= sum(words[i].length) <= 10^5

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/concatenated-words
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

首先单词按照长度排序,然后创建一个字典树
然后边往字典树里添加单词边判断是否是一个连接词,如果是一个连接词,那么就不需要就如字典树中,因为树中有其他单词可以组成这个连接词;如果不是,加入字典树
因为是从短到长添加的,所以可以保证成立

判断是否是连接词的时候,用回溯的方式
也可以加上记忆化搜索,用visited数组表示以当前位置为开头的这个单词是否被搜索过
如果被搜索过,那么说明后面肯定不是一个合法的词,直接返回;因为合法的话就会返回true了

class Solution {
    //字典树
    class Node{
        Node[] next;
        boolean isEnd;

        public Node(){
            next = new Node[26];
            isEnd = false;
        }

        public void insert(String s){
            Node root = this;
            for(char c : s.toCharArray()){
                if(root.next[c - 'a'] == null){
                    root.next[c - 'a'] = new Node();
                }
                root = root.next[c - 'a'];
            }
            root.isEnd = true;
        }

        public int query(String s){
            Node root = this;
            int l = s.length();
            if(l == 0 || s.equals(null))
                return 0;
            for(int i = 0; i < l; i++){
                char c = s.charAt(i);
                if(root.next[c - 'a'] == null){
                    return -1;
                }
                root = root.next[c - 'a'];

                //如果当前有结束的话,那么就递归查后面的字符串
                if(root.isEnd){
                    //后面的结果
                    int temp = query(s.substring(i + 1, l));
                    //如果等于-1,那么返回继续查
                    //如果后面有值,那么说明后面也能被拼接,所以返回拼接的个数
                    if(temp != -1)
                        return 1 + temp;
                    else{
                        continue;
                    }
                }
            }
            return -1;
        }
    }

    public List<String> findAllConcatenatedWordsInADict(String[] words) {
        //又是一个困难题,怎么连接呢,数据范围又是这么大
        //每个长度建一个set,然后比较长度,再看有没有这个连接词?
        //所有字符串长度之和又只有10的5次
        //字典树,动规
        //想想写一棵字典树的话,然后遍历这棵树,如果一个分支中,比如catsdogcats
        //第一个单词找到了cat,然后再去找sdogcats,如果此时字典中有sd,但是后面没了,就回溯
        //发现cats也存在,就去找dogcats

        //首先将所有单词加入字典树
        int n = words.length;
        //将单词从短到长排序
        Arrays.sort(words, (a, b) -> (a.length() - b.length()));

        Node trie = new Node();
        List<String> res = new ArrayList<>();
        //边添加边判断,因为是从小到大排序的,所以先添加进去的肯定是短的单词
        //而连接词是不会被添加进去的
        for(String word : words){
            if(trie.query(word) > 1){
                res.add(word);
            }
            else{
                trie.insert(word);
            }
        }

        return res;
    }
}

动态规划 + 字符串哈希

class Solution {
    //学习三叶姐的动态规划+字符串哈希的方法
    //首先明确f[i]的定义,就是当前单词,前i个字符是否可以由其他单词组成
    //然后发下f[i]可以由f[j],j < i, 转移得到(如果j到i可以由其他单词组成)
    
    //判断一个单词是否存在可以用字符串哈希的方式进行优化
    //即用一个哈希表来存储所有单词的哈希值

    Set<Long> set = new HashSet<>();
    int P = 131, OFFSET = 128;  //乘子和偏移量
    public List<String> findAllConcatenatedWordsInADict(String[] words) {
        int n = words.length;
        //计算每个单词的哈希值
        for (int i = 0; i < n; i++) {
            long hash = 0;
            for (char c : words[i].toCharArray()) hash = hash * P + (c - 'a') + OFFSET;
            set.add(hash);
        }
        List<String> ans = new ArrayList<>();
        for (String s : words) {
            if (check(s)) ans.add(s);
        }
        return ans;
    }
    
    boolean check(String s) {
        int n = s.length();
        int[] f = new int[n + 1];
        //赋初值
        Arrays.fill(f, -1);
        f[0] = 0;
        //这里的转移方式有点特别,和哈希值的计算相关
        for (int i = 0; i <= n; i++) {
            if (f[i] == -1) continue;
            long cur = 0;
            for (int j = i + 1; j <= n; j++) {
                cur = cur * P + (s.charAt(j - 1) - 'a') + OFFSET;
                if (set.contains(cur)) f[j] = Math.max(f[j], f[i] + 1);
            }
            if (f[n] > 1) return true;
        }
        return false;
    }
}

1995. 统计特殊四元组

2021.12.29 每日一题

题目描述

给你一个 下标从 0 开始 的整数数组 nums ,返回满足下述条件的 不同 四元组 (a, b, c, d) 的 数目 :

nums[a] + nums[b] + nums[c] == nums[d] ,且
a < b < c < d

示例 1:

输入:nums = [1,2,3,6]
输出:1
解释:满足要求的唯一一个四元组是 (0, 1, 2, 3) 因为 1 + 2 + 3 == 6 。

示例 2:

输入:nums = [3,3,6,4,5]
输出:0
解释:[3,3,6,4,5] 中不存在满足要求的四元组。

示例 3:

输入:nums = [1,1,1,3,5]
输出:4
解释:满足要求的 4 个四元组如下:
-(0, 1, 2, 3): 1 + 1 + 1 == 3
-(0, 1, 3, 4): 1 + 1 + 3 == 5
-(0, 2, 3, 4): 1 + 1 + 3 == 5
-(1, 2, 3, 4): 1 + 1 + 3 == 5

提示:

4 <= nums.length <= 50
1 <= nums[i] <= 100

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/count-special-quadruplets
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

我这个哈希表用的,复杂度低了,但是时间长了

class Solution {
    public int countQuadruplets(int[] nums) {
        //当时周赛直接暴力写的
        //用哈希表存?
        int n = nums.length;
        int res = 0;
        for(int i = 0; i < n; i++){
            for(int j = i + 1; j < n; j++){
                Map<Integer, Integer> map = new HashMap<>();
                for(int k = j + 1; k < n; k++){
                    int key = nums[k] - nums[i] - nums[j];
                    if(map.containsKey(key))
                        res += map.get(key);
                    map.put(nums[k], map.getOrDefault(nums[k], 0) + 1);
                }
            }
        }
        return res;
    }
}

然后就在想怎么能创建一次哈希表就行,就是官解的第二个思路
因为必须满足顺序要求,所以先将后面的值存储到哈希表中,即从后向前遍历,那么可以保证前面的和是满足要求的

class Solution {
    public int countQuadruplets(int[] nums) {
        int n = nums.length;
        int ans = 0;
        Map<Integer, Integer> cnt = new HashMap<Integer, Integer>();
        for (int c = n - 2; c >= 2; --c) {
            cnt.put(nums[c + 1], cnt.getOrDefault(nums[c + 1], 0) + 1);
            for (int a = 0; a < c; ++a) {
                for (int b = a + 1; b < c; ++b) {
                    ans += cnt.getOrDefault(nums[a] + nums[b] + nums[c], 0);
                }
            }
        }
        return ans;
    }
}

两层循环,确实难想啊

class Solution {
    public int countQuadruplets(int[] nums) {
        int n = nums.length;
        //四个数,很自然的可以想到拆成两个两个的形式,然后看关系,
        //但是怎么保证前后顺序关系呢,这又是个问题
        //参考第二个解法的思路
        //首先肯定要取到任意两个数的和,那么必须要双层循环
        //想想怎么在这个上面做文章

        int res = 0;
        Map<Integer, Integer> map = new HashMap<>();
        //从大到小枚举b
        for(int b = n - 3; b >= 1; b--){
            //那么现在c的范围就是b到d
            //枚举d,那么c的值就是nums[d] - nums[b]
            for(int d = b + 2; d < n; d++){
                int key = nums[d] - nums[b + 1];
                map.put(key, map.getOrDefault(key, 0) + 1);
            }

            //从小到大枚举a,然后计算nums[a] + nums[b]
            for(int a = 0; a < b; a++){
                int sum = nums[a] + nums[b];
                res += map.getOrDefault(sum, 0);
            }
        }
        return res;

        //再想想这个为什么可行
        //对于当前b来说,枚举d的值,可以保证取到所有需要的c
        //同时枚举a的值,可以保证取到所有的a+b
        //一般而言的想法是,对于一个数nums[d] - nums[c],枚举前面所有数nums[a] + nums[b]
        //而这里把这个问题转化成了两个方向同时进行的形式
        //既可以把后面的c都取到,也能保证所有情况都不会漏掉
        //其实还是哈希表的思想,只不过把遍历顺序改变了,更巧了
    }
}

三叶姐的多维背包,太牛了

class Solution {
    public int countQuadruplets(int[] nums) {
        //学习三叶姐的多维背包
        //f[i][j][k]表示考虑前i个数中的k个数,和为j的情况有多少种
        int n = nums.length;
        int[][][] f = new int[n + 1][101][4];
        f[0][0][0] = 1;
        for(int i = 1; i <= n; i++){
            for(int j = 0; j < 101; j++){
                for(int k = 0; k < 4; k++){
                    //不考虑当前数
                    f[i][j][k] = f[i - 1][j][k];
                    //考虑当前数
                    if(j >= nums[i - 1] && k > 0)
                        f[i][j][k] += f[i - 1][j - nums[i - 1]][k - 1];
                }
            }
        }
        int res = 0;
        //统计前i个数中,和为nums[i - 1]的个数
        for(int i = 3; i <= n; i++){
            res += f[i][nums[i - 1]][3];
        }
        return res;
    }
}

再练习了一下这个枚举思路,简而言之就是一个位置作为分隔,然后把一边的所有取值都放在哈希表中,另一边在哈希表中找,而为了保证所有情况都能遍历到,中间的两个数b 和 c一定要固定,即都与第一层遍历数相关,否则就会出现重复

class Solution {
    public int countQuadruplets(int[] nums) {
        int n = nums.length;
        Map<Integer, Integer> map = new HashMap<>();
        //然后写成二维的形式
        //主要是需要保证所有情况都遍历到,并且要是二维的
        //遍历所有c
        int res = 0;

        for(int c = n - 2; c >= 2; c--){
            //枚举所有d-c
            for(int d = c + 1; d < n; d++){
                int differ = nums[d] - nums[c];
                map.put(differ, map.getOrDefault(differ, 0) + 1);
            }
            for(int a = 0; a < c - 1; a++){
                res += map.getOrDefault(nums[a] + nums[c - 1], 0);
            }
        }
        return res;
    }
}

846. 一手顺子

2021.12.30 每日一题

题目描述

Alice 手中有一把牌,她想要重新排列这些牌,分成若干组,使每一组的牌数都是 groupSize ,并且由 groupSize 张连续的牌组成。

给你一个整数数组 hand 其中 hand[i] 是写在第 i 张牌,和一个整数 groupSize 。如果她可能重新排列这些牌,返回 true ;否则,返回 false 。

示例 1:

输入:hand = [1,2,3,6,2,3,4,7,8], groupSize = 3
输出:true
解释:Alice 手中的牌可以被重新排列为 [1,2,3],[2,3,4],[6,7,8]。

示例 2:

输入:hand = [1,2,3,4,5], groupSize = 4
输出:false
解释:Alice 手中的牌无法被重新排列成几个大小为 4 的组。

提示:

1 <= hand.length <= 10^4
0 <= hand[i] <= 10^9
1 <= groupSize <= hand.length

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/hand-of-straights
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

思路

一手顺子,确实比昨天写的顺多了
因为给的数范围太大,所以创建数组统计个数不太合适,用哈希表来统计
然后将所给的数排序,找出当前最小的数,看后面几个数是否都在哈希表中存在,如果有一个不存在就返回false

class Solution {
    public boolean isNStraightHand(int[] hand, int groupSize) {
        //先统计每个数的个数,不过数有点大,用map
        Map<Integer, Integer> map = new HashMap<>();
        int n = hand.length;
        
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for(int t : hand){
            if(map.containsKey(t)){
                map.put(t, map.get(t) + 1);
            }else{
                map.put(t, 1);
                pq.offer(t);
            }
        }

        while(!pq.isEmpty()){
            int top = pq.peek();
            //如果map中没有这个值,说明被移除了,就跳过
            if(!map.containsKey(top)){
                pq.poll();
                continue;
            }
                
            //如果有这个值,就继续找接下来的几个数
            for(int i = 0; i < groupSize; i++){
                if(!map.containsKey(top + i)){
                    return false;
                }else{
                    map.put(top + i, map.getOrDefault(top + i, 0) - 1);
                    if(map.get(top + i) == 0){
                        map.remove(top + i);
                    }
                }
            }
        }
        return true;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值