每日算法刷题Day61:8.11:leetcode 堆11道题,用时2h30min
分享丨【算法题单】常用数据结构(前缀和/栈/队列/堆/字典树/并查集/树状数组/线段树)- 讨论 - 力扣(LeetCode)
一、基础
1.套路
1.priority_queue<int>
,跟queue
一样,都是容器适配器,默认数组升序,最后一个元素为最大值,堆顶为最大值,故称为大顶堆。
每次取出来的是最大值,即值越大优先级越高。
priority_queue<queue> pq; // 最大堆
int x=pq.top(); // 堆顶
pq.pop(); // 弹出堆顶
pq.push(x); // 插入元素,自动排序
int len=pq.size();
bool tag=pq.empty();
2.vector
直接赋值到优先队列
priority_queue<int> pq(nums.begin(),nums.end())
注意:queue
不行
3.最小堆写法:
priority_queue<int,vector<int>,greater<int>> pq;
// 数组降序排列,故最后一个元素最小,为小顶堆
4.结构体自定义优先级写法(较难):
struct Node{int x,y;
};// 比较器结构体,不写在Node里面
struct cmp{// 比较器,是(),而不是<bool operator()(const Node &a, const Node &b){return a.x>b.x; // 为true时,前面的是parent,为更大值,故为大顶堆}
}priority_queue<Node,vector<Node>,cmp> pq;
如果理解return a.x>b.x;
?
(1)从priority_queue
的底层逻辑看,他是在维护堆时判断:
if(comp(parent,child)) then swap(parent,child)
所以当a.x>b.x
为true
时,a
从parent
变成child
,而b
从child
变成parent
,所以较小的元素成为parent
,即较小的元素优先级最高,为小顶堆。
(2)(好记忆)类比堆排序,a.x>b.x
降序排序,数组(类比,不是严格降序,但能保证最后一个元素为最小值)的最后一个元素为最小值,而最后一个元素为堆顶,故为小顶堆。
2.题目描述
3.学习经验
1.快速获取最大值/最小值,并进行一些操作,将操作后的值放回去(要用优先队列),然后往复操作。
2.topK问题[[九.堆(优先队列)#8. 703. 数据流中的第K大元素(简单,学习)]],堆中元素数量维护为K
个。
topK大,则建立小顶堆,因为堆顶为第K大元素,是前K个元素最小的。
topK小,则建立大顶堆。
1. 1046. 最后一块石头的重量(简单,学习)
1046. 最后一块石头的重量 - 力扣(LeetCode)
思想
1.有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回0
。
2.每次取两块最重的就想到大顶堆
代码
class Solution {
public:int lastStoneWeight(vector<int>& stones) {int n = stones.size();priority_queue<int> pq;for (int i = 0; i < n; ++i) {pq.push(stones[i]);}while (pq.size() > 1) {int x = pq.top();pq.pop();int y = pq.top();pq.pop();if (x > y)pq.push(x - y);}return pq.empty() ? 0 : pq.top();}
};
2. 3264. K次乘运算后的最终数组I(简单)
3264. K 次乘运算后的最终数组 I - 力扣(LeetCode)
思想
1.给你一个整数数组 nums
,一个整数 k
和一个整数 multiplier
。
你需要对 nums
执行 k
次操作,每次操作中:
- 找到
nums
中的 最小 值x
,如果存在多个最小值,选择最 前面 的一个。 - 将
x
替换为x * multiplier
。
请你返回执行完k
次乘运算之后,最终的nums
数组。
2.这题每次取最小值能想到小顶堆,但是要更新x
并返回原顺序的结果数组,还得知道x
的下标,所以要用到结构体。所以要自定义优先队列优先级。
代码
class Solution {
public:struct Node {int id;int x;Node(int _id, int _x) : id(_id), x(_x) {}};struct cmp {bool operator()(const Node& a, const Node& b) {// 小顶堆if (a.x != b.x)return a.x > b.x;return a.id > b.id;}};priority_queue<Node, vector<Node>, cmp> pq;vector<int> getFinalState(vector<int>& nums, int k, int multiplier) {int n = nums.size();for (int i = 0; i < n; ++i) {pq.push(Node(i, nums[i]));}while (k) {Node t = pq.top();pq.pop();int val = nums[t.id] * multiplier, tid = t.id;nums[tid] = val;;pq.push(Node{tid, val});--k;}return nums;}
};
3. 2558. 从数量最多的堆取走礼物(简单)
2558. 从数量最多的堆取走礼物 - 力扣(LeetCode)
思想
1.给你一个整数数组 gifts
,表示各堆礼物的数量。每一秒,你需要执行以下操作:
- 选择礼物数量最多的那一堆。
- 如果不止一堆都符合礼物数量最多,从中选择任一堆即可。
- 将堆中的礼物数量减少到堆中原来礼物数量的平方根,向下取整。
返回在k
秒后剩下的礼物数量。
2.每次获得礼物数量最多的那一堆,想到最大堆。
放回平方根,但是结果不要求数组顺序,所以比第2题更简单,无需结构体。
代码
class Solution {
public:long long pickGifts(vector<int>& gifts, int k) {int n = gifts.size();long long res = 0;priority_queue<int> pq;for (int i = 0; i < n; ++i) {pq.push(gifts[i]);res += gifts[i];}while (k) {int x = pq.top();pq.pop();int curx = sqrt(x);pq.push(curx);res -= (x - curx);--k;}return res;}
};
4. 2336. 无限集中的最小数字(中等,学习思想)
2336. 无限集中的最小数字 - 力扣(LeetCode)
思想
1.现有一个包含所有正整数的集合 [1, 2, 3, 4, 5, ...]
。
实现 SmallestInfiniteSet
类:
SmallestInfiniteSet()
初始化 SmallestInfiniteSet 对象以包含 所有 正整数。int popSmallest()
移除 并返回该无限集中的最小整数。void addBack(int num)
如果正整数num
不 存在于无限集中,则将一个num
添加 到该无限集中。
2.取最小整数肯定能想到最小堆,但是这题一开始是无限集,最小堆里面有无限个元素,无法实现。
可以将原无限集划分为[id,+无穷]
的连续集合,和离散集合,离散集合的最小值就用最小堆实现,但是还要去重,所以来个集合。
所以两个操作的逻辑如下:
(1)popSmallest
:- 最小堆不为空,则取堆顶,为最小整数。最小堆和集合删去元素。
- 否则,取连续集合第一个
id
,然后++id
(2)addBack()
: num>=id
:不产生任何影响num=id-1
,连续集合向左移一个,--id
num<id
:num
在集合中,不产生任何影响,可以与第一种合并num
不在集合中,num
进入最小堆和集合
代码
class SmallestInfiniteSet {
public:int id; // [id,=无穷]都在无限集中priority_queue<int, vector<int>, greater<int>> pq; // 小顶堆set<int> st; // 去重SmallestInfiniteSet() { id = 1; }int popSmallest() {// 优先取小顶堆int res = 0;if (!pq.empty()) {res = pq.top();pq.pop();st.erase(res);} else { // 否则取无限集res = id;++id;}return res;}void addBack(int num) {// num在无限集中才操作if (num >= id || st.count(num))return;else if (num == id - 1)--id;else {pq.push(num);st.insert(num);}}
};/*** Your SmallestInfiniteSet object will be instantiated and called as such:* SmallestInfiniteSet* obj = new SmallestInfiniteSet();* int param_1 = obj->popSmallest();* obj->addBack(num);*/
5. 2530. 执行K次操作后的最大分数(中等)
2530. 执行 K 次操作后的最大分数 - 力扣(LeetCode)
思想
1.给你一个下标从 0 开始的整数数组 nums
和一个整数 k
。你的 起始分数 为 0
。
在一步 操作 中:
- 选出一个满足
0 <= i < nums.length
的下标i
, - 将你的 分数 增加
nums[i]
,并且 - 将
nums[i]
替换为ceil(nums[i] / 3)
。
返回在 恰好 执行k
次操作后,你可能获得的最大分数。
向上取整函数ceil(val)
的结果是大于或等于val
的最小整数。
2.取最大分数就是大顶堆,然后先上取整利用套路加上2。
代码
class Solution {
public:long long maxKelements(vector<int>& nums, int k) {int n = nums.size();long long res = 0;priority_queue<int> pq;for (int i = 0; i < n; ++i)pq.push(nums[i]);while (k) {int x = pq.top();pq.pop();res += x;pq.push((x + 3 - 1) / 3);--k;}return res;}
};
6. 3066. 超过阈值的最少操作数II(中等)
[3066. 超过阈值的最少操作数 II - 力扣(LeetCode)](https://leetcode.cn/problems/minimum-operations-to-exceed-threshold-value-ii/description/
思想
1.给你一个下标从 0 开始的整数数组 nums
和一个整数 k
。
你可以对 nums
执行一些操作,在一次操作中,你可以:
- 选择
nums
中 最小 的两个整数x
和y
。 - 将
x
和y
从nums
中删除。 - 将
min(x, y) * 2 + max(x, y)
添加到数组中的任意位置。
**注意,**只有当nums
至少 包含两个元素时,你才可以执行以上操作。
你需要使数组中的所有元素都 大于或等于k
,请你返回需要的 最少 操作次数。
2.该开long long
还是要开,不要猜测
代码
class Solution {
public:typedef long long ll;int minOperations(vector<int>& nums, int k) {int n = nums.size();priority_queue<ll, vector<ll>, greater<ll>> pq(nums.begin(),nums.end());ll res = 0;while (pq.top() < k) {ll x = pq.top();pq.pop();ll y = pq.top();pq.pop();pq.push(1LL * x * 2 + y);++res;}return res;}
};
7. 1962. 移除石子使总数最小(中等)
1962. 移除石子使总数最小 - 力扣(LeetCode)
思想
1.给你一个整数数组 piles
,数组 下标从 0 开始 ,其中 piles[i]
表示第 i
堆石子中的石子数量。另给你一个整数 k
,请你执行下述操作 恰好 k
次:
- 选出任一石子堆
piles[i]
,并从中 移除floor(piles[i] / 2)
颗石子。
**注意:**你可以对 同一堆 石子多次执行此操作。
返回执行k
次操作后,剩下石子的 最小 总数。
floor(x)
为 小于 或 等于x
的 最大 整数。(即,对x
向下取整)。
2.跟[[九.堆(优先队列)#3. 2558. 从数量最多的堆取走礼物(简单)]]一模一样
代码
class Solution {
public:int minStoneSum(vector<int>& piles, int k) {int n = piles.size();int res = 0;priority_queue<int> pq(piles.begin(), piles.end());for (int i = 0; i < n; ++i)res += piles[i];while (k) {int x = pq.top();pq.pop();int dif = x / 2;pq.push(x - dif);res -= dif;--k;}return res;}
};
8. 703. 数据流中的第K大元素(简单,重点学习)
703. 数据流中的第 K 大元素 - 力扣(LeetCode)
思想
1.设计一个找到数据流中第 k
大元素的类(class)。注意是排序后的第 k
大元素,不是第 k
个不同的元素。
请实现 KthLargest
类:
KthLargest(int k, int[] nums)
使用整数k
和整数流nums
初始化对象。int add(int val)
将val
插入数据流nums
后,返回当前数据流中第k
大的元素。
2.此题是经典题,要求第K大元素。可以利用小根堆维护前K大个元素(用小根堆是因为堆顶就是第K大元素),然后元素进来插入小根堆,内部排序不用我们管,如果堆元素超过K个,则弹出堆顶(比第K大元素小)。
有点像单调队列的右侧进入元素维护单调性,但是此题只维护堆中为前K大元素,堆顶是第K大元素,前K大元素的顺序无法保证。
代码
class KthLargest {
public:int len;priority_queue<int, vector<int>, greater<int>> pq;KthLargest(int k, vector<int>& nums) {len = k;int n = nums.size();for (int i = 0; i < n; ++i) {pq.push(nums[i]);if (pq.size() > len)pq.pop();}}int add(int val) {pq.push(val);if (pq.size() > len)pq.pop();return pq.top();}
};/*** Your KthLargest object will be instantiated and called as such:* KthLargest* obj = new KthLargest(k, nums);* int param_1 = obj->add(val);*/
9. 3275. 第K近障碍物查询(中等)
3275. 第 K 近障碍物查询 - 力扣(LeetCode)
思想
1.有一个无限大的二维平面。
给你一个正整数 k
,同时给你一个二维数组 queries
,包含一系列查询:
queries[i] = [x, y]
:在平面上坐标(x, y)
处建一个障碍物,数据保证之前的查询 不会 在这个坐标处建立任何障碍物。
每次查询后,你需要找到离原点第k
近 障碍物到原点的 距离 。
请你返回一个整数数组results
,其中results[i]
表示建立第i
个障碍物以后,离原地第k
近障碍物距离原点的距离。如果少于k
个障碍物,results[i] == -1
。
注意,一开始 没有 任何障碍物。
坐标在(x, y)
处的点距离原点的距离定义为|x| + |y|
。
2,求topK小距离,所以为大顶堆
代码
class Solution {
public:typedef long long ll;vector<int> resultsArray(vector<vector<int>>& queries, int k) {int n = queries.size();vector<int> res(n, -1);priority_queue<ll> pq;for (int i = 0; i < n; ++i) {int x = queries[i][0], y = queries[i][1];ll dis = abs(x) + abs(y);pq.push(dis);if (pq.size() > k)pq.pop();if (pq.size() < k)res[i] = -1;elseres[i] = pq.top();}return res;}
};
10. 1845. 座位预约管理系统(中等)
1845. 座位预约管理系统 - 力扣(LeetCode)
思想
1.请你设计一个管理 n
个座位预约的系统,座位编号从 1
到 n
。
请你实现 SeatManager
类:
SeatManager(int n)
初始化一个SeatManager
对象,它管理从1
到n
编号的n
个座位。所有座位初始都是可预约的。int reserve()
返回可以预约座位的 最小编号 ,此座位变为不可预约。void unreserve(int seatNumber)
将给定编号seatNumber
对应的座位变成可以预约。
2.- 每一次对reserve
的调用,题目保证至少存在一个可以预约的座位。- 每一次对
unreserve
的调用,题目保证seatNumber
在调用函数前都是被预约状态。
所以一个优先队列就行。但是平时最好来个哈希表去重。
代码
class SeatManager {
public:priority_queue<int, vector<int>, greater<int>> pq;SeatManager(int n) {for (int i = 1; i <= n; ++i) {pq.push(i);}}int reserve() {int x = pq.top();pq.pop();return x;}void unreserve(int seatNumber) { pq.push(seatNumber); }
};/*** Your SeatManager object will be instantiated and called as such:* SeatManager* obj = new SeatManager(n);* int param_1 = obj->reserve();* obj->unreserve(seatNumber);*/
11. 2208. 将数组和减半的最少操作次数(中等,学习避免浮点数方法,但是不常用)
2208. 将数组和减半的最少操作次数 - 力扣(LeetCode)
思想
1.给你一个正整数数组 nums
。每一次操作中,你可以从 nums
中选择 任意 一个数并将它减小到 恰好 一半。(注意,在后续操作中你可以对减半过的数继续执行操作)
请你返回将 nums
数组和 至少 减少一半的 最少 操作数。
2,浮点数会产生误差,因为是每次减半,所以可以把数乘以2的多少次幂进行放大,保证到结束时他还是整数,可以证明次数不会超过20。(证明先放一下,学习这个思想,到时候写个20能过很多)
代码
class Solution {
public:int halveArray(vector<int>& nums) {int n = nums.size();double sum = 0;priority_queue<double> pq;for (int i = 0; i < n; ++i) {sum += 1.0 * nums[i];pq.push(1.0 * nums[i]);}double k = sum / 2;int res = 0;while (sum > k) {double x = pq.top();pq.pop();sum -= x / 2;pq.push(x / 2);++res;}return res;}
};
不用浮点数
class Solution {
public:typedef long long ll;int halveArray(vector<int>& nums) {int n = nums.size();ll sum = 0;priority_queue<ll> pq;for (int i = 0; i < n; ++i) {ll tmp = 1LL * nums[i]<< 20; // 先把nums[i]转成long long才能左移,不然会溢出sum += tmp;pq.push(tmp);}ll k = sum / 2;int res = 0;while (sum > k) {ll x = pq.top();pq.pop();sum -= x / 2;pq.push(x / 2);++res;}return res;}
};