适合野炊的日子 图解题解
这道题到底在问什么
- 输入
- security=[5,3,3,3,5,6,2], time=2
- 输出
- [2,3]
- 输入
- security=[1,1,1,1,1], time=0
- 输出
- [0,1,2,3,4]
先想最直接的笨办法
最直接的笨办法是把每天都当候选,往左数 time 天看是否一路非递增、往右数 time 天看是否一路非递减,可这样相邻两天要比的那批趋势几乎重叠,同一对相邻关系被反复看。与其反复数,不如一次性预处理成两个数:左边非递增攒成 left,右边非递减攒成 right,谁短算谁,够 time 就是好日子。下面先做第一遍,把 left 一格一格算出来。(动画第 3 步)
最优解:为什么这么做
一句话答案:LeetCode 2100 适合野炊的日子:第 i 天前 time 天非递增、后 time 天非递减才算好日子。前后缀递推攒出 left、right 两张表,免去逐天重验,min 够 time 即收,时间 O(n)、空间 O(n)。
什么样的一天才算适合野炊的好日子
security[i] 是第 i 天的出行指数,再给整数 time。第 i 天适合野炊,要求前面连续 time 天非递增(一路不涨,持平也行)、后面连续 time 天非递减(一路不跌),像个谷底。题面例子 security=[5,3,3,3,5,6,2]、time=2,输出 [2,3]。
每天往两边各数 time 天,慢在哪里
候选天往左要比 time 对邻居、往右再比 time 对,n 天合计 O(n×time) 次比较(大 O 描述数据变大时操作次数怎么涨)。慢在重复:第 2 天验左肩比过那对 3 和 3,第 3 天又把同一对再比一遍——相邻两天的窗口几乎整段重叠。既然反复被要,先算一次存下来。
left 和 right 两把尺子各量什么
这就是动态规划(把『这天往左连跌了几天』这类小答案填进表,要用直接查)。left[i] 记第 i 天往左的连续非递增天数,right[i] 记往右的连续非递减天数;left 从左往右攒,right 从右往左攒,一前一后合称前后缀表。判定缩成:两边取短的也得够 time,time ≤ min(left[i], right[i])。
只和邻居比一次,整段趋势凭什么有保证
填表靠递推(用前一格推这一格)。left 每天只和昨天比:security[i] ≤ security[i-1],跌势没断,left[i]=left[i-1]+1;涨了就断,left[i] 归 0。整段的保证在接力里——left[i-1] 已担保前面每对都不涨,今天只补验最新一对。right 反向同理,和后一天比。不等号带等于:持平不断段。
[5,3,3,3,5,6,2]上left、right各长什么样
先填 left:第 0 天左边没人,left[0]=0;3 ≤ 5,left[1]=1;3 ≤ 3,left[2]=2;3 ≤ 3,left[3]=3;5 > 3 断,left[4]=0;6 > 5 断,left[5]=0;2 ≤ 6,left[6]=1。调头填 right:right[6]=0;6 > 2,right[5]=0;5 ≤ 6,right[4]=1;3 ≤ 5,right[3]=2;3 ≤ 3,right[2]=3;3 ≤ 3,right[1]=4;5 > 3,right[0]=0。
逐天取小:第 2 天 min(2,3)=2、第 3 天 min(3,2)=2,都够 2,收;第 4 天 min(0,1)=0,不够;其余日子 left 或 right 为 0。答案正是题面给的 [2,3]。
把 ≤ 写成 <,第 2、3 天为什么双双消失
把 ≤ 抄成 <,两对 3 和 3 立刻成了断点,left[2]、left[3] 归 0,第 2、3 天双双出局,答案变成空数组。time=0 时 0 ≤ min 恒成立,所有下标都收,题面例子 [1,1,1,1,1] 输出 [0,1,2,3,4]。n ≤ 2×time 时哪天都凑不齐左右两肩,代码开头直接返回空。三趟线性扫描,时间 O(n);两张长度 n 的表,空间 O(n)。
▶ 动画逐步走查(共 23 步)——想跟着动画一帧帧对照就展开
- 3最直接的笨办法是把每天都当候选,往左数 time 天看是否一路非递增、往右数 time 天看是否一路非递减,可这样相邻两天要比的那批趋势几乎重叠,同一对相邻关系被反复看。与其反复数,不如一次性预处理成两个数:左边非递增攒成 left,右边非递减攒成 right,谁短算谁,够 time 就是好日子。下面先做第一遍,把 left 一格一格算出来。
- 4这是 7 天的出行指数,下标 0 到 6。要找的好日子像谷底:左肩一路往下走(非递增)、右肩一路往上走(非递减),而且两边各要够 2 天。接下来分两遍处理,先量左肩,再量右肩。
- 5第一遍从左往右量左肩。第 0 天左边没有别的天,非递增段长度是 0,所以 left[0]=0。从第 1 天开始,每天只看它和前一天:今天的指数只要不高于昨天,非递增就延续。
- 6看第 1 天,指数 3,和前一天 5 比,3 不高于 5,非递增接着往下,left[1]=left[0]+1=1。绿色这一段就是到第 1 天为止的连续非递增段,共 1 步。
- 7看第 2 天,指数 3,和前一天 3 比,3 不高于 3,非递增接着往下,left[2]=left[1]+1=2。绿色这一段就是到第 2 天为止的连续非递增段,共 2 步。
- 8看第 3 天,指数 3,和前一天 3 比,3 不高于 3,非递增接着往下,left[3]=left[2]+1=3。绿色这一段就是到第 3 天为止的连续非递增段,共 3 步。
- 9看第 4 天,指数 5,和前一天 3 比,5 反而更大,非递增在这里断了。left[4] 归 0,第 4 天自己重新起头,标红提示这里是个断点。
- 10看第 5 天,指数 6,和前一天 5 比,6 反而更大,非递增在这里断了。left[5] 归 0,第 5 天自己重新起头,标红提示这里是个断点。
- 11看第 6 天,指数 2,和前一天 6 比,2 不高于 6,非递增接着往下,left[6]=left[5]+1=1。绿色这一段就是到第 6 天为止的连续非递增段,共 1 步。
- 12第一遍走完,left=[0,1,2,3,0,0,1]。这排数字的意思是:第 3 天往左能连着 3 天非递增(5≥3≥3≥3),所以 left[3]=3;第 4 天指数反弹到 5,left[4] 归 0。left 越大,说明左肩下坡越长。左肩量好了,换右肩。
- 13第二遍从右往左量右肩,方向反过来。最后一天第 6 天右边没有别的天,right[6]=0。往左每天只看它和后一天:今天不高于后一天,非递减就延续。
- 14看第 5 天,指数 6,和后一天 2 比,6 反而更大,往右非递减断了。right[5] 归 0,标红提示这是右肩的断点。
- 15看第 4 天,指数 5,和后一天 6 比,5 不高于 6,往右非递减接着上,right[4]=right[5]+1=1。绿色这一段是从第 4 天起的连续非递减段,共 1 步。
- 16看第 3 天,指数 3,和后一天 5 比,3 不高于 5,往右非递减接着上,right[3]=right[4]+1=2。绿色这一段是从第 3 天起的连续非递减段,共 2 步。
- 17看第 2 天,指数 3,和后一天 3 比,3 不高于 3,往右非递减接着上,right[2]=right[3]+1=3。绿色这一段是从第 2 天起的连续非递减段,共 3 步。
- 18看第 1 天,指数 3,和后一天 3 比,3 不高于 3,往右非递减接着上,right[1]=right[2]+1=4。绿色这一段是从第 1 天起的连续非递减段,共 4 步。
- 19看第 0 天,指数 5,和后一天 3 比,5 反而更大,往右非递减断了。right[0] 归 0,标红提示这是右肩的断点。
- 20第二遍走完,right=[0,4,3,2,1,0,0]。比如第 1 天往右能连着 4 步非递减(3≤3≤3≤5≤6),right[1]=4;第 5 天指数 6 之后掉到 2,right[5] 归 0。两把尺子都备齐了,接下来逐天裁决。
- 21开始裁决。一天要合格,左肩和右肩都得够 2 天,也就是 min(left[i], right[i]) 至少是 2。灰掉的第 0、1、5、6 天靠边站:它们离数组两端太近,一边根本凑不够 2 天。真正要逐个看的是中间的第 2、3、4 天。
- 22第 2 天,左肩 left=2、右肩 right=3,取小的是 2,达到了 time=2。这一天左边有 2 天非递增、右边有 2 天非递减,正是一个谷底。底色框出它前后各 2 天的范围,把第 2 天收进答案。
- 23第 3 天,左肩 left=3、右肩 right=2,取小的是 2,达到了 time=2。这一天左边有 2 天非递增、右边有 2 天非递减,正是一个谷底。底色框出它前后各 2 天的范围,把第 3 天收进答案。
- 24第 4 天,左肩 left=0、右肩 right=1,取小的是 0,还不到 time=2。它左边非递增不够 2 天,谷底不成立,标红跳过。
- 25全部裁决完。绿色的第 2 天和第 3 天,left 和 right 都够 2,是货真价实的谷底;其余日子灰掉。答案就是 [2, 3],跟开头说的对上了。判定始终只看一条:min(left[i], right[i]) 是不是够 time。
⚠️ 容易写错的地方
✗ 错:把左右两边的方向记反
✓ 对:左边要非递增(往下),右边要非递减(往上)
好日子是谷底,左肩下坡、右肩上坡;方向反了会把山峰当谷底,全错
✗ 错:用严格大于或严格小于判断
✓ 对:两边都用 ≤ 这类允许相等的比较
题面用的是 ≥ 和 ≤,相等的两天(如 3,3,3)仍算非递增或非递减,用严格号会把持平错判成断开
✗ 错:忘了两端凑不够 time 天的日子
✓ 对:min(left[i], right[i]) ≥ time 天然排除两端
left[i] 够 time 就保证左边有 time 天,right[i] 够 time 保证右边有 time 天,取 min 一并卡住,不必另写边界判断
✗ 错:time=0 时漏掉全部日子
✓ 对:time=0 时 min ≥ 0 恒成立,每天都合格
此时不要求任何非递增非递减,应返回所有下标,别被空数组误导
完整代码(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 goodDaysToRobBank(self, security: List[int], time: int) -> List[int]:
n = len(security)
if n <= time * 2:
return []
left, right = [0] * n, [0] * n
for i in range(1, n):
if security[i] <= security[i - 1]:
left[i] = left[i - 1] + 1
for i in range(n - 2, -1, -1):
if security[i] <= security[i + 1]:
right[i] = right[i + 1] + 1
return [i for i in range(n) if time <= min(left[i], right[i])]C++
#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:
vector<int> goodDaysToRobBank(vector<int>& security, int time) {
int n = security.size();
if (n <= time * 2) return {};
vector<int> left(n);
vector<int> right(n);
for (int i = 1; i < n; ++i)
if (security[i] <= security[i - 1])
left[i] = left[i - 1] + 1;
for (int i = n - 2; i >= 0; --i)
if (security[i] <= security[i + 1])
right[i] = right[i + 1] + 1;
vector<int> ans;
for (int i = time; i < n - time; ++i)
if (time <= min(left[i], right[i]))
ans.push_back(i);
return ans;
}
};Java
import java.util.*;
class Solution {
public List<Integer> goodDaysToRobBank(int[] security, int time) {
int n = security.length;
if (n <= time * 2) {
return Collections.emptyList();
}
int[] left = new int[n];
int[] right = new int[n];
for (int i = 1; i < n; ++i) {
if (security[i] <= security[i - 1]) {
left[i] = left[i - 1] + 1;
}
}
for (int i = n - 2; i >= 0; --i) {
if (security[i] <= security[i + 1]) {
right[i] = right[i + 1] + 1;
}
}
List<Integer> ans = new ArrayList<>();
for (int i = time; i < n - time; ++i) {
if (time <= Math.min(left[i], right[i])) {
ans.add(i);
}
}
return ans;
}
}复杂度
时间
O(n)
n 是天数。第一遍扫一遍算 left,第二遍扫一遍算 right,收集答案再扫一遍,三趟都是线性,每格常数操作,合起来仍是 O(n)
空间
O(n)
按峰值算。额外开了 left 和 right 两个长度 n 的数组,峰值占用随 n 线性增长。若允许把结果数组算进输出、只看辅助结构,主要就是这两个前缀数组
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 适合野炊的日子 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
Python 参考代码把所有下标都扫一遍,为什么不会把太靠边的日子误收?+
left[i] 最多是 i(第 i 天前面只有 i 天),所以 left[i] ≥ time 本身就保证 i ≥ time;同理 right[i] ≥ time 保证 i ≤ n-1-time。太靠边的天两张表的值天然不够,min 判定顺手把两端过滤掉了。C++ 和 Java 版把循环限制在 time 到 n-time 之间只是省扫两头,结果完全一样。
能不能一遍循环同时把 left 和 right 都算出来?+
不能。right[i] 依赖 right[i+1],得从结尾往回攒;从左往右扫到第 i 天时,它右边还没看过,右肩的信息拿不到。所以必须一遍正着算 left、一遍倒着算 right,再扫一遍收答案。三趟都是线性,合起来仍是 O(n),不亏。
time=0 时为什么每一天都算好日子?+
条件退化成「前 0 天非递增、后 0 天非递减」,等于什么都不用检查;而 left[i] 和 right[i] 都不小于 0,min(left[i], right[i]) ≥ 0 恒成立,每个下标都过关。题面第二个例子 [1,1,1,1,1]、time=0 输出 [0,1,2,3,4] 就是这种情况。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 适合野炊的日子 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。