2070. Most Beautiful Item for Each Query
Problem Description
You have a collection of items, where each item has two properties: a price and a beauty value. These items are given as a 2D array items
where items[i] = [price_i, beauty_i]
.
You also have a list of queries represented as an array of integers. For each query value queries[j]
, you need to find the maximum beauty among all items whose price is less than or equal to that query value.
For example:
- If
queries[j] = 10
, you need to look at all items with price ≤ 10 and find the one with the highest beauty value - If no items have a price ≤
queries[j]
, the answer for that query is0
Your task is to return an array answer
where answer[j]
contains the maximum beauty value for the corresponding queries[j]
.
The solution uses a sorting and offline query approach:
- First, it sorts the items by price in ascending order
- It processes queries in sorted order (from smallest to largest query value)
- For each query, it uses a pointer to iterate through items whose prices are within the query limit, keeping track of the maximum beauty seen so far
- Since queries are processed in sorted order but need to maintain their original positions, the solution uses
sorted(zip(queries, range(m)))
to keep track of original indices - The answer for each query is placed in its original position in the result array
This approach is efficient because each item is examined at most once across all queries, and the sorting allows us to process items incrementally as query values increase.
Intuition
The key insight is recognizing that if we process queries in a naive way (checking all items for each query), we'd repeatedly examine the same items, leading to inefficiency.
Think about it this way: if we have queries for prices 5
, 10
, and 15
, the items valid for price 5
are also valid for prices 10
and 15
. So why check them multiple times?
This leads us to the idea of sorting both items and queries by price. Once sorted, we can process queries in ascending order and maintain a running maximum beauty value. As we move from a smaller query to a larger one, we only need to consider the additional items that become available (those with prices between the previous and current query values).
The crucial observation is that once an item becomes "available" for a query (its price is within the limit), it remains available for all subsequent larger queries. This means we can use a two-pointer technique: one pointer iterates through queries, and another pointer iterates through items, never going backward.
However, there's a catch - we need to return answers in the original query order, not the sorted order. This is why we use the zip(queries, range(m))
technique to pair each query with its original index. After sorting and processing, we can place each answer back in its correct position.
The beauty of this approach is that each item is examined exactly once across all queries, reducing the time complexity from O(n * m)
for the naive approach to O(n log n + m log m)
for sorting plus O(n + m)
for processing.
Learn more about Binary Search and Sorting patterns.
Solution Approach
The implementation follows the sorting and offline query strategy outlined in the reference approach. Let's walk through the code step by step:
Step 1: Sort the items array
items.sort()
We sort items by price in ascending order. This allows us to process items in order of increasing price.
Step 2: Initialize variables
n, m = len(items), len(queries)
ans = [0] * len(queries)
i = mx = 0
n
andm
store the lengths of items and queries arraysans
is initialized with zeros to store the final resultsi
is a pointer for traversing the items arraymx
tracks the maximum beauty seen so far
Step 3: Process queries in sorted order while preserving original indices
for q, j in sorted(zip(queries, range(m))):
This is the clever part: zip(queries, range(m))
creates pairs of (query_value, original_index)
. When we sort these pairs, they're sorted by query value, but we retain the original index j
to know where to place the answer.
Step 4: Update maximum beauty for current query
while i < n and items[i][0] <= q:
mx = max(mx, items[i][1])
i += 1
For each query q
, we advance the pointer i
through all items whose price is at most q
. We update mx
to track the maximum beauty among all items seen so far. The key insight is that pointer i
never moves backward - once an item is processed, it's available for all subsequent larger queries.
Step 5: Record the answer in the correct position
ans[j] = mx
We place the current maximum beauty mx
at position j
(the original index of this query) in the answer array.
Step 6: Return the result
return ans
The algorithm efficiently processes all queries in O(n log n + m log m + n + m)
time, where the sorting dominates the complexity. Each item is examined exactly once across all queries, making this much more efficient than the naive O(n * m)
approach.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate how the solution works.
Given:
items = [[1, 2], [3, 2], [2, 4], [5, 6], [3, 5]]
queries = [1, 2, 3, 4, 5, 6]
Step 1: Sort items by price
After sorting: items = [[1, 2], [2, 4], [3, 2], [3, 5], [5, 6]]
Now items are arranged by price: 1, 2, 3, 3, 5
Step 2: Pair queries with their original indices and sort
- Original queries with indices:
[(1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5)]
- After sorting by query value:
[(1, 0), (2, 1), (3, 2), (4, 3), (5, 4), (6, 5)]
(In this case, queries were already sorted, but normally they might not be)
Step 3: Process each query in sorted order
Initialize: i = 0
, mx = 0
, ans = [0, 0, 0, 0, 0, 0]
-
Query (1, 0): Find max beauty for price ≤ 1
- Check item [1, 2]: price 1 ≤ 1 ✓, update
mx = max(0, 2) = 2
, advancei
to 1 - Check item [2, 4]: price 2 > 1 ✗, stop
- Store
ans[0] = 2
- Check item [1, 2]: price 1 ≤ 1 ✓, update
-
Query (2, 1): Find max beauty for price ≤ 2
- Continue from
i = 1
(don't restart!) - Check item [2, 4]: price 2 ≤ 2 ✓, update
mx = max(2, 4) = 4
, advancei
to 2 - Check item [3, 2]: price 3 > 2 ✗, stop
- Store
ans[1] = 4
- Continue from
-
Query (3, 2): Find max beauty for price ≤ 3
- Continue from
i = 2
- Check item [3, 2]: price 3 ≤ 3 ✓, update
mx = max(4, 2) = 4
, advancei
to 3 - Check item [3, 5]: price 3 ≤ 3 ✓, update
mx = max(4, 5) = 5
, advancei
to 4 - Check item [5, 6]: price 5 > 3 ✗, stop
- Store
ans[2] = 5
- Continue from
-
Query (4, 3): Find max beauty for price ≤ 4
- Continue from
i = 4
- Check item [5, 6]: price 5 > 4 ✗, stop immediately
- Store
ans[3] = 5
(mx remains 5 from previous queries)
- Continue from
-
Query (5, 4): Find max beauty for price ≤ 5
- Continue from
i = 4
- Check item [5, 6]: price 5 ≤ 5 ✓, update
mx = max(5, 6) = 6
, advancei
to 5 - No more items to check
- Store
ans[4] = 6
- Continue from
-
Query (6, 5): Find max beauty for price ≤ 6
- Continue from
i = 5
- No more items to check (i >= n)
- Store
ans[5] = 6
(mx remains 6)
- Continue from
Final Result: ans = [2, 4, 5, 5, 6, 6]
The key efficiency comes from the fact that we never reset the pointer i
- we process each item exactly once across all queries. The running maximum mx
accumulates the best beauty value as we go, ensuring that larger queries automatically inherit the maximum beauty from all previously processed items.
Solution Implementation
1from typing import List
2
3class Solution:
4 def maximumBeauty(self, items: List[List[int]], queries: List[int]) -> List[int]:
5 # Sort items by price in ascending order
6 items.sort()
7
8 # Get the number of items and queries
9 num_items = len(items)
10 num_queries = len(queries)
11
12 # Initialize result array with zeros for each query
13 result = [0] * num_queries
14
15 # Initialize pointers and maximum beauty tracker
16 item_index = 0
17 max_beauty = 0
18
19 # Process queries in sorted order while maintaining original indices
20 # zip(queries, range(num_queries)) creates pairs of (query_value, original_index)
21 for query_value, original_index in sorted(zip(queries, range(num_queries))):
22 # Process all items with price <= current query value
23 while item_index < num_items and items[item_index][0] <= query_value:
24 # Update maximum beauty seen so far
25 max_beauty = max(max_beauty, items[item_index][1])
26 item_index += 1
27
28 # Store the maximum beauty for this query at its original position
29 result[original_index] = max_beauty
30
31 return result
32
1class Solution {
2 public int[] maximumBeauty(int[][] items, int[] queries) {
3 // Sort items by price in ascending order
4 Arrays.sort(items, (a, b) -> a[0] - b[0]);
5
6 int itemCount = items.length;
7 int queryCount = queries.length;
8 int[] result = new int[queryCount];
9
10 // Create an array of indices to track original query positions
11 Integer[] queryIndices = new Integer[queryCount];
12 for (int i = 0; i < queryCount; i++) {
13 queryIndices[i] = i;
14 }
15
16 // Sort query indices based on corresponding query values in ascending order
17 Arrays.sort(queryIndices, (i, j) -> queries[i] - queries[j]);
18
19 int itemIndex = 0;
20 int maxBeauty = 0;
21
22 // Process queries in sorted order
23 for (int currentQueryIndex : queryIndices) {
24 // For current query, find all items with price <= query value
25 // and track the maximum beauty seen so far
26 while (itemIndex < itemCount && items[itemIndex][0] <= queries[currentQueryIndex]) {
27 maxBeauty = Math.max(maxBeauty, items[itemIndex][1]);
28 itemIndex++;
29 }
30
31 // Store the maximum beauty for this query at its original position
32 result[currentQueryIndex] = maxBeauty;
33 }
34
35 return result;
36 }
37}
38
1class Solution {
2public:
3 vector<int> maximumBeauty(vector<vector<int>>& items, vector<int>& queries) {
4 // Sort items by price in ascending order
5 sort(items.begin(), items.end());
6
7 int itemCount = items.size();
8 int queryCount = queries.size();
9
10 // Create an index array to track original positions of queries
11 vector<int> queryIndices(queryCount);
12 iota(queryIndices.begin(), queryIndices.end(), 0);
13
14 // Sort query indices based on their corresponding query values
15 sort(queryIndices.begin(), queryIndices.end(), [&](int i, int j) {
16 return queries[i] < queries[j];
17 });
18
19 int maxBeauty = 0; // Track maximum beauty seen so far
20 int itemIndex = 0; // Current position in items array
21 vector<int> result(queryCount);
22
23 // Process queries in ascending order of price
24 for (int currentQueryIndex : queryIndices) {
25 int currentQueryPrice = queries[currentQueryIndex];
26
27 // Include all items with price <= current query price
28 while (itemIndex < itemCount && items[itemIndex][0] <= currentQueryPrice) {
29 // Update maximum beauty if current item has higher beauty
30 maxBeauty = max(maxBeauty, items[itemIndex][1]);
31 itemIndex++;
32 }
33
34 // Store the maximum beauty for this query at its original position
35 result[currentQueryIndex] = maxBeauty;
36 }
37
38 return result;
39 }
40};
41
1/**
2 * Finds the maximum beauty value for each query price
3 * @param items - Array of [price, beauty] pairs
4 * @param queries - Array of maximum prices to query
5 * @returns Array of maximum beauty values for each query
6 */
7function maximumBeauty(items: number[][], queries: number[]): number[] {
8 const itemCount: number = items.length;
9 const queryCount: number = queries.length;
10
11 // Sort items by price in ascending order
12 items.sort((itemA: number[], itemB: number[]) => itemA[0] - itemB[0]);
13
14 // Create an array of query indices for tracking original positions
15 const queryIndices: number[] = Array(queryCount)
16 .fill(0)
17 .map((_, index: number) => index);
18
19 // Sort query indices based on their corresponding query values
20 queryIndices.sort((indexI: number, indexJ: number) => queries[indexI] - queries[indexJ]);
21
22 // Initialize tracking variables
23 let itemIndex: number = 0;
24 let maxBeauty: number = 0;
25
26 // Initialize result array
27 const result: number[] = Array(queryCount).fill(0);
28
29 // Process each query in sorted order
30 for (const currentQueryIndex of queryIndices) {
31 // Process all items with price <= current query price
32 while (itemIndex < itemCount && items[itemIndex][0] <= queries[currentQueryIndex]) {
33 // Update maximum beauty encountered so far
34 maxBeauty = Math.max(maxBeauty, items[itemIndex][1]);
35 itemIndex++;
36 }
37
38 // Store the maximum beauty for this query at its original position
39 result[currentQueryIndex] = maxBeauty;
40 }
41
42 return result;
43}
44
Time and Space Complexity
Time Complexity: O(n × log n + m × log m)
The time complexity breaks down as follows:
items.sort()
takesO(n × log n)
wheren
is the length of the items arraysorted(zip(queries, range(m)))
takesO(m × log m)
wherem
is the length of the queries array- The while loop inside the for loop processes each item at most once across all iterations, contributing
O(n)
in total - The for loop iterates
m
times - Overall:
O(n × log n) + O(m × log m) + O(n + m) = O(n × log n + m × log m)
Space Complexity: O(log n + m)
The space complexity consists of:
O(log n)
for the sorting algorithm's recursive stack space when sorting itemsO(m)
for theans
array which stores the resultsO(m)
for the sorted queries with indices (created bysorted(zip(...))
)O(log m)
for the sorting algorithm's recursive stack space when sorting queries- Overall:
O(log n) + O(m) + O(log m) = O(log n + m)
(sincem
dominateslog m
)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Not Handling the Offline Query Indexing Correctly
A common mistake is forgetting to maintain the original query indices when sorting queries for processing. Developers might write:
# WRONG: This loses track of original positions queries.sort() for q in queries: # Process query q ans.append(max_beauty) # Where does this go in the result?
Why it's wrong: After sorting queries, we lose the mapping between each query and its original position in the input array. The problem requires answers to be in the same order as the original queries.
Solution: Use sorted(zip(queries, range(num_queries)))
to create pairs that maintain the original index alongside each query value. This allows processing queries in sorted order while knowing where to place each answer.
Pitfall 2: Resetting the Item Pointer for Each Query
Another mistake is resetting the item pointer for each new query:
# WRONG: Inefficient approach
for query_value, original_index in sorted(zip(queries, range(num_queries))):
item_index = 0 # WRONG: Resetting pointer
max_beauty = 0 # WRONG: Resetting max beauty
while item_index < num_items and items[item_index][0] <= query_value:
max_beauty = max(max_beauty, items[item_index][1])
item_index += 1
result[original_index] = max_beauty
Why it's wrong: This defeats the purpose of the offline query optimization. Since queries are processed in ascending order, items valid for a smaller query remain valid for larger queries. Resetting the pointer causes us to re-examine the same items multiple times, degrading performance to O(n * m).
Solution: Keep the item pointer (item_index
) and max_beauty
variables outside the query loop. Never reset them. This ensures each item is examined exactly once across all queries.
Pitfall 3: Not Considering Items with Same Price but Different Beauty Values
Some might incorrectly assume that when sorting items, they only need to keep the item with maximum beauty for each price:
# WRONG: Losing information during preprocessing
price_to_beauty = {}
for price, beauty in items:
price_to_beauty[price] = max(price_to_beauty.get(price, 0), beauty)
items = [[p, b] for p, b in price_to_beauty.items()]
Why it's wrong: While this might seem like an optimization, it's unnecessary and complicates the code. The original algorithm handles duplicate prices correctly by considering all items and tracking the maximum beauty incrementally.
Solution: Simply sort the items as they are. The algorithm naturally handles multiple items with the same price by updating max_beauty
as it encounters each one.
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!