自己写的超时了,看题解需要使用单调栈 //todo
作者: admin
2831.找出最长等值子数组
目标
给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。
如果子数组中所有元素都相等,则认为子数组是一个 等值子数组 。注意,空数组是 等值子数组 。
从 nums 中删除最多 k 个元素后,返回可能的最长等值子数组的长度。
子数组 是数组中一个连续且可能为空的元素序列。
示例 1:
输入:nums = [1,3,2,3,1,3], k = 3
输出:3
解释:最优的方案是删除下标 2 和下标 4 的元素。
删除后,nums 等于 [1, 3, 3, 3] 。
最长等值子数组从 i = 1 开始到 j = 3 结束,长度等于 3 。
可以证明无法创建更长的等值子数组。
示例 2:
输入:nums = [1,1,2,2,1,1], k = 2
输出:4
解释:最优的方案是删除下标 2 和下标 3 的元素。
删除后,nums 等于 [1, 1, 1, 1] 。
数组自身就是等值子数组,长度等于 4 。
可以证明无法创建更长的等值子数组。
说明:
- 1 <= nums.length <= 10^5
- 1 <= nums[i] <= nums.length
- 0 <= k <= nums.length
思路
给定一个数组 nums
和一个正整数k,最多可以删除数组中k个元素,问数组中最长的等值子数组有多长,所谓等值子数组就是数组中的值完全相同。
直接的想法是记录每个相同元素的下标index,然后计算它们之间的距离 gap
,使用滑动窗口来计算最多可以消除的 gap
数量。
注意不能优先消除序列中距离小的 gap
,也就是说只能按照顺序去消除,否则可能会截断等值子数组。
刚开始使用哈希表记录相同元素的下标序列,结果提示超出内存限制,后来改用数组解决了问题。
Map在不断添加元素时可能需要进行扩容,每次扩容都需要重新分配更大的内存空间。但我直接指定初始大小还是超出限制。
原来用了两个哈希表,indexMap
与 gapMap
,后来 indexMap
改成了 List[]
,gapMap
则直接在循环中计算并添加到 ArrayList
中。
注意这不是一个固定大小的窗口,如果按照固定大小的窗口去实现还是会超时。
代码
/**
* @date 2024-05-23 0:11
*/
public class LongestEqualSubarray2831 {
public int longestEqualSubarray_v1(List<Integer> nums, int k) {
if (nums.size() == 0) {
return 0;
}
int res = 0;
List<Integer>[] indexMap = new List[100001];
for (int i = 0; i < nums.size(); i++) {
int val = nums.get(i);
if (indexMap[val] == null) {
indexMap[val] = new ArrayList<>();
}
indexMap[val].add(i);
}
for (List<Integer> index : indexMap) {
if (index == null) {
continue;
}
int i = 0;
ArrayList<Integer> gaps = new ArrayList<>();
for (int j = 1; j < index.size(); j++) {
gaps.add(index.get(j) - index.get(i++) - 1);
}
int cnt = k;
int gapNums = 0;
int left = 0;
int right = 0;
while (right < gaps.size()) {
while (cnt >= 0 && right < gaps.size()) {
cnt -= gaps.get(right);
if (cnt >= 0) {
right++;
}
}
gapNums = Math.max(gapNums, right - left);
while (left < gaps.size() && cnt < 0) {
cnt += gaps.get(left);
left++;
}
right++;
}
res = Math.max(res, gapNums + 1);
}
return res;
}
}
性能
又是勉强通过。//todo 性能分析与优化
2225.找出输掉零场或一场比赛的玩家
目标
给你一个整数数组 matches 其中 matches[i] = [winneri, loseri] 表示在一场比赛中 winneri 击败了 loseri 。
返回一个长度为 2 的列表 answer :
- answer[0] 是所有 没有 输掉任何比赛的玩家列表。
- answer[1] 是所有恰好输掉 一场 比赛的玩家列表。
两个列表中的值都应该按 递增 顺序返回。
注意:
- 只考虑那些参与 至少一场 比赛的玩家。
- 生成的测试用例保证 不存在 两场比赛结果 相同 。
示例 1:
输入:matches = [[1,3],[2,3],[3,6],[5,6],[5,7],[4,5],[4,8],[4,9],[10,4],[10,9]]
输出:[[1,2,10],[4,5,7,8]]
解释:
玩家 1、2 和 10 都没有输掉任何比赛。
玩家 4、5、7 和 8 每个都输掉一场比赛。
玩家 3、6 和 9 每个都输掉两场比赛。
因此,answer[0] = [1,2,10] 和 answer[1] = [4,5,7,8] 。
示例 2:
输入:matches = [[2,3],[1,3],[5,4],[6,4]]
输出:[[1,2,5,6],[]]
解释:
玩家 1、2、5 和 6 都没有输掉任何比赛。
玩家 3 和 4 每个都输掉两场比赛。
因此,answer[0] = [1,2,5,6] 和 answer[1] = [] 。
说明:
- 1 <= matches.length <= 10^5
- matches[i].length == 2
- 1 <= winneri, loseri <= 10^5
- winneri != loseri
- 所有 matches[i] 互不相同
思路
给定一个记录比赛结果的二维数组,数组元素为[winneri, loseri]
,求没有输掉任何比赛的玩家以及仅输掉一场比赛的玩家。
简单实现可以直接利用TreeSet保存返回结果,往里面放的时候先判断是否输掉过比赛,如果后面输掉了比赛还要将其从从没输掉比赛的集合中移除。
对于仅输掉一次比赛的集合,不能根据它来判断是否输掉过比赛,因为输掉两次会从集合中移除,如何后续还会输掉比赛,判断集合中没有,就会错误地向其中添加,因此需要额外记录输掉过比赛的集合。
官网题解使用的是哈希表记录失败的次数,效率更高。
代码
/**
* @date 2024-05-22 9:03
*/
public class FindWinners2225 {
public List<List<Integer>> findWinners(int[][] matches) {
List<List<Integer>> res = new ArrayList<>();
Set<Integer> neverLose = new TreeSet<>();
Set<Integer> loseOnce = new TreeSet<>();
Set<Integer> lost = new HashSet<>();
for (int[] match : matches) {
int win = match[0];
int lose = match[1];
neverLose.remove(lose);
if (!lost.contains(win)) {
neverLose.add(win);
}
if (!loseOnce.contains(lose) && !lost.contains(lose)) {
loseOnce.add(lose);
} else {
loseOnce.remove(lose);
}
lost.add(lose);
}
List<Integer> winner = new ArrayList<>(neverLose);
List<Integer> loser = new ArrayList<>(loseOnce);
res.add(winner);
res.add(loser);
return res;
}
public List<List<Integer>> findWinners_v1(int[][] matches) {
Map<Integer, Integer> loseCnt = new HashMap<>(matches.length);
for (int[] match : matches) {
//可以直接在这里记录没有一次失败的
loseCnt.merge(match[1], 1, Integer::sum);
}
List<List<Integer>> res = new ArrayList<>();
Set<Integer> neverLose = new HashSet<>();
for (int[] match : matches) {
if(!loseCnt.containsKey(match[0])){
neverLose.add(match[0]);
}
}
List<Integer> winner = new ArrayList<>(neverLose);
Collections.sort(winner);
List<Integer> lostOnce = loseCnt.entrySet().stream()
.filter((entry) -> entry.getValue() == 1)
.map(Map.Entry::getKey).sorted()
.collect(Collectors.toList());
res.add(winner);
res.add(lostOnce);
return res;
}
}
性能
暴力解
哈希加排序
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
,使 x
与 num
相向变化。很明显,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
与除数数组 divisors
,nums
中能够被 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
思路
现在有一组任务,其难度与收益分别使用两个数组 difficulty
与 profit
表示,还有一个数组表示一组工人的能力。现在需要给工人分配工作,如果分配的工作难度大于工人的能力则无法获取收益,求把工人分配到岗后能够获得的最大收益。
我们只需为每个工人分配其能力范围内的收益最高的工作即可。需要注意的是,题目中没有说难度越高收益越高,并且相同难度的收益也会不同。
难度与收益是通过下标关联的,并且是无序的。
一个很自然的想法是维护一个难度与最大收益的映射,然后直接根据工人的能力二分查找相应的收益并累加。
那么如何同时对两个相关联的数组进行排序就是解题的关键。这里直接将 difficulty
与 profit
的映射通过 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;
}
性能
方法一
方法二