优先级队列(堆)
用到优先级队列 (priority queue) 或堆 (heap) 的题一般需要维护一个动态更新的池,元素会被频繁加入到池中或从池中被取走,每次取走的元素为池中优先级最高的元素 (可以简单理解为最大或者最小)。用堆来实现优先级队列是效率非常高的方法,加入或取出都只需要 O ( l o g N ) O(log N) O(logN) 的复杂度。
Kth largest/smallest
数据流中的第K大元素
设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。
请实现 KthLargest 类:
KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。
class KthLargest {
public:
priority_queue<int, vector<int>, greater<int>> hp;
int kk;
KthLargest(int k, vector<int>& nums) {
kk = k;
for (auto& n : nums){
add(n);
}
}
int add(int val) {
hp.push(val);
if (hp.size() > kk){
hp.pop();
}
return hp.top();
}
};
有序矩阵中第K小的元素
给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。
你必须找到一个内存复杂度优于 O(n2) 的解决方案。
- 思路1:每一行相当于一个单调递增的队列,将每个队首元素放入小顶堆中,第K个从堆中弹出的便是所求元素。
class Solution {
public:
class num{
public:
int val, x, y;
num(int _val, int _x, int _y):val(_val), x(_x), y(_y){}
bool operator > (const num& n) const {return this->val > n.val;}
};
int kthSmallest(vector<vector<int>>& matrix, int k) {
priority_queue<num, vector<num>, greater<num>> q;
for (int i = 0; i < matrix.size(); ++i){
q.emplace(matrix[i][0], i, 0);
}
for (int i = 0; i < k - 1; ++i){
num n = q.top();
q.pop();
if (n.y < matrix.size() - 1){
q.emplace(matrix[n.x][n.y + 1], n.x, n.y + 1);
}
}
return q.top().val;
}
};
- 思路2:二分查找,对于一个数mid,矩阵中小于该数的一定在左上角,大于该数的一定在右下角,可以进行二分查找,找到刚好左上角数字个数为k的mid。
class Solution {
public:
bool check(vector<vector<int>>& matrix, int k, int mid, int n){
int i = n - 1, j = 0, num = 0;
while (i >= 0 && j <= n - 1){
if (matrix[i][j] <= mid){
num += i + 1;
++j;
}else{
--i;
}
}
return num >= k;
}
int kthSmallest(vector<vector<int>>& matrix, int k) {
int n = matrix.size(), left = matrix[0][0], right = matrix[n - 1][n - 1];
while (left < right){
int mid = left + ((right - left) >> 1);
if (check(matrix, k, mid, n)){
right = mid;
}else{
left = mid + 1;
}
}
return left;
}
};
查找和最小的K对数字
给定两个以 升序排列 的整数数组 nums1 和 nums2 , 以及一个整数 k 。
定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2 。
请找到和最小的 k 个数对 (u1,v1), (u2,v2) ... (uk,vk) 。
- 思路:优先队列(小顶堆),弹出k个元素
class Solution {
public:
vector<vector<int>> kSmallestPairs(vector<int>& nums1, vector<int>& nums2, int k) {
vector<vector<int>> ans;
int m = nums1.size(), n = nums2.size();
auto cmp = [&nums1, &nums2](const pair<int, int>& p1, const pair<int, int>& p2){
return nums1[p1.first] + nums2[p1.second] > nums1[p2.first] + nums2[p2.second];
};
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> q(cmp);
for (int i = 0; i < min(k, m); ++i){
q.emplace(i, 0);
}
while (k-- > 0 && !q.empty()){
int x = q.top().first, y = q.top().second;
q.pop();
vector<int> temp = {nums1[x], nums2[y]};
ans.emplace_back(temp);
if (y < n - 1){
q.emplace(x, y + 1);
}
}
return ans;
}
};
贪心+堆
最大的团队表现值
公司有编号为 1 到 n 的 n 个工程师,给你两个数组 speed 和 efficiency ,其中 speed[i] 和 efficiency[i] 分别代表第 i 位工程师的速度和效率。请你返回由最多 k 个工程师组成的 最大团队表现值 ,由于答案可能很大,请你返回结果对 10^9 + 7 取余后的结果。
团队表现值 的定义为:一个团队中「所有工程师速度的和」乘以他们「效率值中的最小值」。
- 思路:先将工程师按照效率降序排列,从高到低枚举效率值,选取效率值高于枚举值的,人数小于等于k,速度尽可能大,的工程师。可以将工程师入堆,小顶堆(按速度比较),当堆中元素达到k时,弹出堆顶元素,再进行下一次循环,这样能保证堆中元素是目前的前k大。
class Solution {
public:
using ll = long long;
int maxPerformance(int n, vector<int>& speed, vector<int>& efficiency, int k) {
vector<pair<int, int>> v;
auto cmp = [](const pair<int, int>& s1, const pair<int, int>& s2) {return s1.first > s2.first; };
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> q(cmp);
for (int i = 0; i < n; ++i) {
v.emplace_back(speed[i], efficiency[i]);
}
sort(v.begin(), v.end(), [](const pair<int, int>& s1, const pair<int, int>& s2) {return s1.second > s2.second; });
ll ans = 0, sum = 0;
for (int i = 0; i < n; ++i) {
ll mine = v[i].second;
ll maxs = sum + v[i].first;
ans = max(ans, mine * maxs);
q.push(v[i]);
sum += v[i].first;
if (q.size() == k) {
sum -= q.top().first;
q.pop();
}
}
return ans % (int(1e9) + 7);
}
};
IPO
假设 力扣(LeetCode)即将开始 IPO 。为了以更高的价格将股票卖给风险投资公司,力扣 希望在 IPO 之前开展一些项目以增加其资本。 由于资源有限,它只能在 IPO 之前完成最多 k 个不同的项目。帮助 力扣 设计完成最多 k 个不同项目后得到最大总资本的方式。
给你 n 个项目。对于每个项目 i ,它都有一个纯利润 profits[i] ,和启动该项目需要的最小资本 capital[i] 。
最初,你的资本为 w 。当你完成一个项目时,你将获得纯利润,且利润将被添加到你的总资本中。
总而言之,从给定项目中选择 最多 k 个不同项目的列表,以 最大化最终资本 ,并输出最终可获得的最多资本。
- 思路:首先,将项目按照资本升序排序,将满足w的项目入大顶堆(按照利润),堆顶便是当前能购买的利润最大的项目。购买一次后,会更新w,再接着进入入堆操作。
class Solution {
public:
int findMaximizedCapital(int k, int w, vector<int>& profits, vector<int>& capital) {
vector<pair<int, int>> projects;
for(int i = 0; i < capital.size(); ++i){
projects.emplace_back(profits[i], capital[i]);
}
sort(projects.begin(), projects.end(), [](const pair<int, int>& p1, const pair<int, int>& p2){return p1.second < p2.second;});
int index = 0;
auto cmp = [](const pair<int, int>& p1, const pair<int, int>& p2){return p1.first < p2.first;};
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(cmp)> q(cmp);
for (int i = 0; i < k; ++i){
while (index < projects.size() && projects[index].second <= w){
q.push(projects[index++]);
}
if (!q.empty()){
w += q.top().first;
q.pop();
}else{
break;
}
}
return w;
}
};
会议室2
给你一个会议时间安排的数组 intervals ,每个会议时间都会包括开始和结束的时间 intervals[i] = [starti, endi] ,为避免会议冲突,同时要考虑充分利用会议室资源,请你计算至少需要多少间会议室,才能满足这些会议安排。
- 思路:先将会议按开始时间排序,然后建立小顶堆,放入会议结束的房间。堆顶元素即是结束最早的会议室的结束时间,若小于当前会议开始时间,则弹出堆顶会议室,更新后再入堆,若堆顶元素大于当前会议结束时间,说明没有会议室结束,需要新开,所以直接入堆。
class Solution {
public:
int minMeetingRooms(vector<vector<int>>& intervals) {
if (intervals.size() == 0) {
return 0;
}
priority_queue<int, vector<int>, greater<int>> allocator;
sort(intervals.begin(), intervals.end(),
[](auto a, auto b) { return a[0] < b[0]; });
allocator.push(intervals[0][1]);
for (int i = 1; i < intervals.size(); i++) {
if (intervals[i][0] >= allocator.top()) {
allocator.pop();
}
allocator.push(intervals[i][1]);
}
return allocator.size();
}
};
重构字符串
给定一个字符串 s ,检查是否能重新排布其中的字母,使得两相邻的字符不同。
返回 s 的任意可能的重新排列。若不可行,返回空字符串 "" 。
- 思路:将每个字母出现的次数放入大顶堆,若堆的大小大于2,每次取堆顶两个元素,将其拼入答案中,重新入堆。最后若堆中还剩一个元素,则直接拼入答案
class Solution {
public:
string reorganizeString(string s) {
if (s.size() < 2) {return s;}
vector<int> counts(26, 0);
for (int i = 0; i < s.size(); ++i) {++counts[s[i] - 'a'];}
if (*max_element(counts.begin(), counts.end()) > (s.size() + 1) / 2) {return "";}
auto cmp = [&counts](const char& c1, const char& c2){return counts[c1 - 'a'] < counts[c2 - 'a'];};
priority_queue<char, vector<char>, decltype(cmp)> q(cmp);
for (char c = 'a'; c <= 'z'; ++c) {if (counts[c - 'a']) {q.push(c);}}
string ans = "";
while (q.size() > 1){
char c1 = q.top();
q.pop();
--counts[c1 - 'a'];
char c2 = q.top();
q.pop();
--counts[c2 - 'a'];
if (counts[c1 - 'a']) {q.push(c1);}
if (counts[c2 - 'a']) {q.push(c2);}
ans += c1;
ans += c2;
}
if (!q.empty()) {ans += q.top();}
return ans;
}
};
连接所有点的最小费用
由于原repo中的题目为lintcode中的收费题,这里就找一题leetcode中的来代替。这里知识点是堆优化的Prim算法。
给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。
连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。
请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。
- 思路:在Prim算法中,寻找最短边时采用堆
class Solution {
public:
struct edge{
int to, w;
};
int dist_cal(vector<int>& p1, vector<int>& p2){
return abs(p1[0] - p2[0]) + abs(p1[1] - p2[1]);
}
int minCostConnectPoints(vector<vector<int>>& points) {
int num_v = points.size();
if (num_v == 0) {return 0;}
int ans = 0;
vector<vector<edge>> edges(num_v);
for (int i = 0; i < num_v; ++i){
for (int j = i + 1; j < num_v; ++j){
edge e1 = {j, dist_cal(points[i], points[j])};
edges[i].emplace_back(e1);
edge e2 = {i, dist_cal(points[i], points[j])};
edges[j].emplace_back(e2);
}
}
vector<int> lowestdist(num_v, INT_MAX);
vector<bool> visited(num_v, false);
priority_queue < pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q;
q.push(make_pair(0, 0));
while (!q.empty()){
int temp = q.top().first;
int p = q.top().second;
q.pop();
if (visited[p]) {continue;}
visited[p] = 1;
ans += temp;
for (int i = 0; i < edges[p].size(); ++i){
int to = edges[p][i].to;
lowestdist[to] = min(lowestdist[to], edges[p][i].w);
if (!visited[to]) { q.push(make_pair(lowestdist[to], to));}
}
}
return ans;
}
};
网络延迟时间
Dijkstra算法的堆优化
有 n 个网络节点,标记为 1 到 n。
给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。
现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。
- 思路:在Dijkstra算法中,寻找最短边采用堆
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<vector<pair<int, int>>> edges(n + 1);
for (int i = 0; i < times.size(); ++i){
edges[times[i][0]].emplace_back(make_pair(times[i][2], times[i][1]));
}
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> q;
vector<int> dist(n + 1, INT_MAX);
vector<bool> visited(n + 1, false);
q.push(make_pair(0, k));
dist[k] = 0;
while (!q.empty()){
int p = q.top().second;
q.pop();
if (visited[p]) {continue;}
visited[p] = true;
for (auto& e:edges[p]){
int to = e.second;
dist[to] = min(dist[to], dist[p] + e.first);
if (!visited[to]) {q.push(make_pair(dist[to], to));}
}
}
int ans = *max_element(dist.begin() + 1, dist.end());
return ans == INT_MAX ? -1 : ans;
}
};
K站中转内最便宜的航班
有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flights[i] = [fromi, toi, pricei] ,表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi。
现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1。
- 思路1:维护每个结点最短路径的边数,当遇到更小的路径时结点需要重新入堆
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
vector<vector<pair<int, int>>> edges(n);
for (int i = 0; i < flights.size(); ++i) {
edges[flights[i][0]].emplace_back(make_pair(flights[i][2], flights[i][1]));
}
unordered_map<int, int> prices, steps;
auto cmp = [](const vector<int>& e1, const vector<int>& e2) {return e1[0] > e2[0]; };
priority_queue<vector<int>, vector<vector<int>>, decltype(cmp)> q(cmp);
q.push({ 0, 0, src });
while (!q.empty()) {
int price = q.top()[0], step = q.top()[1], node = q.top()[2];
q.pop();
if (node == dst) { return price; }
if (prices.find(node) == prices.end()) { prices[node] = price; }
steps[node] = step;
if (step <= k) {
++step;
for (auto& e : edges[node]) {
int nn = e.second, pp = e.first;
if ((prices.find(nn) == prices.end()) || step < steps[nn]) {
q.push({ pp + price, step, nn });
}
}
}
}
return -1;
}
};
- 思路2:动态规划,转移方程为 f [ t ] [ i ] = min ( j , i ) ∈ flights { f [ t − 1 ] [ j ] + cost ( j , i ) } f[t][i]=\min _{(j, i) \in \text { flights }}\{f[t-1][j]+\operatorname{cost}(j, i)\} f[t][i]=min(j,i)∈ flights {f[t−1][j]+cost(j,i)},由于状态t只与状态t-1有关,所以可以采用两个一维数组进行空间优化
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
vector<int> f(n, 101 * 10000 + 1);
f[src] = 0;
vector<int> new_f(n, 101 * 10000 + 1);
for (int t = 1; t <= k + 1; ++t){
for (auto&& flight : flights){
int j = flight[0], i = flight[1], cost = flight[2];
new_f[i] = min(new_f[i], f[j] + cost);
}
f = new_f;
}
int ans = 101 * 10000 + 1;
for (int t = 1; t <= k + 1; ++t){
ans = min(ans, f[dst]);
}
return ans == 101 * 10000 + 1 ? -1 : ans;
}
};
题目总结
✅IPO
✅会议室2