1535.找出数组游戏的赢家

目标

给你一个由 不同 整数组成的整数数组 arr 和一个整数 k 。

每回合游戏都在数组的前两个元素(即 arr[0] 和 arr[1] )之间进行。比较 arr[0] 与 arr[1] 的大小,较大的整数将会取得这一回合的胜利并保留在位置 0 ,较小的整数移至数组的末尾。当一个整数赢得 k 个连续回合时,游戏结束,该整数就是比赛的 赢家 。

返回赢得比赛的整数。

题目数据 保证 游戏存在赢家。

示例 1:

输入:arr = [2,1,3,5,4,6,7], k = 2
输出:5
解释:一起看一下本场游戏每回合的情况:

因此将进行 4 回合比赛,其中 5 是赢家,因为它连胜 2 回合。

示例 2:

输入:arr = [3,2,1], k = 10
输出:3
解释:3 将会在前 10 个回合中连续获胜。

示例 3:

输入:arr = [1,9,8,2,3,7,6,4,5], k = 7
输出:9

示例 4:

输入:arr = [1,11,22,33,44,55,66,77,88,99], k = 1000000000
输出:99

说明:

  • 2 <= arr.length <= 10^5
  • 1 <= arr[i] <= 10^6
  • arr 所含的整数 各不相同 。
  • 1 <= k <= 10^9

思路

给定一个数组arr和整数k,比较数组的第一与第二个元素,较大的赢得游戏,较小的移到数组末尾,如果连续赢得k次游戏则为最终赢家winner。题目限定arr中所有元素都不相同,并且一定存在winner。

很容易使用队列模拟这个操作过程,但其实没有必要。当前的胜者一定大于放到末尾的元素,如果一轮循环还没有得到结果,就直接返回当前胜者,它一定是数组的最大值,也是最终赢家。

代码

/**
 * @date 2024-05-19 9:51
 */
public class GetWinner1535 {

    public int getWinner(int[] arr, int k) {
        int n = arr.length;
        int winner = arr[0];
        int cnt = 0;
        for (int i = 1; i < n; i++) {
            if (cnt == k) {
                return winner;
            }
            if (arr[i] < winner) {
                cnt++;
            } else {
                winner = arr[i];
                // 这里应该重置为1而不是0
                cnt = 1;
            }
        }
        return winner;
    }
}

性能

826.安排工作以达到最大收益

目标

你有 n 个工作和 m 个工人。给定三个数组: difficulty, profit 和 worker ,其中:

  • difficulty[i] 表示第 i 个工作的难度,profit[i] 表示第 i 个工作的收益。
  • worker[i] 是第 i 个工人的能力,即该工人只能完成难度小于等于 worker[i] 的工作。

每个工人 最多 只能安排 一个 工作,但是一个工作可以 完成多次 。

  • 举个例子,如果 3 个工人都尝试完成一份报酬为 $1 的同样工作,那么总收益为 $3 。如果一个工人不能完成任何工作,他的收益为 $0 。

返回 在把工人分配到工作岗位后,我们所能获得的最大利润 。

示例 1:

输入: difficulty = [2,4,6,8,10], profit = [10,20,30,40,50], worker = [4,5,6,7]
输出: 100 
解释: 工人被分配的工作难度是 [4,4,6,6] ,分别获得 [20,20,30,30] 的收益。

示例 2:

输入: difficulty = [85,47,57], profit = [24,66,99], worker = [40,25,25]
输出: 0

说明:

  • n == difficulty.length
  • n == profit.length
  • m == worker.length
  • 1 <= n, m <= 10^4
  • 1 <= difficulty[i], profit[i], worker[i] <= 10^5

思路

现在有一组任务,其难度与收益分别使用两个数组 difficultyprofit 表示,还有一个数组表示一组工人的能力。现在需要给工人分配工作,如果分配的工作难度大于工人的能力则无法获取收益,求把工人分配到岗后能够获得的最大收益。

我们只需为每个工人分配其能力范围内的收益最高的工作即可。需要注意的是,题目中没有说难度越高收益越高,并且相同难度的收益也会不同

难度与收益是通过下标关联的,并且是无序的。

一个很自然的想法是维护一个难度与最大收益的映射,然后直接根据工人的能力二分查找相应的收益并累加。

那么如何同时对两个相关联的数组进行排序就是解题的关键。这里直接将 difficultyprofit 的映射通过 hashmap 保存起来,然后对 difficulty 从小到大排序。遍历排序后的 difficulty 数组,计算小于该难度的最大收益并更新到profit 中。根据工人的能力二分查找 profit 并累加即可。

容易出错的点是忘记处理相同难度收益不同的情况,二分查找结果为-1时表示无法完成任务任务,不应取难度最低的任务。

官网题解使用的是 javafx.util.Pair/awt包的Point/ 二维数组来保存映射关系。后面收益最高工作的计算,先对 worker 从小到大排序,使用双指针一个指向worker,一个指向难度,后面工人只需从前一个工人的难度开找即可,没用二分查找。

代码

/**
 * @date 2024-05-17 9:20
 */
public class MaxProfitAssignment826 {

    public int maxProfitAssignment(int[] difficulty, int[] profit, int[] worker) {
        int res = 0;
        int n = difficulty.length;
        int m = worker.length;
        Map<Integer, Integer> profitMap = new HashMap<>();
        for (int i = 0; i < n; i++) {
            // 存在难度相同的,取最大的
            if (profitMap.get(difficulty[i]) == null) {
                profitMap.put(difficulty[i], profit[i]);
            } else {
                profitMap.put(difficulty[i], Math.max(profitMap.get(difficulty[i]), profit[i]));
            }
        }
        Arrays.sort(difficulty);
        // 难度从小到大排,更新对应难度可以获得的最大收益
        profit[0] = profitMap.get(difficulty[0]);
        for (int i = 1; i < n; i++) {
            profit[i] = Math.max(profit[i - 1], profitMap.get(difficulty[i]));
        }
        for (int i = 0; i < m; i++) {
            int index = Arrays.binarySearch(difficulty, worker[i]);
            if (index >= 0) {
                res += profit[index];
            } else {
                index = -index - 2;
                if (index >= 0) {
                    // 说明没有能力完成
                    res += profit[index];
                }
            }
        }
        return res;
    }

    // 参考官网题解的答案
    public static class Point{
        public int x;
        public int y;

        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    public int maxProfitAssignment_v1(int[] difficulty, int[] profit, int[] worker) {
        int res = 0;
        int n = difficulty.length;
        Point[] jobs = new Point[n];
        for (int i = 0; i < n; i++) {
            jobs[i] = new Point(difficulty[i], profit[i]);
        }
        Arrays.sort(jobs, (a, b) -> a.x - b.x);
        // 根据工人技能排序
        // 越往后能力越高,可以直接接着上一次难度位置向后遍历
        Arrays.sort(worker);
        int index = 0;
        int best = 0;
        for (int capability : worker) {
            while (index < n && capability >= jobs[index].x) {
                best = Math.max(best, jobs[index++].y);
            }
            res += best;
        }
        return res;
    }
}

性能

最后计算最大收益时在循环中使用二分查找,时间复杂度为O(mlogn),而使用双指针 difficulty 最多遍历一遍,时间复杂度是O(m + n)应该更快一点。另外使用hashMap效率不高,因为需要计算hashcode,不如直接访问。

改进后

1953.你可以工作的最大周数

目标

给你 n 个项目,编号从 0 到 n - 1 。同时给你一个整数数组 milestones ,其中每个 milestones[i] 表示第 i 个项目中的阶段任务数量。

你可以按下面两个规则参与项目中的工作:

  • 每周,你将会完成 某一个 项目中的 恰好一个 阶段任务。你每周都 必须 工作。
  • 在 连续的 两周中,你 不能 参与并完成同一个项目中的两个阶段任务。

一旦所有项目中的全部阶段任务都完成,或者仅剩余一个阶段任务都会导致你违反上面的规则,那么你将 停止工作 。注意,由于这些条件的限制,你可能无法完成所有阶段任务。

返回在不违反上面规则的情况下你 最多 能工作多少周。

示例 1:

输入:milestones = [1,2,3]
输出:6
解释:一种可能的情形是:
​​​​- 第 1 周,你参与并完成项目 0 中的一个阶段任务。
- 第 2 周,你参与并完成项目 2 中的一个阶段任务。
- 第 3 周,你参与并完成项目 1 中的一个阶段任务。
- 第 4 周,你参与并完成项目 2 中的一个阶段任务。
- 第 5 周,你参与并完成项目 1 中的一个阶段任务。
- 第 6 周,你参与并完成项目 2 中的一个阶段任务。
总周数是 6 。

示例 2:

输入:milestones = [5,2,1]
输出:7
解释:一种可能的情形是:
- 第 1 周,你参与并完成项目 0 中的一个阶段任务。
- 第 2 周,你参与并完成项目 1 中的一个阶段任务。
- 第 3 周,你参与并完成项目 0 中的一个阶段任务。
- 第 4 周,你参与并完成项目 1 中的一个阶段任务。
- 第 5 周,你参与并完成项目 0 中的一个阶段任务。
- 第 6 周,你参与并完成项目 2 中的一个阶段任务。
- 第 7 周,你参与并完成项目 0 中的一个阶段任务。
总周数是 7 。
注意,你不能在第 8 周参与完成项目 0 中的最后一个阶段任务,因为这会违反规则。
因此,项目 0 中会有一个阶段任务维持未完成状态。

说明:

  • n == milestones.length
  • 1 <= n <= 10^5
  • 1 <= milestones[i] <= 10^9

思路

给定一个数组,其元素值表示项目的任务数,每一周可以完成任一项目的一个任务,不能连续两周完成同一项目的任务,即每完成一个任务必须切换项目,问最多能完成多少任务。

观察上面的示例发现不能优先选择任务数少的项目,因为任务少的项目先完成后,任务多的项目可能由于没有项目切换被剩下。

刚开始的想法就是先从小到大排序,将个项目的任务数放入优先队列,然后每次从最大与次最大的项目中选取任务,提交之后提示超时。

然后想到没必要一次只减1,于是就直接从最大中减去次最大 next,然后累加 2 * next,提交之后发现对于max == next 时会给出错误答案,于是就记录最大值相同的个数 times,后面累加next * (times + 1) 即可。提交之后发现下面的案例给出的答案不对:

正确的处理过程可以是每次取最大与次最大:

task1:8 task2:6 task3:4 cost
7 5(end) 4 2
6 4(end) 4 2
5 3(end) 4 2
4 3 3(end) 2
3 2(end) 3 2
2 2 2(end) 2
1 1 1(end) 3
0 0 0(end) 3
- - - 合计:18

而提交的算法是这样处理的,违背了上面的处理原则。

task1:8 task2:6 task3:4 cost
2 0(end) 4 12
0(end) 0 2 4
0 0 1 1
- - - 合计:17

如果考虑第三大元素 third,一次性只减到 third,累加 2 * (next - third) 的话,后续还是要一个一个算。

task1:8 task2:6 task3:4 cost
6 4(end) 4 2
5 3(end) 4 2
4 3 3(end) 2
3 2(end) 3 2
2 2 2(end) 2
1 1 1(end) 3
0 0 0(end) 3
- - - 合计:18

然后还发现对于某些例子是能够计算出正确结果的,例如

task1:5 task2:2 task3:1 cost
3 0(end) 1 4
2 0 0(end) 2
1(end) 0 0 1
- - - 合计:7

于是发现,只要除了最大值其余元素和只要大于等于最大值就可以全部完成。

贪心算法难想,难证明,大多凭直觉,没有通用性。我没有想到这个结论竟然可以推广到多个元素的情况。感觉有点像脑筋急转弯,想通了就很简单。

考虑下面的例子:

最大值每减1,后面的元素都可以同步的减1。

8 8 8 8 8
7 7 7 7 7
......
0 0 0 0 0 
----------------

这个就不能同步减了,否则最大值消不完

8 4 3 2 1
7 3 3 2 1
6 2 3 2 1
5 2 2 2 1
4 1 2 2 1
3 1 1 2 1
2 1 1 1 1
1 1 1 1 0
0 0 0 0 0

可以想象成两个柱子,最大的是一个柱子,其余的垒成一个柱子,如果大于最大的柱子就截断,作为新的柱子如果还大接着截,不能超过最大的柱子。极端的情况就是全相同,可以全部完成。只要保证有一个柱子与最大的柱子一样高就可以来回切换来完成全部任务。而如果其余的柱子垒起来没有最大的高,那么只能完成 其余之和*2+1

代码

package medium;

/**
 * @date 2024-05-16 8:37
 */
public class NumberOfWeeks1953 {

    /**
     * 执行通过
     * 如果最大的小于等于其余之和,那么所有任务均可完成
     * 如果最大的大于其余之和,那么可以完成其余之和*2+1
     */
    public long numberOfWeeks_v2(int[] milestones) {
        long sum = 0;
        int max = 0;
        for (int milestone : milestones) {
            if (milestone > max) {
                max = milestone;
            }
            sum += milestone;
        }
        long remainder = sum - max;
        return remainder < max ? remainder * 2 + 1 : sum;
    }

    /**
     * 这个算法是调试出来的
     */
    public long numberOfWeeks_v1(int[] milestones) {
        long res = 0;
        PriorityQueue<Integer> q = new PriorityQueue<>(Comparator.reverseOrder());
        for (int milestone : milestones) {
            q.offer(milestone);
        }
        while (!q.isEmpty()) {
            int head = q.poll();
            // 输入数组中的值均大于0,res放在这里加,以免漏掉最后一个
            res++;
            if (q.isEmpty()) {
                // 如果后面没有任务,连续两周完成同一项目的任务会违反规则,直接返回
                return res;
            }
            int next = q.poll();
            int times = 1;
            while (!q.isEmpty() && next == head) {
                times++;
                next = q.poll();
            }
            if (q.size() == 1 && q.peek() + next > head) {
                // 处理最后三个,如果其余两个大于最大值就返回它们的和,减1是因为为了防止漏掉最后一个加了1
                return res + head * times + next + q.poll() - 1;
            }
            res += (long) next * (times + 1) - 1;
            head -= next;
            if (head > 0) {
                for (int i = 0; i < times; i++) {
                    q.offer(head);
                }
            }
        }
        return res;
    }

性能

方法一

方法二

2244.完成所有任务需要的最少轮数

目标

给你一个下标从 0 开始的整数数组 tasks ,其中 tasks[i] 表示任务的难度级别。在每一轮中,你可以完成 2 个或者 3 个 相同难度级别 的任务。

返回完成所有任务需要的 最少 轮数,如果无法完成所有任务,返回 -1 。

示例 1:

输入:tasks = [2,2,3,3,2,4,4,4,4,4]
输出:4
解释:要想完成所有任务,一个可能的计划是:
- 第一轮,完成难度级别为 2 的 3 个任务。 
- 第二轮,完成难度级别为 3 的 2 个任务。 
- 第三轮,完成难度级别为 4 的 3 个任务。 
- 第四轮,完成难度级别为 4 的 2 个任务。 
可以证明,无法在少于 4 轮的情况下完成所有任务,所以答案为 4 。

示例 2:

输入:tasks = [2,3,3]
输出:-1
解释:难度级别为 2 的任务只有 1 个,但每一轮执行中,只能选择完成 2 个或者 3 个相同难度级别的任务。因此,无法完成所有任务,答案为 -1 。

说明:

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

思路

有一个任务数组,元素值表示任务级别,现在需要我们完成任务,每一轮只能完成2个或3个相同级别的任务,问完成所有任务最少需要多少轮,如果无法完成所有任务返回-1。

首先统计序列中等级相同的任务个数,可以使用哈希表,也可以先排序再在循环的过程中使用滑动窗口统计(这个效率最高,但是容易出错)。然后:

  • 如果序列中只有1个同级别任务,那么无法完成,返回-1。
  • 如果序列中有2个同级别任务,累加1。
  • 如果序列中有3个同级别任务,累加1。
  • 如果序列中同级别任务数 n 大于3,累加 (n + 2)/3 向下取整
    • n % 3 == 1,这时只需将之前3个一组的任务与余下的1个任务组合,然后拆成2组即可:loop = (n - 4)/3 + 2 = (n + 2)/3
    • n % 3 == 2,多余的两个自成一组:loop = (n -2)/3 + 1 = (n + 1)/3。这时 (n+1) 刚好可以被3整除,(n+2)/3 向下取整不会影响结果。
    • n % 3 == 0,直接整除即可:loop = n/3。道理同上。

以上就是贪心策略,每次取尽可能多的任务。

代码

/**
 * @date 2024-05-14 0:11
 */
public class MinimumRounds2244 {
    public int minimumRounds(int[] tasks) {
        Map<Integer, Integer> map = new HashMap<>();
        int res = 0;
        for (int t : tasks) {
            map.merge(t, 1, Integer::sum);
        }
        for (Integer cnt : map.values()) {
            if (cnt == 1) {
                return -1;
            } else if (cnt == 2 || cnt == 3) {
                res++;
            } else if (cnt % 3 == 0) {
                res += cnt / 3;
            } else if (cnt % 3 == 2) {
                res += (cnt - 2) / 3 + 1;
            } else if (cnt % 3 == 1) {
                res += (cnt - 4) / 3 + 2;
            }
        }
        return res;
    }
}

性能

2391.收集垃圾的最少总时间

目标

给你一个下标从 0 开始的字符串数组 garbage ,其中 garbage[i] 表示第 i 个房子的垃圾集合。garbage[i] 只包含字符 'M' ,'P' 和 'G' ,但可能包含多个相同字符,每个字符分别表示一单位的金属、纸和玻璃。垃圾车收拾 一 单位的任何一种垃圾都需要花费 1 分钟。

同时给你一个下标从 0 开始的整数数组 travel ,其中 travel[i] 是垃圾车从房子 i 行驶到房子 i + 1 需要的分钟数。

城市里总共有三辆垃圾车,分别收拾三种垃圾。每辆垃圾车都从房子 0 出发,按顺序 到达每一栋房子。但它们 不是必须 到达所有的房子。

任何时刻只有 一辆 垃圾车处在使用状态。当一辆垃圾车在行驶或者收拾垃圾的时候,另外两辆车 不能 做任何事情。

请你返回收拾完所有垃圾需要花费的 最少 总分钟数。

示例 1:

输入:garbage = ["G","P","GP","GG"], travel = [2,4,3]
输出:21
解释:
收拾纸的垃圾车:
1. 从房子 0 行驶到房子 1
2. 收拾房子 1 的纸垃圾
3. 从房子 1 行驶到房子 2
4. 收拾房子 2 的纸垃圾
收拾纸的垃圾车总共花费 8 分钟收拾完所有的纸垃圾。
收拾玻璃的垃圾车:
1. 收拾房子 0 的玻璃垃圾
2. 从房子 0 行驶到房子 1
3. 从房子 1 行驶到房子 2
4. 收拾房子 2 的玻璃垃圾
5. 从房子 2 行驶到房子 3
6. 收拾房子 3 的玻璃垃圾
收拾玻璃的垃圾车总共花费 13 分钟收拾完所有的玻璃垃圾。
由于没有金属垃圾,收拾金属的垃圾车不需要花费任何时间。
所以总共花费 8 + 13 = 21 分钟收拾完所有垃圾。

示例 2:

输入:garbage = ["MMM","PGM","GP"], travel = [3,10]
输出:37
解释:
收拾金属的垃圾车花费 7 分钟收拾完所有的金属垃圾。
收拾纸的垃圾车花费 15 分钟收拾完所有的纸垃圾。
收拾玻璃的垃圾车花费 15 分钟收拾完所有的玻璃垃圾。
总共花费 7 + 15 + 15 = 37 分钟收拾完所有的垃圾。

说明:

  • 2 <= garbage.length <= 10^5
  • garbage[i] 只包含字母 'M' ,'P' 和 'G' 。
  • 1 <= garbage[i].length <= 10
  • travel.length == garbage.length - 1
  • 1 <= travel[i] <= 100

思路

暴力解法,计算每个房子的垃圾数以及回收各种垃圾需要到达的最远距离。

官网题解给出了一次遍历的解法。没时间看了。// todo

代码

/**
 * @date 2024-05-11 8:37
 */
public class GarbageCollection2391 {

    public int garbageCollection(String[] garbage, int[] travel) {
        int res = 0;
        int n = garbage.length;
        int[] m = new int[n];
        int[] p = new int[n];
        int[] g = new int[n];
        int[] t = new int[n-1];
        t[0] = travel[0];
        for (int i = 1; i < travel.length; i++) {
            t[i] = t[i - 1] + travel[i];
        }
        for (char c : garbage[0].toCharArray()) {
            if (c == 'M') {
                m[0]++;
            } else if (c == 'P') {
                p[0]++;
            } else if (c == 'G') {
                g[0]++;
            }
        }
        int mEnd = 0;
        int pEnd = 0;
        int gEnd = 0;
        for (int i = 1; i < n; i++) {
            char[] chars = garbage[i].toCharArray();
            for (char c : chars) {
                if (c == 'M') {
                    m[i]++;
                } else if (c == 'P') {
                    p[i]++;
                } else if (c == 'G') {
                    g[i]++;
                }
            }
            m[i] += m[i - 1];
            p[i] += p[i - 1];
            g[i] += g[i - 1];
            if (m[i] != m[i - 1]) {
                mEnd = i;
            }
            if (p[i] != p[i - 1]) {
                pEnd = i;
            }
            if (g[i] != g[i - 1]) {
                gEnd = i;
            }
        }
        res += m[mEnd] + p[pEnd] + g[gEnd];
        if (mEnd > 0) {
            res += t[mEnd - 1];
        }
        if (pEnd > 0) {
            res += t[pEnd - 1];
        }
        if (gEnd > 0) {
            res += t[gEnd - 1];
        }

        return res;
    }

}

性能

2105.给植物浇水II

目标

Alice 和 Bob 打算给花园里的 n 株植物浇水。植物排成一行,从左到右进行标记,编号从 0 到 n - 1 。其中,第 i 株植物的位置是 x = i 。

每一株植物都需要浇特定量的水。Alice 和 Bob 每人有一个水罐,最初是满的 。他们按下面描述的方式完成浇水:

  • Alice 按 从左到右 的顺序给植物浇水,从植物 0 开始。Bob 按 从右到左 的顺序给植物浇水,从植物 n - 1 开始。他们 同时 给植物浇水。
  • 如果没有足够的水 完全 浇灌下一株植物,他 / 她会立即重新灌满浇水罐。
  • 不管植物需要多少水,浇水所耗费的时间都是一样的。
  • 不能 提前重新灌满水罐。
  • 每株植物都可以由 Alice 或者 Bob 来浇水。
  • 如果 Alice 和 Bob 到达同一株植物,那么当前水罐中水更多的人会给这株植物浇水。如果他俩水量相同,那么 Alice 会给这株植物浇水。

给你一个下标从 0 开始的整数数组 plants ,数组由 n 个整数组成。其中,plants[i] 为第 i 株植物需要的水量。另有两个整数 capacityA 和 capacityB 分别表示 Alice 和 Bob 水罐的容量。返回两人浇灌所有植物过程中重新灌满水罐的 次数 。

示例 1:

输入:plants = [2,2,3,3], capacityA = 5, capacityB = 5
输出:1
解释:
- 最初,Alice 和 Bob 的水罐中各有 5 单元水。
- Alice 给植物 0 浇水,Bob 给植物 3 浇水。
- Alice 和 Bob 现在分别剩下 3 单元和 2 单元水。
- Alice 有足够的水给植物 1 ,所以她直接浇水。Bob 的水不够给植物 2 ,所以他先重新装满水,再浇水。
所以,两人浇灌所有植物过程中重新灌满水罐的次数 = 0 + 0 + 1 + 0 = 1 。

示例 2:

输入:plants = [2,2,3,3], capacityA = 3, capacityB = 4
输出:2
解释:
- 最初,Alice 的水罐中有 3 单元水,Bob 的水罐中有 4 单元水。
- Alice 给植物 0 浇水,Bob 给植物 3 浇水。
- Alice 和 Bob 现在都只有 1 单元水,并分别需要给植物 1 和植物 2 浇水。
- 由于他们的水量均不足以浇水,所以他们重新灌满水罐再进行浇水。
所以,两人浇灌所有植物过程中重新灌满水罐的次数 = 0 + 1 + 1 + 0 = 2 。

示例 3:

输入:plants = [5], capacityA = 10, capacityB = 8
输出:0
解释:
- 只有一株植物
- Alice 的水罐有 10 单元水,Bob 的水罐有 8 单元水。因此 Alice 的水罐中水更多,她会给这株植物浇水。
所以,两人浇灌所有植物过程中重新灌满水罐的次数 = 0 。

说明:

  • n == plants.length
  • 1 <= n <= 10^5
  • 1 <= plants[i] <= 10^6
  • max(plants[i]) <= capacityA, capacityB <= 10^9

思路

两个人同时从左右两边开始浇水,如果水不够可以立即装满,浇水耗时都相同,如果同时到达相同的植物则剩余水多的浇,问总重新灌水的次数。

由于不计算装水时间,浇水耗时又相同,两人同时从左右开始浇水,同时到达同一植物只有在总植物数量为奇数时才会发生。

先计算二人各自需要重新灌水的次数然后再考虑是否存在同时到达以及是否需要重新灌水即可。

需要注意,无论n的奇偶性, for (int i = 0; i < (n >> 1); i++) {} 与 for (int i = n - 1; i > (n - 1 >> 1); i--) {} 都不会访问到中间节点。

代码

/**
 * @date 2024-05-09 8:40
 */
public class MinimumRefill2105 {
    public int minimumRefill(int[] plants, int capacityA, int capacityB) {
        int n = plants.length;
        int remainderA = capacityA;
        int remainderB = capacityB;
        int res = 0;
        for (int i = 0; i < (n >> 1); i++) {
            if (remainderA < plants[i]) {
                remainderA = capacityA - plants[i];
                res++;
            } else {
                remainderA -= plants[i];
            }
        }
        for (int i = n - 1; i > (n - 1 >> 1); i--) {
            if (remainderB < plants[i]) {
                remainderB = capacityB - plants[i];
                res++;
            } else {
                remainderB -= plants[i];
            }
        }
        if (n % 2 == 1) {
            int remainder = Math.max(remainderA, remainderB);
            if (remainder < plants[n >> 1]) {
                res++;
            }
        }

        return res;
    }
}

性能

2079.给植物浇水

目标

你打算用一个水罐给花园里的 n 株植物浇水。植物排成一行,从左到右进行标记,编号从 0 到 n - 1 。其中,第 i 株植物的位置是 x = i 。x = -1 处有一条河,你可以在那里重新灌满你的水罐。

每一株植物都需要浇特定量的水。你将会按下面描述的方式完成浇水:

  • 按从左到右的顺序给植物浇水。
  • 在给当前植物浇完水之后,如果你没有足够的水 完全 浇灌下一株植物,那么你就需要返回河边重新装满水罐。
  • 你 不能 提前重新灌满水罐。

最初,你在河边(也就是,x = -1),在 x 轴上每移动 一个单位 都需要 一步 。

给你一个下标从 0 开始的整数数组 plants ,数组由 n 个整数组成。其中,plants[i] 为第 i 株植物需要的水量。另有一个整数 capacity 表示水罐的容量,返回浇灌所有植物需要的 步数 。

示例 1:

输入:plants = [2,2,3,3], capacity = 5
输出:14
解释:从河边开始,此时水罐是装满的:
- 走到植物 0 (1 步) ,浇水。水罐中还有 3 单位的水。
- 走到植物 1 (1 步) ,浇水。水罐中还有 1 单位的水。
- 由于不能完全浇灌植物 2 ,回到河边取水 (2 步)。
- 走到植物 2 (3 步) ,浇水。水罐中还有 2 单位的水。
- 由于不能完全浇灌植物 3 ,回到河边取水 (3 步)。
- 走到植物 3 (4 步) ,浇水。
需要的步数是 = 1 + 1 + 2 + 3 + 3 + 4 = 14 。

示例 2:

输入:plants = [1,1,1,4,2,3], capacity = 4
输出:30
解释:从河边开始,此时水罐是装满的:
- 走到植物 0,1,2 (3 步) ,浇水。回到河边取水 (3 步)。
- 走到植物 3 (4 步) ,浇水。回到河边取水 (4 步)。
- 走到植物 4 (5 步) ,浇水。回到河边取水 (5 步)。
- 走到植物 5 (6 步) ,浇水。
需要的步数是 = 3 + 3 + 4 + 4 + 5 + 5 + 6 = 30 。

示例 3:

输入:plants = [7,7,7,7,7,7,7], capacity = 8
输出:49
解释:每次浇水都需要重新灌满水罐。
需要的步数是 = 1 + 1 + 2 + 2 + 3 + 3 + 4 + 4 + 5 + 5 + 6 + 6 + 7 = 49 。

说明:

  • n == plants.length
  • 1 <= n <= 1000
  • 1 <= plants[i] <= 10^6
  • max(plants[i]) <= capacity <= 10^9

思路

简单的动态规划。

代码

/**
 * @date 2024-05-08 0:01
 */
public class WateringPlants2079 {
    public int wateringPlants(int[] plants, int capacity) {
        int n = plants.length;
        int[] dp = new int[n];
        int remainder = capacity - plants[0];
        dp[0] = 1;
        for (int i = 1; i < plants.length; i++) {
            if (remainder >= plants[i]) {
                remainder -= plants[i];
                dp[i] = dp[i - 1] + 1;
            } else {
                remainder = capacity - plants[i];
                dp[i] = dp[i - 1] + (i << 1) + 1;
            }
        }
        return dp[n - 1];
    }
}

性能

2462.雇佣K位工人的总代价

目标

给你一个下标从 0 开始的整数数组 costs ,其中 costs[i] 是雇佣第 i 位工人的代价。

同时给你两个整数 k 和 candidates 。我们想根据以下规则恰好雇佣 k 位工人:

  • 总共进行 k 轮雇佣,且每一轮恰好雇佣一位工人。
  • 在每一轮雇佣中,从最前面 candidates 和最后面 candidates 人中选出代价最小的一位工人,如果有多位代价相同且最小的工人,选择下标更小的一位工人。
    • 比方说,costs = [3,2,7,7,1,2] 且 candidates = 2 ,第一轮雇佣中,我们选择第 4 位工人,因为他的代价最小 [3,2,7,7,1,2] 。
    • 第二轮雇佣,我们选择第 1 位工人,因为他们的代价与第 4 位工人一样都是最小代价,而且下标更小,[3,2,7,7,2] 。注意每一轮雇佣后,剩余工人的下标可能会发生变化。
  • 如果剩余员工数目不足 candidates 人,那么下一轮雇佣他们中代价最小的一人,如果有多位代价相同且最小的工人,选择下标更小的一位工人。
  • 一位工人只能被选择一次。

返回雇佣恰好 k 位工人的总代价。

示例 1:

输入:costs = [17,12,10,2,7,2,11,20,8], k = 3, candidates = 4
输出:11
解释:我们总共雇佣 3 位工人。总代价一开始为 0 。
- 第一轮雇佣,我们从 [17,12,10,2,7,2,11,20,8] 中选择。最小代价是 2 ,有两位工人,我们选择下标更小的一位工人,即第 3 位工人。总代价是 0 + 2 = 2 。
- 第二轮雇佣,我们从 [17,12,10,7,2,11,20,8] 中选择。最小代价是 2 ,下标为 4 ,总代价是 2 + 2 = 4 。
- 第三轮雇佣,我们从 [17,12,10,7,11,20,8] 中选择,最小代价是 7 ,下标为 3 ,总代价是 4 + 7 = 11 。注意下标为 3 的工人同时在最前面和最后面 4 位工人中。
总雇佣代价是 11 。

示例 2:

输入:costs = [1,2,4,1], k = 3, candidates = 3
输出:4
解释:我们总共雇佣 3 位工人。总代价一开始为 0 。
- 第一轮雇佣,我们从 [1,2,4,1] 中选择。最小代价为 1 ,有两位工人,我们选择下标更小的一位工人,即第 0 位工人,总代价是 0 + 1 = 1 。注意,下标为 1 和 2 的工人同时在最前面和最后面 3 位工人中。
- 第二轮雇佣,我们从 [2,4,1] 中选择。最小代价为 1 ,下标为 2 ,总代价是 1 + 1 = 2 。
- 第三轮雇佣,少于 3 位工人,我们从剩余工人 [2,4] 中选择。最小代价是 2 ,下标为 0 。总代价为 2 + 2 = 4 。
总雇佣代价是 4 。

说明:

  • 1 <= costs.length <= 10^5
  • 1 <= costs[i] <= 10^5
  • 1 <= k, candidates <= costs.length

思路

给我们一个数组 costs 和一个整数 candidates,每次从数组的前candidates与后candidates中选取值最小的元素,如果最小值相等则取下标最小的,costs中的每个值只能被选一次,求选出的k个元素和。

很直接的想法是使用优先队列保存这些元素,考虑到相同值需要取下标最小的,于是还要保存下标信息。循环从队列取出头元素累加到res,每取出一个元素需要从前/后补一个元素到队列中。

注意题目的数据范围,需要返回long类型。还有就是放队列以及补元素的停止条件以免重复。

代码

/**
 * @date 2024-05-01 17:03
 */
public class TotalCost2462 {
    public long totalCost(int[] costs, int k, int candidates) {
        PriorityQueue<Integer[]> q = new PriorityQueue<>((x, y) -> {
            int compare = x[0] - y[0];
            return compare == 0 ? x[1] - y[1] : compare;
        });
        int n = costs.length;
        int leftPos;
        int rightPos = n - 1;
        for (leftPos = 0; leftPos < candidates; leftPos++) {
            if (leftPos < rightPos) {
                // rightPos 与 leftPos指向的都是需要添加的下一个位置,
                q.offer(new Integer[]{costs[leftPos], leftPos});
                q.offer(new Integer[]{costs[rightPos], rightPos});
                rightPos--;
            }
            else if (leftPos == rightPos) {
                // 这里已经覆盖了数组的所有元素,循环结束后leftPos > rightPos
                q.offer(new Integer[]{costs[leftPos], leftPos});
            }
            else {
                break;
            }
        }
        // 不使用long会溢出
        long res = 0;
        // 选到最小之后,补够candidates
        for (int i = 0; i < k && !q.isEmpty(); i++) {
            Integer[] c = q.poll();
            res += c[0];
            // 如果已经覆盖了所有元素就无需再添加了
            if (leftPos > rightPos) {
                continue;
            }
            if (c[1] < leftPos) {
                q.offer(new Integer[]{costs[leftPos], leftPos});
                leftPos++;
            } else if (c[1] > rightPos) {
                q.offer(new Integer[]{costs[rightPos], rightPos});
                rightPos--;
            }
        }

        return res;
    }
}

性能

勉强通过,官网也是这个思路,耗时82ms。有网友的解法是分别使用了两个优先队列,只需要22ms。