股票平滑下跌阶段的数目 图解题解
这道题到底在问什么
- 输入
- prices = [3,2,1,4]
- 输出
- 7
- 输入
- prices = [8,6,7,7]
- 输出
- 4
- 输入
- prices = [1]
- 输出
- 1
最优解:为什么这么做
一句话答案:LeetCode 2110 股票平滑下跌阶段的数目用一维 DP:dp[i] 记以第 i 天结尾的下降段数,今天恰比昨天少 1 则 dp[i-1]+1、否则归 1,全部相加即答案——每段有唯一结尾天不会重数;时间 O(n)、空间 O(1)。
恰好少 1 的阶段,到底数的是什么
prices[i] 是第 i 天股价,平滑下降阶段指连续一段天数、每天都比前一天恰好少 1,单独一天也算。例子 [3,2,1,4]:单天的 4 个,连降的 [3,2]、[2,1]、[3,2,1] 共 3 个,共 7。『恰好少 1』卡得死:[8,6,7,7] 差 2、涨、持平都不算,只剩 4 个单天阶段。
十万天约五十亿个连续段,试得完吗
长度 n 的数组有 n(n+1)/2 个连续段,n 顶到十万约五十亿段;每段再逐天验差,总量到 O(n³)(大 O:数据翻倍时工作量跟着涨几倍的记法),跑不进时限。浪费在哪?验 [3,2,1] 时,[3,2] 和 [2,1] 早各验过,长段平滑与否全由短段定。
dp[i] 为什么定成以第 i 天结尾
把短段答案存住复用,就是动态规划(DP,这题存的是『以某天结尾的下降段有几个』,后一天在上面加)。状态(dp 每格记的事):dp[i] = 以第 i 天结尾的平滑下降阶段个数。每个阶段有唯一结尾天,按结尾天分堆相加,谁也不会被数两次,答案就是所有 dp 之和。
转移(由前一格推出这一格的规则)看相邻差:prices[i-1] - prices[i] 恰好等于 1 时,昨天每个阶段拖到今天照样恰差 1,再加今天单独成段,dp[i] = dp[i-1] + 1;差不是 1——涨、持平、跌超 1 都一样——旧段全接不上,dp[i] = 1。第 0 天前面没人,dp[0] = 1。
参考代码没有 dp 数组,cnt 在数什么
参考代码按段结账:外层 i 站在段开头,内层 j 从 i+1 往右,prices[j-1] - prices[j] == 1 就继续,停下时得到最长连降段,长度 cnt = j - i。段内逐天 dp 依次是 1、2、…、cnt,求和正是等差数列 (1+cnt)*cnt//2,记进 ans,i 跳到 j 找下一段。
[3,2,1,4] 上 dp 一天天加到 7
第 0 天股价 3,前面没人,dp[0] = 1,累计 1。第 1 天股价 2,3 - 2 = 1 接得上,dp[1] = dp[0] + 1 = 2,累计 3。第 2 天股价 1,2 - 1 = 1 又接上,dp[2] = 3,累计 6。第 3 天股价 4,1 - 4 = -3,断了,dp[3] = 1,累计 7,题面也是 7。
再走 [8,6,7,7]:相邻差依次 2、-1、0,没有一处恰好是 1,四天各自 dp = 1,加起来正是 4。
答案存进 int,十万天为什么会炸
i 和 j 只往右走,每天被访问一次,时间 O(n);额外空间只几个下标和累加变量,O(1)。出事在数值范围:十万天一路连降 1 只有一段,贡献 (1+100000)*100000//2 = 5000050000,超出 32 位整型上限 2147483647,C++ 用 long long、Java 用 long,乘法要在 64 位下做完再除以 2。
断段时 dp 要归 1 而不是 0,归 0 会漏掉单天成段的那 1 个,[1] 就该返回 1。全数组没一处相邻恰差 1,答案就是天数。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3记牢这一句:dp[i] 是以第 i 天为结尾的下降阶段个数。今天比昨天恰好少 1,就 dp[i] = dp[i-1] + 1;否则 dp[i] = 1。答案就是所有 dp 之和。下面从第 0 天开始,一天一天往右扫。
- 4这是我们要处理的 9 天股价,从左到右下标 0 到 8。右边这排面板要记每一天的 dp 值,也就是以那天结尾的下降阶段个数,现在都还是问号。我们的活儿就是从左往右扫一遍,一天一天把 dp 填出来,边填边把它们累加进答案。
- 5先看第 0 天,股价 5。它前面没有更早的一天可以接,所以它只能自己单独成一个下降阶段。这一步不用比较,直接定下来。
- 6第 0 天定成 dp[0] = 1,把它记到右边面板。累计答案也从 0 加到 1。这一个 1 代表阶段 [5]。开局第一个战果拿下,继续往右走。
- 7来到第 1 天,股价 4。看它和前一天:第 0 天是 5,5 减 4 正好等于 1,接得上!前一天那条绿色的下降段可以整段延长到今天。
- 8于是 dp[1] = dp[0] + 1 = 2。这条绿色下降段现在长到 2 天。把 2 记进面板,累计答案从 1 加到 3。
- 9来到第 2 天,股价 3。看它和前一天:第 1 天是 4,4 减 3 正好等于 1,接得上!前一天那条绿色的下降段可以整段延长到今天。
- 10于是 dp[2] = dp[1] + 1 = 3。这条绿色下降段现在长到 3 天。把 3 记进面板,累计答案从 3 加到 6。
- 11来到第 3 天,股价 6。看它和前一天:第 2 天是 3,今天反而涨了,差是 -3,不是恰好少 1。接不上!前面那条下降段到此为止,今天只能自己重新起一段。
- 12于是 dp[3] = 1,今天自己单独成一段,面板记上 1。累计答案从 6 加到 7。别小看这个 1,漏了它答案就少。
- 13来到第 4 天,股价 6。看它和前一天:第 3 天是 6,两天持平,差是 0,不是恰好少 1。接不上!前面那条下降段到此为止,今天只能自己重新起一段。
- 14于是 dp[4] = 1,今天自己单独成一段,面板记上 1。累计答案从 7 加到 8。别小看这个 1,漏了它答案就少。
- 15来到第 5 天,股价 9。看它和前一天:第 4 天是 6,今天反而涨了,差是 -3,不是恰好少 1。接不上!前面那条下降段到此为止,今天只能自己重新起一段。
- 16于是 dp[5] = 1,今天自己单独成一段,面板记上 1。累计答案从 8 加到 9。别小看这个 1,漏了它答案就少。
- 17来到第 6 天,股价 8。看它和前一天:第 5 天是 9,9 减 8 正好等于 1,接得上!前一天那条绿色的下降段可以整段延长到今天。
- 18于是 dp[6] = dp[5] + 1 = 2。这条绿色下降段现在长到 2 天。把 2 记进面板,累计答案从 9 加到 11。
- 19来到第 7 天,股价 7。看它和前一天:第 6 天是 8,8 减 7 正好等于 1,接得上!前一天那条绿色的下降段可以整段延长到今天。
- 20于是 dp[7] = dp[6] + 1 = 3。这条绿色下降段现在长到 3 天。把 3 记进面板,累计答案从 11 加到 14。
- 21来到第 8 天,股价 2。看它和前一天:第 7 天是 7,今天跌得太多,差是 5,不是恰好少 1。接不上!前面那条下降段到此为止,今天只能自己重新起一段。
- 22于是 dp[8] = 1,今天自己单独成一段,面板记上 1。累计答案从 14 加到 15。别小看这个 1,漏了它答案就少。
- 23九天全部扫完,dp 依次是 1 2 3 1 1 1 2 3 1。把它们从左到右加起来:1 加 2 加 3 加 1 加 1 加 1 加 2 加 3 加 1,正好是 15。这就是平滑下降阶段的总数。每一天的 dp,数的就是以那天结尾的阶段个数,全加起来自然不重不漏。
- 24参考代码没有逐天记 dp,而是把股价切成一段段极大下降段:[5,4,3] 是一段长 3,[6] 长 1,[6] 长 1,[9,8,7] 长 3,[2] 长 1。一段长 cnt 的下降段,内部能选出的连续子段个数是 cnt 乘以 cnt 加 1 的整体,再除以 2。于是 6 加 1 加 1 加 6 加 1 = 15,和逐天 dp 求和完全一致。两种写法思路相同,复杂度都是线性。
⚠️ 容易写错的地方
✗ 错:用 int 累加答案
✓ 对:C++ 用 long long、Java 用 long,乘法处提升到 64 位
天数可达十万,最坏是一整段长下降,子段个数约为 n 乘以 n 加 1 的整体,再除以 2,远超 32 位整型上限,会溢出成负数或错值
✗ 错:把持平也当成下降,判 prices[i-1] ≥ prices[i]
✓ 对:必须是恰好少 1,即差严格等于 1
两天股价相等时差是 0、跌 2 时差是 2,都不算平滑下降;题目要的是每天比前一天恰好少 1
✗ 错:漏算单独一天的阶段
✓ 对:每天自己都算一个阶段,dp 至少是 1
哪怕它接不上前一天,自己单独一天也是一个合法的平滑下降阶段,dp 归 1 而不是归 0
✗ 错:用 O(n 方) 枚举所有子数组再逐段验证
✓ 对:一次线性扫描即可
每段长 cnt 的贡献有闭式 cnt 乘以 cnt 加 1 的整体,再除以 2,或用 dp 递推一遍带过,没必要双重循环枚举
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class Solution:
def getDescentPeriods(self, prices: List[int]) -> int:
ans = 0
i, n = 0, len(prices)
while i < n:
j = i + 1
while j < n and prices[j - 1] - prices[j] == 1:
j += 1
cnt = j - i
ans += (1 + cnt) * cnt // 2
i = j
return ansC++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
class Solution {
public:
long long getDescentPeriods(vector<int>& prices) {
long long ans = 0;
int n = prices.size();
for (int i = 0, j = 0; i < n; i = j) {
j = i + 1;
while (j < n && prices[j - 1] - prices[j] == 1) {
++j;
}
int cnt = j - i;
ans += (1LL + cnt) * cnt / 2;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public long getDescentPeriods(int[] prices) {
long ans = 0;
int n = prices.length;
for (int i = 0, j = 0; i < n; i = j) {
j = i + 1;
while (j < n && prices[j - 1] - prices[j] == 1) {
++j;
}
int cnt = j - i;
ans += (1L + cnt) * cnt / 2;
}
return ans;
}
}复杂度
时间
O(n)
n 是天数。无论逐天记 dp 还是分组求和,每天只被看常数次:分组法里内层 j 一路只往前走、绝不回头,i 直接跳到 j,合起来每天恰好被访问一次,整体随天数线性增长
空间
O(1)
分组法只用了几个下标和一个累加变量,不额外开数组;逐天 dp 也只需记住前一天的 dp,一个变量滚动即可。都是常数额外空间。动画里的 dp 面板只是为了讲解,真实代码不必存下整排 dp
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 股票平滑下跌阶段的数目 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
今天恰好比昨天少 1 时,为什么 dp[i] 恰好等于 dp[i-1] + 1?+
以昨天结尾的每个平滑下降阶段,末尾都停在昨天的股价上,今天恰好再低 1,整段接上今天后仍然天天恰差 1,所以这 dp[i-1] 个阶段全部原样延长;再加上今天单独成段的 1 个,合计 dp[i-1] + 1。反过来,任何以今天结尾、长度超过 1 的阶段,去掉今天就是一个以昨天结尾的阶段,两边一一对应,既不会多数也不会漏数。
参考代码的 (1+cnt)*cnt//2 和逐天 dp 求和是什么关系?+
同一笔账的两种记法。一段长 cnt 的最长连续下降段里,逐天的 dp 依次是 1、2、…、cnt——第 k 天能往左接 k-1 天。把它们加起来正好是等差数列求和 (1+cnt)*cnt//2。参考代码按段直接套这个式子,省掉逐天累加,也就不需要 dp 数组,几个下标变量加一个累加器就够了。
为什么 C++ 和 Java 的答案要用 64 位整数?+
prices 最长十万天。每天都恰好降 1 时全数组是一段,贡献 (1+100000)*100000//2 = 5000050000,约 50 亿,超过 32 位整型 2147483647 的上限。所以 C++ 用 long long、Java 用 long,且乘法要在 64 位下算完再除以 2,先用 int 乘完再赋给 long 已经溢出了。Python 的整数没有位数限制,不需要额外处理。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 股票平滑下跌阶段的数目 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。