Facebook Pixel

1237. Find Positive Integer Solution for a Given Equation

Problem Description

You are given a callable function f(x, y) with a hidden formula and a target value z. Your task is to find all positive integer pairs (x, y) such that f(x, y) == z.

The function f(x, y) has the following properties:

  • It takes two positive integers x and y as inputs
  • It returns a positive integer based on a hidden formula
  • It is monotonically increasing in both parameters:
    • f(x, y) < f(x + 1, y) (increasing with respect to x)
    • f(x, y) < f(x, y + 1) (increasing with respect to y)

You need to implement a findSolution method that:

  • Takes a CustomFunction object and an integer z as parameters
  • Returns a list of all [x, y] pairs where f(x, y) == z
  • The pairs can be returned in any order
  • Both x and y must be positive integers

The solution approach leverages the monotonic property of the function. Since f(x, y) is increasing in both dimensions, for each value of x from 1 to z, we can use binary search to find if there exists a y value such that f(x, y) == z. The binary search is performed on the range [1, z] for y, using the function value f(x, y) as the comparison key. If a valid y is found for a given x, the pair [x, y] is added to the result list.

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

Intuition

The key insight comes from understanding the monotonic property of the function. Since f(x, y) increases as either x or y increases, we can think of the function values as forming a 2D grid where values grow both rightward and upward.

Imagine we fix x at some value. As y increases from 1 onwards, f(x, y) will produce an increasing sequence of values. At some point, this sequence might contain our target z, or it might skip over it entirely. Because the sequence is sorted, we can efficiently search for z using binary search instead of checking every possible y value.

Why enumerate x and binary search y? We could do it either way, but we need to fix one variable to create a one-dimensional sorted sequence that we can binary search. By iterating through all possible x values from 1 to z, we ensure we don't miss any valid pairs.

Why can we limit both x and y to the range [1, z]? Since f(x, y) is monotonically increasing and both x and y are positive integers starting from 1, the minimum possible value is f(1, 1). If f(1, 1) ≤ z, then for any x > z or y > z, we would have f(x, 1) > f(z, 1) ≥ f(1, 1) ≥ z or similarly for y. This means f(x, y) would definitely exceed z when either parameter is greater than z.

The binary search finds the leftmost position where f(x, y) >= z. If the function value at that position equals z, we've found a valid pair. If it's greater than z, it means no y value for this particular x gives us exactly z.

Learn more about Math, Two Pointers and Binary Search patterns.

Solution Approach

The implementation uses enumeration combined with binary search to efficiently find all valid pairs.

Step-by-step implementation:

  1. Initialize the result list: Create an empty list ans to store all valid [x, y] pairs.

  2. Enumerate x values: Loop through all possible x values from 1 to z:

    for x in range(1, z + 1):
  3. Binary search for y: For each fixed x, use binary search to find a y value where f(x, y) = z. The code uses Python's bisect_left function with a custom key:

    y = 1 + bisect_left(
        range(1, z + 1), z, key=lambda y: customfunction.f(x, y)
    )

    Here's what happens:

    • range(1, z + 1) creates the search space for y values
    • bisect_left searches for the leftmost position where we could insert z to maintain sorted order
    • key=lambda y: customfunction.f(x, y) tells the binary search to compare based on function values, not the y values themselves
    • We add 1 to the result because bisect_left returns an index (0-based) but our y values start from 1
  4. Check if exact match found: After binary search, verify if the found position gives us exactly z:

    if customfunction.f(x, y) == z:
        ans.append([x, y])

    The binary search finds where f(x, y) >= z. If f(x, y) == z, we have a valid pair. If f(x, y) > z, it means no y value for this x produces exactly z.

  5. Return all pairs: After checking all possible x values, return the collected pairs.

Time Complexity: O(z * log z) - We iterate through z values of x, and for each x, we perform a binary search over at most z values of y.

Space Complexity: O(1) if we don't count the output array, as we only use a constant amount of extra space for variables.

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 a concrete example where we have a hidden function and need to find all pairs (x, y) where f(x, y) = 5.

Suppose the hidden function is f(x, y) = x + y (we don't know this, but it helps visualize the process).

Target: z = 5

We'll iterate through x values from 1 to 5 and use binary search to find corresponding y values:

When x = 1:

  • We binary search for y in range [1, 5] where f(1, y) = 5
  • Binary search process:
    • Check middle: y = 3, f(1, 3) = 4 < 5 (go right)
    • Check y = 4, f(1, 4) = 5 = 5 ✓
  • Found pair: [1, 4]

When x = 2:

  • Binary search for y in range [1, 5] where f(2, y) = 5
  • Binary search process:
    • Check middle: y = 3, f(2, 3) = 5 = 5 ✓
  • Found pair: [2, 3]

When x = 3:

  • Binary search for y in range [1, 5] where f(3, y) = 5
  • Binary search process:
    • Check middle: y = 3, f(3, 3) = 6 > 5 (go left)
    • Check y = 1, f(3, 1) = 4 < 5 (go right)
    • Check y = 2, f(3, 2) = 5 = 5 ✓
  • Found pair: [3, 2]

When x = 4:

  • Binary search for y in range [1, 5] where f(4, y) = 5
  • Binary search process:
    • Check middle: y = 3, f(4, 3) = 7 > 5 (go left)
    • Check y = 1, f(4, 1) = 5 = 5 ✓
  • Found pair: [4, 1]

When x = 5:

  • Binary search for y in range [1, 5] where f(5, y) = 5
  • Binary search process:
    • Check middle: y = 3, f(5, 3) = 8 > 5 (go left)
    • Check y = 1, f(5, 1) = 6 > 5 (go left further)
    • No valid y exists (all values > 5)
  • No pair found

Result: [[1, 4], [2, 3], [3, 2], [4, 1]]

The key insight is that for each fixed x, the function values f(x, 1), f(x, 2), f(x, 3)... form an increasing sequence, allowing us to efficiently binary search for the target value rather than checking every possibility.

Solution Implementation

1from typing import List
2from bisect import bisect_left
3
4
5class Solution:
6    def findSolution(self, customfunction: 'CustomFunction', z: int) -> List[List[int]]:
7        """
8        Find all pairs [x, y] where customfunction.f(x, y) == z.
9      
10        Since f(x, y) is strictly increasing with respect to both x and y,
11        we can use binary search to find valid y values for each x.
12      
13        Args:
14            customfunction: Custom function object with method f(x, y)
15            z: Target value to find
16          
17        Returns:
18            List of [x, y] pairs where f(x, y) == z
19        """
20        result = []
21      
22        # Try each possible x value from 1 to z
23        for x in range(1, z + 1):
24            # Binary search for y where f(x, y) == z
25            # Since f is increasing, we search in range [1, z]
26            left, right = 1, z
27          
28            while left <= right:
29                mid = (left + right) // 2
30                current_value = customfunction.f(x, mid)
31              
32                if current_value == z:
33                    # Found a valid pair
34                    result.append([x, mid])
35                    break
36                elif current_value < z:
37                    # Need larger y value
38                    left = mid + 1
39                else:
40                    # Need smaller y value
41                    right = mid - 1
42      
43        return result
44
1/*
2 * // This is the custom function interface.
3 * // You should not implement it, or speculate about its implementation
4 * class CustomFunction {
5 *     // Returns f(x, y) for any given positive integers x and y.
6 *     // Note that f(x, y) is increasing with respect to both x and y.
7 *     // i.e. f(x, y) < f(x + 1, y), f(x, y) < f(x, y + 1)
8 *     public int f(int x, int y);
9 * };
10 */
11
12class Solution {
13    public List<List<Integer>> findSolution(CustomFunction customfunction, int z) {
14        // List to store all pairs (x, y) where f(x, y) = z
15        List<List<Integer>> result = new ArrayList<>();
16      
17        // Iterate through all possible x values from 1 to 1000
18        for (int x = 1; x <= 1000; x++) {
19            // Use binary search to find y such that f(x, y) = z
20            int left = 1;
21            int right = 1000;
22          
23            // Binary search for the target y value
24            while (left < right) {
25                int mid = (left + right) / 2;
26              
27                // Since f is increasing with respect to y:
28                // If f(x, mid) >= z, the answer is in the left half (including mid)
29                if (customfunction.f(x, mid) >= z) {
30                    right = mid;
31                } else {
32                    // Otherwise, the answer is in the right half (excluding mid)
33                    left = mid + 1;
34                }
35            }
36          
37            // Check if we found an exact match where f(x, left) = z
38            if (customfunction.f(x, left) == z) {
39                // Add the valid pair [x, y] to the result list
40                result.add(Arrays.asList(x, left));
41            }
42        }
43      
44        return result;
45    }
46}
47
1/*
2 * // This is the custom function interface.
3 * // You should not implement it, or speculate about its implementation
4 * class CustomFunction {
5 * public:
6 *     // Returns f(x, y) for any given positive integers x and y.
7 *     // Note that f(x, y) is increasing with respect to both x and y.
8 *     // i.e. f(x, y) < f(x + 1, y), f(x, y) < f(x, y + 1)
9 *     int f(int x, int y);
10 * };
11 */
12
13class Solution {
14public:
15    vector<vector<int>> findSolution(CustomFunction& customfunction, int z) {
16        vector<vector<int>> result;
17      
18        // Iterate through all possible x values from 1 to 1000
19        for (int x = 1; x <= 1000; ++x) {
20            // Binary search for y value where f(x, y) == z
21            // Since f is monotonically increasing with respect to y
22            int left = 1;
23            int right = 1000;
24          
25            // Binary search to find the smallest y where f(x, y) >= z
26            while (left < right) {
27                int mid = (left + right) >> 1;  // Equivalent to (left + right) / 2
28              
29                if (customfunction.f(x, mid) >= z) {
30                    // If f(x, mid) >= z, the answer might be at mid or smaller
31                    right = mid;
32                } else {
33                    // If f(x, mid) < z, we need to search in the right half
34                    left = mid + 1;
35                }
36            }
37          
38            // Check if we found an exact match where f(x, y) == z
39            if (customfunction.f(x, left) == z) {
40                result.push_back({x, left});
41            }
42        }
43      
44        return result;
45    }
46};
47
1/**
2 * // This is the CustomFunction's API interface.
3 * // You should not implement it, or speculate about its implementation
4 * class CustomFunction {
5 *      f(x: number, y: number): number {}
6 * }
7 */
8
9/**
10 * Finds all pairs [x, y] where customfunction.f(x, y) equals z
11 * The function f is guaranteed to be monotonically increasing
12 * 
13 * @param customfunction - The custom function object with method f(x, y)
14 * @param z - The target value to find
15 * @returns Array of [x, y] pairs where f(x, y) = z
16 */
17function findSolution(customfunction: CustomFunction, z: number): number[][] {
18    // Array to store all valid [x, y] pairs
19    const results: number[][] = [];
20  
21    // Iterate through all possible x values from 1 to 1000
22    for (let x = 1; x <= 1000; ++x) {
23        // Binary search for y value where f(x, y) = z
24        let left = 1;
25        let right = 1000;
26      
27        // Binary search to find the smallest y where f(x, y) >= z
28        while (left < right) {
29            // Calculate middle point using bit shift for efficiency
30            const mid = (left + right) >> 1;
31          
32            // If f(x, mid) >= z, the answer is in the left half (including mid)
33            if (customfunction.f(x, mid) >= z) {
34                right = mid;
35            } else {
36                // Otherwise, the answer is in the right half (excluding mid)
37                left = mid + 1;
38            }
39        }
40      
41        // Check if we found an exact match where f(x, left) = z
42        if (customfunction.f(x, left) === z) {
43            results.push([x, left]);
44        }
45    }
46  
47    return results;
48}
49

Time and Space Complexity

Time Complexity: O(n log n), where n is the value of z.

The outer loop iterates from x = 1 to x = z, which gives us O(n) iterations. For each value of x, we perform a binary search using bisect_left on the range [1, z] to find the appropriate value of y. The binary search operation takes O(log n) time since we're searching through a range of size z. The key function lambda y: customfunction.f(x, y) is called O(log n) times during each binary search. Since we perform this binary search for each of the n values of x, the total time complexity is O(n) × O(log n) = O(n log n).

Space Complexity: O(1) (excluding the output array).

The algorithm uses only a constant amount of extra space for variables x and y. The bisect_left function operates on a range object and uses O(1) additional space for its internal operations. The ans list stores the output, but this is typically not counted in space complexity analysis as it's required for the return value. Therefore, the auxiliary space complexity is O(1).

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

Common Pitfalls

1. Inefficient Search Range for y

Pitfall: The code searches for y in the range [1, z] for every x value, but this can be optimized. Due to the monotonic property, once we find a valid pair [x₁, y₁], for the next iteration with x₂ = x₁ + 1, we know that if a valid y₂ exists, it must satisfy y₂ < y₁ (since f(x₂, y₁) > f(x₁, y₁) = z).

Solution: Use a two-pointer approach that leverages both monotonic properties simultaneously:

def findSolution(self, customfunction: 'CustomFunction', z: int) -> List[List[int]]:
    result = []
    x, y = 1, z
  
    while x <= z and y >= 1:
        current = customfunction.f(x, y)
        if current == z:
            result.append([x, y])
            x += 1
            y -= 1
        elif current < z:
            x += 1
        else:
            y -= 1
  
    return result

This reduces time complexity from O(z log z) to O(z).

2. Early Termination Opportunity Missed

Pitfall: The current solution continues searching even when it's impossible to find more solutions. For example, if f(x, 1) > z, then for all y ≥ 1, f(x, y) > z, so we can stop searching entirely.

Solution: Add an early termination check:

for x in range(1, z + 1):
    # Early termination: if f(x, 1) > z, no valid y exists for this or larger x
    if customfunction.f(x, 1) > z:
        break
  
    # Binary search for y...

3. Unnecessary Function Calls in Binary Search

Pitfall: The shown implementation using bisect_left with a lambda function can result in redundant function calls. After bisect_left returns, we call f(x, y) again to check if it equals z.

Solution: Use a manual binary search that stores the function result:

left, right = 1, z
while left <= right:
    mid = (left + right) // 2
    current_value = customfunction.f(x, mid)
  
    if current_value == z:
        result.append([x, mid])
        break
    elif current_value < z:
        left = mid + 1
    else:
        right = mid - 1

This ensures each f(x, y) value is computed only once during the search.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How would you design a stack which has a function min that returns the minimum element in the stack, in addition to push and pop? All push, pop, min should have running time O(1).


Recommended Readings

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

Load More