找到两个给定有理数之间最简单的有理数

时间:2016-07-01 08:43:29

标签: algorithm math language-agnostic fractions rational-numbers

我发现了与有理数相关的问题。

给出了两个有理数,任务是找到它们之间最简单的有理数。

对于这个问题,有理数的简单性可以定义为具有最小分子的有理数,尽管我对这个度量的其他建议持开放态度,例如: similar question to Math stack exchange,如果它使解决方案更容易。

样本输入和输出可能是:

Inputs: 1110/416 and 1110/417, Output: 8/3
Inputs: 500/166 and 500/167, Output: 3/1

有关如何解决此问题的任何想法或至少建议?我很挣扎。

由于

编辑:

补充意见:

  • 虽然在两个给定的有理数之间存在无穷多的有理数,但确实有限的许多有理数比两者更简单。
  • 简单的解决方案可能只是尝试分子/分母的所有组合(分别从1到最高分子或分母),减少它们,并查看数字是否介于两者之间。我不确定它的O复杂性是什么,但我猜想像n 2

4 个答案:

答案 0 :(得分:5)

关于continued fractions的维基百科文章中描述了相关数学。简而言之,您计算下端点和上端点的两个连续分数,然后尝试四个组合,其中连续分数在公共端点之后被截断。

这是一个Python实现。

import fractions


F = fractions.Fraction


def to_continued_fractions(x):
    a = []
    while True:
        q, r = divmod(x.numerator, x.denominator)
        a.append(q)
        if r == 0:
            break
        x = F(x.denominator, r)
    return (a, a[:-1] + [a[-1] - 1, 1])


def combine(a, b):
    i = 0
    while i < len(a) and i < len(b):
        if a[i] != b[i]:
            return a[:i] + [min(a[i], b[i]) + 1]
        i += 1
    if i < len(a):
        return a[:i] + [a[i] + 1]
    if i < len(b):
        return a[:i] + [b[i] + 1]
    assert False


def from_continued_fraction(a):
    x = fractions.Fraction(a[-1])
    for i in range(len(a) - 2, -1, -1):
        x = a[i] + 1 / x
    return x


def between(x, y):
    def predicate(z):
        return x < z < y or y < z < x

    return predicate


def simplicity(x):
    return x.numerator


def simplest_between(x, y):
    return min(filter(between(x, y), (from_continued_fraction(combine(a, b)) for a in to_continued_fractions(x) for b in to_continued_fractions(y))), key=simplicity)


print(simplest_between(F(1110, 416), F(1110, 417)))
print(simplest_between(F(500, 166), F(500, 167)))

答案 1 :(得分:3)

如果有一些分母在你的输入之间产生一个有理数,那么假设分子是好的。

您可以检查O(1)中的分子是否良好。假设您要检查分子n,输入为w,x(对于w / x)和y,z(对于y / z)。

如果n x / w和n z / y之间存在整数,则

n是好的。

然后,你可以通过检查所有分子直到找到一个好的分子来在O(好的分子)中做到这一点。如果端点有效,则最多需要min(w,y)。

答案 2 :(得分:1)

这是上面David Eisenstat出色的answer的一个变体。秘密地,它基于找到区间端点连续分数扩展的共同初始部分的完全相同的原理,但是从它的编码方式来看这并不明显,并且无需给出参考即可直接给出正确性证明连续分数理论。该证明的草图在下面进一步给出。

提醒一下,目标是找到给定间隔中最简单的分数。在这里,最简单具有特定的含义(并且很强):我们说分数x = s/t比分数y = u/v 更简单(两者都如果用abs(s) <= abs(u) t <= v 中的至少两个不等式中的至少一个是严格的,则用最小的术语来表示。请注意,使用这个定义 simpler 不会引起总排序:2/53/4的分数都不比另一个更简单;尽管如此,这是一个(不立即显而易见的)定理,即包含至少一个分数的实线的任何子区间都包含一个最简单的分数,该分数比该子区间中的所有其他分数更简单。

代码

事不宜迟,以下是我们的simplest_between版本的一些Python代码。接下来是解释和正确性证明的草图。

def simplest_between(x: Fraction, y: Fraction) -> Fraction:
    """
    Simplest fraction strictly between fractions x and y.
    """
    if x == y:
        raise ValueError("no fractions between x and y")

    # Reduce to case 0 <= x < y
    x, y = min(x, y), max(x, y)
    if y <= 0:
        return -simplest_between(-y, -x)
    elif x < 0:
        return Fraction(0, 1)

    # Find the simplest fraction in (s/t, u/v)
    s, t, u, v = x.numerator, x.denominator, y.numerator, y.denominator
    a, b, c, d = 1, 0, 0, 1
    while True:
        q = s // t
        s, t, u, v = v, u - q * v, t, s - q * t
        a, b, c, d = b + q * a, a, d + q * c, c
        if t > s:
            return Fraction(a + b, c + d)

说明

代码的第一部分(简化为0 <= x < y的情况)应该是不言自明的:如果间隔(x, y)完全位于 负实数,我们使用关于零的对称性并找到最简单的分数 (-y, -x)中,然后取反。否则,如果间隔(x, y)包含零,则0/1(x, y)中最简单的分数。否则,(x, y)位于正实数之内,我们继续进行代码的第二部分。

第二部分是它变得更有趣的地方。在算法的每一步:

  • stuv是非负整数,它们描述一个 正实线(J = (s/t, u/v)的子间隔v 可以为零,因此u/v表示一个无限端点。
  • abcd是非负整数,它们描述一个 线性分数变换T : z ↦ (az + b) / (cz + d)
  • T给出J与原始间隔(x, y)之间的双射。
  • ad-bc = ±1(符号在每次迭代中交替出现)

最初,J = (s/t, u/v)是原始间隔(x, y),而T是原始间隔 身份转换(由a = d = 1b = c = 0提供)。 while循环用间隔J重复替换1/(J - q),其中qJ左端点的底,并同时更新a,{{ 1}},bc以便在两者之间保持双射d TJ

只要间隔(x, y)包含J,循环就会退出。终止 保证了循环:和1是一个正整数,每次迭代都会严格减少,但第一次迭代可能会例外(其中s + t + u + v可以是q)。

在循环退出时,0中每个分数(x, y)的形式为(ap + bq)/(cp + dq)p/q中某个分数gcd(p, q) = 1(带有J);此外,表达式(ap + bq)/(cp + dq)的用语最低:它与gcd(p, q) = 1一起来自ad - bc = ±1。由于abcd均为非负数,因此(a+b)/(c+d)(x, y)中最简单的分数。

关闭间隔怎么样?

与David的答案一样,simplest_between总是在给定端点之间严格严格地产生分数。下一个变体非常相似,但是在给定的 closed 间隔内产生最简单的分数 改为[x, y]

def simplest_between_lax(x: Fraction, y: Fraction) -> Fraction:
    """
    Simplest fraction between fractions x and y, inclusive of x and y.
    """
    # Reduce to case 0 < x <= y
    x, y = min(x, y), max(x, y)
    if y < 0:
        return -simplest_between_lax(-y, -x)
    elif x <= 0:
        return fractions.Fraction(0, 1)

    # Find the simplest fraction in [s/t, u/v]
    s, t, u, v = x.numerator, x.denominator, y.numerator, y.denominator
    a, b, c, d = 1, 0, 0, 1
    while True:
        q = (s - 1) // t
        s, t, u, v = v, u - q * v, t, s - q * t
        a, b, c, d = b + q * a, a, d + q * c, c
        if t >= s:
            return fractions.Fraction(a + b, c + d)

以下是OP的输入示例:

>>> F = fractions.Fraction
>>> simplest_between(F(1110, 416), F(1110, 417))
Fraction(8, 3)
>>> simplest_between(F(500, 166), F(500, 167))
Fraction(3, 1)

闭区间版本会产生相同的结果,当然:

>>> simplest_between_lax(F(1110, 416), F(1110, 417))
Fraction(8, 3)
>>> simplest_between_lax(F(500, 166), F(500, 167))
Fraction(3, 1)

但是simplest_between_lax允许考虑端点:

>>> simplest_between(3, 4)
Fraction(7, 2)
>>> simplest_between_lax(3, 4)
Fraction(3, 1)
>>> simplest_between(F(7, 6), F(6, 5))
Fraction(13, 11)
>>> simplest_between_lax(F(7, 6), F(6, 5))
Fraction(6, 5)

答案 3 :(得分:0)

您可以尝试以下O(n^2 log n)算法:

  • 从一个分子循环到另一个分子
  • 在这个循环中,从一个分母循环到另一个分母。引理是由循环创建的每个分数都在两个末端分数之间。
  • 对于每个分数,通过查找分子和分母的最大公约数来简化它,并将分子除以它。这应该使您能够找到最小的分子。 Euclid算法在O(log n)中执行此操作。