1233.删除子文件夹

目标

你是一位系统管理员,手里有一份文件夹列表 folder,你的任务是要删除该列表中的所有 子文件夹,并以 任意顺序 返回剩下的文件夹。

如果文件夹 folder[i] 位于另一个文件夹 folder[j] 下,那么 folder[i] 就是 folder[j] 的 子文件夹 。folder[j] 的子文件夹必须以 folder[j] 开头,后跟一个 "/"。例如,"/a/b" 是 "/a" 的一个子文件夹,但 "/b" 不是 "/a/b/c" 的一个子文件夹。

文件夹的「路径」是由一个或多个按以下格式串联形成的字符串:'/' 后跟一个或者多个小写英文字母。

  • 例如,"/leetcode" 和 "/leetcode/problems" 都是有效的路径,而空字符串和 "/" 不是。

示例 1:

输入:folder = ["/a","/a/b","/c/d","/c/d/e","/c/f"]
输出:["/a","/c/d","/c/f"]
解释:"/a/b" 是 "/a" 的子文件夹,而 "/c/d/e" 是 "/c/d" 的子文件夹。

示例 2:

输入:folder = ["/a","/a/b/c","/a/b/d"]
输出:["/a"]
解释:文件夹 "/a/b/c" 和 "/a/b/d" 都会被删除,因为它们都是 "/a" 的子文件夹。

示例 3:

输入: folder = ["/a/b/c","/a/b/ca","/a/b/d"]
输出: ["/a/b/c","/a/b/ca","/a/b/d"]

说明:

  • 1 <= folder.length <= 4 * 10^4
  • 2 <= folder[i].length <= 100
  • folder[i] 只包含小写字母和 '/'
  • folder[i] 总是以字符 '/' 起始
  • folder 每个元素都是 唯一 的

思路

有一个文件夹列表,删除其中所有的子文件夹。

将文件夹列表排序,父目录总是会先出现,记为 prev,使用 startsWith 判断当前目录是否是子目录,如果不是更新 prev,并加入结果集合,否则直接跳过。需要注意 prev 需要以 / 结尾,否则会错误地匹配,比如 /a/b/c 并不是 /a/b/ca 的父目录。

也可以使用字典树来解决该问题,构造字典树,将最后一个节点标记为文件夹列表的下标,dfs 字典树,如果遇到标记非空则直接返回,不再查找子文件夹。

代码


/**
 * @date 2025-07-19 10:57
 */
public class RemoveSubfolders1233 {

    public List<String> removeSubfolders(String[] folder) {
        Arrays.sort(folder);
        String prev = ".";
        List<String> res = new ArrayList<>();
        for (String path : folder) {
            if (path.startsWith(prev)) {
                continue;
            }
            res.add(path);
            prev = path + "/";
        }
        return res;
    }

}

性能

2163.删除元素后和的最小差值

目标

给你一个下标从 0 开始的整数数组 nums ,它包含 3 * n 个元素。

你可以从 nums 中删除 恰好 n 个元素,剩下的 2 * n 个元素将会被分成两个 相同大小 的部分。

  • 前面 n 个元素属于第一部分,它们的和记为 sumfirst 。
  • 后面 n 个元素属于第二部分,它们的和记为 sumsecond 。

两部分和的 差值 记为 sumfirst - sumsecond 。

  • 比方说,sumfirst = 3 且 sumsecond = 2 ,它们的差值为 1 。
  • 再比方,sumfirst = 2 且 sumsecond = 3 ,它们的差值为 -1 。

请你返回删除 n 个元素之后,剩下两部分和的 差值的最小值 是多少。

示例 1:

输入:nums = [3,1,2]
输出:-1
解释:nums 有 3 个元素,所以 n = 1 。
所以我们需要从 nums 中删除 1 个元素,并将剩下的元素分成两部分。
- 如果我们删除 nums[0] = 3 ,数组变为 [1,2] 。两部分和的差值为 1 - 2 = -1 。
- 如果我们删除 nums[1] = 1 ,数组变为 [3,2] 。两部分和的差值为 3 - 2 = 1 。
- 如果我们删除 nums[2] = 2 ,数组变为 [3,1] 。两部分和的差值为 3 - 1 = 2 。
两部分和的最小差值为 min(-1,1,2) = -1 。

示例 2:

输入:nums = [7,9,5,8,1,3]
输出:1
解释:n = 2 。所以我们需要删除 2 个元素,并将剩下元素分为 2 部分。
如果我们删除元素 nums[2] = 5 和 nums[3] = 8 ,剩下元素为 [7,9,1,3] 。和的差值为 (7+9) - (1+3) = 12 。
为了得到最小差值,我们应该删除 nums[1] = 9 和 nums[4] = 1 ,剩下的元素为 [7,5,8,3] 。和的差值为 (7+5) - (8+3) = 1 。
观察可知,最优答案为 1 。

说明:

  • nums.length == 3 * n
  • 1 <= n <= 10^5
  • 1 <= nums[i] <= 10^5

思路

有一个长度为 3n 的数组,删除其中的 n 个元素,使得剩余的 2n 个元素中,前 n 个元素的和 减去 后 n 个元素和 最小。

将数组划分为长度不小于 k 的两部分,枚举分界线,进行前后缀分解,计算左侧前 k 小元素的和,以及右侧前 k 大元素的和。

可以使用长度为 k 的优先队列来维护第 k 大/小元素,根据加入与移出的元素,可以知道队列所有元素的和的变化。

代码


/**
 * @date 2025-07-18 8:43
 */
public class MinimumDifference2163 {

    public long minimumDifference(int[] nums) {
        int l = nums.length;
        int n = l / 3;
        long[] suffix = new long[l + 1];
        long[] prefix = new long[l + 1];
        for (int i = 1; i <= n; i++) {
            prefix[i] = prefix[i - 1] + nums[i - 1];
            suffix[l - i] = suffix[l - i + 1] + nums[l - i];
        }
        PriorityQueue<Integer> left = new PriorityQueue<>((a, b) -> b - a);
        PriorityQueue<Integer> right = new PriorityQueue<>();
        for (int i = 0; i < 2 * n; i++) {
            left.add(nums[i]);
            if (left.size() > n) {
                Integer num = left.poll();
                prefix[i + 1] = prefix[i] + nums[i] - num;
            }
        }
        for (int i = l - 1; i >= n; i--) {
            right.add(nums[i]);
            if (right.size() > n) {
                Integer num = right.poll();
                suffix[i] = suffix[i + 1] + nums[i] - num;
            }
        }
        long res = Long.MAX_VALUE;
        for (int i = n; i <= 2 * n; i++) {
            res = Math.min(res, prefix[i] - suffix[i]);
        }
        return res;
    }

}

性能

3202.找出有效子序列的最大长度II

目标

给你一个整数数组 nums 和一个 正 整数 k 。

nums 的一个 子序列 sub 的长度为 x ,如果其满足以下条件,则称其为 有效子序列 :

  • (sub[0] + sub[1]) % k == (sub[1] + sub[2]) % k == ... == (sub[x - 2] + sub[x - 1]) % k

返回 nums 的 最长有效子序列 的长度。

示例 1:

输入:nums = [1,2,3,4,5], k = 2
输出:5
解释:
最长有效子序列是 [1, 2, 3, 4, 5] 。

示例 2:

输入:nums = [1,4,2,3,1,4], k = 3
输出:4
解释:
最长有效子序列是 [1, 4, 1, 4] 。

说明:

  • 2 <= nums.length <= 10^3
  • 1 <= nums[i] <= 10^7
  • 1 <= k <= 10^3

思路

找出数组 nums 的有效子序列的最大长度,有效子序列指相邻元素之和模 k 的值相等。

3201.找出有效子序列的最大长度I 是本题 k = 2 的特例。这个题目可能的组合有 k^2 种。

定义 dp[i][j] 表示子序列后两项模 k 的值为 ij 的子序列长度。

代码


/**
 * @date 2025-07-16 10:18
 */
public class MaximumLength3202 {

    public int maximumLength(int[] nums, int k) {
        int[][] dp = new int[k][k];
        int res = 0;
        for (int num : nums) {
            int m = num % k;
            for (int i = 0; i < k; i++) {
                dp[i][m] = dp[m][i] + 1;
                res = Math.max(res, dp[i][m]);
            }
        }
        return res;
    }

}

性能

3201.找出有效子序列的最大长度I

目标

给你一个整数数组 nums。

nums 的子序列 sub 的长度为 x ,如果其满足以下条件,则称其为 有效子序列:

  • (sub[0] + sub[1]) % 2 == (sub[1] + sub[2]) % 2 == ... == (sub[x - 2] + sub[x - 1]) % 2

返回 nums 的 最长的有效子序列 的长度。

一个 子序列 指的是从原数组中删除一些元素(也可以不删除任何元素),剩余元素保持原来顺序组成的新数组。

示例 1:

输入: nums = [1,2,3,4]
输出: 4
解释:
最长的有效子序列是 [1, 2, 3, 4]。

示例 2:

输入: nums = [1,2,1,1,2,1,2]
输出: 6
解释:
最长的有效子序列是 [1, 2, 1, 2, 1, 2]。

示例 3:

输入: nums = [1,3]
输出: 2
解释:
最长的有效子序列是 [1, 3]。

说明:

  • 2 <= nums.length <= 2 * 10^5
  • 1 <= nums[i] <= 10^7

思路

找出有效子序列的最大长度,使得子序列中相邻元素和的奇偶性相同。

注意到有效子序列中奇数下标的奇偶性必须相同,同时偶数下标的奇偶性也必须相同。总共四种情况:奇偶、奇奇、偶奇、偶偶。

代码


/**
 * @date 2025-07-16 8:43
 */
public class MaximumLength3201 {

    public int maximumLength(int[] nums) {
        int res = 0;
        for (int a = 0; a <= 1; a++) {
            for (int b = 0; b <= 1; b++) {
                int l = 0;
                int[] p = new int[]{a, b};
                int k = 0;
                for (int num : nums) {
                    if (num % 2 == p[k]) {
                        l++;
                        k ^= 1;
                    }
                }
                res = Math.max(l, res);
            }
        }
        return res;
    }

}

性能

3136.有效单词

目标

有效单词 需要满足以下几个条件:

  • 至少 包含 3 个字符。
  • 由数字 0-9 和英文大小写字母组成。(不必包含所有这类字符。)
  • 至少 包含一个 元音字母 。
  • 至少 包含一个 辅音字母 。

给你一个字符串 word 。如果 word 是一个有效单词,则返回 true ,否则返回 false 。

注意:

  • 'a'、'e'、'i'、'o'、'u' 及其大写形式都属于 元音字母 。
  • 英文中的 辅音字母 是指那些除元音字母之外的字母。

示例 1:

输入:word = "234Adas"
输出:true
解释:
这个单词满足所有条件。

示例 2:

输入:word = "b3"
输出:false
解释:
这个单词的长度少于 3 且没有包含元音字母。

示例 3:

输入:word = "a3$e"
输出:false
解释:
这个单词包含了 '$' 字符且没有包含辅音字母。

说明:

  • 1 <= word.length <= 20
  • word 由英文大写和小写字母、数字、'@'、'#' 和 '$' 组成。

思路

判断字符串是否至少包含 3 个字符,且仅由英文字母与数字组成,且至少包含一个元音字母和一个辅音字母。

正则表达式,前瞻断言。

代码


/**
 * @date 2025-07-15 8:52
 */
public class IsValid3136 {

    public static Pattern p = Pattern.compile("^(?=.*[aeiouAEIOU])(?=.*[b-df-hj-np-tv-zB-DF-HJ-NP-TV-Z])[0-9a-zA-z]{3,}$");

    public boolean isValid(String word) {
        return p.matcher(word).find();
    }

}

性能

1290.二进制链表转整数

目标

给你一个单链表的引用结点 head。链表中每个结点的值不是 0 就是 1。已知此链表是一个整数数字的二进制表示形式。

请你返回该链表所表示数字的 十进制值 。

示例 1:

输入:head = [1,0,1]
输出:5
解释:二进制数 (101) 转化为十进制数 (5)

示例 2:

输入:head = [0]
输出:0

示例 3:

输入:head = [1]
输出:1

示例 4:

输入:head = [1,0,0,1,0,0,1,1,1,0,0,0,0,0,0]
输出:18880

示例 5:

输入:head = [0,0]
输出:0

说明:

  • 链表不为空。
  • 链表的结点总数不超过 30。
  • 每个结点的值不是 0 就是 1。

思路

有一个链表的元素值为 01,求该链表表示的十进制数字。

代码


/**
 * @date 2025-07-14 8:48
 */
public class GetDecimalValue1290 {

    public int getDecimalValue(ListNode head) {
        int res = 0;
        while (head != null) {
            res = (res << 1) + head.val;
            head = head.next;
        }
        return res;
    }

}

性能

2410.运动员和训练师的最大匹配数

目标

给你一个下标从 0 开始的整数数组 players ,其中 players[i] 表示第 i 名运动员的 能力 值,同时给你一个下标从 0 开始的整数数组 trainers ,其中 trainers[j] 表示第 j 名训练师的 训练能力值 。

如果第 i 名运动员的能力值 小于等于 第 j 名训练师的能力值,那么第 i 名运动员可以 匹配 第 j 名训练师。除此以外,每名运动员至多可以匹配一位训练师,每位训练师最多可以匹配一位运动员。

请你返回满足上述要求 players 和 trainers 的 最大 匹配数。

示例 1:

输入:players = [4,7,9], trainers = [8,2,5,8]
输出:2
解释:
得到两个匹配的一种方案是:
- players[0] 与 trainers[0] 匹配,因为 4 <= 8 。
- players[1] 与 trainers[3] 匹配,因为 7 <= 8 。
可以证明 2 是可以形成的最大匹配数。

示例 2:

输入:players = [1,1,1], trainers = [10]
输出:1
解释:
训练师可以匹配所有 3 个运动员
每个运动员至多只能匹配一个训练师,所以最大答案是 1 。

说明:

  • 1 <= players.length, trainers.length <= 10^5
  • 1 <= players[i], trainers[j] <= 10^9

思路

有两个数组 arr1arr2,如果 arr1[i] <= arr2[j],则称 arr1[i]arr2[j] 相匹配。arr1 中的元素最多只能匹配一个 arr2 中的元素,同时,arr2 中的元素最多只能匹配一个 arr1 中的元素,求最大匹配数。

贪心策略,arr1 中的最小元素优先匹配 arr2 中的最小元素。

代码


/**
 * @date 2025-07-13 20:20
 */
public class MatchPlayersAndTrainers2410 {

    public int matchPlayersAndTrainers(int[] players, int[] trainers) {
        Arrays.sort(players);
        Arrays.sort(trainers);
        int m = players.length;
        int n = trainers.length;
        int res = 0;
        int i = 0, j = 0;
        while (i < m && j < n) {
            if (players[i] <= trainers[j]) {
                res++;
                i++;
            }
            j++;
        }
        return res;
    }

}

性能

1900.最佳运动员的比拼回合

目标

n 名运动员参与一场锦标赛,所有运动员站成一排,并根据 最开始的 站位从 1 到 n 编号(运动员 1 是这一排中的第一个运动员,运动员 2 是第二个运动员,依此类推)。

锦标赛由多个回合组成(从回合 1 开始)。每一回合中,这一排从前往后数的第 i 名运动员需要与从后往前数的第 i 名运动员比拼,获胜者将会进入下一回合。如果当前回合中运动员数目为奇数,那么中间那位运动员将轮空晋级下一回合。

例如,当前回合中,运动员 1, 2, 4, 6, 7 站成一排

  • 运动员 1 需要和运动员 7 比拼
  • 运动员 2 需要和运动员 6 比拼
  • 运动员 4 轮空晋级下一回合

每回合结束后,获胜者将会基于最开始分配给他们的原始顺序(升序)重新排成一排。

编号为 firstPlayer 和 secondPlayer 的运动员是本场锦标赛中的最佳运动员。在他们开始比拼之前,完全可以战胜任何其他运动员。而任意两个其他运动员进行比拼时,其中任意一个都有获胜的可能,因此你可以 裁定 谁是这一回合的获胜者。

给你三个整数 n、firstPlayer 和 secondPlayer 。返回一个由两个值组成的整数数组,分别表示两位最佳运动员在本场锦标赛中比拼的 最早 回合数和 最晚 回合数。

示例 1:

输入:n = 11, firstPlayer = 2, secondPlayer = 4
输出:[3,4]
解释:
一种能够产生最早回合数的情景是:
回合 1:1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
回合 2:2, 3, 4, 5, 6, 11
回合 3:2, 3, 4
一种能够产生最晚回合数的情景是:
回合 1:1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11
回合 2:1, 2, 3, 4, 5, 6
回合 3:1, 2, 4
回合 4:2, 4

示例 2:

输入:n = 5, firstPlayer = 1, secondPlayer = 5
输出:[1,1]
解释:两名最佳运动员 1 和 5 将会在回合 1 进行比拼。
不存在使他们在其他回合进行比拼的可能。

说明:

  • 2 <= n <= 28
  • 1 <= firstPlayer < secondPlayer <= n

思路

代码

性能

3169.无需开会的工作日

目标

给你一个正整数 days,表示员工可工作的总天数(从第 1 天开始)。另给你一个二维数组 meetings,长度为 n,其中 meetings[i] = [start_i, end_i] 表示第 i 次会议的开始和结束天数(包含首尾)。

返回员工可工作且没有安排会议的天数。

注意:会议时间可能会有重叠。

示例 1:

输入:days = 10, meetings = [[5,7],[1,3],[9,10]]
输出:2
解释:
第 4 天和第 8 天没有安排会议。

示例 2:

输入:days = 5, meetings = [[2,4],[1,3]]
输出:1
解释:
第 5 天没有安排会议。

示例 3:

输入:days = 6, meetings = [[1,6]]
输出:0
解释:
所有工作日都安排了会议。

说明:

  • 1 <= days <= 10^9
  • 1 <= meetings.length <= 10^5
  • meetings[i].length == 2
  • 1 <= meetings[i][0] <= meetings[i][1] <= days

思路

在区间 [1, days] 上有一些区间 [si, ei],求没有被这些区间覆盖的正整数个数。

将区间按起点排序,记录已访问区间的最大终点 prevEndsi - prevEnd - 1 即为当前区间与上一个区间之间的整数个数。注意特殊处理开头与结尾。

网友题解则是合并相交的区间,用总数减去区间覆盖的整数即为答案。

代码


/**
 * @date 2025-07-11 18:53
 */
public class CountDays3169 {

    public int countDays(int days, int[][] meetings) {
        Arrays.sort(meetings, (a, b) -> a[0] - b[0]);
        int res = 0;
        int prevEnd = 0;
        for (int[] meeting : meetings) {
            int interval = meeting[0] - prevEnd - 1;
            res += Math.max(interval, 0);
            prevEnd = Math.max(prevEnd, meeting[1]);
        }
        return res + Math.max(0, days - prevEnd);
    }

}

性能

3440.重新安排会议得到最多空余时间II

目标

给你一个整数 eventTime 表示一个活动的总时长,这个活动开始于 t = 0 ,结束于 t = eventTime 。

同时给你两个长度为 n 的整数数组 startTime 和 endTime 。它们表示这次活动中 n 个时间 没有重叠 的会议,其中第 i 个会议的时间为 [startTime[i], endTime[i]] 。

你可以重新安排 至多 一个会议,安排的规则是将会议时间平移,且保持原来的 会议时长 ,你的目的是移动会议后 最大化 相邻两个会议之间的 最长 连续空余时间。

请你返回重新安排会议以后,可以得到的 最大 空余时间。

注意,会议 不能 安排到整个活动的时间以外,且会议之间需要保持互不重叠。

注意:重新安排会议以后,会议之间的顺序可以发生改变

示例 1:

输入:eventTime = 5, startTime = [1,3], endTime = [2,5]
输出:2
解释:
将 [1, 2] 的会议安排到 [2, 3] ,得到空余时间 [0, 2] 。

示例 2:

输入:eventTime = 10, startTime = [0,7,9], endTime = [1,8,10]
输出:7
解释:
将 [0, 1] 的会议安排到 [8, 9] ,得到空余时间 [0, 7] 。

示例 3:

输入:eventTime = 10, startTime = [0,3,7,9], endTime = [1,4,8,10]
输出:6
解释:
将 [3, 4] 的会议安排到 [8, 9] ,得到空余时间 [1, 7] 。

示例 4:

输入:eventTime = 5, startTime = [0,1,2,3,4], endTime = [1,2,3,4,5]
输出:0
解释:
活动中的所有时间都被会议安排满了。

说明:

  • 1 <= eventTime <= 10^9
  • n == startTime.length == endTime.length
  • 2 <= n <= 10^5
  • 0 <= startTime[i] < endTime[i] <= eventTime
  • endTime[i] <= startTime[i + 1] 其中 i 在范围 [0, n - 2] 之间。

思路

有一个活动有 n 个时间不重叠的会议,重新安排 1 个会议议程,使得空余时间最大。

3439.重新安排会议得到最多空余时间I 相比,本题只能重新安排一个会议,但是允许打乱原有的会议顺序。

对于当前会议,如果除了其左右两侧的空余位置之外存在空余位置大于会议时间,那么空余时间是其左右两侧的空余时间加上会议时间。否则,可以将会议左移或者右移到边界,空余时间为左右空余时间之和。

因此可以求出前三大的空余时间,如果会议时间小于等于第三大的空余时间,说明总是可以移到左右空余之外。否则就需要判断当前空余时间是否大于第二、第一大的空余时间,以及是否恰好是其左右空余。

代码


/**
 * @date 2025-07-10 22:15
 */
public class MaxFreeTime3440 {

    public int maxFreeTime(int eventTime, int[] startTime, int[] endTime) {
        int n = startTime.length;
        int[] gap = new int[n + 1];
        int[] intervals = new int[n];
        Arrays.setAll(intervals, i -> endTime[i] - startTime[i]);
        int[] a = new int[]{0, 0};
        int[] b = new int[]{0, 0};
        int[] c = new int[]{0, 0};
        for (int i = 0; i <= n; i++) {
            if (i == 0) {
                gap[i] = startTime[i];
            } else if (i == n) {
                gap[i] = eventTime - endTime[i - 1];
            } else {
                gap[i] = startTime[i] - endTime[i - 1];
            }
            if (gap[i] >= a[0]) {
                c = b;
                b = a;
                a = new int[]{gap[i], i};
            } else if (gap[i] >= b[0]) {
                c = b;
                b = new int[]{gap[i], i};
            } else if (gap[i] >= c[0]) {
                c = new int[]{gap[i], i};
            }
        }
        int res = a[0];
        for (int i = 0; i < n; i++) {
            int g = gap[i] + gap[i + 1];
            if (intervals[i] > a[0]) {
                res = Math.max(res, g);
            } else if (intervals[i] <= c[0]) {
                res = Math.max(res, g + intervals[i]);
            } else if (intervals[i] <= b[0] && (b[1] != i && b[1] != i + 1)) {
                res = Math.max(res, g + intervals[i]);
            } else if (a[1] != i && a[1] != i + 1) {
                res = Math.max(res, g + intervals[i]);
            } else {
                res = Math.max(res, g);
            }
        }
        return res;
    }

}

性能