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

Posted Zephyr丶J

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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以上是关于LeetCode 472. 连接词(字典树+回溯) / 1995. 统计特殊四元组(标记这个题) / 846. 一手顺子的主要内容,如果未能解决你的问题,请参考以下文章

leetcode-472-连接词

leetcode之并查集+记忆化搜索+回溯+最小生成树刷题总结1

[LeetCode] 139. 单词拆分 ☆☆☆(动态规划 回溯)

leetcode-139回溯超时动态规划单词拆分

leetcode算法题基础(四十五) 回溯算法总结 回溯法的解空间表示方法

综合笔试题难度 3.5/5,常见序列 DP 题目及其优化思路