这是David Eisenstat上面出色答案的变体。秘密地,它基于完全相同的原理,即找到区间端点的连分数展开的共同初始部分,但这从它的编码方式中并不明显,并且无需参考即可直接提供正确性证明连分数理论。下面进一步给出了该证明的草图。
提醒一下,目的是在给定的区间内找到最简单的分数。这里最简单有一个特定的(并且非常强烈的)含义:如果并且并且至少这两个不等式中的一个是严格的,我们会说分数比分数x = s/t
更简单y = u/v
(都用最低的术语写成) 。请注意,使用此定义,更简单不会产生总排序:任何一个分数都不比另一个更简单;尽管如此,它是一个(不是立即显而易见的)定理,即包含至少一个分数的实线的任何子区间都包含一个最简单的分数——一个比子区间中所有其他分数更简单的分数。abs(s) <= abs(u)
t <= v
2/5
3/4
编码
事不宜迟,这是我们版本的simplest_between
. 解释和正确性证明的草图如下。
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)
位于正实数内,我们继续代码的第二部分。
第二部分是它变得更有趣的地方。在算法的每一步:
s
, t
,u
和v
是描述J = (s/t, u/v)
正实线的子区间的非负整数(v
可以为零,因此u/v
表示无限端点)。
a
, b
,c
和d
是描述线性分数变换的非负整数T : z ↦ (az + b) / (cz + d)
。
T
J
给出和原始区间之间的双射(x, y)
ad-bc = ±1
(符号随着每次迭代而交替)
最初,J = (s/t, u/v)
是原始区间(x, y)
并且T
是恒等变换(由a = d = 1
,给出b = c = 0
)。while 循环反复用J
区间替换1/(J - q)
,其中q
是 的左端点的下限J
,同时更新a
, b
,c
并d
相应地更新 , 以保持 和 之间的双T
射
。J
(x, y)
一旦区间J
包含,循环就退出1
。循环的终止是有保证的:总和s + t + u + v
是一个正整数,在每次迭代时严格减少,第一次迭代可能例外(其中q
可以是0
)。
在循环退出时, in 中的每个分数都(x, y)
具有(ap + bq)/(cp + dq)
某些分数p/q
(with gcd(p, q) = 1
) in的形式J
;此外,该表达式(ap + bq)/(cp + dq)
已经是最低级的:这是从gcd(p, q) = 1
与 一起得出的ad - bc = ±1
。由于a
、b
和都是非负数,因此它c
是中最简单的分数。d
(a+b)/(c+d)
(x, y)
闭区间呢?
与大卫的回答一样,总是在给定端点之间严格simplest_between
产生分数。下一个变体非常相似,但在给定的封闭区间
内产生最简单的分数。[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)