LeetCode第 57 场力扣夜喵双周赛(差分数组、单调栈) and 第 251 场力扣周赛(状态压缩动规,树的序列化,树哈希,字典树)

LeetCode第 57 场力扣夜喵双周赛

离knight勋章越来越近,不过水平没有丝毫涨进

1941. 检查是否所有字符出现次数相同

题目描述

给你一个字符串 s ,如果 s 是一个 好 字符串,请你返回 true ,否则请返回 false 。

如果 s 中出现过的 所有 字符的出现次数 相同 ,那么我们称字符串 s 是 好 字符串。

示例 1:

输入:s = “abacbc”
输出:true
解释:s 中出现过的字符为 ‘a’,‘b’ 和 ‘c’ 。s 中所有字符均出现 2 次。
示例 2:

输入:s = “aaabb”
输出:false
解释:s 中出现过的字符为 ‘a’ 和 ‘b’ 。
‘a’ 出现了 3 次,‘b’ 出现了 2 次,两者出现次数不同。

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

思路

我这个就是一般思路
官解有个理论频数的概念,用哈希表实现可能比较快

class Solution {
    public boolean areOccurrencesEqual(String s) {
        int l = s.length();
        int[] count = new int[26];
        for(int i = 0; i < l; i++){
            count[s.charAt(i) - 'a']++; 
        }
        int temp = 0;
        int i = 0;
        for(; i < 26; i++){
            if(count[i] != 0){
                temp = count[i];
                break;
            }
        }
        for(int j = i + 1; j < 26; j++){
            if(count[j] != 0 && count[j] != temp){
                return false;
            }
        }
        return true;
    }
}

1942. 最小未被占据椅子的编号

题目描述

有 n 个朋友在举办一个派对,这些朋友从 0 到 n - 1 编号。派对里有 无数 张椅子,编号为 0 到 infinity 。当一个朋友到达派对时,他会占据 编号最小 且未被占据的椅子。

比方说,当一个朋友到达时,如果椅子 0 ,1 和 5 被占据了,那么他会占据 2 号椅子。
当一个朋友离开派对时,他的椅子会立刻变成未占据状态。如果同一时刻有另一个朋友到达,可以立即占据这张椅子。

给你一个下标从 0 开始的二维整数数组 times ,其中 times[i] = [arrivali, leavingi] 表示第 i 个朋友到达和离开的时刻,同时给你一个整数 targetFriend 。所有到达时间 互不相同 。

请你返回编号为 targetFriend 的朋友占据的 椅子编号 。

示例 1:

输入:times = [[1,4],[2,3],[4,6]], targetFriend = 1
输出:1
解释:

  • 朋友 0 时刻 1 到达,占据椅子 0 。
  • 朋友 1 时刻 2 到达,占据椅子 1 。
  • 朋友 1 时刻 3 离开,椅子 1 变成未占据。
  • 朋友 0 时刻 4 离开,椅子 0 变成未占据。
  • 朋友 2 时刻 4 到达,占据椅子 0 。
    朋友 1 占据椅子 1 ,所以返回 1 。
    示例 2:

输入:times = [[3,10],[1,5],[2,6]], targetFriend = 0
输出:2
解释:

  • 朋友 1 时刻 1 到达,占据椅子 0 。
  • 朋友 2 时刻 2 到达,占据椅子 1 。
  • 朋友 0 时刻 3 到达,占据椅子 2 。
  • 朋友 1 时刻 5 离开,椅子 0 变成未占据。
  • 朋友 2 时刻 6 离开,椅子 1 变成未占据。
  • 朋友 0 时刻 10 离开,椅子 2 变成未占据。
    朋友 0 占据椅子 2 ,所以返回 2 。

提示:

n == times.length
2 <= n <= 104
times[i].length == 2
1 <= arrivali < leavingi <= 105
0 <= targetFriend <= n - 1
每个 arrivali 时刻 互不相同 。

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

思路

一个优先队列放椅子编号,表示未被占据的椅子
然后对给定的时间数组排序,模拟这个过程,每次占据一个椅子,然后将离开时间和对应的椅子编号存在一个优先队列中,每次遍历要注意查看当前时间是否有椅子空闲下来,将空闲下来的椅子再放入优先队列中

class Solution {
    class Person{
        int table;
        int endtime;
        
        public Person(int t, int e){
            table = t;
            endtime = e;
        }
    }
    public int smallestChair(int[][] times, int targetFriend) {
        int l = times.length;
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for(int i = 0; i < l; i++){
            pq.offer(i);
        }
        int arrive = times[targetFriend][0];
        Arrays.sort(times, (a,b) -> a[0] - b[0]);
        
        PriorityQueue<Person> per = new PriorityQueue<>((a,b) -> (a.endtime - b.endtime));
        for(int i = 0; i < l; i++){
            int start = times[i][0];
            while(!per.isEmpty() && per.peek().endtime <= start){
                int ta = per.poll().table;
                pq.offer(ta);
            }
            int temp = pq.poll();
            Person person = new Person(temp, times[i][1]);
            per.offer(person);
            if(start == arrive)
                return temp;
        }
        return -1;
    }
}

1943. 描述绘画结果

题目描述

给你一个细长的画,用数轴表示。这幅画由若干有重叠的线段表示,每个线段有 独一无二 的颜色。给你二维整数数组 segments ,其中 segments[i] = [starti, endi, colori] 表示线段为 半开区间 [starti, endi) 且颜色为 colori 。

线段间重叠部分的颜色会被 混合 。如果有两种或者更多颜色混合时,它们会形成一种新的颜色,用一个 集合 表示这个混合颜色。

比方说,如果颜色 2 ,4 和 6 被混合,那么结果颜色为 {2,4,6} 。
为了简化题目,你不需要输出整个集合,只需要用集合中所有元素的 和 来表示颜色集合。

你想要用 最少数目 不重叠 半开区间 来 表示 这幅混合颜色的画。这些线段可以用二维数组 painting 表示,其中 painting[j] = [leftj, rightj, mixj] 表示一个 半开区间[leftj, rightj) 的颜色 和 为 mixj 。

比方说,这幅画由 segments = [[1,4,5],[1,7,7]] 组成,那么它可以表示为 painting = [[1,4,12],[4,7,7]] ,因为:
[1,4) 由颜色 {5,7} 组成(和为 12),分别来自第一个线段和第二个线段。
[4,7) 由颜色 {7} 组成,来自第二个线段。
请你返回二维数组 painting ,它表示最终绘画的结果(没有 被涂色的部分不出现在结果中)。你可以按 任意顺序 返回最终数组的结果。

半开区间 [a, b) 是数轴上点 a 和点 b 之间的部分,包含 点 a 且 不包含 点 b 。

示例 1:
在这里插入图片描述

输入:segments = [[1,4,5],[4,7,7],[1,7,9]]
输出:[[1,4,14],[4,7,16]]
解释:绘画借故偶可以表示为:

  • [1,4) 颜色为 {5,9} (和为 14),分别来自第一和第二个线段。
  • [4,7) 颜色为 {7,9} (和为 16),分别来自第二和第三个线段。
    示例 2:
    在这里插入图片描述

输入:segments = [[1,7,9],[6,8,15],[8,10,7]]
输出:[[1,6,9],[6,7,24],[7,8,15],[8,10,7]]
解释:绘画结果可以以表示为:

  • [1,6) 颜色为 9 ,来自第一个线段。
  • [6,7) 颜色为 {9,15} (和为 24),来自第一和第二个线段。
  • [7,8) 颜色为 15 ,来自第二个线段。
  • [8,10) 颜色为 7 ,来自第三个线段。
    示例 3:
    在这里插入图片描述

输入:segments = [[1,4,5],[1,4,7],[4,7,1],[4,7,11]]
输出:[[1,4,12],[4,7,12]]
解释:绘画结果可以表示为:

  • [1,4) 颜色为 {5,7} (和为 12),分别来自第一和第二个线段。
  • [4,7) 颜色为 {1,11} (和为 12),分别来自第三和第四个线段。
    注意,只返回一个单独的线段 [1,7) 是不正确的,因为混合颜色的集合不相同。

提示:

1 <= segments.length <= 2 * 104
segments[i].length == 3
1 <= starti < endi <= 105
1 <= colori <= 109
每种颜色 colori 互不相同。

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

思路

看到这个题,第一反应和上周有道每日一题,扫描线很像,然后就死磕,最后也只能写了个超时的方法,唉
我的思路是这样的,首先将所有边都存在一个set中,是为了去重,然后放在优先队列中排序,然后从优先队列中取出,相邻的两个数就是一个结果区间。然后遍历每一个线段,用二分查找,找到这个线段出现的区间范围,然后在这些区间上加上这个数…
现在想来,确实很麻烦,难怪超时

class Solution {
    public List<List<Long>> splitPainting(int[][] segments) {
        int l = segments.length;
        //Arrays.sort(segments, (a,b) -> a[0] == b[0] ? a[1] - b[1] : a[0] - b[0]);
        
        //扫描线
        Set<Integer> set = new HashSet<>();
        for(int i = 0; i < l; i++){
            set.add(segments[i][0]);
            set.add(segments[i][1]);
        }
        
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for(int t : set){
            pq.offer(t);
        }
        
        List<List<Long>> res = new ArrayList<>();
        int left = pq.poll();
        
        while(!pq.isEmpty()){
            List<Long> list = new ArrayList<>();
            int right = pq.poll();
            list.add((long)left);
            list.add((long)right);
            list.add((long)0);
            res.add(list);
            left = right;
        }
        int size = res.size();
        //System.out.println(size);
        for(int i = 0; i < l; i++){
            int start = segments[i][0];
            int end = segments[i][1];
            int ll = 0;
            int rr = size - 1;
            while(ll < rr){
                int mid = (ll + rr + 1) / 2;
                if(res.get(mid).get(0) <= start){
                    ll = mid;;
                }else{
                    rr = mid - 1;
                }
            }
            int begin = ll;
            //System.out.println(begin);
            ll = 0;
            rr = size - 1;
            while(ll < rr){
                int mid = (ll + rr + 1) / 2;
                if(res.get(mid).get(1) <= end){
                    ll = mid;;
                }else{
                    rr = mid - 1;
                }
            }
            int finall = ll;
            //System.out.println(begin);
            
            for(int j = begin; j <= finall; j++){
                long temp = res.get(j).get(2);
                res.get(j).remove(2);
                temp += segments[i][2];
                res.get(j).add(temp);
            }
            
        }
        for(int i = 0; i < size; i++){
            if(res.get(i).get(2) == 0){
                res.remove(i);
                size = size - 1;
                i -= 1;
            }
        }
        return res;
    }
}

那么怎么做呢,差分数组加前缀和?
前几天也做了差分数组的题。看来还是没学到家
首先明确定义:差分数组 diff 维护相邻两个整数的被覆盖区间数量变化量
明确这个定义以后,思考一下自己能不能做
首先可以确定结果每个区间的开始位置和结束位置,并对这些位置排序去重,放在一个哈希表中

然后遍历每个线段,线段的左右端点肯定在哈希表中,然后左端点加上当前颜色值,右端点减去当前颜色值,即表示相邻的两个数被涂色的变化量(差分数组)

然后对这个集合求前缀和,就可以得到每个区间的颜色值

需要注意,颜色是long,需要转换类型

另外,做完看题解发现,特别强调了每个线段都有独一无二的颜色,所以混合的时候,也是新的颜色,所以当两个区间数值相同的时候,颜色也是不同的,因此不需要合并区间(见第三个例子)…这个我是没有考虑到这一层的

class Solution {
    public List<List<Long>> splitPainting(int[][] segments) {
        int l = segments.length;
        Map<Integer, Long> map = new HashMap<>();
        //统计边界数和计算差分数组
        for(int i = 0; i < l; i++){
            int a = segments[i][0];
            int b = segments[i][1];
            long color = segments[i][2];
            map.put(a, map.getOrDefault(a, (long)0) + color);
            map.put(b, map.getOrDefault(b, (long)0) - color);
        }
        //然后将这些点放在一个数组中排序
        int[] point = new int[map.size()];
        int id = 0;
        for(int key : map.keySet()){
            point[id++] = key;
        }
        Arrays.sort(point);
        List<List<Long>> res = new ArrayList<>();
        int prepoint = point[0];
        long precolor = 0;
        //计算前缀和
        for(int i = 1; i < point.length; i++){
            int temp = point[i];
            List<Long> list = new ArrayList<>();
            precolor += map.get(prepoint);
            //为0的去掉
            if(precolor == 0){
                prepoint = temp;
                continue;
            }
            list.add((long)prepoint);
            list.add((long)temp);
            list.add(precolor);
            res.add(list);
            prepoint = temp;
        }
        return res;
    }
}

1944. 队列中可以看到的人数

题目描述

有 n 个人排成一个队列,从左到右 编号为 0 到 n - 1 。给你以一个整数数组 heights ,每个整数 互不相同,heights[i] 表示第 i 个人的高度。

一个人能 看到 他右边另一个人的条件是这两人之间的所有人都比他们两人 矮 。更正式的,第 i 个人能看到第 j 个人的条件是 i < j 且 min(heights[i], heights[j]) > max(heights[i+1], heights[i+2], …, heights[j-1]) 。

请你返回一个长度为 n 的数组 answer ,其中 answer[i] 是第 i 个人在他右侧队列中能 看到 的 人数 。

示例 1:
在这里插入图片描述

输入:heights = [10,6,8,5,11,9]
输出:[3,1,2,1,1,0]
解释:
第 0 个人能看到编号为 1 ,2 和 4 的人。
第 1 个人能看到编号为 2 的人。
第 2 个人能看到编号为 3 和 4 的人。
第 3 个人能看到编号为 4 的人。
第 4 个人能看到编号为 5 的人。
第 5 个人谁也看不到因为他右边没人。
示例 2:

输入:heights = [5,1,2,3,10]
输出:[4,1,1,1,0]

提示:

n == heights.length
1 <= n <= 105
1 <= heights[i] <= 105
heights 中所有数 互不相同 。

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

思路

死磕第三题,导致这个题没看唉
然后十来分钟写出来了…啊啊啊啊啊啊啊啊啊啊

class Solution {
    public int[] canSeePersonsCount(int[] heights) {
        int l = heights.length;
        
        //单调栈,单调递减的
        //如果比栈顶大,就弹出,更新弹出的能看到的人
        //如果比栈顶小,就放入,并且相邻的人能看到人数加1
        //栈中存放下标
        Stack<Integer> stack = new Stack<>();
        int[] res = new int[l];

        stack.push(0);
        for(int i = 1; i < l; i++){
            while(!stack.isEmpty() && heights[i] >= heights[stack.peek()]){
                //如果大于等于栈顶的值,那么就弹出并且使人数加1
                res[stack.pop()]++;
            }
            //如果能加入一个值,说明当前值肯定在两个比它大的值中间,所以它前面的人肯定能看到它,所以前面人看到数量加1
            if(!stack.isEmpty())
                res[stack.peek()]++;
            stack.push(i);
        }
        return res;
    }
}

基本上所有题解都在逆序遍历,这是为啥呢
自己理解做了一下,感觉没什么太大的区别啊.

class Solution {
    public int[] canSeePersonsCount(int[] heights) {
        //理解一下逆序的做法
        int l = heights.length;
        int[] res = new int[l];
        Stack<Integer> stack = new Stack<>();

        for(int i = l - 1; i >= 0; i--){
            //栈中从栈顶到栈底是从小到大的
            //如果当前高度大于栈顶的高度,那么它左边的人肯定看不到栈里面的人,所以把栈中的弹出
            //并且当前人看到的人数加1
            while(!stack.isEmpty() && heights[stack.peek()] < heights[i]){
                stack.pop();
                res[i]++;
            }
            //如果此时栈不为空,说明栈顶高于当前人,此时当前人能看到的人就是栈顶的那个人
            if(!stack.isEmpty()){
                res[i]++;
            }
            stack.push(i);
        }
        return res;

    }
}

小结

这次周赛,两道600多,速度看了一下算可以的了。但是没想到最后一个题这么简单,死磕第三道给把自己磕没了…
第三道又学习了一下差分数组,做了差分数组的三道题,都是区间求和。
区间求和的题,差分数组用来表示两个相邻位置区间的变化量,然后配合前缀和,得到每个位置上的区间和。牢记

Leetcode 第251场周赛

1945. 字符串转化后的各位数字之和

题目描述

给你一个由小写字母组成的字符串 s ,以及一个整数 k 。

首先,用字母在字母表中的位置替换该字母,将 s 转化 为一个整数(也就是,‘a’ 用 1 替换,‘b’ 用 2 替换,… ‘z’ 用 26 替换)。接着,将整数 转换 为其 各位数字之和 。共重复 转换 操作 k 次 。

例如,如果 s = “zbax” 且 k = 2 ,那么执行下述步骤后得到的结果是整数 8 :

转化:“zbax” ➝ “(26)(2)(1)(24)” ➝ “262124” ➝ 262124
转换 #1:262124 ➝ 2 + 6 + 2 + 1 + 2 + 4 ➝ 17
转换 #2:17 ➝ 1 + 7 ➝ 8
返回执行上述操作后得到的结果整数。

示例 1:

输入:s = “iiii”, k = 1
输出:36
解释:操作如下:

  • 转化:“iiii” ➝ “(9)(9)(9)(9)” ➝ “9999” ➝ 9999
  • 转换 #1:9999 ➝ 9 + 9 + 9 + 9 ➝ 36
    因此,结果整数为 36 。
    示例 2:

输入:s = “leetcode”, k = 2
输出:6
解释:操作如下:

  • 转化:“leetcode” ➝ “(12)(5)(5)(20)(3)(15)(4)(5)” ➝ “12552031545” ➝ 12552031545
  • 转换 #1:12552031545 ➝ 1 + 2 + 5 + 5 + 2 + 0 + 3 + 1 + 5 + 4 + 5 ➝ 33
  • 转换 #2:33 ➝ 3 + 3 ➝ 6
    因此,结果整数为 6 。

提示:

1 <= s.length <= 100
1 <= k <= 10
s 由小写英文字母组成

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

思路

简单的模拟

class Solution {
    public int getLucky(String s, int k) {
        int l = s.length();
        StringBuffer sb = new StringBuffer();
        for(int i = 0; i < l; i++){
            char c= s.charAt(i);
            sb.append((c - 'a' + 1) + "");
        }
        int res = 0;
        for(int i = 0; i < k; i++){
            res = 0;
            for(int j = 0; j < sb.length(); j++)
                res += sb.charAt(j) - '0';
            StringBuffer temp = new StringBuffer();
            temp.append(res + "");
            sb = temp;
        }
        return res;
    }
}

1946. 子字符串突变后可能得到的最大整数

题目描述

给你一个字符串 num ,该字符串表示一个大整数。另给你一个长度为 10 且 下标从 0 开始 的整数数组 change ,该数组将 0-9 中的每个数字映射到另一个数字。更规范的说法是,数字 d 映射为数字 change[d] 。

你可以选择 突变 num 的任一子字符串。突变 子字符串意味着将每位数字 num[i] 替换为该数字在 change 中的映射(也就是说,将 num[i] 替换为 change[num[i]])。

请你找出在对 num 的任一子字符串执行突变操作(也可以不执行)后,可能得到的 最大整数 ,并用字符串表示返回。

子字符串 是字符串中的一个连续序列。

示例 1:

输入:num = “132”, change = [9,8,5,0,3,6,4,2,6,8]
输出:“832”
解释:替换子字符串 “1”:

  • 1 映射为 change[1] = 8 。
    因此 “132” 变为 “832” 。
    “832” 是可以构造的最大整数,所以返回它的字符串表示。
    示例 2:

输入:num = “021”, change = [9,4,3,5,7,2,1,9,0,6]
输出:“934”
解释:替换子字符串 “021”:

  • 0 映射为 change[0] = 9 。
  • 2 映射为 change[2] = 3 。
  • 1 映射为 change[1] = 4 。
    因此,“021” 变为 “934” 。
    “934” 是可以构造的最大整数,所以返回它的字符串表示。
    示例 3:

输入:num = “5”, change = [1,4,7,5,3,2,5,6,9,4]
输出:“5”
解释:“5” 已经是可以构造的最大整数,所以返回它的字符串表示。

提示:

1 <= num.length <= 105
num 仅由数字 0-9 组成
change.length == 10
0 <= change[d] <= 9

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

思路

这个题刚开始想复杂了,其实贪心突变开头的数就行了
但是注意,有可能突变过后的数和开始的数是一样的,这种情况就需要谨慎了
需要从第一个能变大的数开始突变,然后后面如果突变一样的就可以跳过继续突变
这两个点,导致我错两次

class Solution {
    public String maximumNumber(String num, int[] change) {
        Set<Integer> set = new HashSet();
        for(int i = 0; i < 10; i++){
            if(i <= change[i]){
                set.add(i);
            }
        }
        
        int l = num.length();
        char[] cc = num.toCharArray();
        int i = 0;
        for(; i < l; i++){
            char c = cc[i];
            int temp = cc[i] - '0';
            //System.out.println(temp);
            if(set.contains(temp) && temp != change[temp]){
                cc[i] = (char)(change[temp] + '0');
                break;
            }
        }
       // System.out.println(cc[0]);
        for(int j = i + 1; j < l; j++){
            char c = cc[j];
            int temp = cc[j] - '0';
            if(set.contains(temp)){
                cc[j] = (char)(change[temp] + '0');    
            }else{
                break;
            }
        }
        return new String(cc);
    }   
}

1947. 最大兼容性评分和

题目描述

有一份由 n 个问题组成的调查问卷,每个问题的答案要么是 0(no,否),要么是 1(yes,是)。

这份调查问卷被分发给 m 名学生和 m 名导师,学生和导师的编号都是从 0 到 m - 1 。学生的答案用一个二维整数数组 students 表示,其中 students[i] 是一个整数数组,包含第 i 名学生对调查问卷给出的答案(下标从 0 开始)。导师的答案用一个二维整数数组 mentors 表示,其中 mentors[j] 是一个整数数组,包含第 j 名导师对调查问卷给出的答案(下标从 0 开始)。

每个学生都会被分配给 一名 导师,而每位导师也会分配到 一名 学生。配对的学生与导师之间的兼容性评分等于学生和导师答案相同的次数。

例如,学生答案为[1, 0, 1] 而导师答案为 [0, 0, 1] ,那么他们的兼容性评分为 2 ,因为只有第二个和第三个答案相同。
请你找出最优的学生与导师的配对方案,以 最大程度上 提高 兼容性评分和 。

给你 students 和 mentors ,返回可以得到的 最大兼容性评分和 。

示例 1:

输入:students = [[1,1,0],[1,0,1],[0,0,1]], mentors = [[1,0,0],[0,0,1],[1,1,0]]
输出:8
解释:按下述方式分配学生和导师:

  • 学生 0 分配给导师 2 ,兼容性评分为 3 。
  • 学生 1 分配给导师 0 ,兼容性评分为 2 。
  • 学生 2 分配给导师 1 ,兼容性评分为 3 。
    最大兼容性评分和为 3 + 2 + 3 = 8 。
    示例 2:

输入:students = [[0,0],[0,0],[0,0]], mentors = [[1,1],[1,1],[1,1]]
输出:0
解释:任意学生与导师配对的兼容性评分都是 0 。

提示:

m == students.length == mentors.length
n == students[i].length == mentors[j].length
1 <= m, n <= 8
students[i][k] 为 0 或 1
mentors[j][k] 为 0 或 1

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

思路

回溯,每个都匹配一次,看看哪个最大

class Solution {
    int[][] s;
    int[][] t;
    boolean[] usedt;
    int max;
    int m;
    int n;
    public int maxCompatibilitySum(int[][] students, int[][] mentors) {
        s = students;
        t = mentors;
        //这个范围直接暴力了
        //用二进制表示
        m = students.length;
        n = students[0].length;
        usedt = new boolean[m];
        dfs(0, 0);
        return max;
    }
    
    
    public void dfs(int ids, int grade){
        if(ids == m){
            max = Math.max(max, grade);
            return;
        }
        for(int i = 0; i < m; i++){
            int[] a = s[ids];
            int[] b = t[i];
            int temp = 0;
            if(usedt[i])
                continue;
            for(int j = 0; j < n; j++){
                if(a[j] == b[j]){
                    temp++;
                }
            }
            usedt[i] = true;
            dfs(ids + 1, grade + temp);
            usedt[i] = false;
        }
    }
}

状态压缩的动态规划,因为在周赛中还是很常见的,所以特地练一下

当数据范围很小,且值是0 和1 的时候,可以考虑状态压缩
当前关键还是动态规划的状态定义,这里的状态定义为对于当老师分配学生的状态为mask时,兼容性评分的最大值,并且mask中1的个数c就是分配了前c个学生
具体思路看代码注释:

class Solution {
    public int maxCompatibilitySum(int[][] students, int[][] mentors) {
        //写一下状态压缩的动态规划,毕竟这个还是很常见的
        //首先预处理每个学生和老师的兼容性评分
        int l = students.length;
        int n = students[0].length;
        int[][] grade = new int[l][l];
        for(int i = 0; i < l; i++){
            for(int j = 0; j < l; j++){
                for(int k = 0; k < n; k++){
                    grade[i][j] += students[i][k] == mentors[j][k] ? 1 : 0;
                }
            }
        }

        //然后用一个二进制数表示老师是否被分配到了学生,如果mask的第i位为1,那么表示第i位老师被分配到了学生,否则没有
        //然后转换为动态规划问题
        //dp定义为当老师分配学生的状态为mask时,兼容性评分的最大值
        int[] dp = new int[(1 << l)];
        for(int mask = 1; mask < (1 << l); mask++){
            //有c个老师被分配到学生,并且是按照编号的顺序来分配的,即前c个老师就分配前c个学生
            //当前要分配第c个学生
            int c = Integer.bitCount(mask);
            //System.out.println(c + "dfadfs");
            //枚举对于第c个学生,分配到了哪个老师
            for(int i = 0; i < l; i++){
                //如果当前第i个位置为1,也就是这个老师可以分配学生
                if((mask & (1 << i)) != 0){
                    //然后对这个老师分配第c个学生,状态转移就是使当前位置为0,然后其他位置不变,
                    //再加上给当前学生分配当前老师的分数
                    dp[mask] = Math.max(dp[mask], dp[(mask ^ (1 << i))] + grade[c - 1][i]);
                    //System.out.println(dp[mask]);
                }
            }
        }
        //最后返回给m个老师分配m个学生的最大值
        return dp[(1 << l) - 1];
    }
}

1948. 删除系统中的重复文件夹

题目描述

由于一个漏洞,文件系统中存在许多重复文件夹。给你一个二维数组 paths,其中 paths[i] 是一个表示文件系统中第 i 个文件夹的绝对路径的数组。

例如,[“one”, “two”, “three”] 表示路径 “/one/two/three” 。
如果两个文件夹(不需要在同一层级)包含 非空且相同的 子文件夹 集合 并具有相同的子文件夹结构,则认为这两个文件夹是相同文件夹。相同文件夹的根层级 不 需要相同。如果存在两个(或两个以上)相同 文件夹,则需要将这些文件夹和所有它们的子文件夹 标记 为待删除。

例如,下面文件结构中的文件夹 “/a” 和 “/b” 相同。它们(以及它们的子文件夹)应该被 全部 标记为待删除:
/a
/a/x
/a/x/y
/a/z
/b
/b/x
/b/x/y
/b/z
然而,如果文件结构中还包含路径 “/b/w” ,那么文件夹 “/a” 和 “/b” 就不相同。注意,即便添加了新的文件夹 “/b/w” ,仍然认为 “/a/x” 和 “/b/x” 相同。
一旦所有的相同文件夹和它们的子文件夹都被标记为待删除,文件系统将会 删除 所有上述文件夹。文件系统只会执行一次删除操作。执行完这一次删除操作后,不会删除新出现的相同文件夹。

返回二维数组 ans ,该数组包含删除所有标记文件夹之后剩余文件夹的路径。路径可以按 任意顺序 返回。

示例 1:
在这里插入图片描述

输入:paths = [[“a”],[“c”],[“d”],[“a”,“b”],[“c”,“b”],[“d”,“a”]]
输出:[[“d”],[“d”,“a”]]
解释:文件结构如上所示。
文件夹 “/a” 和 “/c”(以及它们的子文件夹)都会被标记为待删除,因为它们都包含名为 “b” 的空文件夹。
示例 2:
在这里插入图片描述

输入:paths = [[“a”],[“c”],[“a”,“b”],[“c”,“b”],[“a”,“b”,“x”],[“a”,“b”,“x”,“y”],[“w”],[“w”,“y”]]
输出:[[“c”],[“c”,“b”],[“a”],[“a”,“b”]]
解释:文件结构如上所示。
文件夹 “/a/b/x” 和 “/w”(以及它们的子文件夹)都会被标记为待删除,因为它们都包含名为 “y” 的空文件夹。
注意,文件夹 “/a” 和 “/c” 在删除后变为相同文件夹,但这两个文件夹不会被删除,因为删除只会进行一次,且它们没有在删除前被标记。
示例 3:
在这里插入图片描述

输入:paths = [[“a”,“b”],[“c”,“d”],[“c”],[“a”]]
输出:[[“c”],[“c”,“d”],[“a”],[“a”,“b”]]
解释:文件系统中所有文件夹互不相同。
注意,返回的数组可以按不同顺序返回文件夹路径,因为题目对顺序没有要求。
示例 4:
在这里插入图片描述

输入:paths = [[“a”],[“a”,“x”],[“a”,“x”,“y”],[“a”,“z”],[“b”],[“b”,“x”],[“b”,“x”,“y”],[“b”,“z”]]
输出:[]
解释:文件结构如上所示。
文件夹 “/a/x” 和 “/b/x”(以及它们的子文件夹)都会被标记为待删除,因为它们都包含名为 “y” 的空文件夹。
文件夹 “/a” 和 “/b”(以及它们的子文件夹)都会被标记为待删除,因为它们都包含一个名为 “z” 的空文件夹以及上面提到的文件夹 “x” 。
示例 5:
在这里插入图片描述

输入:paths = [[“a”],[“a”,“x”],[“a”,“x”,“y”],[“a”,“z”],[“b”],[“b”,“x”],[“b”,“x”,“y”],[“b”,“z”],[“b”,“w”]]
输出:[[“b”],[“b”,“w”],[“b”,“z”],[“a”],[“a”,“z”]]
解释:本例与上例的结构基本相同,除了新增 “/b/w” 文件夹。
文件夹 “/a/x” 和 “/b/x” 仍然会被标记,但 “/a” 和 “/b” 不再被标记,因为 “/b” 中有名为 “w” 的空文件夹而 “/a” 没有。
注意,"/a/z" 和 “/b/z” 不会被标记,因为相同子文件夹的集合必须是非空集合,但这两个文件夹都是空的。

提示:

1 <= paths.length <= 2 * 104
1 <= paths[i].length <= 500
1 <= paths[i][j].length <= 10
1 <= sum(paths[i][j].length) <= 2 * 105
path[i][j] 由小写英文字母组成
不会存在两个路径都指向同一个文件夹的情况
对于不在根层级的任意文件夹,其父文件夹也会包含在输入中

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

思路

看了几十分钟,没有任何思路…想到可能是字典树啥的,但还是不知道怎么做
官解写的很详细了,然后来理解一下:

树哈希,也叫作树的序列化表示
三个步骤:首先能想到的就是,通过所给的文件路径,得到一个文件系统的树形结构,它是一个多叉树
第二,对于这个树,需要记录每一条路径,并存在一个数据结构中。因为需要在处理完子树以后处理当前节点,所以对应着后序遍历
第三,再从根节点进行一次遍历,如果遍历到节点x,如果x的结构在数据结构中出现过超过1次,那么就删除x。否则加入结果,并继续遍历它的子节点

对于第一步建树,就是一个字典树的建立过程
对于第二步,存储路径,需要将每条路径序列化,用一个字符串表示。如果x是子节点,那么因为它不包含文件夹,所以为空。如果不是子节点,那么需要使用一种序列化的表示形式来使其变成一个字符串;或者是用哈希值处理

第三步,用哈希表判断是否存在相同的路径,如果相同就删除了

想自己写,发现根本不行,这个题先放一放吧,大致思想懂了
这个代码有错误,是用序列化的方法做的

class Solution {
    
    //字典树节点
    class TrieNode {
        Map<String, TrieNode> children = new HashMap<>();
        boolean deleted;
    }
    public List<List<String>> deleteDuplicateFolder(List<List<String>> paths) {
        TrieNode root = new TrieNode();
        //遍历路径,建立字典树
        for (List<String> path : paths) {
            TrieNode curr = root;
            for (String folder : path) {
                if (!curr.children.containsKey(folder)) {
                    curr.children.put(folder, new TrieNode());
                }
                curr = curr.children.get(folder);
            }
        }
        //序列化,并且统计需要删除的点
        delete(root, new HashMap<>());
        
        List<List<String>> ans = new ArrayList<>();
        
        dfs(root, new ArrayList<>(), ans);
        return ans;
    }
    
    //
    String delete(TrieNode root, Map<String, TrieNode> map) {
        //如果当前节点为叶子节点,那么其序列化表示就是空
        if (root.children.isEmpty()) return "";
        StringBuilder sb = new StringBuilder();
        //否则,遍历当前节点的子节点,对当前节点进行序列化
        for (Map.Entry<String, TrieNode> e : root.children.entrySet()) {
            String folder = e.getKey();
            TrieNode child = e.getValue();
            //形式是(name())(name())(name)...的形式
            sb.append('(').append(folder).append(delete(child, map)).append(')');
        }
        //当前节点的序列化值
        String serialized = sb.toString();
        // 已经存在此序列化值,两者都删除,将删除标记记为true
        if (map.containsKey(serialized)) {
            map.get(serialized).deleted = true;
            root.deleted = true;
        //如果不存在当前序列化值,则放入哈希表中
        } else {
            map.put(serialized, root);
        }
        return serialized;
    }
    //递归进行删除,将没有删除标记的节点加入到答案列表中
    void dfs(TrieNode root, List<String> path, List<List<String>> ans) {
        for (Map.Entry<String, TrieNode> e : root.children.entrySet()) {
            String folder = e.getKey();
            TrieNode child = e.getValue();
            if (child.deleted) continue;
            //如果没有标记删除,加入path中
            path.add(folder);
            //递归子树
            dfs(child, path, ans);
            //回溯
            path.remove(path.size() - 1);
        }
        if (!path.isEmpty()) ans.add(new ArrayList<>(path));
    }
}

数哈希的方法

class Solution {
    public List<List<String>> deleteDuplicateFolder(List<List<String>> paths) {
        // 构建前缀树
        Node root = new Node();
        for (List<String> path : paths) {
            Node node = root;
            for (String s : path) {
                node = node.nextMap.computeIfAbsent(s, t -> new Node());
            }
        }

        // 对前缀树的所有节点进行标记,判断子树是否相等
        tagDupFolderByDfs(root, new HashMap<>());

        ArrayList<List<String>> resList = new ArrayList<>();
        // 结果入栈
        deleteDuplicateFolder(root, new ArrayList<>(), resList);

        return resList;
    }

    // 对前缀树的所有节点进行标记,判断子树是否相等
    private int tagDupFolderByDfs(Node root, HashMap<Integer, ArrayList<Node>> map) {
        int hash = Objects.hash();
        for (Map.Entry<String, Node> entry : root.nextMap.entrySet()) {
            // 求当前节点的hash值,记录了当前节点为头的所有子树节点的信息
            hash = Objects.hash(hash, entry.getKey(), tagDupFolderByDfs(entry.getValue(), map));
        }

        // 标记hash值相等的节点
        if (!root.nextMap.isEmpty()) {
            for (Node node : map.getOrDefault(hash, new ArrayList<>())) {
                if (root.equals(node)) {
                    root.mark = node.mark = true;
                    break;
                }
            }
        }
        
        // 将hash值相等的节点存入同一个list中
        map.computeIfAbsent(hash, t -> new ArrayList<>()).add(root);
        return hash;
    }

    // 统计结果,删除所有标记为true的节点及其子树
    private void deleteDuplicateFolder(Node root, ArrayList<String> stack, ArrayList<List<String>> resList) {
        if (!root.mark) {
            if (!stack.isEmpty()) {
                resList.add(new ArrayList<>(stack));
            }
            
            for (Map.Entry<String, Node> entry : root.nextMap.entrySet()) {
                stack.add(entry.getKey());
                deleteDuplicateFolder(entry.getValue(), stack, resList);
                stack.remove(stack.size() - 1);
            }
        }
    }

    // 前缀树节点
    private class Node {
        // TreeMap存储节点所有分支,这样最后统计结果时会按字典序进行排列
        private TreeMap<String, Node> nextMap;
        // 标记为true则代表,节点为头的子树存在重复,需要删除
        private boolean mark;
        
        public Node() {
            this.nextMap = new TreeMap<>();
            this.mark = false;
        }

        // 自定义判断节点是否相等的规则,即以当前节点为头的子树分支路径必须完全相等
        public boolean equals(Node node) {
            if (!nextMap.keySet().equals(node.nextMap.keySet())) {
                return false;
            }
            for (Map.Entry<String, Node> entry : nextMap.entrySet()) {
                if (!entry.getValue().equals(node.nextMap.get(entry.getKey()))) {
                    return false;
                }
            }
            return true;
        }
    }
}

作者:zi-tian-jiang-b
链接:https://leetcode-cn.com/problems/delete-duplicate-folders-in-system/solution/1948-shan-chu-xi-tong-zhong-de-zhong-fu-ks4b5/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值