2040. Kth Smallest Product of Two Sorted Arrays
Problem Description
You are given two sorted integer arrays nums1 and nums2 (both 0-indexed), and an integer k. Your task is to find the k-th smallest product among all possible products formed by multiplying one element from nums1 with one element from nums2.
Specifically, you need to consider all products of the form nums1[i] * nums2[j] where 0 <= i < nums1.length and 0 <= j < nums2.length. After calculating all these products, sort them in ascending order and return the k-th value (using 1-based indexing, meaning k=1 refers to the smallest product).
For example, if nums1 = [2, 5] and nums2 = [3, 4], the possible products are:
2 * 3 = 62 * 4 = 85 * 3 = 155 * 4 = 20
Sorted in ascending order: [6, 8, 15, 20]. If k = 2, the answer would be 8.
Note that the arrays can contain negative numbers, zero, or positive numbers, and the arrays are already sorted in non-decreasing order.
Intuition
The naive approach would be to generate all possible products nums1[i] * nums2[j], sort them, and return the k-th element. However, with arrays of size up to 10^5, this would create up to 10^10 products, which is too memory-intensive and time-consuming.
The key insight is that we don't need to generate all products explicitly. Instead, we can use binary search on the answer space. If we pick a candidate value p, we can efficiently count how many products are less than or equal to p without generating all products.
Why binary search? Because if we can count how many products are <= p for any given p, we can find the smallest p such that exactly k products are <= p. This p would be our k-th smallest product.
The challenge becomes: given a value p, how do we count products <= p efficiently?
For each element x in nums1, we need to count how many elements y in nums2 satisfy x * y <= p. This depends on the sign of x:
-
If
x > 0: We needy <= p/x. Sincenums2is sorted and multiplication by positivexpreserves order, we can use binary search to find the rightmost position wherenums2[j] <= p/x. -
If
x < 0: We needy >= p/x(inequality flips when dividing by negative). Sincenums2is sorted and multiplication by negativexreverses order, larger values ofygive smaller products. We can use binary search to find the leftmost position wherenums2[j] >= p/x. -
If
x = 0: The product is always0. Ifp >= 0, all elements innums2contribute to the count.
By efficiently counting products for each candidate p, we can binary search for the exact k-th smallest product value. The search range is bounded by the maximum possible product magnitude, which is the product of the maximum absolute values from both arrays.
Learn more about Binary Search patterns.
Solution Implementation
1class Solution:
2 def kthSmallestProduct(self, nums1: List[int], nums2: List[int], k: int) -> int:
3 def count_products_less_than_or_equal(target_product: int) -> int:
4 """
5 Count how many products from nums1[i] * nums2[j] are <= target_product
6 """
7 total_count = 0
8 nums2_length = len(nums2)
9
10 for num1 in nums1:
11 if num1 > 0:
12 # For positive num1, find how many nums2[j] satisfy num1 * nums2[j] <= target_product
13 # This means nums2[j] <= target_product / num1
14 total_count += bisect_right(nums2, target_product / num1)
15 elif num1 < 0:
16 # For negative num1, find how many nums2[j] satisfy num1 * nums2[j] <= target_product
17 # This means nums2[j] >= target_product / num1 (inequality flips for negative)
18 total_count += nums2_length - bisect_left(nums2, target_product / num1)
19 else:
20 # For num1 = 0, product is always 0
21 # Count all of nums2 if target_product >= 0, otherwise count 0
22 total_count += nums2_length * int(target_product >= 0)
23
24 return total_count
25
26 # Calculate the maximum possible absolute value of products
27 # This will be our search range boundary
28 max_absolute_value = max(abs(nums1[0]), abs(nums1[-1])) * max(abs(nums2[0]), abs(nums2[-1]))
29
30 # Binary search for the k-th smallest product in range [-max_absolute_value, max_absolute_value]
31 # bisect_left finds the leftmost position where count >= k
32 # Subtract max_absolute_value to convert from index to actual value
33 return bisect_left(range(-max_absolute_value, max_absolute_value + 1), k,
34 key=count_products_less_than_or_equal) - max_absolute_value
351class Solution {
2 private int[] nums1;
3 private int[] nums2;
4
5 /**
6 * Find the k-th smallest product from all possible products of nums1[i] * nums2[j]
7 * @param nums1 First sorted array
8 * @param nums2 Second sorted array
9 * @param k The k-th position (1-indexed)
10 * @return The k-th smallest product
11 */
12 public long kthSmallestProduct(int[] nums1, int[] nums2, long k) {
13 this.nums1 = nums1;
14 this.nums2 = nums2;
15
16 int m = nums1.length;
17 int n = nums2.length;
18
19 // Find the maximum absolute values in both arrays
20 int maxAbsNums1 = Math.max(Math.abs(nums1[0]), Math.abs(nums1[m - 1]));
21 int maxAbsNums2 = Math.max(Math.abs(nums2[0]), Math.abs(nums2[n - 1]));
22
23 // Set binary search boundaries for possible products
24 long right = (long) maxAbsNums1 * maxAbsNums2; // Maximum possible product
25 long left = -right; // Minimum possible product (negative)
26
27 // Binary search using template: find first product value where count >= k
28 long firstTrueValue = right; // Default if not found
29
30 while (left <= right) {
31 long mid = left + (right - left) / 2;
32
33 // Count how many products are <= mid
34 if (count(mid) >= k) {
35 // Feasible: at least k products are <= mid
36 firstTrueValue = mid;
37 right = mid - 1; // Search for smaller valid value
38 } else {
39 // Not feasible: fewer than k products <= mid
40 left = mid + 1;
41 }
42 }
43
44 return firstTrueValue;
45 }
46
47 /**
48 * Count how many products from nums1[i] * nums2[j] are less than or equal to threshold
49 * @param threshold The upper bound for counting products
50 * @return Number of products <= threshold
51 */
52 private long count(long threshold) {
53 long count = 0;
54 int n = nums2.length;
55
56 // For each element in nums1, count valid products with nums2
57 for (int x : nums1) {
58 if (x > 0) {
59 // For positive x, find first index where x * nums2[index] > threshold
60 // Using template: feasible(mid) = (x * nums2[mid] > threshold)
61 int left = 0;
62 int right = n - 1;
63 int firstTrueIndex = n; // Default: no product > threshold
64
65 while (left <= right) {
66 int mid = left + (right - left) / 2;
67
68 if ((long) x * nums2[mid] > threshold) {
69 // Feasible: product > threshold
70 firstTrueIndex = mid;
71 right = mid - 1;
72 } else {
73 // Not feasible: product <= threshold
74 left = mid + 1;
75 }
76 }
77
78 // All products from index 0 to firstTrueIndex-1 are valid
79 count += firstTrueIndex;
80
81 } else if (x < 0) {
82 // For negative x, find first index where x * nums2[index] <= threshold
83 // Using template: feasible(mid) = (x * nums2[mid] <= threshold)
84 int left = 0;
85 int right = n - 1;
86 int firstTrueIndex = n; // Default: no product <= threshold
87
88 while (left <= right) {
89 int mid = left + (right - left) / 2;
90
91 if ((long) x * nums2[mid] <= threshold) {
92 // Feasible: product <= threshold
93 firstTrueIndex = mid;
94 right = mid - 1;
95 } else {
96 // Not feasible: product > threshold
97 left = mid + 1;
98 }
99 }
100
101 // All products from index firstTrueIndex to n-1 are valid
102 count += n - firstTrueIndex;
103
104 } else {
105 // x == 0, product is always 0
106 if (threshold >= 0) {
107 // All products with 0 are valid
108 count += n;
109 }
110 }
111 }
112
113 return count;
114 }
115}
1161class Solution {
2public:
3 long long kthSmallestProduct(vector<int>& nums1, vector<int>& nums2, long long k) {
4 int m = nums1.size();
5 int n = nums2.size();
6
7 // Calculate the range for binary search
8 // The maximum absolute value from nums1
9 int maxAbsNums1 = max(abs(nums1[0]), abs(nums1[m - 1]));
10 // The maximum absolute value from nums2
11 int maxAbsNums2 = max(abs(nums2[0]), abs(nums2[n - 1]));
12 // The maximum possible product value
13 long long maxProduct = 1LL * maxAbsNums1 * maxAbsNums2;
14 // Search range: [-maxProduct, maxProduct]
15 long long left = -maxProduct;
16 long long right = maxProduct;
17
18 // Lambda function to count how many products are <= threshold
19 auto countProductsLessOrEqual = [&](long long threshold) {
20 long long count = 0;
21
22 // For each element in nums1, count valid products with nums2
23 for (int num1 : nums1) {
24 if (num1 > 0) {
25 // For positive num1, find first index where num1 * nums2[j] > threshold
26 // Using template: feasible(mid) = (num1 * nums2[mid] > threshold)
27 int left = 0;
28 int right = n - 1;
29 int firstTrueIndex = n; // Default: no product > threshold
30
31 while (left <= right) {
32 int mid = left + (right - left) / 2;
33 if (1LL * num1 * nums2[mid] > threshold) {
34 firstTrueIndex = mid;
35 right = mid - 1;
36 } else {
37 left = mid + 1;
38 }
39 }
40 count += firstTrueIndex;
41 } else if (num1 < 0) {
42 // For negative num1, find first index where num1 * nums2[j] <= threshold
43 // Using template: feasible(mid) = (num1 * nums2[mid] <= threshold)
44 int left = 0;
45 int right = n - 1;
46 int firstTrueIndex = n; // Default: no product <= threshold
47
48 while (left <= right) {
49 int mid = left + (right - left) / 2;
50 if (1LL * num1 * nums2[mid] <= threshold) {
51 firstTrueIndex = mid;
52 right = mid - 1;
53 } else {
54 left = mid + 1;
55 }
56 }
57 count += n - firstTrueIndex;
58 } else {
59 // num1 == 0, product is always 0
60 // If threshold >= 0, all products with nums2 are valid
61 if (threshold >= 0) {
62 count += n;
63 }
64 }
65 }
66 return count;
67 };
68
69 // Binary search using template: find first product value where count >= k
70 long long firstTrueValue = right; // Default if not found
71
72 while (left <= right) {
73 long long mid = left + (right - left) / 2;
74 // If there are at least k products <= mid, this is feasible
75 if (countProductsLessOrEqual(mid) >= k) {
76 firstTrueValue = mid;
77 right = mid - 1; // Search for smaller valid value
78 } else {
79 left = mid + 1;
80 }
81 }
82
83 return firstTrueValue;
84 }
85};
861/**
2 * Finds the k-th smallest product from all possible products of pairs (nums1[i], nums2[j])
3 * @param nums1 - First sorted array of integers
4 * @param nums2 - Second sorted array of integers
5 * @param k - The k-th position to find (1-indexed)
6 * @returns The k-th smallest product
7 */
8function kthSmallestProduct(nums1: number[], nums2: number[], k: number): number {
9 const firstArrayLength = nums1.length;
10 const secondArrayLength = nums2.length;
11
12 // Calculate the maximum absolute values to determine search bounds
13 const maxAbsFirstArray = BigInt(Math.max(Math.abs(nums1[0]), Math.abs(nums1[firstArrayLength - 1])));
14 const maxAbsSecondArray = BigInt(Math.max(Math.abs(nums2[0]), Math.abs(nums2[secondArrayLength - 1])));
15
16 // Set binary search bounds for the product value
17 let leftBound = -maxAbsFirstArray * maxAbsSecondArray;
18 let rightBound = maxAbsFirstArray * maxAbsSecondArray;
19
20 /**
21 * Counts how many products are less than or equal to the threshold
22 * @param threshold - The product value threshold to count against
23 * @returns Number of products <= threshold
24 */
25 const countProductsLessThanOrEqual = (threshold: bigint): bigint => {
26 let totalCount = 0n;
27
28 for (const num1 of nums1) {
29 const bigNum1 = BigInt(num1);
30
31 if (bigNum1 > 0n) {
32 // For positive num1, find first index where num1 * nums2[j] > threshold
33 // Using template: feasible(mid) = (num1 * nums2[mid] > threshold)
34 let left = 0;
35 let right = secondArrayLength - 1;
36 let firstTrueIndex = secondArrayLength; // Default: no product > threshold
37
38 while (left <= right) {
39 const mid = Math.floor((left + right) / 2);
40 const product = bigNum1 * BigInt(nums2[mid]);
41
42 if (product > threshold) {
43 firstTrueIndex = mid;
44 right = mid - 1;
45 } else {
46 left = mid + 1;
47 }
48 }
49 totalCount += BigInt(firstTrueIndex);
50
51 } else if (bigNum1 < 0n) {
52 // For negative num1, find first index where num1 * nums2[j] <= threshold
53 // Using template: feasible(mid) = (num1 * nums2[mid] <= threshold)
54 let left = 0;
55 let right = secondArrayLength - 1;
56 let firstTrueIndex = secondArrayLength; // Default: no product <= threshold
57
58 while (left <= right) {
59 const mid = Math.floor((left + right) / 2);
60 const product = bigNum1 * BigInt(nums2[mid]);
61
62 if (product <= threshold) {
63 firstTrueIndex = mid;
64 right = mid - 1;
65 } else {
66 left = mid + 1;
67 }
68 }
69 totalCount += BigInt(secondArrayLength - firstTrueIndex);
70
71 } else if (threshold >= 0n) {
72 // If num1 is 0, all products are 0
73 // Count all pairs if threshold is non-negative
74 totalCount += BigInt(secondArrayLength);
75 }
76 }
77
78 return totalCount;
79 };
80
81 // Binary search using template: find first product value where count >= k
82 let firstTrueValue = rightBound; // Default if not found
83
84 while (leftBound <= rightBound) {
85 const midValue = (leftBound + rightBound) / 2n;
86
87 if (countProductsLessThanOrEqual(midValue) >= BigInt(k)) {
88 // Feasible: at least k products are <= mid
89 firstTrueValue = midValue;
90 rightBound = midValue - 1n;
91 } else {
92 leftBound = midValue + 1n;
93 }
94 }
95
96 return Number(firstTrueValue);
97}
98Solution Approach
The solution uses binary search on the answer space combined with a counting function. Here's the step-by-step implementation:
1. Define the Search Range: First, we calculate the maximum possible absolute product value:
mx = max(abs(nums1[0]), abs(nums1[-1])) * max(abs(nums2[0]), abs(nums2[-1]))
Since the arrays are sorted, the extreme values are at the endpoints. The search range becomes [-mx, mx].
2. Implement the Counting Function:
The count(p) function calculates how many products are less than or equal to p:
def count(p: int) -> int:
cnt = 0
n = len(nums2)
for x in nums1:
if x > 0:
cnt += bisect_right(nums2, p / x)
elif x < 0:
cnt += n - bisect_left(nums2, p / x)
else:
cnt += n * int(p >= 0)
return cnt
For each element x in nums1:
-
When
x > 0: We find how many elements innums2satisfyy <= p/x. Usingbisect_right(nums2, p/x)gives us the count of such elements since it returns the insertion position after all elements<= p/x. -
When
x < 0: We need elements wherey >= p/x(inequality flips). Usingbisect_left(nums2, p/x)finds the first position wherenums2[j] >= p/x. Therefore,n - bisect_left(nums2, p/x)gives the count of valid elements. -
When
x = 0: All products equal0. Ifp >= 0, allnelements ofnums2produce valid products; otherwise, none do.
3. Binary Search for the k-th Smallest:
The main binary search uses Python's bisect_left with a custom key function:
return bisect_left(range(-mx, mx + 1), k, key=count) - mx
This searches for the leftmost position in the range [-mx, mx] where count(p) >= k. The key=count parameter transforms each value p in the range to count(p) before comparison.
The function effectively finds the smallest p such that at least k products are <= p. Since we're searching in range(-mx, mx + 1) which starts from 0, we subtract mx from the result to get the actual value.
Time Complexity: O(m * log(n) * log(P)) where m = len(nums1), n = len(nums2), and P is the range of possible products. The outer binary search takes O(log(P)) iterations, and each iteration calls count() which takes O(m * log(n)) time.
Space Complexity: O(1) as we only use constant extra space beyond the input arrays.
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 small example to illustrate the solution approach.
Example: nums1 = [-2, 1, 3], nums2 = [-1, 2, 4], k = 5
Step 1: Calculate all possible products (for understanding) Let's first see what all products look like:
-2 * -1 = 2-2 * 2 = -4-2 * 4 = -81 * -1 = -11 * 2 = 21 * 4 = 43 * -1 = -33 * 2 = 63 * 4 = 12
Sorted: [-8, -4, -3, -1, 2, 2, 4, 6, 12]
The 5th smallest is 2.
Step 2: Apply our binary search solution
Calculate search range:
mx = max(|-2|, |3|) * max(|-1|, |4|) = 3 * 4 = 12- Search range:
[-12, 12]
Step 3: Binary search process
We'll binary search for the smallest value p where count(p) >= 5.
Let's trace through some key iterations:
When p = 0:
- For
x = -2(negative): Needy >= 0/-2 = 0. Elements innums2that are>= 0:[2, 4]. Count = 2 - For
x = 1(positive): Needy <= 0/1 = 0. Elements innums2that are<= 0:[-1]. Count = 1 - For
x = 3(positive): Needy <= 0/3 = 0. Elements innums2that are<= 0:[-1]. Count = 1 - Total count(0) = 2 + 1 + 1 = 4 (less than k=5)
When p = 2:
- For
x = -2(negative): Needy >= 2/-2 = -1. Elements innums2that are>= -1:[-1, 2, 4]. Count = 3 - For
x = 1(positive): Needy <= 2/1 = 2. Elements innums2that are<= 2:[-1, 2]. Count = 2 - For
x = 3(positive): Needy <= 2/3 = 0.67. Elements innums2that are<= 0.67:[-1]. Count = 1 - Total count(2) = 3 + 2 + 1 = 6 (greater than or equal to k=5)
When p = 1:
- For
x = -2: Needy >= -0.5. Elements>= -0.5:[2, 4]. Count = 2 - For
x = 1: Needy <= 1. Elements<= 1:[-1]. Count = 1 - For
x = 3: Needy <= 0.33. Elements<= 0.33:[-1]. Count = 1 - Total count(1) = 2 + 1 + 1 = 4 (less than k=5)
The binary search finds that p = 2 is the smallest value where count(p) >= 5, which matches our expected answer.
Key Insight: We never actually computed all 9 products. Instead, we efficiently counted how many products were less than or equal to candidate values, allowing us to pinpoint the k-th smallest product through binary search.
Time and Space Complexity
Time Complexity: O(m × log n × log M)
The time complexity breaks down as follows:
- The outer binary search using
bisect_lefton the range[-mx, mx + 1]performsO(log M)iterations, whereM = max(abs(nums1[0]), abs(nums1[-1])) × max(abs(nums2[0]), abs(nums2[-1]))represents the maximum possible absolute value of products. - For each binary search iteration, the
countfunction is called, which iterates through allmelements innums1, takingO(m)time. - Within each iteration of
count, eitherbisect_rightorbisect_leftis called onnums2, which takesO(log n)time wherenis the length ofnums2. - Combining these operations:
O(log M) × O(m) × O(log n) = O(m × log n × log M)
Space Complexity: O(1)
The space complexity analysis:
- The
countfunction uses only a constant amount of extra space for variablescnt,n, andx. - The
bisect_leftandbisect_rightfunctions operate in-place and useO(1)extra space. - The range object
range(-mx, mx + 1)is a lazy iterator that doesn't create the entire list in memory, usingO(1)space. - No additional data structures are created that scale with input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Using the Wrong Binary Search Template Variant
A common mistake is using while left < right with right = mid instead of the standard boundary-finding template. The correct template uses while left <= right with right = mid - 1 and tracks the answer with firstTrueIndex.
Incorrect Implementation:
while (left < right) { long mid = (left + right) >> 1; if (count(mid) >= k) { right = mid; // Wrong pattern } else { left = mid + 1; } } return left; // Returns left directly
Correct Implementation:
long firstTrueValue = right; // Default if not found while (left <= right) { long mid = left + (right - left) / 2; if (count(mid) >= k) { firstTrueValue = mid; // Track the answer right = mid - 1; } else { left = mid + 1; } } return firstTrueValue;
2. Floating-Point Precision Issues
The most critical pitfall in this solution is using floating-point division (p / x) when working with potentially large integers. This can lead to precision errors that cause incorrect counting.
Problem Example:
# When p = 10^9 and x = 3 # p / x = 333333333.3333... # Due to floating-point representation, this might become 333333333.33333334 # This can cause bisect_right/bisect_left to return wrong positions
Solution: Replace floating-point division with integer arithmetic:
def count_products_less_than_or_equal(target_product: int) -> int:
total_count = 0
nums2_length = len(nums2)
for num1 in nums1:
if num1 > 0:
# Find how many nums2[j] satisfy num1 * nums2[j] <= target_product
left, right = 0, nums2_length
while left < right:
mid = (left + right) // 2
if num1 * nums2[mid] <= target_product:
left = mid + 1
else:
right = mid
total_count += left
elif num1 < 0:
# Find how many nums2[j] satisfy num1 * nums2[j] <= target_product
left, right = 0, nums2_length
while left < right:
mid = (left + right) // 2
if num1 * nums2[mid] <= target_product:
right = mid
else:
left = mid + 1
total_count += nums2_length - left
else:
total_count += nums2_length * int(target_product >= 0)
return total_count
2. Integer Overflow in Product Calculation
When calculating max_absolute_value or products during comparison, the multiplication might overflow in languages with fixed integer sizes (though Python handles arbitrary precision integers).
Problem Example:
# If nums1[-1] = 10^5 and nums2[-1] = 10^5 # Their product is 10^10, which exceeds 32-bit integer range
Solution: For languages with overflow concerns, use long/int64 types or implement overflow-safe multiplication checks:
def safe_multiply(a: int, b: int, limit: int) -> int:
"""Returns min(a * b, limit) without overflow"""
if a == 0 or b == 0:
return 0
if abs(a) > limit // abs(b):
return limit if (a > 0) == (b > 0) else -limit
return a * b
3. Incorrect Handling of Mixed Sign Arrays
The initial calculation of max_absolute_value assumes the maximum absolute product comes from endpoint values, but with mixed signs, the actual range might be different.
Problem Example:
# nums1 = [-10, -1, 1, 10] # nums2 = [-10, -1, 1, 10] # Maximum product: 10 * 10 = 100 # Minimum product: -10 * 10 = -100 # But if we only check endpoints, we might miss interior combinations
Solution: Calculate the actual minimum and maximum possible products:
# Calculate all possible extreme products
candidates = [
nums1[0] * nums2[0],
nums1[0] * nums2[-1],
nums1[-1] * nums2[0],
nums1[-1] * nums2[-1]
]
min_product = min(candidates)
max_product = max(candidates)
# Then use range [min_product, max_product] for binary search
return bisect_left(range(min_product, max_product + 1), k,
key=count_products_less_than_or_equal) - min_product
4. Off-by-One Error in Binary Search Range
Using range(-mx, mx + 1) assumes all products fall within [-mx, mx], but the actual minimum product might not be -mx.
Solution: Always calculate the exact minimum and maximum products rather than assuming symmetric ranges around zero.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
Recommended Readings
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
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
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!