1. 优先级队列
1.1. LC 2182 构造限制重复的字符串
- 大根堆pq存储现有的字符种类
- 哈希表cnt存储每种字符的数量
- 每次弹出一种字符,直至没有字符可用
- 如果限制次数没被用完,使用,更新哈希表,剩余次数-1,把当前字符放回去
- 如果限制次数用完
- 如果此时没有另外的字符,结束
- 如果有另外的字符,弹出,使用,更新哈希表,剩余次数复位,把这两种字符全塞回去。
import java.util.Comparator;
import java.util.PriorityQueue;
class Solution {
public String repeatLimitedString(String s, int repeatLimit) {
char[] ch = s.toCharArray();
int[] cnt = new int[26];
PriorityQueue<Character> pq = new PriorityQueue<>((o1, o2) -> -Character.compare(o1,o2));
for (char c : ch) {
int dis = c - 'a';
if(cnt[dis]==0){
pq.offer(c);
}
cnt[dis]++;
}
StringBuilder sb = new StringBuilder();
int lim = repeatLimit;
while(!pq.isEmpty()){
Character f = pq.poll();
if(lim>0){
sb.append(f);
lim--;
cnt[f-'a']--;
if(cnt[f-'a']!=0){
pq.offer(f);
}else{
lim = repeatLimit;
}
}else{
if(pq.isEmpty()){
return sb.toString();
}else{
Character se = pq.poll();
sb.append(se);
cnt[se-'a']--;
lim = repeatLimit;
if(cnt[se-'a']!=0){
pq.offer(se);
}
pq.offer(f);
}
}
}
return sb.toString();
}
}
1.2. LCP 30 魔塔游戏
可以这样想,如果前k个房间的数值累加到≤0,那么挑选其中绝对值最大的负数,把他调换到最后,这样增加的量可以尽可能多的被后续的负数消耗。从而达到交换次数最小。可以注意到如果每遍历一个索引都这样操作的话,累加和肯定是>0的(因为初始值1>0)。
但这样的问题是,如果所有的负数的和的绝对值 > 所有正数的和的绝对值,那么以上的贪心将认为可以通过。所以先查询整个数组的前缀和,保证能通过,再跑以上算法即可。
挑选前k个中绝对值最大的负数,可以小根堆维护。调换到最后,意思就是把它从小根堆移除(注意我们先查询了一次前缀和,保证了一定可以通过,所以简单地移除没问题)
import java.util.Comparator;
import java.util.PriorityQueue;
class Solution {
public int magicTower(int[] nums) {
long sum = 1L;
for (int num : nums) {
sum += num;
}
if(sum<=0){
return -1;
}
int ans = 0;
PriorityQueue<Integer> pq = new PriorityQueue<>(Integer::compare);
sum = 1L;
for (int num : nums) {
sum += num;
if(num<0){
pq.offer(num);
}
if(sum<=0){
sum -= pq.poll();
ans++;
}
}
return ans;
}
}
1.3. LC 1696 跳跃游戏Ⅵ
这题很容易想到一个O(nk)的做法。就是找前k个状态中的最大值,加上现在遍历到的数,就是到这个位置的最大分数。
但仅仅是这样是T的。我是想维护一个长度为K的窗口的最大值。但是一开始不知道怎么做。后来看了提示,手法就是,开一个大根堆,每个元素存dp的值以及其对应索引。这样后续转移状态时,不断查看堆顶,如果堆顶元素的索引过期了(即堆顶索引 hi < i-k),那么就移除这个元素(因为后续的状态转移时,这个元素肯定也是过期的),然后找最大的不过期的元素转移状态即可。
import java.util.Comparator;
import java.util.PriorityQueue;
class Solution {
public int maxResult(int[] nums, int k) {
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
PriorityQueue<int[]> pq = new PriorityQueue<>((o1, o2) -> Integer.compare(o2[0],o1[0]));
pq.offer(new int[]{dp[0],0});
for(int i=1;i<nums.length;i++){
while(!pq.isEmpty() && pq.peek()[1]<i-k){
pq.poll();
}
dp[i] = nums[i]+pq.peek()[0];
pq.offer(new int[]{dp[i],i});
}
return dp[n-1];
}
}
最坏的情况就是每个用来维护当前状态的最大值在下一次就立刻过期。这样每轮都要poll出去,由于过期的都已经poll出去了,所以大根堆最大也就k。而offer操作也是log复杂度,所以就是O(nlogk)。
这道题对我来说,价值最大的就是这个维护区间(滑动窗口最大值)的手法。但是这个手法有个更高级的,叫做双端队列,放在滑窗里面写吧。
class Solution {
public int maxResult(int[] nums, int k) {
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
Deque<Integer> queue = new ArrayDeque<>();
queue.offerLast(0);
for (int i = 1; i < n; i++) {
while (queue.peekFirst() < i - k) {
queue.pollFirst();
}
dp[i] = dp[queue.peekFirst()] + nums[i];
while (!queue.isEmpty() && dp[queue.peekLast()] <= dp[i]) {
queue.pollLast();
}
queue.offerLast(i);
}
return dp[n - 1];
}
}
2. 单调队列
2.1. LC 862 和至少为K的最短子数组
子数组元素和显然要算前缀和。
维护一个单调增的单调队列。也就是如果队尾元素大于当前前缀和的话,就应该把队尾踢掉。这是因为:当后续前缀和查询队列中维护的前缀和时,如果当前前缀和与其之差≥k的话,那么与一个更小的前缀和之差也一定≥k。由于我们是正序的,所以踢走队尾的前缀和的索引一定更靠后。也就更接近日后查询时的索引。因此长度就更短。
对于查询,查询检查点直到查无可查为止。取最小值作为答案。
import java.util.ArrayDeque;
class Solution {
public int shortestSubarray(int[] nums, int k) {
ArrayDeque<long[]> q = new ArrayDeque<>();
long sum = 0;
long ans = Long.MAX_VALUE;
q.push(new long[]{0,-1});
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
while(!q.isEmpty() && sum-q.peekFirst()[0]>=k){
ans = Math.min(ans,i-q.pollFirst()[1]);
}
while(!q.isEmpty() && sum<q.peekLast()[0]){
q.pollLast();
}
q.add(new long[]{sum,i});
}
return ans==Long.MAX_VALUE?-1: (int) ans;
}
}
之所以不用单调栈而是一个单调的双端队列,是因为栈没办法查询或操作栈底元素。我们想要最短区间,是肯定要查看之前最早的检查点的,也就是踢人的时候不能从队尾踢,队尾的前缀和要留给更后面的位置查询。栈满足不了这个需求。