'Binary search for a fraction

I'm trying to solve an exercise from the book Algorithms from Sedgewick that goes as follows:

Devise a method that uses a logarithmic number of queries of the form Is the number less than x? to find a rational number p/q such that 0 < p < q < N. Hint : Two fractions with denominators less than N cannot differ by more than 1/N^2.

I'm aware that the interval in which I have to Binary Search is ]0, 1[ but I'm not sure of what I should be looking and what N is. Can somebody please explain it to me?



Solution 1:[1]

Ignoring the hint, here is a solution to a much harder problem.

Namely find any rational by binary search, with a logarithmic bound on the absolute value of the numerator/denominator, without knowing in advance how big that is.

It is a binary search of the Stern-Brocot tree.

class GuessState:
    def __init__ (self):
        self.direction = None
        self.start = [0, 1]
        self.bound = [0, 0]
        self.multiple_upper = 1
        self.multiple_lower = 1
        self.is_widening = True
        self.is_choosing = None

    def next_guess (self):
        if self.is_widening:
            multiple = self.multiple_upper
        else:
            multiple = (self.multiple_lower + self.multiple_upper) // 2
        return (self.start[0] + multiple * self.bound[0], self.start[1] + multiple * self.bound[1])

    def add_response (self, response):
        next_level = False
        if self.direction is None:
            if 0 < response:
                self.bound[0] = 1
                self.direction = 1
            else:
                self.bound[0] = -1
                self.direction = -1
            self.is_choosing = True
            return
        elif self.is_choosing:
            if self.direction * response < 0:
                # Reverse direction.
                self.direction = - self.direction
                (self.start, self.bound) = (self.bound, self.start)
            self.multiple_upper = 2
            self.is_choosing = False
        elif self.is_widening:
            if 0 < response * self.direction:
                self.multiple_lower = self.multiple_upper
                self.multiple_upper += self.multiple_upper
            else:
                self.is_widening = False
                if self.multiple_lower + 1 == self.multiple_upper:
                    next_level = True
        elif self.multiple_lower + 1 < self.multiple_upper:
            if 0 < self.direction * response:
                self.multiple_lower = (self.multiple_lower + self.multiple_upper) // 2
            else:
                self.multiple_upper = (self.multiple_lower + self.multiple_upper) // 2
        else:
            next_level = True

        if next_level:
            next_start = (self.start[0] + self.multiple_lower * self.bound[0], self.start[1] + self.multiple_lower * self.bound[1])
            next_bound = (self.start[0] + self.multiple_upper * self.bound[0], self.start[1] + self.multiple_upper * self.bound[1])
            self.start = next_start
            self.bound = next_bound
            self.multiple_lower = 1
            self.multiple_upper = 1
            self.is_choosing = True
            self.is_widening = True

def guesser (answerer):
    state = GuessState()
    response = answerer(state.next_guess())
    while response != 0:
        state.add_response(response)
        response = answerer(state.next_guess())
    return state.next_guess()

def answerer (answer):
    def compare (guess):
        val = guess[0] / guess[1]
        print(f"Comparing answer {answer} to guess {val} ({guess[0]}/{guess[1]})")
        if val < answer:
            return 1
        elif answer < val:
            return -1
        else:
            return 0
    return compare

print(guesser(answerer(0.124356)))

Solution 2:[2]

What you are asked here is to find the closest Farey number of order n = N-1 to a given rational x, where x is within [0,1], and x and N are input parameters. You could possibly just do a brute-force search of the closest number to x as in the following snippet:

public static void BruteForce(double x, int N, out int numerator, out int denominator)
{
    // Farey sequence of order n = N-1: 
    int n = N - 1;

    // Find the closest Farey number [0/1,1/n,...(n-1)/n,1/1] to the 'x' given... 
    double min_diff = 1.0;
    int num_closest = 0;
    int denom_closest = 0;
    for (int denom = 1; denom < n + 1; denom++)
    {
        for (int num = 1; num < denom; num++)
        {
            double r = (double)num / (double)denom;
            double diff = System.Math.Abs(x - r);
            if (diff < min_diff)
            {
                min_diff = diff;
                num_closest = num;
                denom_closest = denom;
            }
        }
    }

    numerator = num_closest;
    denominator = denom_closest;
}

This algorithm though has the quadratic order of growth and not so useful.

The better approach is to exploit the properties of Farey pairs. As the Farey mediant property theorem states if p=a/b and q=c/d>p are fractions in lowest terms and there's no fraction between them with smaller denominator than either, the fraction ? with the smallest denominator is (a+c)/(b+d). This fraction is called "mediant" and it splits the Farey sequence into two intervals [0/1,...,?] and [?,...,1/1] with neither has numbers with denominators lower than the mediant itself. The above theorem can now be applied to the interval containing the rational parameter x. If the mediant found has the denominator higher than the order n of the Farey sequence, it means that the search is over and the search interval now contains only two Farey rationals and we need to pick the one closest to x. The algorithm is as follows (thanks to Yakov Galka) with some subtle tweaks:

public static void MediantSearch(double x, int N, out int numerator, out int denominator)
{
    // Farey sequence of order n = N-1: 
    int order = N - 1;

    // Numerator and denominator bounds... 
    int n = order - 1;
    int d = order;

    // Define the search bounds... 
    int lo_num = 0; int lo_denom = 1;
    int hi_num = 1; int hi_denom = 1;

    while(true)
    {
        int mediant_num = lo_num + hi_num;
        int mediant_denom = lo_denom + hi_denom;

        if(mediant_denom > d)
        {
            // Break the search and return the closest... 
            if((x - (double)lo_num/(double)lo_denom) < ((double)hi_num/(double)hi_denom - x))
            {
                numerator = lo_num;
                denominator = lo_denom;
            }
            else
            {
                numerator = hi_num;
                denominator = hi_denom;
            }

            break;
        }

        if((double)mediant_num/(double)mediant_denom < x)
        {
            lo_num = mediant_num;
            lo_denom = mediant_denom;
        }
        else if((double)mediant_num / (double)mediant_denom > x)
        {
            hi_num = mediant_num;
            hi_denom = mediant_denom;
        }
        else
        {
            numerator = mediant_num;
            denominator = mediant_denom;
            break;
        }
    }
}

The algorithm has logarithmic order of growth O(log(n)).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 btilly
Solution 2 vgorosh