优雅的暴力 :「分块」简单入门

题目描述

这是 LeetCode 上的 901. 股票价格跨度 ,难度为 中等

Tag : 「分块」、「单调栈」

编写一个 StockSpanner 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。

今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。

例如,如果未来 7 天股票的价格是 [100, 80, 60, 70, 60, 75, 85],那么股票跨度将是 [1, 1, 1, 2, 1, 4, 6]

示例:

scss 复制代码
输入:["StockSpanner","next","next","next","next","next","next","next"], [[],[100],[80],[60],[70],[60],[75],[85]]

输出:[null,1,1,1,2,1,4,6]

解释:
首先,初始化 S = StockSpanner(),然后:
S.next(100) 被调用并返回 1,
S.next(80) 被调用并返回 1,
S.next(60) 被调用并返回 1,
S.next(70) 被调用并返回 2,
S.next(60) 被调用并返回 1,
S.next(75) 被调用并返回 4,
S.next(85) 被调用并返回 6。

注意 (例如) S.next(75) 返回 4,因为截至今天的最后 4 个价格
(包括今天的价格 75) 小于或等于今天的价格。

提示:

  • 调用 StockSpanner.next(int price) 时,将有 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 < = p r i c e < = 1 0 5 1 <= price <= 10^5 </math>1<=price<=105。
  • 每个测试用例最多可以调用 10000StockSpanner.next
  • 在所有测试用例中,最多调用 150000StockSpanner.next
  • 此问题的总时间限制减少了 50%

分块

又名优雅的暴力。

这是一道在线问题,在调用 next 往数据流存入元素的同时,返回连续段不大于当前元素的数的个数。

一个朴素的想法是:使用数组 nums 将所有 price 进行存储,每次返回时往前找到第一个不满足要求的位置,并返回连续段的长度。

但对于 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 0 4 10^4 </math>104 的调用次数来看,该做法的复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n 2 ) O(n^2) </math>O(n2),计算量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 0 8 10^8 </math>108,不满足要求。

实际上我们可以利用「分块」思路对其进行优化:将与连续段的比较转换为与最值的比较

具体的,我们仍然使用 nums 对所有的 price 进行存储,同时使用 region 数组来存储每个连续段的最大值,其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> r e g i o n [ l o c ] = x region[loc] = x </math>region[loc]=x 含义为块编号为 loc 的最大值为 x,其中块编号 loc 块对应了数据编号 idx 的范围 <math xmlns="http://www.w3.org/1998/Math/MathML"> [ ( l o c − 1 ) × l e n + 1 , l o c × l e n ] [(loc - 1) \times len + 1, loc \times len] </math>[(loc−1)×len+1,loc×len]。

对于 next 操作而言,除了直接更新数据数组 nums[++idx] = price 以外,我们还需要更新 idx 所在块的最值 region[loc]

然后从当前块 loc 开始往前扫描其余块,使用 leftright 代指当前处理到的块的左右端点:

若当前块满足 region[loc] <= price,说明块内所有元素均满足要求,直接将当前块 loc 所包含元素个数累加到答案中。

直到遇到不满足的块或达到块数组边界。

若存在遇到不满足要求的块,使用 rightleft 统计块内满足要求 nums[i] <= price 的个数。

对于块个数和大小的设定,是运用分块降低复杂度的关键。

数的个数为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 0 4 10^4 </math>104,我们可以设定块大小为 <math xmlns="http://www.w3.org/1998/Math/MathML"> n = 100 \sqrt{n} = 100 </math>n =100,这样也限定了块的个数为 <math xmlns="http://www.w3.org/1998/Math/MathML"> n = 100 \sqrt{n} = 100 </math>n =100 个。

这样对于单次操作而言,我们最多遍历进行 <math xmlns="http://www.w3.org/1998/Math/MathML"> n \sqrt{n} </math>n 次的块间操作,同时最多进行一次块内操作,整体复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(\sqrt{n}) </math>O(n )。

单次 next 操作计算量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 × 1 0 2 2 \times 10^2 </math>2×102 以内,单样例计算量为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 2 × 1 0 6 2 \times 10^6 </math>2×106,可以过。

为了方便,我们令块编号 loc 和数据编号 idx 均从 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 1 </math>1 开始;同时为了防止每个样例都 new 大数组,我们采用 static 优化,并在 StockSpanner 的初始化中做重置工作。

Java 代码:

Java 复制代码
class StockSpanner {
    static int N = 10010, len = 100, idx = 0;
    static int[] nums = new int[N], region = new int[N / len + 10];
    public StockSpanner() {
        for (int i = 0; i <= getIdx(idx); i++) region[i] = 0;
        idx = 0;
    }
    int getIdx(int x) {
        return (x - 1) / len + 1;
    }
    int query(int price) {
        int ans = 0, loc = getIdx(idx), left = (loc - 1) * len + 1, right = idx;
        while (loc >= 1 && region[loc] <= price) {
            ans += right - left + 1;
            loc--; right = left - 1; left = (loc - 1) * len + 1;
        }
        for (int i = right; loc >= 1 && i >= left && nums[i] <= price; i--) ans++;
        return ans;
    }
    public int next(int price) {
        nums[++idx] = price;
        int loc = getIdx(idx);
        region[loc] = Math.max(region[loc], price);
        return query(price);
    }
}

TypeScript 代码:

TypeScript 复制代码
class StockSpanner {
    N: number = 10010; sz: number = 100; idx: number = 0
    nums: number[] = new Array<number>(this.N).fill(0);
    region = new Array<number>(Math.floor(this.N / this.sz) + 10).fill(0)
    getIdx(x: number): number {
        return Math.floor((x - 1) / this.sz) + 1
    }
    query(price: number): number {
        let ans = 0, loc = this.getIdx(this.idx), left = (loc - 1) * this.sz + 1, right = this.idx
        while (loc >= 1 && this.region[loc] <= price) {
            ans += right - left + 1
            loc--; right = left - 1; left = (loc - 1) * this.sz + 1
        }
        for (let i = right; loc >= 1 && i >= left && this.nums[i] <= price; i--) ans++
        return ans
    }
    next(price: number): number {
        this.nums[++this.idx] = price
        const loc = this.getIdx(this.idx)
        this.region[loc] = Math.max(this.region[loc], price)
        return this.query(price)
    }
}

Python3 代码:

Python 复制代码
class StockSpanner:
    def __init__(self):
        self.N, self.sz, self.idx = 10010, 110, 0
        self.nums, self.region = [0] * self.N, [0] * (self.N // self.sz + 10)

    def next(self, price: int) -> int:
        def getIdx(x):
            return (x - 1) // self.sz + 1

        def query(price):
            ans, loc = 0, getIdx(self.idx)
            left, right = (loc - 1) * self.sz + 1, self.idx
            while loc >= 1 and self.region[loc] <= price:
                ans += right - left + 1
                loc -= 1
                right, left = left - 1, (loc - 1) * self.sz + 1
            while loc >= 1 and right >= left and self.nums[right] <= price:
                right, ans = right - 1, ans + 1
            return ans

        self.idx += 1
        loc = getIdx(self.idx)
        self.nums[self.idx] = price
        self.region[loc] = max(self.region[loc], price)
        return query(price)
  • 时间复杂度:由于使用了 static 优化,StockSpanner 初始化时,需要对上一次使用的块进行重置,复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(\sqrt{n}) </math>O(n );由于块大小和数量均为 <math xmlns="http://www.w3.org/1998/Math/MathML"> n \sqrt{n} </math>n ,next 操作复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(\sqrt{n}) </math>O(n )
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)

单调栈

另外一个容易想到的想法是使用「单调栈」,栈内以二元组 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( i d x , p r i c e ) (idx, price) </math>(idx,price) 形式维护比当前元素 price 大的元素。

每次执行 next 操作时,从栈顶开始处理,将所有满足「不大于 price」的元素进行出栈,从而找到当前元素 price 左边最近一个比其大的位置

Java 代码:

Java 复制代码
class StockSpanner {
    Deque<int[]> d = new ArrayDeque<>();
    int cur = 0;
    public int next(int price) {
        while (!d.isEmpty() && d.peekLast()[1] <= price) d.pollLast();
        int prev = d.isEmpty() ? -1 : d.peekLast()[0], ans = cur - prev;
        d.addLast(new int[]{cur++, price});
        return ans;
    }
}

TypeScript 代码:

TypeScript 复制代码
class StockSpanner {
    stk = new Array<Array<number>>(10010).fill([0, 0])
    he = 0; ta = 0; cur = 0
    next(price: number): number {
        while (this.he < this.ta && this.stk[this.ta - 1][1] <= price) this.ta--
        const prev = this.he >= this.ta ? -1 : this.stk[this.ta - 1][0], ans = this.cur - prev
        this.stk[this.ta++] = [this.cur++, price]
        return ans
    }
}

Python3 代码:

Python 复制代码
class StockSpanner:
    def __init__(self):
        self.stk = []
        self.cur = 0

    def next(self, price: int) -> int:
        while self.stk and self.stk[-1][1] <= price:
            self.stk.pop()
        prev = -1 if not self.stk else self.stk[-1][0]
        ans = self.cur - prev
        self.stk.append([self.cur, price])
        self.cur += 1
        return ans
  • 时间复杂度:next 操作的均摊复杂度为 <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( 1 ) O(1) </math>O(1)
  • 空间复杂度: <math xmlns="http://www.w3.org/1998/Math/MathML"> O ( n ) O(n) </math>O(n)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.901 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour...

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

更多更全更热门的「笔试/面试」相关资料可访问排版精美的 合集新基地 🎉🎉

相关推荐
bin915312 分钟前
npm报错
前端·npm·node.js
一指流沙q19 分钟前
Chrome被360导航篡改了怎么改回来?
前端·chrome
编码浪子27 分钟前
Springboot高并发乐观锁
后端·restful
laocooon5238578861 小时前
HTML CSS 超链
前端·css·html
LUwantAC1 小时前
CSS(二):美化网页元素
前端·css
m0_748251081 小时前
docker安装nginx,docker部署vue前端,以及docker部署java的jar部署
java·前端·docker
Mr.朱鹏1 小时前
操作002:HelloWorld
java·后端·spring·rabbitmq·maven·intellij-idea·java-rabbitmq
我是ed1 小时前
# thingjs 基础案例整理
前端
Ashore_1 小时前
从简单封装到数据响应:Vue如何引领开发新模式❓❗️
前端·vue.js
落魄实习生1 小时前
小米路由器开启SSH,配置阿里云ddns,开启外网访问SSH和WEB管理界面
前端·阿里云·ssh