2769.找出最大的可达成数字

目标

给你两个整数 num 和 t 。

如果整数 x 可以在执行下述操作不超过 t 次的情况下变为与 num 相等,则称其为 可达成数字 :

  • 每次操作将 x 的值增加或减少 1 ,同时可以选择将 num 的值增加或减少 1 。

返回所有可达成数字中的最大值。可以证明至少存在一个可达成数字。

示例 1:

输入:num = 4, t = 1
输出:6
解释:最大可达成数字是 x = 6 ,执行下述操作可以使其等于 num :
- x 减少 1 ,同时 num 增加 1 。此时,x = 5 且 num = 5 。 
可以证明不存在大于 6 的可达成数字。

示例 2:

输入:num = 3, t = 2
输出:7
解释:最大的可达成数字是 x = 7 ,执行下述操作可以使其等于 num :
- x 减少 1 ,同时 num 增加 1 。此时,x = 6 且 num = 4 。 
- x 减少 1 ,同时 num 增加 1 。此时,x = 5 且 num = 5 。 
可以证明不存在大于 7 的可达成数字。

说明:

  • 1 <= num, t <= 50

思路

给定一个正整数 num,以及操作次数 t,求一个数字 x 能够在 t 次操作后与 num 相等。每一次操作可以使 x 增大或减小1,同时可以选择使 num 增大或减少1。问 x 最大是多少。

要使 x 最大则应该选大于 num 的数,操作减1,为了使 x 尽可能大,应该同时增大 num,使 xnum 相向变化。很明显,x 最大为 num + 2 * t

代码

/**
 * @date 2024-05-21 8:47
 */
public class TheMaximumAchievableX2769 {
    public int theMaximumAchievableX(int num, int t) {
        return num + 2 * t;
    }
}

性能

1542.找出最长的超赞子字符串

目标

给你一个字符串 s 。请返回 s 中最长的 超赞子字符串 的长度。

「超赞子字符串」需满足满足下述两个条件:

  • 该字符串是 s 的一个非空子字符串
  • 进行任意次数的字符交换后,该字符串可以变成一个回文字符串

示例 1:

输入:s = "3242415"
输出:5
解释:"24241" 是最长的超赞子字符串,交换其中的字符后,可以得到回文 "24142"

示例 2:

输入:s = "12345678"
输出:1

示例 3:

输入:s = "213123"
输出:6
解释:"213123" 是最长的超赞子字符串,交换其中的字符后,可以得到回文 "231132"

示例 4:

输入:s = "00"
输出:2

说明:

  • 1 <= s.length <= 10^5
  • s 仅由数字组成

思路

卡在152/153超出时间限制,使用的是滑动窗口。

152测试用例是9999个1,9999个2,......,9999个9。

看了题解使用0-9十个数字保存奇偶性想到了,子串最多只包含一个奇数字符也想到了,使用前缀和也想到了,但是没想到将前缀和的状态压缩保存并通过哈希表记录。

代码

// todo

性能

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;
    }
}

性能

2644.找出可整除性得分最大的整数

目标

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

divisors[i] 的 可整除性得分 等于满足 nums[j] 能被 divisors[i] 整除的下标 j 的数量。

返回 可整除性得分 最大的整数 divisors[i] 。如果有多个整数具有最大得分,则返回数值最小的一个。

示例 1:

输入:nums = [4,7,9,3,9], divisors = [5,2,3]
输出:3
解释:divisors 中每个元素的可整除性得分为:
divisors[0] 的可整除性得分为 0 ,因为 nums 中没有任何数字能被 5 整除。
divisors[1] 的可整除性得分为 1 ,因为 nums[0] 能被 2 整除。 
divisors[2] 的可整除性得分为 3 ,因为 nums[2]、nums[3] 和 nums[4] 都能被 3 整除。 
因此,返回 divisors[2] ,它的可整除性得分最大。

示例 2:

输入:nums = [20,14,21,10], divisors = [5,7,5]
输出:5
解释:divisors 中每个元素的可整除性得分为:
divisors[0] 的可整除性得分为 2 ,因为 nums[0] 和 nums[3] 都能被 5 整除。
divisors[1] 的可整除性得分为 2 ,因为 nums[1] 和 nums[2] 都能被 7 整除。
divisors[2] 的可整除性得分为 2 ,因为 nums[0] 和 nums[3] 都能被5整除。 
由于 divisors[0]、divisors[1] 和 divisors[2] 的可整除性得分都是最大的,因此,我们返回数值最小的一个,即 divisors[2] 。

示例 3:

输入:nums = [12], divisors = [10,16]
输出:10
解释:divisors 中每个元素的可整除性得分为:
divisors[0] 的可整除性得分为 0 ,因为 nums 中没有任何数字能被 10 整除。
divisors[1] 的可整除性得分为 0 ,因为 nums 中没有任何数字能被 16 整除。 
由于 divisors[0] 和 divisors[1] 的可整除性得分都是最大的,因此,我们返回数值最小的一个,即 divisors[0] 。

说明:

  • 1 <= nums.length, divisors.length <= 1000
  • 1 <= nums[i], divisors[i] <= 10^9

思路

给定一个被除数数组 nums 与除数数组 divisorsnums 中能够被 divisors 整除的元素个数称为相应除数的分数,求分数最大的除数 divisor,如果分数相同则取最小的。

通俗的讲就是找到能够被更多的元素整除的除数,如果有多个则取最小的。

简单题直接放入优先队列即可,但排序其实是没必要的,可以直接在循环中记录结果。

网友还提供了一种算法,不用对每一个 nums 中的元素进行mod运算,而是将 nums 记录到map中,value是其重复次数,同时求出 nums 的最大值。在循环除数的时候,只需要判断 i * divisor 是否在map中即可,结束条件是小于等于最大值。但是这种算法太有针对性,对于那种最大值很大而除数很小的情况可能会超时。

也有网友提供了排序加优化的算法,更通用一些。

代码

/**
 * @date 2024-05-18 10:10
 */
public class MaxDivScore2644 {

    /**
     * 网友的解法 排序加优化 70ms
     */
    public int maxDivScore_v3(int[] nums, int[] divisors) {
        Arrays.sort(nums);
        int n = nums.length;
        int dup = 0;
        for (int i = 1; i < n; i++) {
            if (nums[i] == nums[i - 1]) {
                dup++;
            }
        }
        Arrays.sort(divisors);

        int ans = 0;
        int maxCnt = -1;
        for (int d : divisors) {
            // 提前结束说明 d 的倍数 d,2d,3d,⋯ ,(maxCnt−dup+1)⋅d 中的最大值已经超出了 nums 的最大值,
            // 即使把 nums 中的重复元素也算上,我们也无法统计出比 maxCnt 还多的倍数。
            if (maxCnt - dup >= nums[n - 1] / d) {
                break;
            }
            int cnt = 0;
            for (int i = n - 1; i >= 0; i--) {
                int x = nums[i];
                if (x < d) {
                    break;
                }
                if (x % d == 0) {
                    cnt++;
                }
            }
            if (cnt > maxCnt) {
                maxCnt = cnt;
                ans = d;
            }
        }
        return ans;
    }

    /**
     * 25ms
     */
    public int maxDivScore_v1(int[] nums, int[] divisors) {
        Map<Integer, Integer> cntMap = new HashMap<>();
        int maxNum = 0;
        for (int num : nums) {
            cntMap.put(num, cntMap.getOrDefault(num, 0) + 1);
            maxNum = Math.max(maxNum, num);
        }
        // 出错点:这里maxCnt的初值为-1,如果为0会跳过无法整除的情况,进入不到if条件里,res还是初值0,而非dividsor
        int res = 0, maxCnt = -1;
        for (int divisor : divisors) {
            int cnt = 0;
            int num = divisor;
            for (int i = 2; num <= maxNum; i++) {
                if (cntMap.containsKey(num)) {
                    cnt += cntMap.get(num);
                }
                num = i * divisor;
            }
            if (cnt > maxCnt || (cnt == maxCnt && res > divisor)) {
                maxCnt = cnt;
                res = divisor;
            }
        }
        return res;
    }

    /**
     * 使用优先队列 180ms
     */
    public int maxDivScore(int[] nums, int[] divisors) {
        PriorityQueue<int[]> q = new PriorityQueue<>((a, b) -> {
            int c = b[1] - a[1];
            return c == 0 ? a[0] - b[0] : c;
        });
        for (int divisor : divisors) {
            int cnt = 0;
            for (int num : nums) {
                if (num % divisor == 0) {
                    cnt++;
                }
            }
            q.offer(new int[]{divisor, cnt});
        }
        return q.poll()[0];
    }

}

性能

使用优先队列

不使用排序,使用hashmap保存nums,成倍增加除数,当除数很大的时候可以跳过一些循环。

但还是取决于测试用例,如果nums数组没有几个元素,而max又很大,循环次数并不会减少,反而会增加。

例如这个测试用例,上面的方法直接超出限制

排序加优化:

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;
    }
}

性能

1553.吃掉N个橘子的最少天数

有思路但是今天只剩下几十分钟了,有机会再做吧。

下面的代码超时了。// 5.13更新:这种贪心策略是不对的,不能优先选第三种策略。同时,最后一行的返回值minDays(n - 1)等于从0~n每一个值都要递归一遍,不可取。最后还要使用记忆化搜索。

/**
 * @date 2024-05-12 23:11
 */

public class MinDays1553 {

    @Deprecated
    public int minDays(int n) {
        int res = 0;
        if (n <= 0) {
            return 0;
        }
        if (n % 3 == 0) {
            res = minDays(n - 2 * (n / 3)) + 1;
        } else if (n % 2 == 0) {
            res = minDays(n - n / 2) + 1;
        } else {
            return minDays(n - 1) + 1;
        }
        return Math.min(res, minDays(n - 1) + 1);
    }
}