3510.移除最小数对使数组有序II

目标

给你一个数组 nums,你可以执行以下操作任意次数:

  • 选择 相邻 元素对中 和最小 的一对。如果存在多个这样的对,选择最左边的一个。
  • 用它们的和替换这对元素。

返回将数组变为 非递减 所需的 最小操作次数 。

如果一个数组中每个元素都大于或等于它前一个元素(如果存在的话),则称该数组为非递减。

示例 1:

输入: nums = [5,2,3,1]
输出: 2
解释:
元素对 (3,1) 的和最小,为 4。替换后 nums = [5,2,4]。
元素对 (2,4) 的和为 6。替换后 nums = [5,6]。
数组 nums 在两次操作后变为非递减。

示例 2:

输入: nums = [1,2,2]
输出: 0
解释:
数组 nums 已经是非递减的。

说明:

  • 1 <= nums.length <= 10^5
  • -10^9 <= nums[i] <= 10^9

提示:

  • We can perform the simulation using data structures.
  • Maintain an array index and value using a map since we need to find the next and previous ones.
  • Maintain the indices to be removed using a hash set.
  • Maintain the neighbor sums with the smaller indices (set or priority queue).
  • Keep the 3 structures in sync during the removals.

思路

代码

性能

3507.移除最小数对使数组有序I

目标

给你一个数组 nums,你可以执行以下操作任意次数:

  • 选择 相邻 元素对中 和最小 的一对。如果存在多个这样的对,选择最左边的一个。
  • 用它们的和替换这对元素。

返回将数组变为 非递减 所需的 最小操作次数 。

如果一个数组中每个元素都大于或等于它前一个元素(如果存在的话),则称该数组为非递减。

示例 1:

输入: nums = [5,2,3,1]
输出: 2
解释:
元素对 (3,1) 的和最小,为 4。替换后 nums = [5,2,4]。
元素对 (2,4) 的和为 6。替换后 nums = [5,6]。
数组 nums 在两次操作后变为非递减。

示例 2:

输入: nums = [1,2,2]
输出: 0
解释:
数组 nums 已经是非递减的。

说明:

  • 1 <= nums.length <= 50
  • -1000 <= nums[i] <= 1000

思路

有一个数组 nums,每一次操作可以将数组中和最小的相邻元素用它们的和替换掉,求使得数组非递减所需要的最少操作次数。

暴力解法是每次遍历找到和最小的数对 并替换,直到数组非递减。可以使用 next 数组模拟链表来删除元素。

代码


/**
 * @date 2026-01-22 9:02
 */
public class MinimumPairRemoval3507 {

    public int minimumPairRemoval_v1(int[] nums) {
        int res = 0;
        boolean decrease;
        int n = nums.length;
        int[] next = new int[n];
        Arrays.setAll(next, i -> i + 1);
        do {
            decrease = false;
            int index = 0, sum = Integer.MAX_VALUE;
            for (int i = 0; next[i] < n; i = next[i]) {
                if (nums[next[i]] < nums[i]) {
                    decrease = true;
                }
                int s = nums[i] + nums[next[i]];
                if (s < sum) {
                    sum = s;
                    index = i;
                }
            }
            if (decrease) {
                nums[index] = sum;
                next[index] = next[next[index]];
                res++;
            }
        } while (decrease);
        return res;
    }

}

性能

2402.会议室III

目标

给你一个整数 n ,共有编号从 0n - 1n 个会议室。

给你一个二维整数数组 meetings ,其中 meetings[i] = [starti, endi] 表示一场会议将会在 半闭 时间区间 [starti, endi) 举办。所有 starti 的值 互不相同 。

会议将会按以下方式分配给会议室:

  1. 每场会议都会在未占用且编号 最小 的会议室举办。
  2. 如果没有可用的会议室,会议将会延期,直到存在空闲的会议室。延期会议的持续时间和原会议持续时间 相同 。
  3. 当会议室处于未占用状态时,将会优先提供给原 开始 时间更早的会议。

返回举办最多次会议的房间 编号 。如果存在多个房间满足此条件,则返回编号 最小 的房间。

半闭区间 [a, b)ab 之间的区间,包括 a 但 不包括 b

示例 1:

输入:n = 2, meetings = [[0,10],[1,5],[2,7],[3,4]]
输出:0
解释:
- 在时间 0 ,两个会议室都未占用,第一场会议在会议室 0 举办。
- 在时间 1 ,只有会议室 1 未占用,第二场会议在会议室 1 举办。
- 在时间 2 ,两个会议室都被占用,第三场会议延期举办。
- 在时间 3 ,两个会议室都被占用,第四场会议延期举办。
- 在时间 5 ,会议室 1 的会议结束。第三场会议在会议室 1 举办,时间周期为 [5,10) 。
- 在时间 10 ,两个会议室的会议都结束。第四场会议在会议室 0 举办,时间周期为 [10,11) 。
会议室 0 和会议室 1 都举办了 2 场会议,所以返回 0 。

示例 2:

输入:n = 3, meetings = [[1,20],[2,10],[3,5],[4,9],[6,8]]
输出:1
解释:
- 在时间 1 ,所有三个会议室都未占用,第一场会议在会议室 0 举办。
- 在时间 2 ,会议室 1 和 2 未占用,第二场会议在会议室 1 举办。
- 在时间 3 ,只有会议室 2 未占用,第三场会议在会议室 2 举办。
- 在时间 4 ,所有三个会议室都被占用,第四场会议延期举办。 
- 在时间 5 ,会议室 2 的会议结束。第四场会议在会议室 2 举办,时间周期为 [5,10) 。
- 在时间 6 ,所有三个会议室都被占用,第五场会议延期举办。 
- 在时间 10 ,会议室 1 和 2 的会议结束。第五场会议在会议室 1 举办,时间周期为 [10,12) 。 
会议室 1 和会议室 2 都举办了 2 场会议,所以返回 1 。

说明:

  • 1 <= n <= 100
  • 1 <= meetings.length <= 10^5
  • meetings[i].length == 2
  • 0 <= starti < endi <= 5 * 10^5
  • starti 的所有值 互不相同

思路

n 个会议室,编号为 0 ~ n - 1。有一个二维数组,meetings[i] 表示 [starti, endi) 范围内需要举办一场会议,会优先使用未被占用且编号最小的会议室。如果没有会议室可用,则需要延后举办,如果同一时间有多个会议需要举办,原开始时间最小的优先举办。

这个题目的关键点是,当有多个满足条件的会议室时,选择编号最小的,而不是最早可用的。需要使用两个优先队列,一个追踪会议室的最早空闲时间,另一个则追踪空闲会议室中编号最小的。将会议按照开始时间排序,按顺序遍历,将可选的会议室加入空闲编号堆,如果这个堆非空,从中取一个会议室,并将结束时间(可用时间)放入空闲时间堆中;否则,从空闲时间堆中取一个会议室,将其空闲时间加上会议持续时间后再放入堆中。

代码


/**
 * @date 2025-12-30 14:01
 */
public class MostBooked2402 {

    public int mostBooked(int n, int[][] meetings) {
        int[] cnt = new int[n];
        PriorityQueue<int[]> rooms = new PriorityQueue<>((a, b) -> {
            int compare = a[0] - b[0];
            if (compare != 0) {
                return compare;
            }
            return a[1] - b[1];
        });
        PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> a[1] - b[1]);
        for (int i = 0; i < n; i++) {
            rooms.offer(new int[]{0, i});
        }
        Arrays.sort(meetings, (a, b) -> a[0] - b[0]);
        for (int[] meeting : meetings) {
            while (!rooms.isEmpty() && rooms.peek()[0] <= meeting[0]) {
                q.offer(rooms.poll());
            }
            int[] room;
            if (!q.isEmpty()) {
                room = q.poll();
                rooms.offer(new int[]{meeting[1], room[1]});
            } else {
                room = rooms.poll();
                rooms.offer(new int[]{room[0] + meeting[1] - meeting[0], room[1]});
            }
            cnt[room[1]]++;
        }
        int res = 0;
        for (int i = 0; i < n; i++) {
            if (cnt[i] > cnt[res]) {
                res = i;
            }
        }
        return res;
    }

}

性能

3408.设计任务管理器

目标

一个任务管理器系统可以让用户管理他们的任务,每个任务有一个优先级。这个系统需要高效地处理添加、修改、执行和删除任务的操作。

请你设计一个 TaskManager 类:

TaskManager(vector<vector>& tasks) 初始化任务管理器,初始化的数组格式为 [userId, taskId, priority] ,表示给 userId 添加一个优先级为 priority 的任务 taskId 。

void add(int userId, int taskId, int priority) 表示给用户 userId 添加一个优先级为 priority 的任务 taskId ,输入 保证 taskId 不在系统中。

void edit(int taskId, int newPriority) 更新已经存在的任务 taskId 的优先级为 newPriority 。输入 保证 taskId 存在于系统中。

void rmv(int taskId) 从系统中删除任务 taskId 。输入 保证 taskId 存在于系统中。

int execTop() 执行所有用户的任务中优先级 最高 的任务,如果有多个任务优先级相同且都为 最高 ,执行 taskId 最大的一个任务。执行完任务后,taskId 从系统中 删除 。同时请你返回这个任务所属的用户 userId 。如果不存在任何任务,返回 -1 。

注意 ,一个用户可能被安排多个任务。

示例 1:

输入:
["TaskManager", "add", "edit", "execTop", "rmv", "add", "execTop"]
[[[[1, 101, 10], [2, 102, 20], [3, 103, 15]]], [4, 104, 5], [102, 8], [], [101], [5, 105, 15], []]
输出:
[null, null, null, 3, null, null, 5]
解释:
TaskManager taskManager = new TaskManager([[1, 101, 10], [2, 102, 20], [3, 103, 15]]); // 分别给用户 1 ,2 和 3 初始化一个任务。
taskManager.add(4, 104, 5); // 给用户 4 添加优先级为 5 的任务 104 。
taskManager.edit(102, 8); // 更新任务 102 的优先级为 8 。
taskManager.execTop(); // 返回 3 。执行用户 3 的任务 103 。
taskManager.rmv(101); // 将系统中的任务 101 删除。
taskManager.add(5, 105, 15); // 给用户 5 添加优先级为 15 的任务 105 。
taskManager.execTop(); // 返回 5 。执行用户 5 的任务 105 。

说明:

  • 1 <= tasks.length <= 10^5
  • 0 <= userId <= 10^5
  • 0 <= taskId <= 10^5
  • 0 <= priority <= 10^9
  • 0 <= newPriority <= 10^9
  • add ,edit ,rmv 和 execTop 的总操作次数 加起来 不超过 2 * 10^5 次。
  • 输入保证 taskId 是合法的。

思路

2349.设计数字容器系统 类似,同样是懒删除。

代码


/**
 * @date 2025-09-18 8:42
 */
public class TaskManager3408 {

    static class TaskManager {

        private final Map<Integer, int[]> taskIdMap = new HashMap<>();
        private final PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> {
            int compare = b[2] - a[2];
            if (compare != 0) {
                return compare;
            }
            return b[1] - a[1];
        });

        public TaskManager(List<List<Integer>> tasks) {
            for (List<Integer> task : tasks) {
                int[] t = {task.get(0), task.get(1), task.get(2)};
                taskIdMap.put(t[1], t);
                q.offer(t);
            }
        }

        public void add(int userId, int taskId, int priority) {
            int[] t = {userId, taskId, priority};
            taskIdMap.put(taskId, t);
            q.offer(t);
        }

        public void edit(int taskId, int newPriority) {
            int[] t = taskIdMap.get(taskId);
            t = new int[]{t[0], t[1], newPriority};
            taskIdMap.put(taskId, t);
            q.offer(t);
        }

        public void rmv(int taskId) {
            taskIdMap.remove(taskId);
        }

        public int execTop() {
            while (!q.isEmpty() && (taskIdMap.get(q.peek()[1]) == null || taskIdMap.get(q.peek()[1])[2] != q.peek()[2]|| taskIdMap.get(q.peek()[1])[0] != q.peek()[0])) {
                q.poll();
            }
            if (q.isEmpty()) {
                return -1;
            }
            int[] task = q.poll();
            taskIdMap.remove(task[1]);
            return task[0];
        }
    }

}

性能

1792.最大平均通过率

目标

一所学校里有一些班级,每个班级里有一些学生,现在每个班都会进行一场期末考试。给你一个二维数组 classes ,其中 classes[i] = [passi, totali] ,表示你提前知道了第 i 个班级总共有 totali 个学生,其中只有 passi 个学生可以通过考试。

给你一个整数 extraStudents ,表示额外有 extraStudents 个聪明的学生,他们 一定 能通过任何班级的期末考。你需要给这 extraStudents 个学生每人都安排一个班级,使得 所有 班级的 平均 通过率 最大 。

一个班级的 通过率 等于这个班级通过考试的学生人数除以这个班级的总人数。平均通过率 是所有班级的通过率之和除以班级数目。

请你返回在安排这 extraStudents 个学生去对应班级后的 最大 平均通过率。与标准答案误差范围在 10^-5 以内的结果都会视为正确结果。

示例 1:

输入:classes = [[1,2],[3,5],[2,2]], extraStudents = 2
输出:0.78333
解释:你可以将额外的两个学生都安排到第一个班级,平均通过率为 (3/4 + 3/5 + 2/2) / 3 = 0.78333 。

示例 2:

输入:classes = [[2,4],[3,9],[4,5],[2,10]], extraStudents = 4
输出:0.53485

说明:

  • 1 <= classes.length <= 10^5
  • classes[i].length == 2
  • 1 <= passi <= totali <= 10^5
  • 1 <= extraStudents <= 10^5

思路

有一个二维数组 classesclasses[i][0] 表示班级 i 期末考试中通过考试的人数,classes[i][1] 表示班级 i 的总人数。有 extraStudents 个聪明学生一定可以通过期末考试,现在需要将这些学生分配到班级中去,使得班级通过率的平均值最大。返回最大平均通过率。

为了使平均值更大,可以优先将聪明学生安排到通过率提升最大的班级,使用优先队列。

代码


/**
 * @date 2025-09-01 21:21
 */
public class MaxAverageRatio1792 {
    public double maxAverageRatio(int[][] classes, int extraStudents) {
        PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> (int) (((b[1] - b[0]) * (long) a[1] * (a[1] + 1) - (a[1] - a[0]) * (long) b[1] * (b[1] + 1)) % 1000000007));
        for (int[] item : classes) {
            q.offer(item);
        }
        for (int i = 0; i < extraStudents; i++) {
            int[] peek = q.poll();
            peek[0]++;
            peek[1]++;
            q.offer(peek);
        }
        double res = 0.0;
        for (int[] item : classes) {
            res += (double) item[0] / item[1];
        }
        return res / classes.length;
    }

}

性能

2353.设计食物评分系统

目标

设计一个支持下述操作的食物评分系统:

  • 修改 系统中列出的某种食物的评分。
  • 返回系统中某一类烹饪方式下评分最高的食物。

实现 FoodRatings 类:

  • FoodRatings(String[] foods, String[] cuisines, int[] ratings) 初始化系统。食物由 foods、cuisines 和 ratings 描述,长度均为 n 。
  • foods[i] 是第 i 种食物的名字。
  • cuisines[i] 是第 i 种食物的烹饪方式。
  • ratings[i] 是第 i 种食物的最初评分。
  • void changeRating(String food, int newRating) 修改名字为 food 的食物的评分。
  • String highestRated(String cuisine) 返回指定烹饪方式 cuisine 下评分最高的食物的名字。如果存在并列,返回 字典序较小 的名字。

注意,字符串 x 的字典序比字符串 y 更小的前提是:x 在字典中出现的位置在 y 之前,也就是说,要么 x 是 y 的前缀,或者在满足 x[i] != y[i] 的第一个位置 i 处,x[i] 在字母表中出现的位置在 y[i] 之前。

示例:

输入
["FoodRatings", "highestRated", "highestRated", "changeRating", "highestRated", "changeRating", "highestRated"]
[[["kimchi", "miso", "sushi", "moussaka", "ramen", "bulgogi"], ["korean", "japanese", "japanese", "greek", "japanese", "korean"], [9, 12, 8, 15, 14, 7]], ["korean"], ["japanese"], ["sushi", 16], ["japanese"], ["ramen", 16], ["japanese"]]
输出
[null, "kimchi", "ramen", null, "sushi", null, "ramen"]

解释
FoodRatings foodRatings = new FoodRatings(["kimchi", "miso", "sushi", "moussaka", "ramen", "bulgogi"], ["korean", "japanese", "japanese", "greek", "japanese", "korean"], [9, 12, 8, 15, 14, 7]);
foodRatings.highestRated("korean"); // 返回 "kimchi"
                                    // "kimchi" 是分数最高的韩式料理,评分为 9 。
foodRatings.highestRated("japanese"); // 返回 "ramen"
                                      // "ramen" 是分数最高的日式料理,评分为 14 。
foodRatings.changeRating("sushi", 16); // "sushi" 现在评分变更为 16 。
foodRatings.highestRated("japanese"); // 返回 "sushi"
                                      // "sushi" 是分数最高的日式料理,评分为 16 。
foodRatings.changeRating("ramen", 16); // "ramen" 现在评分变更为 16 。
foodRatings.highestRated("japanese"); // 返回 "ramen"
                                      // "sushi" 和 "ramen" 的评分都是 16 。
                                      // 但是,"ramen" 的字典序比 "sushi" 更小。

说明:

  • 1 <= n <= 2 * 10^4
  • n == foods.length == cuisines.length == ratings.length
  • 1 <= foods[i].length, cuisines[i].length <= 10
  • foods[i]、cuisines[i] 由小写英文字母组成
  • 1 <= ratings[i] <= 10^8
  • foods 中的所有字符串 互不相同
  • 在对 changeRating 的所有调用中,food 是系统中食物的名字。
  • 在对 highestRated 的所有调用中,cuisine 是系统中 至少一种 食物的烹饪方式。
  • 最多调用 changeRating 和 highestRated 总计 2 * 10^4 次

思路

设计一个食物评分系统,返回指定类别评分最高的食物,支持修改食物的评分。

要知道类别中评分最高的食物,优先队列/TreeSet 的元素应为 (rating, food) 键值对,根据评分从大到小排序,如果评分相同根据食物的字典序排列。

修改食物评分后需要更新对应类别的评分排名,因此需要维护 (food, cuisine) 的映射关系。如果使用懒加载,还需要记录食物最新的评分,维护 (food, rating)。如果使用红黑树,需要根据更新前的评分删除树中数据,同样需要维护 (food, rating)

有人使用优先队列超时是因为删除元素的复杂度是 O(n)。考虑使用懒删除或者使用 有序集合 TreeSet。有序集合查找最大/最小节点的复杂度是 O(logn),最大/小节点是最右/左叶子节点,查找复杂度是树的高度。

代码


/**
 * @date 2025-02-28 0:10
 */
public class FoodRatings {

    Map<String, PriorityQueue<String[]>> map;
    Map<String, String> foodMap;
    Map<String, Integer> ratingMap;

    public FoodRatings(String[] foods, String[] cuisines, int[] ratings) {
        int n = foods.length;
        map = new HashMap<>(n);
        foodMap = new HashMap<>(n);
        ratingMap = new HashMap<>(n);
        for (int i = 0; i < n; i++) {
            foodMap.put(foods[i], cuisines[i]);
            ratingMap.put(foods[i], ratings[i]);
            map.putIfAbsent(cuisines[i], new PriorityQueue<>((a, b) -> {
                int compare = Integer.parseInt(b[0]) - Integer.parseInt(a[0]);
                if (compare != 0) {
                    return compare;
                }
                return a[1].compareTo(b[1]);
            }));
            map.get(cuisines[i]).offer(new String[]{String.valueOf(ratings[i]), foods[i]});
        }
    }

    public void changeRating(String food, int newRating) {
        ratingMap.put(food, newRating);
        map.get(foodMap.get(food)).offer(new String[]{String.valueOf(newRating), food});
    }

    public String highestRated(String cuisine) {
        PriorityQueue<String[]> q = map.get(cuisine);
        while (Integer.parseInt(q.peek()[0]) != ratingMap.get(q.peek()[1])) {
            q.poll();
        }
        return q.peek()[1];
    }
}

性能

3066.超过阈值的最少操作数II

目标

给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。

一次操作中,你将执行:

  • 选择 nums 中最小的两个整数 x 和 y 。
  • 将 x 和 y 从 nums 中删除。
  • 将 min(x, y) * 2 + max(x, y) 添加到数组中的任意位置。

注意,只有当 nums 至少包含两个元素时,你才可以执行以上操作。

你需要使数组中的所有元素都大于或等于 k ,请你返回需要的 最少 操作次数。

示例 1:

输入:nums = [2,11,10,1,3], k = 10
输出:2
解释:第一次操作中,我们删除元素 1 和 2 ,然后添加 1 * 2 + 2 到 nums 中,nums 变为 [4, 11, 10, 3] 。
第二次操作中,我们删除元素 3 和 4 ,然后添加 3 * 2 + 4 到 nums 中,nums 变为 [10, 11, 10] 。
此时,数组中的所有元素都大于等于 10 ,所以我们停止操作。
使数组中所有元素都大于等于 10 需要的最少操作次数为 2 。

示例 2:

输入:nums = [1,1,2,4,9], k = 20
输出:4
解释:第一次操作后,nums 变为 [2, 4, 9, 3] 。
第二次操作后,nums 变为 [7, 4, 9] 。
第三次操作后,nums 变为 [15, 9] 。
第四次操作后,nums 变为 [33] 。
此时,数组中的所有元素都大于等于 20 ,所以我们停止操作。
使数组中所有元素都大于等于 20 需要的最少操作次数为 4 。

说明:

  • 2 <= nums.length <= 2 * 10^5
  • 1 <= nums[i] <= 10^9
  • 1 <= k <= 10^9
  • 输入保证答案一定存在,也就是说一定存在一个操作序列使数组中所有元素都大于等于 k 。

思路

求使数组 nums 中所有元素均大于等于 k 的操作次数。每次操作可以将数组中最小的两个元素删除,并将 min(x, y) * 2 + max(x, y) 加入数组。

使用最小堆模拟即可

代码


/**
 * @date 2025-01-14 8:51
 */
public class MinOperations3066 {

    public int minOperations(int[] nums, int k) {
        PriorityQueue<Long> q = new PriorityQueue<>();
        for (int num : nums) {
            q.offer((long) num);
        }
        int res = 0;
        while (q.size() >= 2) {
            Long a = q.poll();
            Long b = q.poll();
            if (a >= k) {
                break;
            }
            q.offer(a * 2L + b);
            res++;
        }
        return res;
    }
}

性能

1366.通过投票对团队排名

目标

现在有一个特殊的排名系统,依据参赛团队在投票人心中的次序进行排名,每个投票者都需要按从高到低的顺序对参与排名的所有团队进行排位。

排名规则如下:

  • 参赛团队的排名次序依照其所获「排位第一」的票的多少决定。如果存在多个团队并列的情况,将继续考虑其「排位第二」的票的数量。以此类推,直到不再存在并列的情况。
  • 如果在考虑完所有投票情况后仍然出现并列现象,则根据团队字母的字母顺序进行排名。

给你一个字符串数组 votes 代表全体投票者给出的排位情况,请你根据上述排名规则对所有参赛团队进行排名。

请你返回能表示按排名系统 排序后 的所有团队排名的字符串。

示例 1:

输入:votes = ["ABC","ACB","ABC","ACB","ACB"]
输出:"ACB"
解释:
A 队获得五票「排位第一」,没有其他队获得「排位第一」,所以 A 队排名第一。
B 队获得两票「排位第二」,三票「排位第三」。
C 队获得三票「排位第二」,两票「排位第三」。
由于 C 队「排位第二」的票数较多,所以 C 队排第二,B 队排第三。

示例 2:

输入:votes = ["WXYZ","XYZW"]
输出:"XWYZ"
解释:
X 队在并列僵局打破后成为排名第一的团队。X 队和 W 队的「排位第一」票数一样,但是 X 队有一票「排位第二」,而 W 没有获得「排位第二」。

示例 3:

输入:votes = ["ZMNAGUEDSJYLBOPHRQICWFXTVK"]
输出:"ZMNAGUEDSJYLBOPHRQICWFXTVK"
解释:只有一个投票者,所以排名完全按照他的意愿。

说明:

  • 1 <= votes.length <= 1000
  • 1 <= votes[i].length <= 26
  • votes[i].length == votes[j].length for 0 <= i, j < votes.length
  • votes[i][j] 是英文 大写 字母
  • votes[i] 中的所有字母都是唯一的
  • votes[0] 中出现的所有字母 同样也 出现在 votes[j] 中,其中 1 <= j < votes.length

思路

投票人对所有团队的排名用一个字符串 votes[i] 表示,团队的先后顺序就代表排名。根据所有投票人的排名对团队排名,排名第一次数最多的为第一名,如果相同则根据排名第二的次数排名,以此类推,如果最终都相同,则以团队名在字母表的先后顺序排名。

考虑自定义数据结构,维护团队在每一个名次的得票数,然后使用优先队列自定义排序。

代码


/**
 * @date 2024-12-29 18:36
 */
public class RankTeams1366 {

    public static class Rank {
        public int name;
        public int[] cnt;

        public Rank(int name, int[] cnt) {
            this.name = name;
            this.cnt = cnt;
        }
    }

    public String rankTeams(String[] votes) {
        int n = votes[0].length();
        PriorityQueue<Rank> q = new PriorityQueue<>((a, b) -> {
            int[] cnta = a.cnt;
            int[] cntb = b.cnt;
            int i = 0;
            while (i < n && cnta[i] == cntb[i]) {
                i++;
            }
            if (i == n) {
                return a.name - b.name;
            }
            return cntb[i] - cnta[i];
        });

        int[][] cnt = new int[26][n];
        for (String vote : votes) {
            for (int i = 0; i < vote.length(); i++) {
                int c = vote.charAt(i) - 'A';
                cnt[c][i]++;
            }
        }
        for (int i = 0; i < 26; i++) {
            q.offer(new Rank(i, cnt[i]));
        }
        char[] chars = new char[n];
        int i = 0;
        while (i < n) {
            chars[i++] = (char) (q.poll().name + (int) 'A');
        }
        return new String(chars);
    }

}

性能

1705.吃苹果的最大数目

目标

有一棵特殊的苹果树,一连 n 天,每天都可以长出若干个苹果。在第 i 天,树上会长出 apples[i] 个苹果,这些苹果将会在 days[i] 天后(也就是说,第 i + days[i] 天时)腐烂,变得无法食用。也可能有那么几天,树上不会长出新的苹果,此时用 apples[i] == 0 且 days[i] == 0 表示。

你打算每天 最多 吃一个苹果来保证营养均衡。注意,你可以在这 n 天之后继续吃苹果。

给你两个长度为 n 的整数数组 days 和 apples ,返回你可以吃掉的苹果的最大数目。

示例 1:

输入:apples = [1,2,3,5,2], days = [3,2,1,4,2]
输出:7
解释:你可以吃掉 7 个苹果:
- 第一天,你吃掉第一天长出来的苹果。
- 第二天,你吃掉一个第二天长出来的苹果。
- 第三天,你吃掉一个第二天长出来的苹果。过了这一天,第三天长出来的苹果就已经腐烂了。
- 第四天到第七天,你吃的都是第四天长出来的苹果。

示例 2:

输入:apples = [3,0,0,0,0,2], days = [3,0,0,0,0,2]
输出:5
解释:你可以吃掉 5 个苹果:
- 第一天到第三天,你吃的都是第一天长出来的苹果。
- 第四天和第五天不吃苹果。
- 第六天和第七天,你吃的都是第六天长出来的苹果。

说明:

  • apples.length == n
  • days.length == n
  • 1 <= n <= 2 * 10^4
  • 0 <= apples[i], days[i] <= 2 * 10^4
  • 只有在 apples[i] = 0 时,days[i] = 0 才成立

思路

有一颗特殊的苹果树,第 i 天会结出 apples[i] 个果子,这些果子将在第 i + days[i] 天腐烂。求每天吃一个未腐烂的苹果,最多可以吃几个。

我们的贪心策略是优先吃快腐烂的苹果,首先将当天结果的苹果个数以及腐烂时间放入最小堆,获取堆顶最近要腐烂的苹果,判断是否已经腐烂,如果没有将苹果个数减一,如果数量减为 0 或者已经腐烂,将其从堆中移出。

代码


/**
 * @date 2024-12-24 10:35
 */
public class EatenApples1705 {

    public int eatenApples_v2(int[] apples, int[] days) {
        int n = apples.length;
        PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> a[1] - b[1]);
        int res = 0;
        for (int day = 0; day < n || !q.isEmpty(); day++) {
            if (day < n && apples[day] > 0){
                q.offer(new int[]{apples[day], day + days[day]});
            }
            while (!q.isEmpty()) {
                int[] applesInfo = q.peek();
                if (applesInfo[0] == 0 || applesInfo[1] == day) {
                    q.poll();
                    continue;
                }
                if (applesInfo[1] > day && applesInfo[0] > 0) {
                    applesInfo[0]--;
                    res++;
                    break;
                }
            }
        }
        return res;
    }

}

性能

855.考场就座

目标

在考场里,一排有 N 个座位,分别编号为 0, 1, 2, ..., N-1 。

当学生进入考场后,他必须坐在能够使他与离他最近的人之间的距离达到最大化的座位上。如果有多个这样的座位,他会坐在编号最小的座位上。(另外,如果考场里没有人,那么学生就坐在 0 号座位上。)

返回 ExamRoom(int N) 类,它有两个公开的函数:其中,函数 ExamRoom.seat() 会返回一个 int (整型数据),代表学生坐的位置;函数 ExamRoom.leave(int p) 代表坐在座位 p 上的学生现在离开了考场。每次调用 ExamRoom.leave(p) 时都保证有学生坐在座位 p 上。

示例:

输入:["ExamRoom","seat","seat","seat","seat","leave","seat"], [[10],[],[],[],[],[4],[]]
输出:[null,0,9,4,2,null,5]
解释:
ExamRoom(10) -> null
seat() -> 0,没有人在考场里,那么学生坐在 0 号座位上。
seat() -> 9,学生最后坐在 9 号座位上。
seat() -> 4,学生最后坐在 4 号座位上。
seat() -> 2,学生最后坐在 2 号座位上。
leave(4) -> null
seat() -> 5,学生最后坐在 5 号座位上。

说明:

  • 1 <= N <= 10^9
  • 在所有的测试样例中 ExamRoom.seat() 和 ExamRoom.leave() 最多被调用 10^4 次。
  • 保证在调用 ExamRoom.leave(p) 时有学生正坐在座位 p 上。

思路

有一排座位编号为 0 ~ N - 1,当学生进入考场后可以选择距离其它同学最远的位置坐下。实现 ExamRoom 类,提供 seat() 方法返回合法的座位,以及 leave() 方法允许学生离开考场。

显然下一个进入考场的同学应该坐在当前相邻座位最大距离的中间位置,考虑到允许离开座位,那么端点的座位离开的情况也要考虑。我们需要提供一个类,快速地查询相邻学生座位的最大值,并且支持座位的删减。

使用最大堆,堆中元素为 [距离左边相邻同学的距离,座位编号],根据 距离/2 排序,除以 2 是为了消除左右距离不等造成的影响。例如 0 4 9 这种情况,区间长度一个是 4,一个是 5,但是 2 ~ 44 ~ 6 的距离相同,应该取编号最小的。

计算下一个座位时,需要考虑三种情况:

  • 如果是第一个同学进入考场,直接坐在 0 号位置
  • 如果只有一个同学在考场,下一个同学需要坐在距离该位置距离最大的端点
  • 如果有大于两个同学在考场,我们可以选择两个同学中间的位置,或者两端的位置

如何处理离开考场的情况?如果离开的同学左右位置有人,需要进行区间合并,将合并后的区间放入堆中,并且删除堆中左右两侧区间。但是在堆中查找元素的时间复杂度是 O(n),由于最多总共调用 10^4 次,leave 最多调用 k = 5 * 10^3 次,堆中元素个数随着调用减少,总复杂度为 O(k^2) 可能勉强通过。

为了快速访问左右的相邻座位以便区间合并以及左右端点的距离判断,使用 TreeSet 记录同学的座位编号。我们可以将删除操作延迟到获取座位的时候处理。

代码


/**
 * @date 2024-12-23 14:28
 */
public class ExamRoom {

    private TreeSet<Integer> occupied;
    // [distance, no]
    private PriorityQueue<int[]> q;
    private int n;

    public ExamRoom(int n) {
        this.n = n;
        occupied = new TreeSet<>();
        q = new PriorityQueue<>((a, b) -> {
            int compare = b[0] / 2 - a[0] / 2;
            if (compare != 0) {
                return compare;
            }
            return a[1] - b[1];
        });
    }

    public int seat() {
        if (occupied.size() == 0) {
            occupied.add(0);
            return 0;
        } else if (occupied.size() == 1) {
            Integer no = occupied.first();
            int distance = n - 1 - no;
            if (distance > no) {
                q.offer(new int[]{distance, n - 1});
                occupied.add(n - 1);
                return n - 1;
            } else {
                q.offer(new int[]{no, no});
                occupied.add(0);
                return 0;
            }
        } else {
            while (true) {
                int first = occupied.first();
                int last = occupied.last();
                int rd = n - 1 - last;
                int[] dn = q.peek();
                int r = dn[1];
                int l = r - dn[0];
                // 注意第三个条件,可以防止离开座位后又有同学坐下,由于延迟删除,距离并未更新导致的计算错误
                if (!occupied.contains(l) || !occupied.contains(r) || occupied.higher(l) != r) {
                    q.poll();
                    continue;
                }
                int distance = dn[0] / 2;
                if (distance <= first || distance < rd) {
                    if (first < rd) {
                        q.offer(new int[]{rd, n - 1});
                        occupied.add(n - 1);
                        return n - 1;
                    } else {
                        q.offer(new int[]{l, l});
                        occupied.add(0);
                        return 0;
                    }
                } else {
                    q.poll();
                    int m = l + (r - l) / 2;
                    occupied.add(m);
                    q.offer(new int[]{m - l, m});
                    q.offer(new int[]{r - m, r});
                    return m;
                }
            }
        }
    }

    public void leave(int p) {
        if (p != occupied.first() && p != occupied.last()) {
            int l = occupied.lower(p);
            int r = occupied.higher(p);
            q.offer(new int[]{r - l, r});
        }
        occupied.remove(p);
    }
}

性能