Facebook Pixel

786. K-th Smallest Prime Fraction

Problem Description

You have a sorted array arr that contains the number 1 and prime numbers. All numbers in the array are unique. You also have an integer k.

The task is to consider all possible fractions formed by dividing elements in the array. Specifically, for every pair of indices i and j where i < j, you form the fraction arr[i] / arr[j].

Once you have all these fractions, you need to find the k-th smallest fraction among them. The answer should be returned as an array of two integers [arr[i], arr[j]], representing the numerator and denominator of that k-th smallest fraction.

For example, if arr = [1, 2, 3, 5], the possible fractions would be:

  • 1/2, 1/3, 1/5 (fractions with numerator 1)
  • 2/3, 2/5 (fractions with numerator 2)
  • 3/5 (fraction with numerator 3)

These fractions in ascending order would be: 1/5, 1/3, 2/5, 1/2, 3/5, 2/3. So if k = 3, the answer would be [2, 5] since 2/5 is the 3rd smallest fraction.

The solution uses a min-heap approach where it starts with the smallest possible fractions (those with numerator arr[0] = 1) and incrementally explores larger fractions by advancing the numerator index while keeping the denominator fixed, maintaining the k smallest fractions seen so far.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that since the array is sorted, for any fixed denominator arr[j], the fractions arr[0]/arr[j], arr[1]/arr[j], ..., arr[j-1]/arr[j] are in increasing order. This property allows us to avoid generating and sorting all possible fractions.

Think of it this way: if we were to list all fractions with denominator arr[j], they would increase as we move the numerator from left to right in the array. Similarly, for a fixed numerator arr[i], the fractions decrease as we move the denominator from left to right (since we're dividing by larger numbers).

This leads us to a pattern similar to merging k sorted lists. We can start with the smallest possible fractions - those with the smallest numerator (arr[0] = 1) paired with each denominator. These are 1/arr[1], 1/arr[2], ..., 1/arr[n-1], which we know are sorted in decreasing order.

We use a min-heap to always extract the smallest fraction among our candidates. When we pop a fraction arr[i]/arr[j] from the heap, we know the next larger fraction with the same denominator arr[j] would be arr[i+1]/arr[j] (if it exists). By adding this next fraction to the heap, we ensure we're always considering the smallest unprocessed fractions.

This approach is efficient because:

  1. We don't generate all O(n²) fractions upfront
  2. We only generate fractions as needed, maintaining at most O(n) fractions in the heap at any time
  3. We leverage the sorted property of the array to systematically explore fractions in increasing order

The process continues until we've popped k-1 fractions from the heap. The fraction at the top of the heap after k-1 pops is our answer - the k-th smallest fraction.

Learn more about Two Pointers, Binary Search, Sorting and Heap (Priority Queue) patterns.

Solution Approach

The implementation uses a min-heap data structure to efficiently find the k-th smallest fraction without generating all possible fractions.

Step 1: Initialize the heap

h = [(1 / y, 0, j + 1) for j, y in enumerate(arr[1:])]

We start by creating initial heap entries. For each element arr[j] where j > 0, we create a tuple containing:

  • The fraction value: 1/arr[j] (since arr[0] = 1)
  • The numerator index: 0 (pointing to arr[0])
  • The denominator index: j + 1 (adjusted because we enumerate from arr[1:])

This gives us fractions 1/arr[1], 1/arr[2], ..., 1/arr[n-1] - all fractions with the smallest possible numerator.

Step 2: Convert to heap

heapify(h)

The heapify operation arranges the list into a min-heap based on the fraction values. The smallest fraction 1/arr[n-1] will be at the root (since arr is sorted and arr[n-1] is the largest).

Step 3: Extract k-1 smallest fractions

for _ in range(k - 1):
    _, i, j = heappop(h)
    if i + 1 < j:
        heappush(h, (arr[i + 1] / arr[j], i + 1, j))

We pop the smallest fraction k-1 times. Each time we pop a fraction arr[i]/arr[j]:

  • We check if there's a next fraction with the same denominator: arr[i+1]/arr[j]
  • The condition i + 1 < j ensures we don't create invalid fractions where the numerator index meets or exceeds the denominator index
  • If valid, we push this next fraction into the heap

This process maintains the invariant that the heap always contains the smallest unprocessed fraction for each "chain" of fractions with the same denominator.

Step 4: Return the k-th smallest fraction

return [arr[h[0][1]], arr[h[0][2]]]

After popping k-1 fractions, the root of the heap contains the k-th smallest fraction. We extract the numerator and denominator indices from the tuple and return the corresponding array values.

Time Complexity: O(k log n) where we perform at most k heap operations, each taking O(log n) time.

Space Complexity: O(n) for storing at most n-1 fractions in the heap at any time.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through finding the 4th smallest fraction with arr = [1, 2, 3, 5] and k = 4.

Step 1: Initialize heap with fractions having numerator 1

We start with all fractions where numerator is arr[0] = 1:

  • 1/2 = 0.5 (numerator index: 0, denominator index: 1)
  • 1/3 ≈ 0.333 (numerator index: 0, denominator index: 2)
  • 1/5 = 0.2 (numerator index: 0, denominator index: 3)

After heapify, our min-heap looks like:

Heap: [(0.2, 0, 3), (0.333, 0, 2), (0.5, 0, 1)]
       1/5         1/3         1/2

Step 2: Extract first smallest (k=1)

Pop (0.2, 0, 3) which represents 1/5.

  • Check if we can add next fraction with same denominator: arr[0+1]/arr[3] = 2/5
  • Since 0+1 < 3, we add (0.4, 1, 3) to heap
Heap: [(0.333, 0, 2), (0.4, 1, 3), (0.5, 0, 1)]
       1/3           2/5         1/2

Step 3: Extract second smallest (k=2)

Pop (0.333, 0, 2) which represents 1/3.

  • Check next fraction: arr[0+1]/arr[2] = 2/3
  • Since 0+1 < 2, we add (0.667, 1, 2) to heap
Heap: [(0.4, 1, 3), (0.5, 0, 1), (0.667, 1, 2)]
       2/5         1/2         2/3

Step 4: Extract third smallest (k=3)

Pop (0.4, 1, 3) which represents 2/5.

  • Check next fraction: arr[1+1]/arr[3] = 3/5
  • Since 1+1 < 3, we add (0.6, 2, 3) to heap
Heap: [(0.5, 0, 1), (0.6, 2, 3), (0.667, 1, 2)]
       1/2         3/5         2/3

Step 5: Return the 4th smallest

After extracting k-1 = 3 fractions, the root of heap (0.5, 0, 1) represents the 4th smallest fraction.

  • Numerator: arr[0] = 1
  • Denominator: arr[1] = 2
  • Answer: [1, 2]

The fractions we encountered in order were: 1/5, 1/3, 2/5, and our answer 1/2 - exactly the 4th smallest fraction.

Solution Implementation

1from typing import List
2from heapq import heapify, heappop, heappush
3
4class Solution:
5    def kthSmallestPrimeFraction(self, arr: List[int], k: int) -> List[int]:
6        # Initialize heap with fractions where numerator is arr[0] and denominators are arr[1:]
7        # Each tuple contains: (fraction_value, numerator_index, denominator_index)
8        heap = [(arr[0] / arr[j], 0, j) for j in range(1, len(arr))]
9      
10        # Convert list to a min-heap based on fraction values
11        heapify(heap)
12      
13        # Pop k-1 smallest fractions to get to the k-th smallest
14        for _ in range(k - 1):
15            fraction_value, numerator_idx, denominator_idx = heappop(heap)
16          
17            # If we can form a new fraction with the next larger numerator
18            # and the same denominator, add it to the heap
19            if numerator_idx + 1 < denominator_idx:
20                new_fraction = arr[numerator_idx + 1] / arr[denominator_idx]
21                heappush(heap, (new_fraction, numerator_idx + 1, denominator_idx))
22      
23        # The k-th smallest fraction is now at the top of the heap
24        _, final_numerator_idx, final_denominator_idx = heap[0]
25      
26        # Return the numerator and denominator as a list
27        return [arr[final_numerator_idx], arr[final_denominator_idx]]
28
1class Solution {
2    public int[] kthSmallestPrimeFraction(int[] arr, int k) {
3        int n = arr.length;
4        // Min heap to store fractions, ordered by their values
5        PriorityQueue<Fraction> minHeap = new PriorityQueue<>();
6      
7        // Initialize heap with smallest fractions arr[0]/arr[j] for j from 1 to n-1
8        // Since arr is sorted, arr[0] is the smallest numerator
9        for (int j = 1; j < n; j++) {
10            minHeap.offer(new Fraction(arr[0], arr[j], 0, j));
11        }
12      
13        // Extract k-1 smallest fractions to get to the kth smallest
14        for (int count = 1; count < k; count++) {
15            Fraction current = minHeap.poll();
16          
17            // If we can form a larger fraction with the same denominator
18            // by incrementing the numerator index
19            if (current.numeratorIndex + 1 < current.denominatorIndex) {
20                minHeap.offer(new Fraction(
21                    arr[current.numeratorIndex + 1], 
22                    arr[current.denominatorIndex], 
23                    current.numeratorIndex + 1, 
24                    current.denominatorIndex
25                ));
26            }
27        }
28      
29        // The kth smallest fraction is now at the top of the heap
30        Fraction result = minHeap.peek();
31        return new int[] {result.numerator, result.denominator};
32    }
33  
34    // Inner class representing a fraction with its indices in the array
35    static class Fraction implements Comparable<Fraction> {
36        int numerator;          // Actual numerator value
37        int denominator;        // Actual denominator value
38        int numeratorIndex;     // Index of numerator in original array
39        int denominatorIndex;   // Index of denominator in original array
40      
41        public Fraction(int numerator, int denominator, int numeratorIndex, int denominatorIndex) {
42            this.numerator = numerator;
43            this.denominator = denominator;
44            this.numeratorIndex = numeratorIndex;
45            this.denominatorIndex = denominatorIndex;
46        }
47      
48        @Override
49        public int compareTo(Fraction other) {
50            // Compare fractions: a/b < c/d if a*d < c*b
51            // Returns negative if this < other, positive if this > other
52            return this.numerator * other.denominator - other.numerator * this.denominator;
53        }
54    }
55}
56
1class Solution {
2public:
3    vector<int> kthSmallestPrimeFraction(vector<int>& arr, int k) {
4        // Define pair type for storing indices of numerator and denominator
5        using IndexPair = pair<int, int>;
6      
7        int n = arr.size();
8      
9        // Custom comparator for min-heap based on fraction values
10        // Compare fractions a/b and c/d by cross multiplication: a*d vs b*c
11        auto fractionComparator = [&](const IndexPair& a, const IndexPair& b) {
12            return arr[a.first] * arr[b.second] > arr[b.first] * arr[a.second];
13        };
14      
15        // Min-heap to store fraction indices, ordered by fraction value
16        priority_queue<IndexPair, vector<IndexPair>, decltype(fractionComparator)> minHeap(fractionComparator);
17      
18        // Initialize heap with smallest fractions for each denominator
19        // For each denominator arr[j], start with numerator arr[0]
20        for (int j = 1; j < n; ++j) {
21            minHeap.push({0, j});  // {numerator_index, denominator_index}
22        }
23      
24        // Extract k-1 smallest fractions to reach the kth smallest
25        for (int i = 1; i < k; ++i) {
26            IndexPair currentFraction = minHeap.top();
27            minHeap.pop();
28          
29            // Add next fraction with same denominator but larger numerator
30            int nextNumeratorIndex = currentFraction.first + 1;
31            int denominatorIndex = currentFraction.second;
32          
33            if (nextNumeratorIndex < denominatorIndex) {
34                minHeap.push({nextNumeratorIndex, denominatorIndex});
35            }
36        }
37      
38        // The kth smallest fraction is now at the top of the heap
39        IndexPair kthFraction = minHeap.top();
40        return {arr[kthFraction.first], arr[kthFraction.second]};
41    }
42};
43
1function kthSmallestPrimeFraction(arr: number[], k: number): number[] {
2    // Type definition for storing indices of numerator and denominator
3    type IndexPair = [number, number];
4  
5    const n = arr.length;
6  
7    // Custom comparator for min-heap based on fraction values
8    // Compare fractions a/b and c/d by cross multiplication: a*d vs b*c
9    const fractionComparator = (a: IndexPair, b: IndexPair): number => {
10        return arr[a[0]] * arr[b[1]] - arr[b[0]] * arr[a[1]];
11    };
12  
13    // Min-heap to store fraction indices, ordered by fraction value
14    // Using an array-based implementation with manual heap operations
15    const minHeap: IndexPair[] = [];
16  
17    // Helper function to maintain heap property after insertion
18    const heapifyUp = (index: number): void => {
19        while (index > 0) {
20            const parentIndex = Math.floor((index - 1) / 2);
21            if (fractionComparator(minHeap[index], minHeap[parentIndex]) < 0) {
22                [minHeap[index], minHeap[parentIndex]] = [minHeap[parentIndex], minHeap[index]];
23                index = parentIndex;
24            } else {
25                break;
26            }
27        }
28    };
29  
30    // Helper function to maintain heap property after extraction
31    const heapifyDown = (index: number): void => {
32        while (true) {
33            let smallest = index;
34            const leftChild = 2 * index + 1;
35            const rightChild = 2 * index + 2;
36          
37            if (leftChild < minHeap.length && fractionComparator(minHeap[leftChild], minHeap[smallest]) < 0) {
38                smallest = leftChild;
39            }
40            if (rightChild < minHeap.length && fractionComparator(minHeap[rightChild], minHeap[smallest]) < 0) {
41                smallest = rightChild;
42            }
43          
44            if (smallest !== index) {
45                [minHeap[index], minHeap[smallest]] = [minHeap[smallest], minHeap[index]];
46                index = smallest;
47            } else {
48                break;
49            }
50        }
51    };
52  
53    // Helper function to push element into heap
54    const heapPush = (pair: IndexPair): void => {
55        minHeap.push(pair);
56        heapifyUp(minHeap.length - 1);
57    };
58  
59    // Helper function to pop minimum element from heap
60    const heapPop = (): IndexPair => {
61        const min = minHeap[0];
62        minHeap[0] = minHeap[minHeap.length - 1];
63        minHeap.pop();
64        if (minHeap.length > 0) {
65            heapifyDown(0);
66        }
67        return min;
68    };
69  
70    // Initialize heap with smallest fractions for each denominator
71    // For each denominator arr[j], start with numerator arr[0]
72    for (let j = 1; j < n; j++) {
73        heapPush([0, j]); // [numerator_index, denominator_index]
74    }
75  
76    // Extract k-1 smallest fractions to reach the kth smallest
77    for (let i = 1; i < k; i++) {
78        const currentFraction = heapPop();
79      
80        // Add next fraction with same denominator but larger numerator
81        const nextNumeratorIndex = currentFraction[0] + 1;
82        const denominatorIndex = currentFraction[1];
83      
84        if (nextNumeratorIndex < denominatorIndex) {
85            heapPush([nextNumeratorIndex, denominatorIndex]);
86        }
87    }
88  
89    // The kth smallest fraction is now at the top of the heap
90    const kthFraction = minHeap[0];
91    return [arr[kthFraction[0]], arr[kthFraction[1]]];
92}
93

Time and Space Complexity

Time Complexity: O(k * log n) where n is the length of the input array and k is the given parameter.

  • Initial heap construction: Creating the initial heap with n-1 elements (fractions with numerator arr[0] and all other elements as denominators) takes O(n) time for list creation and O(n) for heapification.
  • Main loop: The loop runs exactly k-1 times. In each iteration:
    • heappop() operation: O(log n) since the heap can contain at most n-1 elements
    • heappush() operation: O(log n) for the same reason
  • Final answer extraction: O(1) to access the minimum element from the heap
  • Overall: O(n) + O(k * log n) = O(n + k * log n)
  • Since typically k ≤ n(n-1)/2 and often k is much smaller, the dominant term is usually O(k * log n) when k > n, otherwise O(n) dominates for small k.

Space Complexity: O(n) where n is the length of the input array.

  • The heap initially contains n-1 elements (one fraction for each element except the first as denominator)
  • Throughout execution, the heap size never exceeds n-1 elements because:
    • We start with n-1 fractions
    • Each heappop() removes one element and each heappush() adds at most one element
    • The heap maintains at most one fraction for each possible denominator
  • Therefore, the space complexity is O(n)

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

Common Pitfalls

1. Incorrect Index Mapping When Initializing the Heap

One of the most common mistakes is incorrectly mapping indices when creating the initial heap entries. The original solution uses:

h = [(1 / y, 0, j + 1) for j, y in enumerate(arr[1:])]

The pitfall occurs when developers forget that enumerate(arr[1:]) starts counting from 0, but these indices correspond to elements starting from position 1 in the original array. This leads to confusion about whether to use j or j + 1 for the denominator index.

Incorrect attempt:

# Wrong: Using j directly instead of j + 1
heap = [(arr[0] / arr[j], 0, j) for j in range(1, len(arr))]
# This would incorrectly map to arr[1], arr[2], ... arr[n-2] as denominators

Correct solution:

# Clear and explicit indexing
heap = [(arr[0] / arr[j], 0, j) for j in range(1, len(arr))]
# Now j directly represents the index in the original array

2. Off-by-One Error in the Boundary Check

Another common mistake is using the wrong comparison operator when checking if we can create the next fraction:

Incorrect:

if i + 1 <= j:  # Wrong: allows i + 1 == j
    heappush(h, (arr[i + 1] / arr[j], i + 1, j))

This would allow creating fractions where the numerator and denominator are the same element (when i + 1 == j), resulting in a fraction equal to 1, which violates the problem constraint that i < j.

Correct:

if i + 1 < j:  # Ensures numerator index is strictly less than denominator index
    heappush(h, (arr[i + 1] / arr[j], i + 1, j))

3. Accessing Heap After All Elements Are Popped

If k equals the total number of possible fractions, the loop might pop all elements from the heap, leaving it empty:

Problematic scenario:

for _ in range(k - 1):
    _, i, j = heappop(h)
    if i + 1 < j:
        heappush(h, (arr[i + 1] / arr[j], i + 1, j))
# If heap is empty here, h[0] will raise an IndexError
return [arr[h[0][1]], arr[h[0][2]]]

Robust solution:

# Store the last popped element
for _ in range(k - 1):
    fraction_value, i, j = heappop(h)
    if i + 1 < j:
        heappush(h, (arr[i + 1] / arr[j], i + 1, j))

# Check if heap is empty (edge case)
if not h:
    # Return the last popped fraction
    return [arr[i], arr[j]]
else:
    return [arr[h[0][1]], arr[h[0][2]]]

4. Integer Division Instead of Float Division

In Python 2 or when using floor division, this could be a problem:

Incorrect (Python 2 style):

# This would perform integer division in Python 2
heap = [(1 / arr[j], 0, j) for j in range(1, len(arr))]

Correct (Python 3 compatible):

# Ensure float division
heap = [(float(arr[0]) / arr[j], 0, j) for j in range(1, len(arr))]
# Or use: arr[0] * 1.0 / arr[j]

5. Not Handling Edge Cases

The solution should handle edge cases like:

  • k = 1: Should return the smallest fraction without any heap pops
  • Array with only 2 elements: Only one possible fraction
  • Very large arrays where numerical precision might be an issue

Enhanced solution with edge case handling:

def kthSmallestPrimeFraction(self, arr: List[int], k: int) -> List[int]:
    n = len(arr)
  
    # Edge case: only one fraction possible
    if n == 2:
        return arr
  
    # Initialize heap
    heap = [(arr[0] / arr[j], 0, j) for j in range(1, n)]
    heapify(heap)
  
    # Handle k = 1 case efficiently
    if k == 1:
        return [arr[0], arr[-1]]
  
    # Process k-1 smallest fractions
    for _ in range(k - 1):
        if not heap:
            break
        _, i, j = heappop(heap)
        if i + 1 < j:
            heappush(heap, (arr[i + 1] / arr[j], i + 1, j))
  
    # Return the k-th smallest
    return [arr[heap[0][1]], arr[heap[0][2]]] if heap else [arr[i], arr[j]]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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!

Load More