五月训练 Day21


优先队列(堆)的概念并不复杂,难点在于自己手撸一个堆。熟悉这个过程后,做题时 C++ 可以用 priority_queue<T>。不熟悉的可以参考 这里

0. Leetcode 2099. 找到和最大的长度为 K 的子序列

给你一个整数数组 nums 和一个整数 k 。你需要找到 nums 中长度为 k 的 子序列 ,且这个子序列的 和最大 。
请你返回 任意 一个长度为 k 的整数子序列。
子序列 定义为从一个数组里删除一些元素后,不改变剩下元素的顺序得到的数组。

分析与解答

可以将其当作排序,使用堆排序后,选出最大的 k k k 个。这里第一个堆手写一下:

class Solution {
    int numIdx[2000];
    unordered_map<int, int> id2Num;
    int tail; // pos to insert new element
    
public:
    vector<int> maxSubsequence(vector<int>& nums, int k) {
        tail = 0;
        // 构造大顶堆
        for (int i = 0; i < nums.size(); ++i) {
            id2Num[i] = nums[i];
            numIdx[tail++] = i;
            // 上浮
            int curPos(tail - 1);
            int prePos = (curPos - 1) / 2;
            while (prePos >= 0) {
                if (id2Num[numIdx[curPos]] > id2Num[numIdx[prePos]]) {
                    int tmp = numIdx[curPos];
                    numIdx[curPos] = numIdx[prePos];
                    numIdx[prePos] = tmp;
                    curPos = prePos;
                    prePos = (curPos - 1) / 2;
                } else {
                    break;
                }
            }
        }
        
        // 获取前 k 个元素
        vector<int> vec;
        while (k--) {
            vec.push_back(numIdx[0]);
            numIdx[0] = numIdx[tail - 1];
            // 下沉
            int curPos(0);
            int lIdx = curPos * 2 + 1;
            
            while (lIdx < tail - 1) {
                int sonIdx = lIdx;
                int rIdx = curPos * 2 + 2;
                if (id2Num[numIdx[sonIdx]] < id2Num[numIdx[rIdx]]) {
                    sonIdx = rIdx;
                }
                if (id2Num[numIdx[curPos]] < id2Num[numIdx[sonIdx]]) {
                    int curNum = numIdx[curPos];
                    numIdx[curPos] = numIdx[sonIdx];
                    numIdx[sonIdx] = curNum;
                    curPos = sonIdx;
                    lIdx = curPos * 2 + 1;
                } else {
                    break;
                }
            }
            tail--;
        }
        sort(vec.begin(), vec.end());
        vector<int> result;
        for (int i = 0; i < vec.size(); ++i) {
            result.push_back(nums[vec[i]]);
        }
        
        return result;
    }
};

我们也可以建立小顶堆,将前 k k k 个元素放入堆中后,搜索数组中剩下的元素。若当前元素比堆顶元素大,则删除堆顶元素,插入当前元素:

class Solution {
    struct Data {
        int idx;
        int num;

        bool operator<(const Data& o) const {
            if (num == o.num) {
                return idx < o.idx;
            }
            return num > o.num;
        }
    };
    priority_queue<Data> q;

public:
    vector<int> maxSubsequence(vector<int>& nums, int k) {
        for (int i = 0; i < k; ++i) {
            // 构建大小为 k 的堆
            Data curData;
            curData.idx = i;
            curData.num = nums[i];
            q.push(curData);
        }

        for (int i = k; i < nums.size(); ++i) {
            // 确保堆中为 [0, i] 里 k 个最大的元素
            Data curData = q.top();
            if (curData.num < nums[i]) {
                // 弹出堆中最小的元素,放入当前元素
                q.pop();
                Data cData;
                cData.idx = i;
                cData.num = nums[i];
                q.push(cData);
            }
        }

        // 获取下标并排序
        vector<int> resultIdx;
        while (!q.empty()) {
            Data cur = q.top();
            resultIdx.push_back(cur.idx);
            q.pop();
        }
        
        // 根据下标取相应元素
        sort(resultIdx.begin(), resultIdx.end());
        vector<int> result;
        for (int i = 0; i < resultIdx.size(); ++i) {
            result.push_back(nums[resultIdx[i]]);
        }

        return result;
    }
};

1. Leetcode 1792. 最大平均通过率

一所学校里有一些班级,每个班级里有一些学生,现在每个班都会进行一场期末考试。给你一个二维数组 classes ,其中 classes[i] = [passi, totali] ,表示你提前知道了第 i 个班级总共有 totali 个学生,其中只有 passi 个学生可以通过考试。
给你一个整数 extraStudents ,表示额外有 extraStudents 个聪明的学生,他们 一定 能通过任何班级的期末考。你需要给这 extraStudents 个学生每人都安排一个班级,使得 所有 班级的 平均 通过率 最大 。
一个班级的 通过率 等于这个班级通过考试的学生人数除以这个班级的总人数。平均通过率 是所有班级的通过率之和除以班级数目。
请你返回在安排这 extraStudents 个学生去对应班级后的 最大 平均通过率。与标准答案误差范围在 10-5 以内的结果都会视为正确结果。

分析与解答

本题关键在于明确优先队列中比较的 object 是什么。由于可以有 k k k 个必定及格的学生,因此每次分配一名学生到 当前增加一人后及格率变化最大的班级 即可,即:
m a x i x i + 1 y i + 1 − x i y i max_i \frac{x_i + 1}{y_i + 1} - \frac{x_i}{y_i} maxiyi+1xi+1yixi

class Solution {
    struct Pair {
        int a;
        int b;

        Pair(int _a, int _b) {
            a = _a;
            b = _b;
        }

        void update() {
            a++;
            b++;
        }
        
        double getDelta() const {
            return (double)(a + 1) / (b + 1) - (double)a / b;
        }

		// 注意插入时比较元素的 delta 值
        bool operator < (const Pair& o) const {
            return getDelta() < o.getDelta();
        }
    };

public:
    double maxAverageRatio(vector<vector<int>>& classes, int extraStudents) {
        priority_queue<Pair> q;
        int n = classes.size();
        for (int i = 0; i < n; ++i) {
            q.push(Pair(classes[i][0], classes[i][1]));
        }

        while (extraStudents--) {
            // 每次找到通过率变化最大的,插入
            Pair curPair = q.top();
            q.pop();
            curPair.update();
            q.push(curPair);
        }

        double result(0);
        int num = q.size();
        while (!q.empty()) {
            result += (double)q.top().a / q.top().b;
            q.pop();
        }

        return result / num;
    }
};

2. Leetcode 1499. 满足不等式的最大值

给你一个数组 points 和一个整数 k 。数组中每个元素都表示二维平面上的点的坐标,并按照横坐标 x 的值从小到大排序。也就是说 points[i] = [xi, yi] ,并且在 1 <= i < j <= points.length 的前提下, xi < xj 总成立。
请你找出 yi + yj + |xi - xj| 的 最大值,其中 |xi - xj| <= k 且 1 <= i < j <= points.length。
题目测试数据保证至少存在一对能够满足 |xi - xj| <= k 的点。

分析与解答

这里需要做一下转换,由于 x i < x j x_i < x_j xi<xj,因此原式 y i + y j + ∣ x i − x j ∣ = y i + y j + x j − x i y_i + y_j + |x_i - x_j| = y_i + y_j + x_j - x_i yi+yj+xixj=yi+yj+xjxi,由于对每个 j j j 而言, x j + y j x_j + y_j xj+yj 为定值,因此将问题转化为寻找 y i − x i y_i - x_i yixi 的最大值,且 x j − x i ≤ k x_j - x_i \leq k xjxik,很容易想到用堆求解:

class Solution {
    struct Data {
        int idx;
        int val;

        bool operator < (const Data& o) const {
            if (val == o.val) {
                return idx > o.idx;
            }
            return val < o.val;
        }
    };

    priority_queue<Data> q; // 大顶堆

public:
    int findMaxValueOfEquation(vector<vector<int>>& points, int k) {
        int result(INT_MIN);
        // y_i + y_j + x_j - x_i (x_i < x_j)
        // 因此对每个 j,只要找到最大的 y_i - x_i 即可        
        int n = points.size();
        for (int i = 0; i < n; ++i) {
            int xi = points[i][0];
            int yi = points[i][1];
            int del = yi - xi;
            
            // 寻找下标为 i 时最大结果
            while (!q.empty()) {
                Data topData = q.top();
                if (xi - topData.idx > 0 && xi - topData.idx <= k) {
                	// 找到下标为 i 时最大结果,break
                    if (topData.val + xi + yi > result) {
                        result = topData.val + xi + yi;
                    }
                    break;
                } else {
                	// 堆顶元素与 i 超过 k 则删除
                    while (xi - topData.idx > k && !q.empty()) {
                        q.pop();
                        topData = q.top();
                    }
                }
            }

            Data tmp;
            tmp.idx = xi;
            tmp.val = del;
            q.push(tmp); // 将当前元素插进堆中
        }

        return result;
    }
};

3. Leetcode 2163. 删除元素后和的最小差值

给你一个下标从 0 开始的整数数组 nums ,它包含 3 * n 个元素。
你可以从 nums 中删除 恰好 n 个元素,剩下的 2 * n 个元素将会被分成两个 相同大小 的部分。
前面 n 个元素属于第一部分,它们的和记为 sumfirst 。
后面 n 个元素属于第二部分,它们的和记为 sumsecond 。
两部分和的 差值 记为 sumfirst - sumsecond 。
比方说,sumfirst = 3 且 sumsecond = 2 ,它们的差值为 1 。
再比方,sumfirst = 2 且 sumsecond = 3 ,它们的差值为 -1 。
请你返回删除 n 个元素之后,剩下两部分和的 差值的最小值 是多少。

分析与解答

本题先写出了一个超时答案,之后参考题解写出了正确解答…
先说思路,由于前 n n n 个元素必定在 s u m f i r s t sum_{first} sumfirst 中,后 n n n 个元素必定在 s u m s e c o n d sum_{second} sumsecond 中,因此问题集中于中间 n n n 个元素的划分。对此可以遍历中间 n n n 个元素,以每个元素为切割点,将 [ 0 , n + i ] [0, n + i] [0,n+i] 划分为前半集合, [ n + i + 1 , 3 n ] [n + i + 1, 3n] [n+i+1,3n] 划分为后半集合,之后找出前半集合中 n n n 个最小元素;后半集合中 n n n 个最大元素即可得到题中所要求的差的最小值。
此处若直接遍历并构造两个优先队列会超时,因此使用前 n n n 个元素构造前半集合,使用后 n n n 个元素构造后半集合,之后从 [ n , 2 n ] [n, 2n] [n,2n] 遍历,利用当前元素更新前半集合,保留 n n n 个最小元素;从 [ 2 n , 3 n ] [2n, 3n] [2n,3n] 进行逆序遍历,利用当前元素更新后半集合,保留 n n n 个最大元素:

class Solution {
    int n2Del;
    priority_queue<int, vector<int>, less<int>> q0; // 大顶堆
    priority_queue<int, vector<int>, greater<int>> q1; // 小顶堆

public:
    long long minimumDifference(vector<int>& nums) {
        // 分成两个堆,分别找最大与最小;对中间的 n 进行循环
        int n = nums.size();
        n2Del = n / 3;
        long long result(LONG_MAX);
        // 前 n 个元素,每次删除最大的
        // [0, n]; [0, n - 1]; ... [0, 2n]
        vector<long long> numFirstVec;
        long long numFirst(0);
        for (int i = 0; i < n2Del; ++i) {
            q0.push(nums[i]);
            numFirst += nums[i];
        }
        numFirstVec.push_back(numFirst);
        for (int i = n2Del; i < 2 * n2Del; ++i) {
        	// 循环剔除最大的元素
            int cur = q0.top();
            if (cur > nums[i]) {
                q0.pop();
                q0.push(nums[i]);
                numFirst -= cur;
                numFirst += nums[i];
            }
            numFirstVec.push_back(numFirst);
        }

        // 后 n 个元素,每次删除最小的
        // [2n, 3n]; [2n - 1, 3n]; ... [n, 3n]
        vector<long long> numSecondVec;
        long long numSecond(0);
        for (int i = 2 * n2Del; i < n; ++i) {
            q1.push(nums[i]);
            numSecond += nums[i];
        }
        numSecondVec.push_back(numSecond);
        for (int i = 2 * n2Del - 1; i >= n2Del; --i) {
          	// 循环剔除最小的元素
            int cur = q1.top();
            if (cur < nums[i]) {
                q1.pop();
                q1.push(nums[i]);
                numSecond -= cur;
                numSecond += nums[i];
            }
            numSecondVec.push_back(numSecond);
        }

		// 注意这里要倒序后才是一一对应的,计算最小值
        reverse(numSecondVec.begin(), numSecondVec.end());
        for (int i = 0; i < numFirstVec.size(); ++i) {
            if (result > numFirstVec[i] - numSecondVec[i]) {
                result = numFirstVec[i] - numSecondVec[i];
            }
        }
        
        return result;
    }
};

总结

优先队列的概念很好理解,但在实际应用中,如何准确找到排序使用的指标十分重要。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值