2813. Maximum Elegance of a K-Length Subsequence


Problem Description

This problem provides us with a 2D integer array named items that represents a collection of items, each with a profit and category. Our objective is to find a subsequence (a sequence that can be derived from another sequence by deleting some elements without changing the order of the remaining elements) of these items with exactly k elements such that the "elegance" of this subsequence is maximized.

The "elegance" is a measure defined specifically for a subsequence of items. It's calculated as the sum of all profits (total_profit) of items in the subsequence plus the square of the number of distinct categories (distinct_categories^2) present in the subsequence.

To summarize, we are tasked to:

  1. Select k items from items as a subsequence.
  2. Calculate the elegance of the subsequence.
  3. Maximize the elegance across all possible subsequences of size k.

Intuition

The solution to this problem involves a greedy approach. The key insight is that higher profit items are likely to contribute more to the elegance score since the sum of profits is a linear component in the elegance score calculation. On the other hand, the number of distinct categories contributes quadratically to the elegance score. Thus, we should aim to maximize profits while also considering the diversity of categories to a reasonable extent.

To arrive at the solution, we start by sorting all items by profit in descending order because we want to initially consider the items with the highest profits. After sorting, we:

  • Choose the first k items, assuming it gives us the maximum total profit.
  • Use a set to keep track of the distinct categories we have included so far.
  • Use a separate list to keep track of profits from duplicate categories (since selecting these doesn't increase the number of distinct categories).

Once we do the initial calculation of elegance with the first k items, we then consider each item beyond the first k. We have two scenarios:

  1. If the category is already included, we skip the item because including another from the same category won't increase the distinct category count.
  2. If the category isn't included, and we have a previous item from a duplicate category, we can potentially increase the elegance by swapping out a lower profit, duplicate item, with a higher profit, new category item.

By updating the total profit and distinct category count accordingly and evaluating the elegance each time we consider an item, we can determine the maximum possible elegance we can achieve considering the subsequence size restriction.

Finally, the answer (ans) is the highest elegance we can get from all the subsequences considered through this process.

Learn more about Greedy, Sorting and Heap (Priority Queue) patterns.

Solution Approach

The implemented solution leverages a greedy algorithm to optimize for maximum elegance. The algorithm and the data structures used can be broken down as follows:

  1. Sorting: We sort the items in descending order by their profits using items.sort(key=lambda x: -x[0]). Sorting is crucial because it allows us to examine the items with the highest profits first, thus aligning with our greedy approach.

  2. Data Structures:

    • A set vis (short for "visited") to keep track of the categories already included in our subsequence. A set is chosen for its property of storing unique elements, which efficiently handles the requirement for distinct categories.
    • A list dup is used to maintain the profits of items from categories we have already chosen. This list functions as a stack, where we add profits of duplicate category items as we encounter them among the first k items.
  3. Initial Calculation:

    • We iterate through the first k items and calculate an initial total profit tot by summing up their profits and populating vis and dup with the categories and profits of duplicate items, respectively.
    • We also keep track of an ans as the current maximum elegance.
  4. Iterative Optimization:

    • We continue to iterate beyond the first k items. For each item, if its category is already in vis, we ignore it, as explained in the intuition part.
    • If an item's category is not in vis and there are items in dup, we have an opportunity to possibly increase our elegance score:
      • We add the new category to vis.
      • We update the total profit tot by replacing one of the duplicate category items with the current one. This is done by subtracting the profit of the duplicate item (the last element in dup, because dup operates as a stack) and adding the profit of the current item.
  5. Elegance Calculation:

    • After considering each new item and potential swaps, we calculate the current elegance as tot + len(vis) ** 2 and check whether it is greater than our previously recorded maximum. If it is, we update ans with this value.
  6. Return Value:

    • The maximum found elegance ans is returned as the result, representing the maximum elegance from all subsequences of size k.

This approach takes advantage of the greedy method for an initial selection of the most promising items and then iterates over the remaining ones with the potential to yield higher elegance by introducing more diversity in categories. It systematically evaluates each item's impact on total profit and the distinct category count to arrive at the optimal elegance score.

Ready to land your dream job?

Unlock your dream job with a 2-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's illustrate the solution approach with a small example. Suppose we have the following items with the first value representing the profit and the second the category:

items = [[5, 1], [4, 2], [7, 1], [3, 3], [8, 2]]
k = 3

We aim to maximize the elegance of a subsequence of k=3 items from this list. Here's a step-by-step walkthrough implementing the solution approach:

  1. Sort items by profit in descending order:

    We apply sorting to the items to consider the highest profits first.

    Sorted items: [[8, 2], [7, 1], [5, 1], [4, 2], [3, 3]]

  2. Initialization using data structures:

    We create a set vis to track distinct categories, and a list dup to store profits of duplicate category items.

    vis (set): empty. dup (list): empty.

  3. Initial Calculation of elegance:

    We iterate through the first k items, add their profits to tot, and populate vis and dup accordingly.

    For the first k=3 items:

    • item [8, 2]: tot = 8, vis = {2}, dup = []
    • item [7, 1]: tot = 15 (8+7), vis = {2, 1}, dup = []
    • item [5, 1]: tot = 20 (15+5), vis = {2, 1}, dup = [5] (since category 1 is a duplicate)

    Initial ans (maximum elegance): 20 + 2^2 = 24

  4. Iterative Optimization:

    Now we consider items beyond the first k=3.

    • item [4, 2]: skipped, because category 2 is already included in vis.
    • item [3, 3]: since category 3 is not in vis and we have a lower profit duplicate [5, 1] in dup, we can swap them:
      • Add category 3 to vis: vis = {1, 2, 3}
      • Swap [5, 1] with [3, 3]: tot becomes 18 (20 - 5 + 3)
      • Recalculate elegance: 18 + 3^2 = 27, which is greater than the previous ans
      • Update ans: ans = 27
  5. Return Value:

    After evaluating all items, we have our final ans value.

    The maximum elegance ans is 27, which is the answer to our problem.

In this example, we can see that even though the last item has a lower profit than some of the ones we ignored, its unique category increased the elegance score more significantly than a higher profit item from an already existing category would have. This exemplifies the importance of maximizing the sum of profits while also increasing the number of distinct categories, showcasing the effectiveness of the greedy approach in this scenario.

Solution Implementation

1from typing import List
2
3class Solution:
4    def findMaximumElegance(self, items: List[List[int]], k: int) -> int:
5        # Sort the items by price in descending order
6        items.sort(key=lambda x: -x[0])
7
8        total_price = 0
9        seen_colors = set()
10        duplicate_prices = []
11
12        # Process the first K items and calculate their total price
13        # Record if a color has been seen before and keep track of duplicates
14        for price, color in items[:k]:
15            total_price += price
16            if color not in seen_colors:
17                seen_colors.add(color)
18            else:
19                duplicate_prices.append(price)
20
21        # Calculate the current elegance as the sum of prices and the square of
22        # unique colors count
23        max_elegance = total_price + len(seen_colors) ** 2
24
25        # Process the rest of the items beyond the initial K
26        for price, color in items[k:]:
27            # If the color is already seen or there are no duplicates, continue
28            if color in seen_colors or not duplicate_prices:
29                continue
30
31            # Add the new color to the seen set
32            seen_colors.add(color)
33          
34            # Update the total price by swapping the lowest duplicate and the new item
35            total_price += price - duplicate_prices.pop()
36
37            # Recalculate the elegance and update max_elegance if it's greater
38            max_elegance = max(max_elegance, total_price + len(seen_colors) ** 2)
39
40        # Return the maximum elegance found
41        return max_elegance
42
1class Solution {
2    public long findMaximumElegance(int[][] items, int k) {
3        // Sort the items based on their price in descending order
4        Arrays.sort(items, (a, b) -> b[0] - a[0]);
5
6        // Total number of items
7        int itemCount = items.length;
8
9        // Total elegance score
10        long totalElegance = 0;
11
12        // Set to record which colors have been used
13        Set<Integer> usedColors = new HashSet<>();
14      
15        // A deque to store the prices of duplicate colored items
16        Deque<Integer> duplicatePrices = new ArrayDeque<>();
17
18        // Pick the first 'k' items
19        for (int i = 0; i < k; ++i) {
20            int price = items[i][0], color = items[i][1];
21            totalElegance += price;
22          
23            // If the color is already seen, add the price to the deque of duplicates
24            if (!usedColors.add(color)) {
25                duplicatePrices.push(price);
26            }
27        }
28
29        // Calculate the maximum elegance for the first 'k' items
30        long maxElegance = totalElegance + (long) usedColors.size() * usedColors.size();
31
32        // Now try to replace duplicate items with items of unused colors to maximize elegance
33        for (int i = k; i < itemCount; ++i) {
34            int price = items[i][0], color = items[i][1];
35          
36            // Skip if the color is already used and no duplicates to replace
37            if (usedColors.contains(color) || duplicatePrices.isEmpty()) {
38                continue;
39            }
40
41            // Add this unused color and update the total elegance
42            usedColors.add(color);
43            totalElegance += price - duplicatePrices.pop();
44
45            // Calculate the new maximum elegance with this configuration
46            maxElegance = Math.max(maxElegance, totalElegance + (long) usedColors.size() * usedColors.size());
47        }
48
49        // Return the maximum elegance that can be achieved
50        return maxElegance;
51    }
52}
53
1#include <vector>
2#include <unordered_set>
3#include <stack>
4#include <algorithm>
5
6class Solution {
7public:
8    long long findMaximumElegance(std::vector<std::vector<int>>& items, int k) {
9        // Sort the items in decreasing order based on price (first element of each pair)
10        std::sort(items.begin(), items.end(), [](const std::vector<int>& a, const std::vector<int>& b) {
11            return a[0] > b[0];
12        });
13      
14        long long totalElegance = 0; // This will keep track of the total elegance score
15        std::unordered_set<int> uniqueColors; // Set to store unique colors
16        std::stack<int> duplicatePrices; // Stack to store prices of duplicate color items
17      
18        // Process the first 'k' items as they contribute to elegance score
19        for (int i = 0; i < k; ++i) {
20            int price = items[i][0], color = items[i][1];
21            totalElegance += price; // Add item price to the total elegance score
22            if (uniqueColors.count(color)) {
23                // If the color is a duplicate, push its price onto the stack
24                duplicatePrices.push(price);
25            } else {
26                // If the color is unique, insert it into the set
27                uniqueColors.insert(color);
28            }
29        }
30      
31        int itemCount = items.size(); // Total number of items
32        // Calculate initial elegance score including the bonus from unique colors
33        long long maxElegance = totalElegance + static_cast<long long>(uniqueColors.size() * uniqueColors.size());
34      
35        // Process the remaining items to potentially replace duplicates with unique colors
36        for (int i = k; i < itemCount; ++i) {
37            int price = items[i][0], color = items[i][1];
38            // If current color is already used or there are no duplicates, continue
39            if (uniqueColors.count(color) || duplicatePrices.empty()) {
40                continue;
41            }
42            // Insert unique color and update total elegance score by replacing a duplicate
43            uniqueColors.insert(color);
44            totalElegance += price - duplicatePrices.top();
45            duplicatePrices.pop();
46            // Check if the current configuration yields a higher elegance score and update maxElegance if so
47            maxElegance = std::max(maxElegance, totalElegance + static_cast<long long>(uniqueColors.size() * uniqueColors.size()));
48        }
49      
50        return maxElegance; // Return the maximum elegance score found
51    }
52};
53
1function findMaximumElegance(items: number[][], k: number): number {
2    // Sort items in decreasing order of price
3    items.sort((a, b) => b[0] - a[0]);
4
5    // Initialize the total score
6    let totalScore = 0;
7
8    // Set to keep track of unique items
9    const visitedCategories: Set<number> = new Set();
10
11    // Array to keep track of duplicates in the top 'k' items
12    const duplicatePrices: number[] = [];
13
14    // Loop through the top 'k' items
15    for (const [price, category] of items.slice(0, k)) {
16        totalScore += price;
17        if (visitedCategories.has(category)) {
18            // If category is already visited, add the price to duplicates
19            duplicatePrices.push(price);
20        } else {
21            // Else add the category to visited
22            visitedCategories.add(category);
23        }
24    }
25
26    // Calculate initial elegance score
27    let maxElegance = totalScore + visitedCategories.size ** 2;
28
29    // Loop through the rest of the items beyond the top 'k'
30    for (const [price, category] of items.slice(k)) {
31        // Continue if category is already visited or there are no duplicates
32        if (visitedCategories.has(category) || duplicatePrices.length === 0) {
33            continue;
34        }
35        // Replace the lowest duplicate price with the current price and update the score
36        totalScore += price - duplicatePrices.pop()!;
37        // Add the new category
38        visitedCategories.add(category);
39        // Update the maximum elegance score if it's larger
40        maxElegance = Math.max(maxElegance, totalScore + visitedCategories.size ** 2);
41    }
42    // Return the maximum elegance score
43    return maxElegance;
44}
45

Time and Space Complexity

Time Complexity

The time complexity of the code is primarily determined by the sorting operation and the subsequent for-loops.

  1. The sort() function on the list of items, items.sort(key=lambda x: -x[0]), has a time complexity of O(n log n), where n is the number of items. This comes from the fact that Python's Timsort algorithm is used for sorting, which has this time complexity for sorting a list.

  2. The first for-loop, for p, c in items[:k]:, iterates over the first k elements, thus taking O(k) time.

  3. The second for-loop, for p, c in items[k:]:, in the worst case, could iterate over 'n - k' elements if dup is non-empty until the end of the list. This will have a time complexity of O(n - k).

Since k <= n, we have O(k) and O(n - k) both being bounded by O(n). However, the sort operation dominates the time complexity, so we can simplify and say the overall time complexity of this code is O(n log n).

Space Complexity

The space complexity of the code is determined by the storage used for the vis set, the dup list, and the list slice used for iterating items.

  1. The vis set can potentially store a unique color for every item, in the worst case, which would require O(n) space.

  2. The dup list can store at most k prices, in the case where all items among the top k have duplicate colors. So in the worst case, it would need O(k) space.

  3. List slicing, such as items[:k] and items[k:], in Python does not create a shallow copy, hence it doesn't add to the space complexity.

Therefore, the dominating factor here is the space required for the vis set, which is O(n). The dup list, even though it may grow, is always within the limit set by vis, so the space complexity remains O(n).

Learn more about how to find time and space complexity quickly using problem constraints.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

What does the following code do?

1def f(arr1, arr2):
2  i, j = 0, 0
3  new_arr = []
4  while i < len(arr1) and j < len(arr2):
5      if arr1[i] < arr2[j]:
6          new_arr.append(arr1[i])
7          i += 1
8      else:
9          new_arr.append(arr2[j])
10          j += 1
11  new_arr.extend(arr1[i:])
12  new_arr.extend(arr2[j:])
13  return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2  int i = 0, j = 0;
3  List<Integer> newArr = new ArrayList<>();
4
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.add(arr1[i]);
8          i++;
9      } else {
10          newArr.add(arr2[j]);
11          j++;
12      }
13  }
14
15  while (i < arr1.length) {
16      newArr.add(arr1[i]);
17      i++;
18  }
19
20  while (j < arr2.length) {
21      newArr.add(arr2[j]);
22      j++;
23  }
24
25  return newArr;
26}
27
1function f(arr1, arr2) {
2  let i = 0, j = 0;
3  let newArr = [];
4  
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.push(arr1[i]);
8          i++;
9      } else {
10          newArr.push(arr2[j]);
11          j++;
12      }
13  }
14  
15  while (i < arr1.length) {
16      newArr.push(arr1[i]);
17      i++;
18  }
19  
20  while (j < arr2.length) {
21      newArr.push(arr2[j]);
22      j++;
23  }
24  
25  return newArr;
26}
27

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!