[Leetcode Weekly Contest]307
链接:LeetCode
[Leetcode]2383. 赢得比赛需要的最少训练时长
你正在参加一场比赛,给你两个 正 整数 initialEnergy 和 initialExperience 分别表示你的初始精力和初始经验。
另给你两个下标从 0 开始的整数数组 energy 和 experience,长度均为 n 。
你将会 依次 对上 n 个对手。第 i 个对手的精力和经验分别用 energy[i] 和 experience[i] 表示。当你对上对手时,需要在经验和精力上都 严格 超过对手才能击败他们,然后在可能的情况下继续对上下一个对手。
击败第 i 个对手会使你的经验 增加 experience[i],但会将你的精力 减少 energy[i] 。
在开始比赛前,你可以训练几个小时。每训练一个小时,你可以选择将增加经验增加 1 或者 将精力增加 1 。
返回击败全部 n 个对手需要训练的 最少 小时数目。
循环模拟即可。
class Solution {
public int minNumberOfHours(int initialEnergy, int initialExperience, int[] energy, int[] experience) {
int res = 0;
int n = energy.length;
for(int i=0;i<n;++i) {
if(initialEnergy <= energy[i]) {
res += energy[i] - initialEnergy + 1;
initialEnergy += energy[i] - initialEnergy + 1;
}
if(initialExperience <= experience[i]){
res += experience[i] - initialExperience + 1;
initialExperience += experience[i] - initialExperience + 1;
}
initialEnergy -= energy[i];
initialExperience += experience[i];
}
return res;
}
}
[Leetcode]2384. 最大回文数字
给你一个仅由数字(0 - 9)组成的字符串 num 。
请你找出能够使用 num 中数字形成的 最大回文 整数,并以字符串形式返回。该整数不含 前导零 。
注意:
- 你 无需 使用 num 中的所有数字,但你必须使用 至少 一个数字。
- 数字可以重新排序。
贪心。一个回文串(如 998767899,123321)可以被分成两部分:
两边对应的部分(如 9987 和 7899,123 和 321),这两部分中的数字每种都出现偶数次。中间单独一个数字(如 6),这部分是可选的。
因此令 counter[i] 表示数字 i 出现的次数,我们先从 9 到 0 枚举第一部分中出现的数,再看是否还有剩下的数放进中间单独的数字即可。注意不能有前导零,我们只有在有其他前导数字的时候,才可以添加0.
class Solution {
public String largestPalindromic(String num) {
StringBuilder sb = new StringBuilder();
int[] counter = new int[10];
for(char ch:num.toCharArray()) counter[ch-'0'] ++;
for(int i=9;i>=0;--i) {
int add = 0;
if(sb.length()>0 || i!=0) {
if(counter[i] >= 2) add = counter[i] / 2;
for(int j=0;j<add;++j) sb.append((char)('0' + i));
counter[i] -= add*2;
}
}
String prefix = sb.toString();
for(int i=9;i>=0;--i) {
if(counter[i]>0) {
sb.append((char)('0' + i));
break;
}
}
for(int i=prefix.length()-1;i>=0;--i) {
sb.append(prefix.charAt(i));
}
return sb.toString();
}
}
[Leetcode]2385. 感染二叉树需要的总时间
给你一棵二叉树的根节点 root ,二叉树中节点的值 互不相同 。另给你一个整数 start 。在第 0 分钟,感染 将会从值为 start 的节点开始爆发。
每分钟,如果节点满足以下全部条件,就会被感染:
- 节点此前还没有感染。
- 节点与一个已感染节点相邻。
返回感染整棵树需要的分钟数。
BFS或者DFS。一种明显的方法是建图,然后再进行遍历即可。另一种方法需要考虑树的情况进行优化,如下:
- 对于起始结点,若该结点是一棵树的根结点,那么该结点感染整棵树所需时间为树的高度。
- 若起始节点存在父结点,则该结点还可以沿着父结点去感染父结点其其他子树。假设该起始结点在某结点的左子树上,那么它感染整棵树的时间为从起始节点到树的根结点的距离 + 根结点另一颗子树的高度。即根结点与起始节点的高度差 + 根结点另一颗子树的高度。
以上两种情况是同时进行的,所以最短时间为两者的最大值。
class Solution {
public String smallestNumber(String pattern) {
int n = pattern.length();
String[] res = new String[n+1];
for(int i=0;i<n+1;i++) {
res[i] = "" + (char)('1'+i);
}
int i=0;
while(i<n) {
if(pattern.charAt(i) == 'I') {
i++;
} else {
int start_ind = i;
while(i<n && pattern.charAt(i) == 'D') {
i++;
}
reverseArray(res, start_ind, i);
}
}
return String.join("", res);
}
private void reverseArray(String[] list, int start, int end) {
while(start<=end) {
String tmp = list[end];
list[end] = list[start];
list[start] = tmp;
start ++;
end --;
}
}
}
[Leetcode] 2386. 找出数组的第 K 大和
给你一个整数数组 nums 和一个 正 整数 k 。你可以选择数组的任一 子序列 并且对其全部元素求和。
数组的 第 k 大和 定义为:可以获得的第 k 个 最大 子序列和(子序列和允许出现重复)
返回数组的 第 k 大和 。
子序列是一个可以由其他数组删除某些或不删除元素排生而来的数组,且派生过程不改变剩余元素的顺序。
注意:空子序列的和视作 0 。
两种做法:堆 / 二分。
从简化问题开始
首先考虑本题的简化问题:给定 n 个非负数 \(a_1, a_2, \cdots, a_n\) ,求第 k 个最 小 的子序列和。这是一个经典问题。我们先把所有数从小到大排序,记 \((s, i)\) 表示一个总和为 s,且最后一个元素是第 i 个元素的子序列。我们用一个小根堆维护 \((s, i)\),一开始堆中只有一个元素 \((a_1, 1)\)。当我们取出堆顶元素 \((s, i)\) 时,我们可以进行以下操作:
- 把 \(a_{i + 1}\) 接到这个子序列的后面形成新的子序列,也就是将 \((s + a_{i + 1}, i + 1)\)放入堆中。
- 把子序列中的 \(a_i\)直接换成 \(a_{i + 1}\),也就是将 \((s - a_i + a_{i + 1}, i + 1)\)放入堆中。
第 \((k - 1)\) 次取出的 \((s, i)\) 中的 ss 就是答案(\(k = 1\) 时答案为空集之和,也就是 0)。
这个做法的正确性基于以下事实:这种方法能不重不漏地生成所有子序列。每次放进去的数不小于拿出来的数。
接下来回到原问题,考虑给定的数中有负数的情况。记 \(\textit{nums}\) 中所有非负数的和为 \(\textit{sum}\).任意一个子序列的和,都等价于从 \(\textit{sum}\) 中减去某些非负数 / 加上某些负数得到。将 \(\textit{nums}\) 所有数取绝对值,这样可以统一成从 \(\textit{sum}\) 中减去某些数。我们需要按照从小到大的顺序取出 \(\textit{sum}\) 要减去的子序列,可以将nums 所有数取绝对值后排序,然后用最大堆来实现。
我们还可以用二分来求出从 \(\textit{sum}\) 中减去的第 \(k-1\) 小的子序列和。依然是所有元素取绝对值,然后排序。
二分子序列和,记作 \(\textit{limit}\),统计元素和 \(s\) 不超过 \(\textit{limit}\) 的子序列个数 \(\textit{cnt}\)。我们可以写一个简单的回溯,从小到大考虑每个 \(\textit{nums}[i]\) 选或者不选,如果遇到 \(\textit{cnt}\ge k-1\) 或者\(s+\textit{nums}[i]>\textit{limit}\) 的情况就立刻返回。用 \(\textit{sum}\) 减去二分得到的值就是答案。
// 堆
class Solution {
public long kSum(int[] nums, int k) {
int n = nums.length;
long sum = 0;
for(int i=0;i<n;++i) {
if(nums[i] >= 0) sum += nums[i];
else nums[i] = -nums[i];
}
Arrays.sort(nums);
PriorityQueue<Pair<Long, Integer>> pq = new PriorityQueue<>((a,b) -> Long.compare(b.getKey(), a.getKey()));
pq.offer(new Pair<Long, Integer>(sum, 0));
for(int i=0;i<k-1;++i) {
var pair = pq.poll();
long val = pair.getKey();
int ind = pair.getValue();
if(ind < n) {
pq.offer(new Pair<Long, Integer>(val-nums[ind], ind+1));
if(i>0) pq.offer(new Pair<>(val-nums[ind]+nums[ind-1], ind+1));
}
}
return pq.peek().getKey();
}
}
// 二分
class Solution {
int cnt;
public long kSum(int[] nums, int k) {
int n = nums.length;
long sum = 0;
for(int i=0;i<n;++i) {
if(nums[i] >= 0) sum += nums[i];
else nums[i] = -nums[i];
}
Arrays.sort(nums);
return sum-kSmallSum(nums, k-1);
}
//find the kth small number of a sorted array (num>=0)
public long kSmallSum(int[] nums, int k) {
if(k == 0) return 0;
long lo = 0, hi = 0;
for(var n:nums) hi = hi + n;
while(lo <= hi) {
long mid = lo + (hi-lo) / 2;
if(countSmallerOrEqual(nums, mid, k) >= k) hi = mid - 1;
else lo = mid + 1;
}
return lo;
}
public int countSmallerOrEqual(int[] nums, long target, int k) {
cnt = 0;
dfs(nums, target, 0, 0L, k);
return cnt;
}
public void dfs(int[] nums, long target, int index, long cur, int k) {
if(index==nums.length || nums[index] > target || cnt > k) return;
if(index == 0) {
cnt++;
dfs(nums, target, index+1, cur + nums[index], k);
return;
}
if(cur + nums[index] <= target) {
cnt ++;
dfs(nums, target, index+1, cur + nums[index], k);
}
if(cur - nums[index-1] + nums[index] <= target) {
cnt ++;
dfs(nums, target, index+1, cur- nums[index-1] + nums[index], k);
}
return;
}
}