每一个查询的最大美丽值 图解题解
这道题到底在问什么
- 输入
- items=[[1,2],[3,2],[2,4],[5,6],[3,5]], queries=[1,2,3,4,5,6]
- 输出
- [2,4,5,5,6,6]
- 输入
- items=[[10,1000]], queries=[5]
- 输出
- [0] (没有价格 ≤ 5 的商品)
最优解:为什么这么做
一句话答案:LeetCode 2070 每个查询的最大美丽值把商品和查询都按价格排序,离线用单调指针推进、随手维护价格达标里的最大美丽值,一趟扫完所有查询。时间 O(n log n + m log m)、空间 O(m)。
商品带价格和美丽值,每个预算到底求什么
items 每件商品是一对数:价格和美丽值;queries 每个数是一次查询的预算。对每个预算,在价格不超过它的商品里挑出最大美丽值,凑不出记 0,返回和 queries 等长的答案数组。题面 items=[[1,2],[3,2],[2,4],[5,6],[3,5]]、queries=[1,2,3,4,5,6],答案是 [2,4,5,5,6,6]。
一个查询扫一趟商品,慢在哪一步
笨办法是一个查询扫一趟商品:拿这预算把 n 件商品逐个比过去,价格达标的就用它的美丽值刷新当前最大。可 m 个查询各扫一趟,总比较数 O(n×m);商品和查询都到十万量级,乘起来上百亿次,超时。浪费在重复:预算变大,上轮达标的商品又得从头确认。
预算从小到大处理,指针为什么能不回头
让预算从小到大处理,重复确认就能省掉——预算只增不减,上一轮够格的商品这轮必然还够格,指针停在哪儿,下轮从哪儿往前,不退回。为此先把商品按价格升序排好,查询也按预算升序处理(这步叫离线:先把所有查询攒齐、重排后统一处理,不来一个答一个)。查询一重排,它在答案数组里的原位置就乱了,排序时要带上每个查询的原始下标,好把答案填回本来的格子。
指针只管前进,mx 和答案各自怎么落位
维护两个量:指针 i 指下一件待考察的商品,mx 记当前够格商品的最大美丽值,跨查询都不重置。轮到预算 q,让 i 往前走,只要 items[i] 价格 ≤ q 就纳入、用它的美丽值更新 mx、i 进一格;一碰到价格超 q 的商品就停,它右边只会更贵、买不起。此刻的 mx 就是这预算的答案,按记下的原始下标填回答案数组。下一个更大的预算接着用现在的 i 和 mx,不重来。
题面这五件商品,六个预算逐个走一遍
排序后商品价格排成 1、2、3、3、5,对应美丽值 2、4、2、5、6(价格 3 两件,美丽值 2 和 5)。查询本就升序。预算 1:头一件价格 1≤1,mx=2,i 走到 1;价格 2>1 停,ans[0]=2。预算 2:价格 2≤2,mx=4,i 走到 2;价格 3>2 停,ans[1]=4。预算 3:价格 3≤3 但美丽值 2 不及 4,mx 仍是 4,i 走到 3;又一件价格 3≤3 美丽值 5,mx=5,i 走到 4;价格 5>3 停,ans[2]=5。预算 4:价格 5>4 停,ans[3]=5。预算 5:价格 5≤5,mx=6,i 走到 5 到头,ans[4]=6。预算 6:i 已到头,ans[5]=6。答案 [2,4,5,5,6,6]。
排序吞掉扫描后,哪几处一松手就填错
两次排序主导:商品排序 O(n log n),查询带下标排序 O(m log m);之后指针 i 和查询各走一趟,共 O(n+m),被吞掉。额外空间是下标数组 O(m)。几处一松手就出错:排序打乱了查询次序,按扫描序直接输出会填错格子,得写回原始下标;指针 i 跨查询只进不退,每题从头扫会退回 O(n×m);判定用价格 ≤ 预算,严格小于会漏掉价格正好等于预算的商品;查不到商品时答案是 0,mx 从 0 起就满足,别返回 -1。
▶ 动画逐步走查(共 27 步)——想跟着动画一帧帧对照就展开
- 3记牢这套路:商品按价格排序,查询由小到大处理,指针 i 单调前进,mx 单调不减,答案随手就是当下的 mx。下面把这套动作一帧一帧演给你看。
- 4先看原始输入。舞台这排格子写的是每件商品的美丽值,侧栏配上它们各自的价格。你会发现价格是乱的:1, 3, 2, 5, 3。价格乱着排,双指针没法一路只往前走,所以第一步必须先按价格把商品排好队。
- 5排序之后,商品的价格从小到大变成 1, 2, 3, 3, 5,格子里对应的美丽值是 2, 4, 2, 5, 6。注意价格 3 出现了两次,它们的美丽值分别是 2 和 5。侧栏也换成排好序的样子。从现在起,指针 i 从左往右扫,一路只会遇到价格越来越大的商品,这正是双指针能成立的前提。
- 6开始处理查询序列 [1, 2, 3, 4, 5, 6]。本例查询本身就是升序,排好序后顺序不变,答案可以直接按顺序填。第一个预算是 1。接下来看还能不能纳入更多价格达标的商品。
- 7看紫色这件商品 0,价格 1,没有超过预算 1,把它纳入候选。它的美丽值是 2,拿去和当前 mx = 0 取较大值。结果刷新成 2,更漂亮的商品出现了。指针 i 前进到 1。
- 8再看下一件商品 1,价格 2,已经大于预算 1了。它以及它右边的商品价格只会更贵,都买不起,循环就此停下。指针 i 就停在这里,留给下一个更大的预算接着用,不用回退。
- 9预算 1 的答案定了,就是此刻的 mx = 2,填进 answer 的第 0 位。注意下一个查询的预算更大,指针 i 和 mx 都从现在的状态接着走,前面纳入的商品一件都不用重看,这正是排序换来的好处。
- 10轮到预算 2。指针 i 停在上一个查询留下的位置 1,绿色的商品是之前已经纳入的,mx 现在是 2。接下来看还能不能纳入更多价格达标的商品。
- 11看紫色这件商品 1,价格 2,没有超过预算 2,把它纳入候选。它的美丽值是 4,拿去和当前 mx = 2 取较大值。结果刷新成 4,更漂亮的商品出现了。指针 i 前进到 2。
- 12再看下一件商品 2,价格 3,已经大于预算 2了。它以及它右边的商品价格只会更贵,都买不起,循环就此停下。指针 i 就停在这里,留给下一个更大的预算接着用,不用回退。
- 13预算 2 的答案定了,就是此刻的 mx = 4,填进 answer 的第 1 位。注意下一个查询的预算更大,指针 i 和 mx 都从现在的状态接着走,前面纳入的商品一件都不用重看,这正是排序换来的好处。
- 14轮到预算 3。指针 i 停在上一个查询留下的位置 2,绿色的商品是之前已经纳入的,mx 现在是 4。接下来看还能不能纳入更多价格达标的商品。
- 15看紫色这件商品 2,价格 3,没有超过预算 3,把它纳入候选。它的美丽值是 2,拿去和当前 mx = 4 取较大值。美丽值 2 没有 4 大,mx 保持 4 不变,但这件商品照样算已纳入,指针继续前移。指针 i 前进到 3。
- 16看紫色这件商品 3,价格 3,没有超过预算 3,把它纳入候选。它的美丽值是 5,拿去和当前 mx = 4 取较大值。结果刷新成 5,更漂亮的商品出现了。指针 i 前进到 4。
- 17再看下一件商品 4,价格 5,已经大于预算 3了。它以及它右边的商品价格只会更贵,都买不起,循环就此停下。指针 i 就停在这里,留给下一个更大的预算接着用,不用回退。
- 18预算 3 的答案定了,就是此刻的 mx = 5,填进 answer 的第 2 位。注意下一个查询的预算更大,指针 i 和 mx 都从现在的状态接着走,前面纳入的商品一件都不用重看,这正是排序换来的好处。
- 19轮到预算 4。指针 i 停在上一个查询留下的位置 4,绿色的商品是之前已经纳入的,mx 现在是 5。接下来看还能不能纳入更多价格达标的商品。
- 20再看下一件商品 4,价格 5,已经大于预算 4了。它以及它右边的商品价格只会更贵,都买不起,循环就此停下。指针 i 就停在这里,留给下一个更大的预算接着用,不用回退。
- 21预算 4 的答案定了,就是此刻的 mx = 5,填进 answer 的第 3 位。注意下一个查询的预算更大,指针 i 和 mx 都从现在的状态接着走,前面纳入的商品一件都不用重看,这正是排序换来的好处。
- 22轮到预算 5。指针 i 停在上一个查询留下的位置 4,绿色的商品是之前已经纳入的,mx 现在是 5。接下来看还能不能纳入更多价格达标的商品。
- 23看紫色这件商品 4,价格 5,没有超过预算 5,把它纳入候选。它的美丽值是 6,拿去和当前 mx = 5 取较大值。结果刷新成 6,更漂亮的商品出现了。指针 i 前进到 末尾。
- 24指针 i 已经走到末尾,五件商品全被纳入候选,绿了一整排。没有新商品可看,循环停下。此刻的最大美丽值就是 6。
- 25预算 5 的答案定了,就是此刻的 mx = 6,填进 answer 的第 4 位。注意下一个查询的预算更大,指针 i 和 mx 都从现在的状态接着走,前面纳入的商品一件都不用重看,这正是排序换来的好处。
- 26轮到预算 6。指针 i 停在上一个查询留下的位置 末尾,绿色的商品是之前已经纳入的,mx 现在是 6。接下来看还能不能纳入更多价格达标的商品。
- 27指针 i 已经走到末尾,五件商品全被纳入候选,绿了一整排。没有新商品可看,循环停下。此刻的最大美丽值就是 6。
- 28预算 6 的答案定了,就是此刻的 mx = 6,填进 answer 的第 5 位。所有查询处理完毕,answer 收工。
- 29六个查询全部处理完。答案数组是 [2, 4, 5, 5, 6, 6],和一开始预告的完全对上。回头看整条主线:商品按价格排好队,查询由小到大处理,指针 i 从头扫到尾只走了一趟,mx 一路只涨不跌,每个查询都直接读走当下的 mx。排序换来的就是这一趟不回头的扫描。
⚠️ 容易写错的地方
✗ 错:排序打乱查询后,直接按扫描顺序输出答案
✓ 对:把答案写回每个查询的原始下标 j
排序改变了查询的次序,若不记录原下标,答案会对错位置;所以要么排 (值, 原下标),要么排一个下标数组
✗ 错:每个查询都从头重新扫一遍商品
✓ 对:指针 i 跨查询保留,只前进不回退
查询升序保证已纳入的商品对更大预算仍达标,重扫会把复杂度退化成 O(n × m)
✗ 错:用严格小于 价格 < 预算 判定
✓ 对:判定条件是价格 ≤ 预算
价格恰好等于预算的商品也买得起,用严格小于会漏掉它们
✗ 错:没有达标商品时返回 -1 或忘记赋值
✓ 对:mx 初始化为 0,查不到就是 0
题目规定不存在符合条件的商品时答案为 0,mx 从 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 maximumBeauty(self, items: List[List[int]], queries: List[int]) -> List[int]:
items.sort()
n, m = len(items), len(queries)
ans = [0] * len(queries)
i = mx = 0
for q, j in sorted(zip(queries, range(m))):
while i < n and items[i][0] <= q:
mx = max(mx, items[i][1])
i += 1
ans[j] = mx
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:
vector<int> maximumBeauty(vector<vector<int>>& items, vector<int>& queries) {
sort(items.begin(), items.end());
int n = items.size();
int m = queries.size();
vector<int> idx(m);
iota(idx.begin(), idx.end(), 0);
sort(idx.begin(), idx.end(), [&](int i, int j) {
return queries[i] < queries[j];
});
int mx = 0, i = 0;
vector<int> ans(m);
for (int j : idx) {
while (i < n && items[i][0] <= queries[j]) {
mx = max(mx, items[i][1]);
++i;
}
ans[j] = mx;
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int[] maximumBeauty(int[][] items, int[] queries) {
Arrays.sort(items, (a, b) -> a[0] - b[0]);
int n = items.length;
int m = queries.length;
int[] ans = new int[m];
Integer[] idx = new Integer[m];
for (int i = 0; i < m; ++i) {
idx[i] = i;
}
Arrays.sort(idx, (i, j) -> queries[i] - queries[j]);
int i = 0, mx = 0;
for (int j : idx) {
while (i < n && items[i][0] <= queries[j]) {
mx = Math.max(mx, items[i][1]);
++i;
}
ans[j] = mx;
}
return ans;
}
}复杂度
时间
O(n log n + m log m)
n 件商品排序 O(n log n),m 个查询按值排下标 O(m log m),这两次排序是主导;之后双指针扫描里指针 i 与查询各自只走一趟,合计 O(n + m),被排序吞没
空间
O(m)
按峰值算,不计输入与答案本身。为查询排序额外开一个长度 m 的下标数组;商品排序可原地进行。排序内部栈 C plus plus 与 Java 约 O(log n)、Python 最坏 O(n),量级不超过 O(n + m)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 每一个查询的最大美丽值 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
除了离线排序加双指针,还有没有别的解法?+
有,而且它支持在线查询。先把商品按价格排序,再对美丽值求一遍前缀最大值,得到「价格前 k 小的商品里最大美丽值是多少」。之后每个查询用二分(有序里每次砍一半的找法)找出最后一个价格 ≤ 预算的位置,直接读那一格的前缀最大值。复杂度 O((n+m) log n)。它不必先攒齐所有查询,来一个答一个;离线双指针则要等查询到齐、排完序才动。
为什么非得把查询也排序,直接按原顺序处理不行吗?+
排序是为了让指针 i 只进不退。查询若乱序,预算一会儿大一会儿小,指针就得跟着来回挪,享受不到一趟扫到底的好处,复杂度退回 O(n×m)。按预算升序处理,i 只增不减,均摊下来商品总共只扫一遍。代价是查询重排后原位置乱了,得连原始下标一起排,才能把答案填回正确的格子。
判定为什么用价格 ≤ 预算,而不是严格小于?+
题目要的是价格不超过预算的商品,价格正好等于预算的那件也买得起、也该纳入。写成严格小于 价格 < 预算,会把价格恰好卡在预算上的商品漏掉——比如预算 3、商品价格也是 3,严格小于就把它排除了,而它可能正带着最大的美丽值,答案就偏小一档。所以判定必须含等号。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 每一个查询的最大美丽值 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。