2343. Query Kth Smallest Trimmed Number
Problem Description
You have an array of strings nums
where all strings have the same length and contain only digits. You also have a 2D array queries
where each query contains two values: [k, trim]
.
For each query, you need to:
-
Trim all numbers in
nums
by keeping only the rightmosttrim
digits. For example, if a number is"12345"
andtrim = 3
, the trimmed number becomes"345"
. -
Sort the trimmed numbers and find the
k
-th smallest one (1-indexed). When two trimmed numbers are equal, the one that appears earlier in the original array (smaller index) is considered smaller. -
Return the original index of the
k
-th smallest trimmed number. -
Reset the array back to its original state before processing the next query.
The function should return an array where each element is the answer (original index) for the corresponding query.
Example walkthrough:
- If
nums = ["102", "473", "251", "814"]
and query is[2, 1]
:- Trim to rightmost 1 digit:
["2", "3", "1", "4"]
- Sort with original indices:
"1"(index 2) <
"2"(index 0) <"3"(index 1) <
"4"(index 3) - The 2nd smallest is at original index 0
- Trim to rightmost 1 digit:
Key points:
- Trimming means removing digits from the left until only the specified number of rightmost digits remain
- The strings may have leading zeros after trimming
- When comparing equal trimmed values, use the original index as a tiebreaker
Intuition
The key insight is that we need to process each query independently while preserving the original indices of the numbers. Since each query asks for the k-th smallest trimmed number, we need both the trimmed values for comparison and their original positions for the final answer.
The most straightforward approach is simulation - actually perform the trimming operation for each query. Since strings in Python support slicing, we can easily get the rightmost trim
digits using negative indexing: s[-trim:]
gives us the last trim
characters.
For sorting, we need to maintain a connection between each trimmed value and its original index. This naturally leads to creating pairs of (trimmed_value, original_index)
. When we sort these pairs, Python will automatically sort by the trimmed value first, and if two values are equal, it will sort by the second element (the index), which perfectly matches our tiebreaker requirement.
The beauty of this approach is that we don't need to explicitly handle the tiebreaker logic - by pairing each trimmed string with its index and sorting these tuples, we get the correct ordering automatically. After sorting, the k-th smallest element (using 1-based indexing) is at position k-1
in our sorted list, and we just need to extract its original index.
Since we're creating new trimmed strings for each query rather than modifying the original array, there's no need to "reset" anything - the original nums
array remains unchanged throughout the process.
Learn more about Divide and Conquer, Sorting and Heap (Priority Queue) patterns.
Solution Approach
The solution follows a direct simulation approach for each query:
Step 1: Process each query independently
for k, trim in queries:
We iterate through each query, extracting the k value (position we need) and trim value (number of rightmost digits to keep).
Step 2: Create trimmed pairs with indices
t = sorted((v[-trim:], i) for i, v in enumerate(nums))
This line does several things:
enumerate(nums)
gives us each string with its original indexv[-trim:]
extracts the rightmosttrim
digits from each string. For example, ifv = "12345"
andtrim = 3
, thenv[-3:]
gives"345"
- We create tuples of
(trimmed_value, original_index)
for each element sorted()
sorts these tuples lexicographically
The sorting works as follows:
- First compares by the trimmed string value (lexicographic comparison works correctly for digit strings)
- If two trimmed values are equal, it compares by the second element (the original index)
- This automatically handles the tiebreaker rule where smaller indices come first
Step 3: Extract the k-th smallest element's index
ans.append(t[k - 1][1])
- Since the query uses 1-based indexing (k-th smallest), we access
t[k-1]
for 0-based indexing t[k-1]
gives us the tuple(trimmed_value, original_index)
t[k-1][1]
extracts just the original index, which is what we need to return
Time Complexity: O(Q * N * log N)
where Q is the number of queries and N is the number of strings, since we sort for each query.
Space Complexity: O(N)
for storing the temporary sorted list of tuples.
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 with nums = ["24", "37", "96", "04"]
and query [2, 1]
.
Step 1: Extract query parameters
- k = 2 (we want the 2nd smallest)
- trim = 1 (keep only the rightmost 1 digit)
Step 2: Create trimmed pairs For each string, we trim to the rightmost 1 digit and pair with its original index:
nums[0] = "24"
→ trim to"4"
→ pair:("4", 0)
nums[1] = "37"
→ trim to"7"
→ pair:("7", 1)
nums[2] = "96"
→ trim to"6"
→ pair:("6", 2)
nums[3] = "04"
→ trim to"4"
→ pair:("4", 3)
Step 3: Sort the pairs When we sort these tuples, Python compares the trimmed values first:
("4", 0)
and("4", 3)
both have trimmed value "4"("6", 2)
has trimmed value "6"("7", 1)
has trimmed value "7"
For the two "4"s, since they're equal, Python uses the second element (index) as tiebreaker:
("4", 0)
comes before("4", 3)
because 0 < 3
Sorted result: [("4", 0), ("4", 3), ("6", 2), ("7", 1)]
Step 4: Find the k-th smallest Since k = 2 (1-indexed), we need the element at position 1 (0-indexed):
t[1]
=("4", 3)
- Extract the original index:
3
Answer: The 2nd smallest trimmed number comes from index 3 in the original array.
This approach elegantly handles the tiebreaker rule automatically through tuple sorting, without needing any special comparison logic.
Solution Implementation
1class Solution:
2 def smallestTrimmedNumbers(
3 self, nums: List[str], queries: List[List[int]]
4 ) -> List[int]:
5 """
6 Find the k-th smallest number after trimming digits from the right.
7
8 Args:
9 nums: List of numeric strings
10 queries: List of [k, trim] pairs where k is the position and trim is digits to keep
11
12 Returns:
13 List of indices corresponding to k-th smallest trimmed numbers
14 """
15 result = []
16
17 # Process each query
18 for k_position, trim_length in queries:
19 # Create list of tuples: (trimmed_number, original_index)
20 # Keep only the last 'trim_length' digits from each number
21 trimmed_with_indices = [
22 (number[-trim_length:], index)
23 for index, number in enumerate(nums)
24 ]
25
26 # Sort by trimmed number (lexicographically), then by original index
27 trimmed_with_indices.sort()
28
29 # Get the k-th smallest (k is 1-indexed, so use k-1)
30 kth_smallest_index = trimmed_with_indices[k_position - 1][1]
31 result.append(kth_smallest_index)
32
33 return result
34
1class Solution {
2 public int[] smallestTrimmedNumbers(String[] nums, int[][] queries) {
3 int numCount = nums.length;
4 int queryCount = queries.length;
5 int[] result = new int[queryCount];
6
7 // Array to store trimmed numbers with their original indices
8 String[][] trimmedWithIndex = new String[numCount][2];
9
10 // Process each query
11 for (int queryIdx = 0; queryIdx < queryCount; queryIdx++) {
12 int kthPosition = queries[queryIdx][0];
13 int trimLength = queries[queryIdx][1];
14
15 // Create trimmed numbers paired with their original indices
16 for (int numIdx = 0; numIdx < numCount; numIdx++) {
17 String currentNum = nums[numIdx];
18 // Extract the rightmost 'trimLength' digits
19 String trimmedNum = currentNum.substring(currentNum.length() - trimLength);
20 // Store trimmed number and its original index as strings
21 trimmedWithIndex[numIdx] = new String[] {trimmedNum, String.valueOf(numIdx)};
22 }
23
24 // Sort by trimmed number value, then by original index if equal
25 Arrays.sort(trimmedWithIndex, (a, b) -> {
26 int comparisonResult = a[0].compareTo(b[0]);
27 if (comparisonResult == 0) {
28 // If trimmed numbers are equal, sort by original index
29 return Integer.compare(Integer.valueOf(a[1]), Integer.valueOf(b[1]));
30 }
31 return comparisonResult;
32 });
33
34 // Get the index of the kth smallest trimmed number (1-indexed)
35 result[queryIdx] = Integer.valueOf(trimmedWithIndex[kthPosition - 1][1]);
36 }
37
38 return result;
39 }
40}
41
1class Solution {
2public:
3 vector<int> smallestTrimmedNumbers(vector<string>& nums, vector<vector<int>>& queries) {
4 int numCount = nums.size();
5
6 // Pair to store trimmed string and its original index
7 vector<pair<string, int>> trimmedPairs(numCount);
8
9 // Result vector to store answers for each query
10 vector<int> result;
11
12 // Process each query
13 for (auto& query : queries) {
14 int kthPosition = query[0]; // k-th smallest element to find (1-indexed)
15 int trimLength = query[1]; // number of rightmost digits to consider
16
17 // Create pairs of trimmed strings with their original indices
18 for (int i = 0; i < numCount; ++i) {
19 // Extract rightmost 'trimLength' digits from each number string
20 string trimmedString = nums[i].substr(nums[i].size() - trimLength);
21 trimmedPairs[i] = {trimmedString, i};
22 }
23
24 // Sort pairs lexicographically by trimmed string value
25 // If strings are equal, they'll be sorted by original index (stable sort)
26 sort(trimmedPairs.begin(), trimmedPairs.end());
27
28 // Get the index of the k-th smallest trimmed number (k is 1-indexed)
29 result.push_back(trimmedPairs[kthPosition - 1].second);
30 }
31
32 return result;
33 }
34};
35
1function smallestTrimmedNumbers(nums: string[], queries: number[][]): number[] {
2 const numCount = nums.length;
3
4 // Array to store pairs of [trimmed string, original index]
5 const trimmedPairs: [string, number][] = new Array(numCount);
6
7 // Result array to store answers for each query
8 const result: number[] = [];
9
10 // Process each query
11 for (const query of queries) {
12 const kthPosition = query[0]; // k-th smallest element to find (1-indexed)
13 const trimLength = query[1]; // number of rightmost digits to consider
14
15 // Create pairs of trimmed strings with their original indices
16 for (let i = 0; i < numCount; i++) {
17 // Extract rightmost 'trimLength' digits from each number string
18 const trimmedString = nums[i].substring(nums[i].length - trimLength);
19 trimmedPairs[i] = [trimmedString, i];
20 }
21
22 // Sort pairs lexicographically by trimmed string value
23 // If strings are equal, they'll be sorted by original index (stable sort)
24 trimmedPairs.sort((a, b) => {
25 // Compare trimmed strings lexicographically
26 if (a[0] < b[0]) return -1;
27 if (a[0] > b[0]) return 1;
28 // If strings are equal, maintain original order by index
29 return a[1] - b[1];
30 });
31
32 // Get the index of the k-th smallest trimmed number (k is 1-indexed)
33 result.push(trimmedPairs[kthPosition - 1][1]);
34 }
35
36 return result;
37}
38
Time and Space Complexity
The time complexity is O(m × n × log n × s)
, where:
m
is the length ofqueries
(number of queries to process)n
is the length ofnums
(number of strings in the input array)log n
comes from the sorting operation onn
elementss
is the maximum length of strings innums
(for string comparison during sorting)
Breaking down the analysis:
- The outer loop iterates through all queries:
O(m)
- For each query, we create a list of tuples containing trimmed strings and indices:
O(n × trim)
wheretrim ≤ s
- Sorting this list of
n
tuples requiresO(n × log n)
comparisons - Each comparison between trimmed strings takes
O(trim)
time in the worst case, wheretrim ≤ s
- Combined:
O(m × n × log n × s)
The space complexity is O(n × s)
, where:
- For each query, we create a temporary list
t
containingn
tuples - Each tuple stores a trimmed string of length up to
s
and an index - The trimmed strings require
O(n × s)
space in the worst case whentrim = s
- The output list
ans
storesm
integers:O(m)
- Overall space complexity is dominated by
O(n × s)
Note: The reference answer states space complexity as O(n)
, which appears to only account for the number of elements stored but not the string content itself. The more precise analysis should include the space for storing the trimmed strings.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Incorrect String Comparison for Numbers
Issue: A common mistake is assuming that string comparison of numbers works the same as numeric comparison. For instance, comparing "9" and "10" as strings would incorrectly place "9" > "10" because '9' > '1' lexicographically.
Why it works here: This is actually NOT a problem in this specific case because all trimmed strings have the same length (we trim to exactly trim
digits). When comparing equal-length digit strings, lexicographic comparison gives the correct numerical ordering. For example, "09" < "10" and "99" < "100" would never occur since both strings have the same length after trimming.
Pitfall 2: Forgetting 1-Based Indexing
Issue: The problem states that k is 1-indexed (finding the k-th smallest), but Python lists use 0-based indexing. Forgetting to subtract 1 when accessing the sorted list would cause an off-by-one error.
Incorrect:
result.append(trimmed_with_indices[k_position][1]) # Wrong!
Correct:
result.append(trimmed_with_indices[k_position - 1][1]) # Correct
Pitfall 3: Modifying the Original Array
Issue: Some might try to modify the nums
array in-place for efficiency, but the problem requires resetting to the original state between queries.
Incorrect approach:
# Don't do this - it modifies the original array
for i in range(len(nums)):
nums[i] = nums[i][-trim_length:]
# Now nums is permanently changed!
Correct approach: Create new trimmed values without modifying the original:
trimmed_with_indices = [(number[-trim_length:], index) for index, number in enumerate(nums)]
Pitfall 4: Using Only the Trimmed Value for Sorting
Issue: When trimmed values are equal, we need to use the original index as a tiebreaker. Simply sorting by trimmed values alone would give incorrect results.
Incorrect:
# This doesn't handle ties correctly
trimmed = [num[-trim_length:] for num in nums]
sorted_indices = sorted(range(len(nums)), key=lambda i: trimmed[i])
Correct: Using tuples ensures proper tiebreaking:
trimmed_with_indices = [(number[-trim_length:], index) for index, number in enumerate(nums)]
trimmed_with_indices.sort() # Automatically uses index as tiebreaker
Which of the following uses divide and conquer strategy?
Recommended Readings
https assets algo monster cover_photos dnd svg Divide and Conquer The main idea of divide and conquer is to split the main problem into two smaller components assuming that each one of the components had already been solved recursively and then try to solve the bigger problem using the solutions
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
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Want a Structured Path to Master System Design Too? Don’t Miss This!