求一个容器的最值的索引_[力扣239] 单调队列&索引堆

ddbbc9076fd852260efe3574021c4369.png

题目链接

239. 滑动窗口最大值

题目描述

给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

样例

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7

Follow up

你能在线性时间复杂度内解决此题吗?

算法1:平衡树

平衡树维护滑动窗口内的值。STL可以直接用的设施:multiset

平衡树的最后一个节点始终是树中最大的值,对应到 multiset 的接口:

*(setting.rbegin());

先把 [0..k-1] 的数插入平衡树内。

然后枚举 k <= i <= n-1 ,先把 nums[i - k] 从平衡树中删掉 O(logN)。然后插入 nums[i] O(logN),当前树中的最大值 O(logN) 就是区间 [i - k + 1, i] 上的最大值。

代码(c++)

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        if(nums.empty()) return vector<int>();
        int n = nums.size();
        if(n <= k)
        {
            int mx = nums[0];
            for(int i = 1; i < n; ++i)
                mx = max(mx, nums[i]);
            return vector<int>({mx});
        }
        // 滑动 n - k 次
        // 0..k-1
        multiset<int> setting;
        vector<int> result;
        for(int i = 0; i < k; ++i)
            setting.insert(nums[i]);
        result.push_back(*(setting.rbegin()));
        for(int i = k; i < n; ++i)
        {
            setting.erase(setting.find(nums[i - k]));
            setting.insert(nums[i]);
            result.push_back(*setting.rbegin());
        }
        return result;
    }
};

算法2: 单调队列

O(N) 是本题的最好解法。

不定长区间最值问题是著名的RMQ问题,主流解法有线段树,稀疏表等,每次查询都是摊销 O(logN) 的。如果区间长度固定,用单调队列可以优化到每次查询摊销 O(1)。

单调队列的核心使用场景:长度固定的区间最值问题

维护一个保存下标的 deque (与索引堆的思想类似)。队头始终是当前区间的最大值对应的下标。

枚举当前下标 j ,队头是以 j 为右端点的区间的最大值对应的下标。更新 j 对应的答案后,需要将 j 压进队,在此之前可能需要从队尾弹出一些值:如果队尾对应的值小于等于当前值,即 nums[deq.back()] <= nums[j], 则把队尾弹出,直至队尾对应的值大于当前值或队列空了,再将 j 压入,这是因为当 i1 < i2 时, nums[i1] <= nums[i2],则 i1 不会是后续下标 j 的答案了,因为 i2 总是比 i1 要好(有 i2 在,nums[i1] 就不会是最大值)。队列中始终保持单调递减。

当队头与当前枚举的右端点 j 的距离大于窗口长度了,则队头不可能是后续右端点的答案,需要将其从队头弹出。

代码(c++)

class Solution_2 {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        if(nums.empty()) return vector<int>();
        int n = nums.size();
        if(n <= k)
        {
            int mx = nums[0];
            for(int i = 1; i < n; ++i)
                mx = max(mx, nums[i]);
            return vector<int>({mx});
        }
        // 滑动 n - k 次
        // 0..k-1
        deque<int> dq;
        vector<int> result;
        for(int i = 0; i < k; ++i)
        {
            // 插入 i 前先出队
            while(!dq.empty() && nums[dq.back()] <= nums[i])
                dq.pop_back();
            dq.push_back(i);
        }
        result.push_back(nums[dq.front()]);
        for(int i = k; i < n; ++i)
        {
            if(!dq.empty() && dq.front() <= i - k) 
                dq.pop_front();
            while(!dq.empty() && nums[dq.back()] <= nums[i])
                dq.pop_back();
            dq.push_back(i);
            result.push_back(nums[dq.front()]);
        }
        return result;
    }
};

单调队列的通用场景

对每个 0 <= j <= n - 1,摊销 O(1) 地求[i..j] 上的最值以及其对应的下标。其中 0 <= i <= j 且 i 需要满足某些条件,当不满足时 i 需要递增。例如:

  1. [i, j] 上的最值需要满足某些条件:问题1~2;
  2. [i, j] 的区间长度要满足某些条件:本题,问题3。

问题1:两个数组 a, b, 长度均为 N。求有多少个区间 [l, r],使得在区间 [l, r] 上 a 的最大值小于 b 的最小值。

问题2:对应问题1中满足条件的区间 [l, r],找到长度最大的区间,返回区间长度最大值和 a 在该区间上 的最大值

用两个单调队列维护当前最值,一个保存a上最大值的递减队列deq1,一个保存b上最小值的递增队列deq2,如果目前 a上最大值a[deq1.front()] 大于等于 b上最小值 b[deq.front()],要出队,只出队下标小的,出队后更新当前区间。

问题3:求最大子串和(前缀和上的单调队列) [力扣53,918] Kadane算法

算法3:索引堆

O(NlogN) 不是最优解且很难写,仅参考。

动态最值问题最直观的做法:维护一个容量为 k 的最大堆,每次窗口右移一位时,将移动之前窗口最左端的值从堆中弹出,移动后将窗口最右端的值压入,此时堆顶是当前窗口的答案。

问题:将移动之前窗口最左端的值从堆中弹出这一步需要将非堆顶元素删除。这需要知道被删元素在堆数据数组中的位置,但是在此前的插入删除时维持堆性质的 push_up 和 push_down 操作时,元素在对数组中的初始位置已经打乱。

需要有一种办法使得数据在为了维持需要的性质而改变位置后,还能知道它在数组中的位置。索引堆的意思就是堆中保存数据的原始数组 heap 在插入之后就不变了,在 push_up 和 push_down 的过程中,改变的是原始数组 heap 的索引数组 indexes,普通的堆是 heap[i] 保持堆的性质,索引堆是 heap[indexes[i]] 保持堆的性质。索引堆内部使用了索引数组的思想:[力扣315] 索引数组&CDQ分治&归并树

索引堆的插入删除需要带着索引 index。因为窗口长度固定是 k。所以每个删除都对应一个插入,所以每次需要将 i 位置的值删除时,带着删除后要插入的值 x,直接把 i 位置的值改成 x,可以将删除和插入合成一步。

void insert(int index, int x)
void change(int index, int x)

insert 和 change 传入的 index = i % k 是堆中原始数据数组的索引,其中 i 为 nums 对应的下标。在 push_up 和 push_down 中,为保持堆性质要改变的是 indexes 数组,所以需要有一个查找表 reverses 保存从原始数组 heap 的下标到索引数组 indexes 的下标的映射。

vector<int> heap;
vector<int> indexes;
vector<int> reverses;

代码(c++)

class MaxIndexHeap {
public:
    MaxIndexHeap(int n) {
        heap = vector<int>(n + 1, 0); // 数据数组 heap 从 1 开始
        indexes = vector<int>(n + 1, 0); // 索引数组 从 1 开始
        reverses = vector<int>(n + 1, 0); // 反向查找表 从 1 开始
        count = 0;
    }

    int top() {
        return heap[indexes[1]];
    }

    void insert(int index, int x){ // push 改为 insert,既然用索引堆则插入必须带索引
        if(contain(index)) return; // 若index这个位置已经有值,则无动作
        // 若该位置以前有值,但后来被删除了,则该位置又可以插入了
        heap[index + 1] = x;
        indexes[count + 1] = index + 1;
        reverses[index + 1] = count + 1;
        ++count;
        _push_up(count);
    }

    void change(int index, int x) {
        // 索引堆多出来的关键功能
        heap[index + 1] = x;
        _push_up(reverses[index + 1]);
        _push_down(reverses[index + 1]);
    }

    bool contain(int index) {
        return reverses[index + 1] != 0;
    }

private:
    vector<int> heap;
    vector<int> indexes;
    vector<int> reverses;
    int count;

    int _size() {
        return count;
    }

    // 堆的元素下标从1开始
    void _push_down(int u)
    {
        int size = _size();
        if(u > size) return;
        int t = u, left = u * 2, right = u * 2 + 1;
        if(left <= size && heap[indexes[left]] > heap[indexes[t]]) t = left;
        if(right <= size && heap[indexes[right]] > heap[indexes[t]]) t = right;
        if(t != u)
        {
            swap(indexes[u], indexes[t]);
            reverses[indexes[u]] = u;
            reverses[indexes[t]] = t;
            _push_down(t);
        }
    }

    void _push_up(int u)
    {
        int size = _size();
        if(u > size) return;
        while(u / 2 && heap[indexes[u / 2]] < heap[indexes[u]])
        {
            swap(indexes[u / 2], indexes[u]);
            reverses[indexes[u]] = u;
            reverses[indexes[u / 2]] = u / 2;
            u /= 2;
        }
    }
};

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        if(nums.empty()) return vector<int>();
        int n = nums.size();
        if(n <= k)
        {
            int mx = nums[0];
            for(int i = 1; i < n; ++i)
                mx = max(mx, nums[i]);
            return vector<int>({mx});
        }
        // 滑动 n - k 次
        // 0..k-1
        MaxIndexHeap heap(k);
        vector<int> result;
        for(int i = 0; i < k; ++i)
            heap.insert(i, nums[i]);
        result.push_back(heap.top());
        for(int i = k; i < n; ++i)
        {
            heap.change(i % k, nums[i]);
            result.push_back(heap.top());
        }
        return result;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值