每日算法刷题Day64:8.24:leetcode 堆6道题,用时2h30min
五、反悔堆
1.套路
1.基于堆的反悔贪心。
2.堆中存产生坏影响的元素,接着贪心的选择元素,并将坏元素存入堆,若不满足条件,则从堆中选出最坏的元素进行反悔,直至满足条件。
2.题目描述
1.小扣当前位于魔塔游戏第一层,共有 N
个房间,编号为 0 ~ N-1
。每个房间的补血道具/怪物对于血量影响记于数组 nums
,其中正数表示道具补血数值,即血量增加对应数值;负数表示怪物造成伤害值,即血量减少对应数值;0
表示房间对血量无影响。
小扣初始血量为 1,且无上限。假定小扣原计划按房间编号升序访问所有房间补血/打怪,为保证血量始终为正值,小扣需对房间访问顺序进行调整,每次仅能将一个怪物房间(负数的房间)调整至访问顺序末尾。请返回小扣最少需要调整几次,才能顺利访问所有房间。若调整顺序也无法访问完全部房间,请返回 -1。
3.学习经验
1. LCP 30. 魔塔游戏(中等)
LCP 30. 魔塔游戏 - 力扣(LeetCode)
思想
1.小扣当前位于魔塔游戏第一层,共有 N
个房间,编号为 0 ~ N-1
。每个房间的补血道具/怪物对于血量影响记于数组 nums
,其中正数表示道具补血数值,即血量增加对应数值;负数表示怪物造成伤害值,即血量减少对应数值;0
表示房间对血量无影响。
小扣初始血量为 1,且无上限。假定小扣原计划按房间编号升序访问所有房间补血/打怪,为保证血量始终为正值,小扣需对房间访问顺序进行调整,每次仅能将一个怪物房间(负数的房间)调整至访问顺序末尾。请返回小扣最少需要调整几次,才能顺利访问所有房间。若调整顺序也无法访问完全部房间,请返回 -1。
2.堆中存负数房间,贪心的顺序遍历,直至血量为非正,再从堆中取出扣血数最多的房间(负数最小,小顶堆),进行反悔(此题影响是血量上升,调整次序加1,无需显示放到末尾)
代码
class Solution {
public:typedef long long ll;int magicTower(vector<int>& nums) {int n = nums.size();ll blood = 1;ll sum = 0;for (int& x : nums)sum += x;if (blood + sum <= 0)return -1;priority_queue<int, vector<int>, greater<int>> pq;int res = 0;for (int i = 0; i < n; ++i) {blood += nums[i];if (nums[i] < 0)pq.push(nums[i]);while (blood <= 0) {int tmp = pq.top();pq.pop();blood -= tmp;++res;}}return res;}
};
六、懒删除堆
1.套路
1.支持删除堆中任意元素。(普通堆只支持删除堆顶)
2.核心思想:不立即删除目标元素,而是记录下来(可以记录现在的,也可以记录删除的),等到它冒泡到堆顶时再真正删除。(理解核心思想最重要)
代码实现:用哈希表记录要删除的元素及其次数(有重复)以及堆实际存在元素数量,在访问堆顶元素时(获取堆顶和出堆时都要)先判断哈希表中该元素是否要删除,并进行删除,直至获得一个真正存在的元素。而入堆时可以抵消哈希表该元素的删除次数,否则再入堆。
3.模版:
class LazyHeap{// 最大堆和最小堆只有初始定义不同,其他都完全相同priority_queue<int> pq; // 最大堆// priority_queue<int,vector<int>,greater<int>> pq; // 最小堆map<int,int> mp; // 删除元素-删除次数size_t sz=0; // 堆实际大小// 正式执行删除操作void apply_remove(){while(!pq.empty() && mp[pq.top()]>0){--mp[pq.top()];pq.pop();}}
public:// 返回堆的实际大小size_t size(){return sz;}// 删除void remove(int x){++mp[x];--sz;}// 查看堆顶int top(){// 先正式执行删除操作apply_remove();return pq.top(); // 真正堆顶}// 出堆int pop(){// 先正式执行删除操作apply_remove();--sz;int x=pq.top();pq.pop();return x;}// 入堆void push(int x){if(mp[x]>0) --mp[x]; // 抵消之前的删除else pq.push(x);++sz; // 都要加}
}
2.题目描述
1(学习).设计一个数字容器系统,可以实现以下功能:
- 在系统中给定下标处 插入 或者 替换 一个数字。
- 返回 系统中给定数字的最小下标。
请你实现一个NumberContainers
类: NumberContainers()
初始化数字容器系统。void change(int index, int number)
在下标index
处填入number
。如果该下标index
处已经有数字了,那么用number
替换该数字。int find(int number)
返回给定数字number
在系统中的最小下标。如果系统中没有number
,那么返回-1
。
3(学习).设计一个支持下述操作的食物评分系统:- 修改 系统中列出的某种食物的评分。
- 返回系统中某一类烹饪方式下评分最高的食物。
实现FoodRatings
类: FoodRatings(String[] foods, String[] cuisines, int[] ratings)
初始化系统。食物由foods
、cuisines
和ratings
描述,长度均为n
。foods[i]
是第i
种食物的名字。cuisines[i]
是第i
种食物的烹饪方式。ratings[i]
是第i
种食物的最初评分。
void changeRating(String food, int newRating)
修改名字为food
的食物的评分。String highestRated(String cuisine)
返回指定烹饪方式cuisine
下评分最高的食物的名字。如果存在并列,返回 字典序较小 的名字。
注意,字符串x
的字典序比字符串y
更小的前提是:x
在字典中出现的位置在y
之前,也就是说,要么x
是y
的前缀,或者在满足x[i] != y[i]
的第一个位置i
处,x[i]
在字母表中出现的位置在y[i]
之前。
4.你需要在一个集合里动态记录 ID 的出现频率。给你两个长度都为n
的整数数组nums
和freq
,nums
中每一个元素表示一个 ID ,对应的freq
中的元素表示这个 ID 在集合中此次操作后需要增加或者减少的数目。- 增加 ID 的数目:如果
freq[i]
是正数,那么freq[i]
个 ID 为nums[i]
的元素在第i
步操作后会添加到集合中。 - 减少 ID 的数目:如果
freq[i]
是负数,那么-freq[i]
个 ID 为nums[i]
的元素在第i
步操作后会从集合中删除。
请你返回一个长度为n
的数组ans
,其中ans[i]
表示第i
步操作后出现频率最高的 ID 数目 ,如果在某次操作后集合为空,那么ans[i]
为 0 。
3.学习经验
1.一般更新/替换操作有两步:
(1)现在状态哈希映射更新
(2)堆插入新元素(键不同,值相同[[九.堆(优先队列)#1. 2349. 设计数字容器系统(中等,学习)]],或键相同,值不同[[九.堆(优先队列)#3. 2353. 设计食物评分系统(中等,学习)]])
2.题目中要查询最高/最低能想到堆,出现任意更新/删除堆中元素想到懒删除堆
1. 2349. 设计数字容器系统(中等,学习)
2349. 设计数字容器系统 - 力扣(LeetCode)
思想
1.设计一个数字容器系统,可以实现以下功能:
- 在系统中给定下标处 插入 或者 替换 一个数字。
- 返回 系统中给定数字的最小下标。
请你实现一个NumberContainers
类: NumberContainers()
初始化数字容器系统。void change(int index, int number)
在下标index
处填入number
。如果该下标index
处已经有数字了,那么用number
替换该数字。int find(int number)
返回给定数字number
在系统中的最小下标。如果系统中没有number
,那么返回-1
。
2.因为下标-数是一一对应的关系,所以无需像模版一样记录下标的删除次数,而是直接记录当前下标-数的匹配,当调用find
方法时,利用number
找到的下标,再用下标得到当前真实的数,判断是否与number
一致,从而可知是否被替换
3.注意:因为要实际删除,所以要写成引用,不能拷贝:
auto& pq = it->second; // 引用,因为要实际删除
代码
class NumberContainers {
public:map<int, priority_queue<int, vector<int>, greater<int>>>mp; // 数-下标小顶堆map<int, int> mpidx; // 现在的下标-数NumberContainers() {}void change(int index, int number) {mpidx[index] = number;mp[number].push(index); // 直接插入,find再删除}int find(int number) {auto it = mp.find(number);if (it == mp.end())return -1;auto& pq = it->second; // 引用,因为要实际删除// 现在下标的数与查询数不一致,说明被替换,出队列while (!pq.empty() && mpidx[pq.top()] != number)pq.pop();return pq.empty() ? -1 : pq.top();}
};/*** Your NumberContainers object will be instantiated and called as such:* NumberContainers* obj = new NumberContainers();* obj->change(index,number);* int param_2 = obj->find(number);*/
2. 3607. 电网维护(中等,学习dfs建图,等到dfs学完再写一遍,先暂时过,懒删除堆逻辑没有问题)
3607. 电网维护 - 力扣(LeetCode)
思想
代码
3. 2353. 设计食物评分系统(中等,学习)
2353. 设计食物评分系统 - 力扣(LeetCode)
思想
1.设计一个支持下述操作的食物评分系统:
- 修改 系统中列出的某种食物的评分。
- 返回系统中某一类烹饪方式下评分最高的食物。
实现FoodRatings
类: FoodRatings(String[] foods, String[] cuisines, int[] ratings)
初始化系统。食物由foods
、cuisines
和ratings
描述,长度均为n
。foods[i]
是第i
种食物的名字。cuisines[i]
是第i
种食物的烹饪方式。ratings[i]
是第i
种食物的最初评分。
void changeRating(String food, int newRating)
修改名字为food
的食物的评分。String highestRated(String cuisine)
返回指定烹饪方式cuisine
下评分最高的食物的名字。如果存在并列,返回 字典序较小 的名字。
注意,字符串x
的字典序比字符串y
更小的前提是:x
在字典中出现的位置在y
之前,也就是说,要么x
是y
的前缀,或者在满足x[i] != y[i]
的第一个位置i
处,x[i]
在字母表中出现的位置在y[i]
之前。
2.很明显的懒删除堆,但跟1不同,这里是修改同一种食物的评分,所以把这个也当做新状态存入堆中,highestRated
逻辑依旧是堆顶元素评分与现有评分不一致(不必判断小于,大于),则弹出堆(不必插入堆)
代码
class FoodRatings {
public:struct Node {string food;int rate;Node(string _food, int _rate) : food(_food), rate(_rate) {}};struct cmp {bool operator()(const Node& a, const Node& b) {if (a.rate != b.rate)return a.rate < b.rate;return a.food > b.food;}};map<string, priority_queue<Node, vector<Node>, cmp>>mp; // cuisine-<ratring,food>大顶堆map<string, int> curRate; // 现在food-ratingmap<string, string> belong; // food-cuisineFoodRatings(vector<string>& foods, vector<string>& cuisines,vector<int>& ratings) {int n = foods.size();for (int i = 0; i < n; ++i) {mp[cuisines[i]].push(Node(foods[i], ratings[i]));curRate[foods[i]] = ratings[i];belong[foods[i]] = cuisines[i];}}void changeRating(string food, int newRating) {curRate[food] = newRating;mp[belong[food]].push(Node(food, newRating)); // 直接插入新的,查询时删除旧的}string highestRated(string cuisine) {auto it = mp.find(cuisine);if (it == mp.end())return "";auto& pq = it->second;while (!pq.empty() && curRate[pq.top().food] != pq.top().rate) {pq.pop(); // 删除旧数据}return pq.empty() ? "" : pq.top().food;}
};/*** Your FoodRatings object will be instantiated and called as such:* FoodRatings* obj = new FoodRatings(foods, cuisines, ratings);* obj->changeRating(food,newRating);* string param_2 = obj->highestRated(cuisine);*/
4. 3092. 最高频率的ID(中等)
3092. 最高频率的 ID - 力扣(LeetCode)
思想
1.你需要在一个集合里动态记录 ID 的出现频率。给你两个长度都为 n
的整数数组 nums
和 freq
,nums
中每一个元素表示一个 ID ,对应的 freq
中的元素表示这个 ID 在集合中此次操作后需要增加或者减少的数目。
- **增加 ID 的数目:**如果
freq[i]
是正数,那么freq[i]
个 ID 为nums[i]
的元素在第i
步操作后会添加到集合中。 - **减少 ID 的数目:**如果
freq[i]
是负数,那么-freq[i]
个 ID 为nums[i]
的元素在第i
步操作后会从集合中删除。
请你返回一个长度为n
的数组ans
,其中ans[i]
表示第i
步操作后出现频率最高的 ID 数目 ,如果在某次操作后集合为空,那么ans[i]
为 0 。
2.找频率最高的ID数目,想到最大堆,元素为数目-值,要动态更新元素的数目,想到懒删除堆,利用哈希表记录当前值-数目,查询时先将值记录数目与当前数目不等的堆顶删除,直至满足条件。
代码
class Solution {
public:typedef long long ll;typedef pair<ll, int> PII; vector<long long> mostFrequentIDs(vector<int>& nums, vector<int>& freq) {int n = nums.size();priority_queue<PII> pq; // cnt-valmap<int, ll> mp; // val-cntfor (int i = 0; i < n; ++i) {pq.push({0, nums[i]});mp[nums[i]] = 0;}vector<ll> res;for (int i = 0; i < n; ++i) {mp[nums[i]] += freq[i];pq.push({mp[nums[i]], nums[i]});while (!pq.empty() && mp[pq.top().second] != pq.top().first)pq.pop();res.push_back(pq.top().first);}return res;}
};
七、对顶堆(滑动窗口第K小/大)
1.套路
1.不再是获取堆顶最大元素,而是每轮查询获取动态的第K小/大,需用两个堆顶性质相反的对顶堆维护,拥有两种操作:
查询:两个堆的堆顶元素实现
插入:分类讨论插入哪个堆,最终可以合并为插入一个堆,之后再从这个堆插入另一个堆
2.题目描述
1.一个观光景点由它的名字 name
和景点评分 score
组成,其中 name
是所有观光景点中 唯一 的字符串,score
是一个整数。景点按照最好到最坏排序。景点评分 越高 ,这个景点越好。如果有两个景点的评分一样,那么 字典序较小 的景点更好。
你需要搭建一个系统,查询景点的排名。初始时系统里没有任何景点。这个系统支持:
- 添加 景点,每次添加 一个 景点。
- 查询 已经添加景点中第
i
好 的景点,其中i
是系统目前位置查询的次数(包括当前这一次)。- 比方说,如果系统正在进行第
4
次查询,那么需要返回所有已经添加景点中第4
好的。
注意,测试数据保证 任意查询时刻 ,查询次数都 不超过 系统中景点的数目。
请你实现SORTracker
类:
- 比方说,如果系统正在进行第
SORTracker()
初始化系统。void add(string name, int score)
向系统中添加一个名为name
评分为score
的景点。string get()
查询第i
好的景点,其中i
是目前系统查询的次数(包括当前这次查询)。
2(学习).中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。- 例如
arr = [2,3,4]
的中位数是3
。 - 例如
arr = [2,3]
的中位数是(2 + 3) / 2 = 2.5
。
实现 MedianFinder 类: MedianFinder()
初始化MedianFinder
对象。void addNum(int num)
将数据流中的整数num
添加到数据结构中。double findMedian()
返回到目前为止所有元素的中位数。与实际答案相差10-5
以内的答案将被接受。
3.学习经验
1.与简单的top-k问题相比,top-k问题每次查询获取固定的第K小/大,只需要一个堆
而对顶堆是每次查询时获取不固定的第K小/大,所以需要两个堆
1. 2102. 序列顺序查询(困难)
2102. 序列顺序查询 - 力扣(LeetCode)
思想
1.一个观光景点由它的名字 name
和景点评分 score
组成,其中 name
是所有观光景点中 唯一 的字符串,score
是一个整数。景点按照最好到最坏排序。景点评分 越高 ,这个景点越好。如果有两个景点的评分一样,那么 字典序较小 的景点更好。
你需要搭建一个系统,查询景点的排名。初始时系统里没有任何景点。这个系统支持:
-
添加 景点,每次添加 一个 景点。
-
查询 已经添加景点中第
i
好 的景点,其中i
是系统目前位置查询的次数(包括当前这一次)。- 比方说,如果系统正在进行第
4
次查询,那么需要返回所有已经添加景点中第4
好的。
注意,测试数据保证 任意查询时刻 ,查询次数都 不超过 系统中景点的数目。
请你实现SORTracker
类:
- 比方说,如果系统正在进行第
-
SORTracker()
初始化系统。 -
void add(string name, int score)
向系统中添加一个名为name
评分为score
的景点。 -
string get()
查询第i
好的景点,其中i
是目前系统查询的次数(包括当前这次查询)。
2.用两个堆,一个堆pre
记录已查询到的好景点,一个堆cur
记录剩下的未查询到的景点,cur
堆的堆顶就是查询答案。现在考虑添加和查询流程:
查询:
cur
堆顶为答案,cur
弹出堆顶,堆顶入pre
堆
添加,两种情况: -
(1)入堆元素处于
pre
堆,pre
堆最差景点入cur
堆 -
(2)入堆元素处于
cur
堆,直接入 -
所以合并为都先入
pre
堆,pre
堆最差景点入cur
堆
因为要获得pre
堆最差景点,所以它的性质与cur
堆相反,是“最大堆和最小堆”
代码
class SORTracker {
public:struct Node {string name;int score;Node(string _name, int _score) : name(_name), score(_score) {}};struct cmp1 {bool operator()(const Node& a, const Node& b) {if (a.score != b.score)return a.score < b.score;return a.name > b.name;}};struct cmp2 {bool operator()(const Node& a, const Node& b) {if (a.score != b.score)return a.score > b.score;return a.name < b.name;}};priority_queue<Node, vector<Node>, cmp1> cur;priority_queue<Node, vector<Node>, cmp2> pre; // 已遍历过的好景点SORTracker() {}void add(string name, int score) {pre.push(Node(name, score));cur.push(pre.top());pre.pop();}string get() {auto tmp = cur.top();cur.pop();string res = tmp.name;pre.push(tmp);return res;}
};/*** Your SORTracker object will be instantiated and called as such:* SORTracker* obj = new SORTracker();* obj->add(name,score);* string param_2 = obj->get();*/
2. 295. 数据流的中位数(困难,学习)
295. 数据流的中位数 - 力扣(LeetCode)
思想
1.中位数是有序整数列表中的中间值。如果列表的大小是偶数,则没有中间值,中位数是两个中间值的平均值。
- 例如
arr = [2,3,4]
的中位数是3
。 - 例如
arr = [2,3]
的中位数是(2 + 3) / 2 = 2.5
。
实现 MedianFinder 类: MedianFinder()
初始化MedianFinder
对象。void addNum(int num)
将数据流中的整数num
添加到数据结构中。double findMedian()
返回到目前为止所有元素的中位数。与实际答案相差10-5
以内的答案将被接受。
2.这题的意思是能够快速找到一个有序列表的中位数,中位数定义为"可将数值集合划分为相等的两部分",所以可以将有序列表分为两部分left
和right
,
保证left
中所有元素小于等于right
中所有元素,同时人为定义0<=left.size()-right.size()<=1
,那么中位数就来自left
的最大值(大顶堆)和right
的最小值(小顶堆)
计算的中位数就来自`left
查询中位数分类讨论:- (1)有序列表长度为奇数,即
left.size()-right.size()==1
,中位数就是left
的最大值,如left=[1,2],right=[3]
- (2)有序列表长度为偶数,即
left.size()==right.size()
,中位数就是left
的最大值和right
的最小值的平均,如left=[1,2],right=[3,4]
插入数分类讨论: - (1)
left.size()-right.size()==1
,- (I)
val
小,插入left
,导致不平衡,需将left
的最大值放入right
- (II)
val
大,插入right
,无需调整 - 所以上述两种情况可以合并为
val
都先插入left
,然后将left
最大值放入right
中
- (I)
- (2)
left.size()==right.size()
,- (I)
val
小,插入left
,无需调整 - (II)
val
大,插入right
,导致不平衡,需将right
的最小值插入left
- 所以上述两种情况可以合并为
val
都先插入right
,然后将right
最小值放入left
中
上述逻辑要获取left
的最大值和right
的最小值,且能够插入元素,所以left
为大顶堆,right
为小顶堆
- (I)
代码
class MedianFinder {
public:priority_queue<int> left; // 大顶堆priority_queue<int, vector<int>, greater<int>> right; // 小顶堆MedianFinder() {}void addNum(int num) {if (left.size() == right.size()) {right.push(num);left.push(right.top());right.pop();} else {left.push(num);right.push(left.top());left.pop();}}double findMedian() {if (left.size() == right.size()) {return 1.0 * (left.top() + right.top()) / 2;} else {return left.top();}}
};/*** Your MedianFinder object will be instantiated and called as such:* MedianFinder* obj = new MedianFinder();* obj->addNum(num);* double param_2 = obj->findMedian();*/