|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[901. 股票价格跨度](https://leetcode.cn/problems/online-stock-span/solution/by-ac_oier-m8g7/)** ,难度为 **中等**。 |
| 4 | + |
| 5 | +Tag : 「分块」、「单调栈」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +编写一个 `StockSpanner` 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。 |
| 10 | + |
| 11 | +今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。 |
| 12 | + |
| 13 | +例如,如果未来 `7` 天股票的价格是 `[100, 80, 60, 70, 60, 75, 85]`,那么股票跨度将是 `[1, 1, 1, 2, 1, 4, 6]`。 |
| 14 | + |
| 15 | +示例: |
| 16 | +``` |
| 17 | +输入:["StockSpanner","next","next","next","next","next","next","next"], [[],[100],[80],[60],[70],[60],[75],[85]] |
| 18 | +
|
| 19 | +输出:[null,1,1,1,2,1,4,6] |
| 20 | +
|
| 21 | +解释: |
| 22 | +首先,初始化 S = StockSpanner(),然后: |
| 23 | +S.next(100) 被调用并返回 1, |
| 24 | +S.next(80) 被调用并返回 1, |
| 25 | +S.next(60) 被调用并返回 1, |
| 26 | +S.next(70) 被调用并返回 2, |
| 27 | +S.next(60) 被调用并返回 1, |
| 28 | +S.next(75) 被调用并返回 4, |
| 29 | +S.next(85) 被调用并返回 6。 |
| 30 | +
|
| 31 | +注意 (例如) S.next(75) 返回 4,因为截至今天的最后 4 个价格 |
| 32 | +(包括今天的价格 75) 小于或等于今天的价格。 |
| 33 | +``` |
| 34 | + |
| 35 | +提示: |
| 36 | +* 调用 `StockSpanner.next(int price)` 时,将有 $1 <= price <= 10^5$。 |
| 37 | +* 每个测试用例最多可以调用 `10000` 次 `StockSpanner.next`。 |
| 38 | +* 在所有测试用例中,最多调用 `150000` 次 `StockSpanner.next`。 |
| 39 | +* 此问题的总时间限制减少了 `50%`。 |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +### 分块 |
| 44 | + |
| 45 | +又名优雅的暴力。 |
| 46 | + |
| 47 | +这是一道在线问题,在调用 `next` 往数据流存入元素的同时,返回连续段不大于当前元素的数的个数。 |
| 48 | + |
| 49 | +一个朴素的想法是:使用数组 `nums` 将所有 `price` 进行存储,每次返回时往前找到第一个不满足要求的位置,并返回连续段的长度。 |
| 50 | + |
| 51 | +但对于 $10^4$ 的调用次数来看,该做法的复杂度为 $O(n^2)$,计算量为 $10^8$,不满足 `OJ` 要求。 |
| 52 | + |
| 53 | +实际上我们可以利用「分块」思路对其进行优化,将与连续段的比较转换为与最值的比较。 |
| 54 | + |
| 55 | +具体的,我们仍然使用 `nums` 对所有的 `price` 进行存储,同时使用 `region` 数组来存储每个连续段的最大值,其中 $region[loc] = x$ 含义为块编号为 `loc` 的最大值为 `x`,其中块编号 `loc` 块对应了数据编号 `idx` 的范围 $[(loc - 1) \times len + 1, loc \times len]$。 |
| 56 | + |
| 57 | +对于 `next` 操作而言,除了直接更新数据数组 `nums[++idx] = price` 以外,我们还需要更新 `idx` 所在块的最值 `region[loc]`,然后从当前块 `loc` 开始往前扫描其余块,使用 `left` 和 `right` 代指当前处理到的块的左右端点,若当前块满足 `region[loc] <= price`,说明块内所有元素均满足要求,直接将当前块 `loc` 所包含元素个数累加到答案中,直到遇到不满足的块或达到块数组边界,若存在遇到不满足要求的块,再使用 `right` 和 `left` 统计块内满足要求 `nums[i] <= price` 的个数。 |
| 58 | + |
| 59 | +对于块个数和大小的设定,是运用分块降低复杂度的关键,数的个数为 $10^4$,我们可以设定块大小为 $\sqrt{n} = 100$,这样也限定了块的个数为 $\sqrt{n} = 100$ 个。这样对于单次操作而言,我们最多遍历进行 $\sqrt{n}$ 次的块间操作,同时最多进行一次块内操作,整体复杂度为 $O(\sqrt{n})$,单次 `next` 操作计算量为 $2 \times 10^2$ 以内,单样例计算量为 $2 \times 10^6$,可以过。 |
| 60 | + |
| 61 | +为了方便,我们令块编号 `loc` 和数据编号 `idx` 均从 $1$ 开始;同时为了防止每个样例都 `new` 大数组,我们采用 `static` 优化,并在 `StockSpanner` 的初始化中做重置工作。 |
| 62 | + |
| 63 | +Java 代码: |
| 64 | +```Java |
| 65 | +class StockSpanner { |
| 66 | + static int N = 10010, len = 100, idx = 0; |
| 67 | + static int[] nums = new int[N], region = new int[N / len + 10]; |
| 68 | + public StockSpanner() { |
| 69 | + for (int i = 0; i <= getIdx(idx); i++) region[i] = 0; |
| 70 | + idx = 0; |
| 71 | + } |
| 72 | + int getIdx(int x) { |
| 73 | + return (x - 1) / len + 1; |
| 74 | + } |
| 75 | + int query(int price) { |
| 76 | + int ans = 0, loc = getIdx(idx), left = (loc - 1) * len + 1, right = idx; |
| 77 | + while (loc >= 1 && region[loc] <= price) { |
| 78 | + ans += right - left + 1; |
| 79 | + loc--; right = left - 1; left = (loc - 1) * len + 1; |
| 80 | + } |
| 81 | + for (int i = right; loc >= 1 && i >= left && nums[i] <= price; i--) ans++; |
| 82 | + return ans; |
| 83 | + } |
| 84 | + public int next(int price) { |
| 85 | + nums[++idx] = price; |
| 86 | + int loc = getIdx(idx); |
| 87 | + region[loc] = Math.max(region[loc], price); |
| 88 | + return query(price); |
| 89 | + } |
| 90 | +} |
| 91 | +``` |
| 92 | +TypeScript 代码: |
| 93 | +```TypeScript |
| 94 | +class StockSpanner { |
| 95 | + N: number = 10010; sz: number = 100; idx: number = 0 |
| 96 | + nums: number[] = new Array<number>(this.N).fill(0); |
| 97 | + region = new Array<number>(Math.floor(this.N / this.sz) + 10).fill(0) |
| 98 | + getIdx(x: number): number { |
| 99 | + return Math.floor((x - 1) / this.sz) + 1 |
| 100 | + } |
| 101 | + query(price: number): number { |
| 102 | + let ans = 0, loc = this.getIdx(this.idx), left = (loc - 1) * this.sz + 1, right = this.idx |
| 103 | + while (loc >= 1 && this.region[loc] <= price) { |
| 104 | + ans += right - left + 1 |
| 105 | + loc--; right = left - 1; left = (loc - 1) * this.sz + 1 |
| 106 | + } |
| 107 | + for (let i = right; loc >= 1 && i >= left && this.nums[i] <= price; i--) ans++ |
| 108 | + return ans |
| 109 | + } |
| 110 | + next(price: number): number { |
| 111 | + this.nums[++this.idx] = price |
| 112 | + const loc = this.getIdx(this.idx) |
| 113 | + this.region[loc] = Math.max(this.region[loc], price) |
| 114 | + return this.query(price) |
| 115 | + } |
| 116 | +} |
| 117 | +``` |
| 118 | +Python3 代码: |
| 119 | +```Python |
| 120 | +class StockSpanner: |
| 121 | + def __init__(self): |
| 122 | + self.N, self.sz, self.idx = 10010, 110, 0 |
| 123 | + self.nums, self.region = [0] * self.N, [0] * (self.N // self.sz + 10) |
| 124 | + |
| 125 | + def next(self, price: int) -> int: |
| 126 | + def getIdx(x): |
| 127 | + return (x - 1) // self.sz + 1 |
| 128 | + |
| 129 | + def query(price): |
| 130 | + ans, loc = 0, getIdx(self.idx) |
| 131 | + left, right = (loc - 1) * self.sz + 1, self.idx |
| 132 | + while loc >= 1 and self.region[loc] <= price: |
| 133 | + ans += right - left + 1 |
| 134 | + loc -= 1 |
| 135 | + right, left = left - 1, (loc - 1) * self.sz + 1 |
| 136 | + while loc >= 1 and right >= left and self.nums[right] <= price: |
| 137 | + right, ans = right - 1, ans + 1 |
| 138 | + return ans |
| 139 | + |
| 140 | + self.idx += 1 |
| 141 | + loc = getIdx(self.idx) |
| 142 | + self.nums[self.idx] = price |
| 143 | + self.region[loc] = max(self.region[loc], price) |
| 144 | + return query(price) |
| 145 | +``` |
| 146 | +* 时间复杂度:由于使用了 `static` 优化,`StockSpanner` 初始化时,需要对上一次使用的块进行重置,复杂度为 $O(\sqrt{n})$;由于块大小和数量均为 $\sqrt{n}$,`next` 操作复杂度为 $O(\sqrt{n})$ |
| 147 | +* 空间复杂度:$O(n)$ |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +### 单调栈 |
| 152 | + |
| 153 | +另外一个容易想到的想法是使用「单调栈」,栈内以二元组 $(idx, price)$ 形式维护比当前元素 `price` 大的元素。 |
| 154 | + |
| 155 | +每次执行 `next` 操作时,从栈顶开始处理,将所有满足「不大于 `price`」的元素进行出栈,从而找到当前元素 `price` 左边最近一个比其大的位置。 |
| 156 | + |
| 157 | +Java 代码: |
| 158 | +```Java |
| 159 | +class StockSpanner { |
| 160 | + Deque<int[]> d = new ArrayDeque<>(); |
| 161 | + int cur = 0; |
| 162 | + public int next(int price) { |
| 163 | + while (!d.isEmpty() && d.peekLast()[1] <= price) d.pollLast(); |
| 164 | + int prev = d.isEmpty() ? -1 : d.peekLast()[0], ans = cur - prev; |
| 165 | + d.addLast(new int[]{cur++, price}); |
| 166 | + return ans; |
| 167 | + } |
| 168 | +} |
| 169 | +``` |
| 170 | +TypeScript 代码: |
| 171 | +```TypeScript |
| 172 | +class StockSpanner { |
| 173 | + stk = new Array<Array<number>>(10010).fill([0, 0]) |
| 174 | + he = 0; ta = 0; cur = 0 |
| 175 | + next(price: number): number { |
| 176 | + while (this.he < this.ta && this.stk[this.ta - 1][1] <= price) this.ta-- |
| 177 | + const prev = this.he >= this.ta ? -1 : this.stk[this.ta - 1][0], ans = this.cur - prev |
| 178 | + this.stk[this.ta++] = [this.cur++, price] |
| 179 | + return ans |
| 180 | + } |
| 181 | +} |
| 182 | +``` |
| 183 | +Python3 代码: |
| 184 | +```Python |
| 185 | +class StockSpanner: |
| 186 | + def __init__(self): |
| 187 | + self.stk = [] |
| 188 | + self.cur = 0 |
| 189 | + |
| 190 | + def next(self, price: int) -> int: |
| 191 | + while self.stk and self.stk[-1][1] <= price: |
| 192 | + self.stk.pop() |
| 193 | + prev = -1 if not self.stk else self.stk[-1][0] |
| 194 | + ans = self.cur - prev |
| 195 | + self.stk.append([self.cur, price]) |
| 196 | + self.cur += 1 |
| 197 | + return ans |
| 198 | +``` |
| 199 | +* 时间复杂度:`next` 操作的均摊复杂度为 $O(1)$ |
| 200 | +* 空间复杂度:$O(n)$ |
| 201 | + |
| 202 | +--- |
| 203 | + |
| 204 | +### 最后 |
| 205 | + |
| 206 | +这是我们「刷穿 LeetCode」系列文章的第 `No.901` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 207 | + |
| 208 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 209 | + |
| 210 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 211 | + |
| 212 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 213 | + |
0 commit comments