Leetcode 1447 最简分数背后的数学知识

56 阅读2分钟

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这个数列。

Leetcode 1447 最简分数背后的数学-img01.png

搜索结果中写道:

Number of elements in the set {(x,y): 1 <= x < y <= n, 1=gcd(x,y)}.

说明这个数列确实是题目要求的最简分数的个数构成的数列。

注意到下一行,提到了一个叫“(Haros)-Farey series”的东西,赶紧去Wikipedia上查一下。

Leetcode 1447 最简分数背后的数学-img02.png

哦,原来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涉及的逻辑
  • 为了避免频繁向[]stringappend元素引起的扩容,利用法里数列长度的渐近行为(asymptotic behaviour)(见下图),make([]string)时指定了capacityn*n/33*n*n/(pi*pi) < 3*n*n/(3*3)),但n取某些值时,还是有len(ans) > cap(ans)的情况,比如n == 11
  • Wikipedia中的算法生成的第一项为0/1,最后一项为1/1,均需要去掉

Leetcode 1447 最简分数背后的数学-img03.png

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))没有显著差异。

Leetcode 1447 最简分数背后的数学-img04.png

时间复杂度不知道该怎么算,但该算法看着还是指数级的(A列是n的取值,B列是for循环执行次数)。