Leetcode 1447 最简分数背后的数学知识
本文汇总了“Leetcode 1447 最简分数”这一题背后的部分数学知识,并将一个不同于官方题解的解法由Python语言翻译为Go语言。
1. 题目和官方题解
给你一个整数
n
,请你返回所有 0 到 1 之间(不包括 0 和 1)满足分母小于等于n
的 最简 分数 。分数可以以 任意 顺序返回。示例 3:
输入:n = 4 输出:["1/2","1/3","1/4","2/3","3/4"] 解释:"2/4" 不是最简分数,因为它可以化简为 "1/2" 。
值得注意的是,题目中n
的范围很小,1 <= n <= 100
官方题解利用了最简分数的定义——分子、分母只有公因数1的分数,或者说分子和分母互质的分数。
由于要保证分数在(0,1) 范围内,我们可以枚举分母 denominator∈[2,n] 和分子 numerator∈[1,denominator),若分子分母的最大公约数为 1,则我们找到了一个最简分数。
作者:LeetCode-Solution 链接:leetcode-cn.com/problems/si… 来源:力扣(LeetCode) 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
于是问题转化为:如何快速求两个正整数的最大公约数。
2. 发现背后的数学知识
首先,试着输出一下结果的长度
func simplifiedFractions(n int) (ans []string) {
for denominator := 2; denominator <= n; denominator++ {
for numerator := 1; numerator < denominator; numerator++ {
if gcd(numerator, denominator) == 1 {
ans = append(ans, strconv.Itoa(numerator)+"/"+strconv.Itoa(denominator))
}
}
}
fmt.Println(len(ans)) // 试着输出一下结果的长度
return
}
当n
从1~10时,结果的长度依次为0 1 3 5 9 11 17 21 27 31
。
接下来去oeis.org/这个网站搜索结果的长度构成的数列,可以搜索到oeis.org/A015614这个数列。
搜索结果中写道:
Number of elements in the set {(x,y): 1 <= x < y <= n, 1=gcd(x,y)}.
说明这个数列确实是题目要求的最简分数的个数构成的数列。
注意到下一行,提到了一个叫“(Haros)-Farey series”的东西,赶紧去Wikipedia上查一下。
哦,原来Leetcode 1447是要生成法里数列啊!
中文版Wikipedia往往是缩水版,跳到英文版再去看看,果然发现了代码实现en.wikipedia.org/wiki/Farey_…,而且没有使用if gcd(numerator, denominator) == 1
def farey_sequence(n: int, descending: bool = False) -> None:
"""Print the n'th Farey sequence. Allow for either ascending or descending."""
(a, b, c, d) = (0, 1, 1, n)
if descending:
(a, c) = (1, n - 1)
print("{0}/{1}".format(a, b))
while (c <= n and not descending) or (a > 0 and descending):
k = (n + b) // d
(a, b, c, d) = (c, d, k * c - a, k * d - b)
print("{0}/{1}".format(a, b))
3. 翻译为Go语言
翻译过程中主要改动了3点:
- Leetcode 1447说明可以以任意顺序返回最简分数,所以可以去掉
descending
涉及的逻辑 - 为了避免频繁向
[]string
中append
元素引起的扩容,利用法里数列长度的渐近行为(asymptotic behaviour)(见下图),make([]string)
时指定了capacity
为n*n/3
(3*n*n/(pi*pi) < 3*n*n/(3*3)
),但n
取某些值时,还是有len(ans) > cap(ans)
的情况,比如n == 11
- Wikipedia中的算法生成的第一项为
0/1
,最后一项为1/1
,均需要去掉
Go语言版生成法里序列的算法:
func simplifiedFractions(n int) []string {
ans := make([]string, 0, n*n/3) // 法里数列长度的渐近行为3*n*n/(pi*pi)
a, b, c, d := 0, 1, 1, n
for c <= n {
k := (n+b)/d
a, b, c, d = c, d, k*c-a, k*d-b
ans = append(ans, strconv.Itoa(a) + "/" + strconv.Itoa(b))
}
return ans[:len(ans)-1] // 去掉最后一项“1/1”
}
可能是题目中n
的取值范围太小,执行时间与官方题解(时间复杂度:O(n^2logn))没有显著差异。
时间复杂度不知道该怎么算,但该算法看着还是指数级的(A列是n
的取值,B列是for
循环执行次数)。