跳转至

连分数

连分数

连分数 是实数作为有理数的特定收敛序列的表示。它们在算法竞赛(competitive programming)中很有用,因为它们易于计算,并且可以有效地用于在分母不超过给定值的所有数字中,找到基础实数(underlying real number)的最佳可能有理近似(best possible rational approximation)。

除此之外,连分数与欧几里得算法密切相关,这使得它们在一系列数论问题中非常有用。

定义

连分数是一种记号。例如,长为 的连分数:

只是为了形式上简洁,才记成等号左边的样子。这里的四个变元可以任意取值。

连分数各变元的下标从 开始。

简单连分数

可以证明,任何有理数都可以精确地以两种方式表示为连分数:

此外,对于 ,这种连分数的长度 估计为

一旦深入研究了连分数构造的细节,这背后的原因就会很清楚。

定义

。然后表达式

称为有理数 连分数表示,并简短地表示为

示例

。有两种方法可以将其表示为连分数:

对于有限连分数,全体结尾为 的有限连分数和全体结尾不为 的有限连分数一一对应,即同一个连分数有两种表示:

简单连分数:连分数从第 项开始全都是正整数。如果有限,要求最后一项不为 。(第 项可以任意)

简单连分数的值,一定大于偶数的渐进分数,一定小于奇数的渐进分数。无限简单连分数一定收敛。

仿照一般分数的概念,第 项是 的连分数称为「真分数」。显然如果这之后的所有变元都大于等于 ,那么得到的真分数一定落在 之间。

无限连分数

如果分式无限地写下去,有无限个变元,就得到无限连分数。无限连分数收敛等价于渐进分数收敛。

有定理:

无限连分数,如果各变元均大于等于 ,那么一定收敛。

因为只要各变元为正,无限连分数的偶渐进分数单调递增(都比它小),奇渐进分数单调递减(都比它大)。而在均大于等于 时,相邻(奇偶间)两个渐进分数之间距离可以给出估计式,趋于 ,因此收敛。

显然可以看到,连分数关于下标为偶数的变元单调递增,关于下标为奇数的变元单调递减。这无论它有限或无限都成立。

定义

为整数序列,使得 。设 。然后表达式

称为无理数 连分数表示,并简短地表示为

注意,对于 和整数 ,有

另一个重要的观察结果是,当 时,;当 时,

渐进分数

定义

在上面的定义中,有理数 称为 渐进分数(convergents,意为「收敛」)。

相应地,单个 称为 的第 个渐进分数。

示例

考虑 。可以通过归纳法证明 ,其中 是斐波那契序列,定义为 。从 Binet 公式可知

其中 是黄金比率,。因此

请注意,在这种特定情况下,找到 的另一种方法是求解方程

定义

。对于 称为 中间分数(semiconvergents,「semi」意为「半」)。

通常将大于 的分数称为 (upper)渐进分数或中间分数,将小于 者称为 (lower)渐进分数或中间分数。

余项和部分商

定义

作为渐进分数的补充,定义 余项(完全商,complete quotients)为

相应地,将单个 称为 的第 个完全商。

相应地,取整得到的 称为部分商。

对于各项均为正数的连分数,所有的余项也都是正数。

根据以上定义,可以得出 代表 的结论。

视为一个形式代数表达式,并允许任意实数代替 ,得到

特别地,。另一方面,可以将 表示为

这意味着可以从 计算

序列 定义良好,除非 ,这仅在 为有理数时发生。

因此,对于任何无理数 ,连分数表示都是唯一的。

求解简单连分数表示

在代码片段中主要假设有限的连分数。

如果要求 区间内某个数的简单连分数表示(第 项为 ),只需:

  • 取倒数,得到的余项大于
  • 取整得到整数部分为部分商,小数部分在 之间。
  • 对小数部分重复上述操作。

这样就得到了相应的表示。

的转换如下

从这个表达式中,下一个完全商 如下

对于 ,这意味着

因此, 的连分数表示的计算遵循 的欧几里得算法的步骤。

由此,。因此,渐进分数总是不可约的。

1
2
3
4
5
6
7
8
auto fraction(int p, int q) {
  vector<int> a;
  while (q) {
    a.push_back(p / q);
    tie(p, q) = make_pair(q, p % q);
  }
  return a;
}
1
2
3
4
5
6
def fraction(p, q):
    a = []
    while q:
        a.append(p // q)
        p, q = q, p % q
    return a

如果规定第 项是该数的取整,那么全体实数都有「唯一的简单连分数表示」。其中:

如果两个无限简单连分数的值相等,必然逐项相等。

如果两个有限简单连分数的值相等,不仅要逐项相等,而且必然项数也相同。

无限简单连分数不能与有限简单连分数值相等。有理数与有限简单连分数具有一一对应关系,因此无限简单连分数全都是无理数。

性质

为了给连分数的进一步研究提供一些动力,现在给出一些性质。

递推

递推

对于渐进分数 ,以下递推公式适用于它们的快速计算:

其中 并且

渐进分数分子和分母具有完全相同的递推关系:

这里和 Farey 数列的递推关系很像。

形式上记初项:

只是形式上成立。第 项渐进分数是 1/0,没有实际意义。

证明

可以注意到, 对于 都是线性函数。这是因为, 都只出现了一次,无论如何通分也不会有另一个 乘上去。于是通过待定系数,即可解得这个递推关系。

反序定理

如果

如果

证明

对递推关系稍加改造,有:

又利用初值,即可证明反序定理。

渐进分数的差分

计算相邻两项渐进分数的差,需要通分。通分后的分子代入递推关系:

代入初值就有渐进分数的差分:

可以观察到,式 特别像一个行列式,完全可以按「行列式」理解。

渐进分数的递推关系很像行列式的列变换。行列式一列加到另一列上不改变它的值,两列交换则反号。

根据递推式,如果连分数各项均为整数,则渐进分数分子分母总是互素。

对于有理数的简单连分数展开,常用渐进分数差分的等式,求解一次线性不定方程(参见 扩展欧几里得算法):

因为 a 与 b 互素, 就是最简的有理数,也就是它本身的最后一个渐进分数。那么,它的前一个渐进分数就是所求的解。

倒数定理

由于实数与简单连分数一一对应,称实数的简单连分数的渐进分数,就是实数的渐进分数。于是就有倒数定理:

对于大于 1 的实数 x,x 的渐进分数的倒数恰好是 的渐进分数。显然,该定理也应该对于 0 到 1 之间的实数 x 成立。

证明

于是根据新的初值与递推就能发现倒数关系成立。

最佳逼近

最佳逼近

是最小化 的分数,对于某些 ,该分数服从

那么 的中间分数。

因此允许通过检查 是否中间分数来找到其最佳有理逼近。

下面会对这些性质建立一些直觉并做出进一步解释。

连行列式

继续看前面定义的渐近分数。对于 ,其渐近分数为

渐近分数是连分数的核心概念,因此研究它们的性质很重要。

对于数字 ,其第 个渐近分数 可以计算为

其中 是连行列式(continuant),定义为

因此, 的加权中间值(mediant)。

为了一致性,定义了两个额外的渐近分数

详细说明

渐进分数 的分子和分母可以看作 的多元多项式:

根据渐进分数的定义,

由此得出 。这产生了关系

最初,,因此

为了保持一致性,可以方便地定义 ,并正式表示

从数值分析可知,任意三对角矩阵的行列式

可以递归地计算为 。将其与 进行比较,得到一个直接表达式

这个多项式也被称为连行列式(continuant),由于其与连分数的密切关系。如果主对角线上的顺序颠倒,则连行列式不会改变。这产生了一个计算公式:

实现

把渐进分数计算为一对序列

1
2
3
4
5
6
7
8
9
auto convergents(vector<int> a) {
  vector<int> p = {0, 1};
  vector<int> q = {1, 0};
  for (auto it : a) {
    p.push_back(p[p.size() - 1] * it + p[p.size() - 2]);
    q.push_back(q[q.size() - 1] * it + q[q.size() - 2]);
  }
  return make_pair(p, q);
}
1
2
3
4
5
6
7
def convergents(a):
    p = [0, 1]
    q = [1, 0]
    for it in a:
        p.append(p[-1]*it + p[-2])
        q.append(q[-1]*it + q[-2])
    return p, q

误差和余项的估计

误差

渐进分数 的误差(deviation)通常可以估计为

将两边乘以 ,得到另一个估计:

从上面的循环可以看出, 的增长速度至少与斐波那契数一样快。

在下图中可以看到收敛 渐进分数 的可视化:

无理数 由蓝色虚线表示。奇数渐进分数从上面接近它,偶数渐进分数从下面接近它。

实数 x 也可以写成:

最后一项渐近分数就是 x 本身。于是根据渐进分数的递推式,就有:

于是可以估计渐进分数的误差:

分别对 k 取奇数偶数就得到,x 总小于其奇数阶渐近分数,大于其偶数阶渐近分数。

对于数字 及其第 个渐进分数 ,以下公式成立:

特别地,这意味着

并且

由此可以得出结论

后一种不等式是由于 通常位于 的不同侧面,因此

详细说明

为了估计 ,首先估计相邻渐进分数之间的差异。根据定义,

将分子中的 替换为它们的循环,得到

因此, 的分子总是与 相反。反过来,它等于

因此

这产生了 作为无限级数的部分和的替代表示:

根据递归关系, 单调增加的速度至少与斐波那契数一样快,因此

总是定义明确的,因为基础系列总是收敛的。值得注意的是,剩余系列

由于 下降的速度,与 具有相同的符号。因此,偶数索引的 从下面接近 ,而奇数索引的 从上面接近:

从这张图中可以看到

因此, 之间的距离永远不会大于 之间的间距:

例题 扩展欧几里得

您将获得 。查找 ,使 .

解答

虽然这个问题通常是用扩展欧几里得算法解决的,但有一个简单而直接的连分数的解决方案。

。上面证明了 。将 替换为 ,得到

其中 。如果 可被 整除,则解为

1
2
3
4
5
6
7
# return (x, y) such that Ax+By=C
# assumes that such (x, y) exists
def dio(A, B, C):
    p, q = convergents(fraction(A, B))
    C //= A // p[-1] # divide by gcd(A, B)
    t = (-1) if len(p) % 2 else 1
    return t*C*q[-2], -t*C*p[-2]

几何解释

格点

考虑线 上方和下方的点的凸包。

奇数渐进分数 是上壳的顶点,而偶数渐进分数 则是下壳的顶点。

外壳上的所有整数顶点都作为 获得,这样

对于整数 。换句话说,外壳上的格点集对应于中间分数。

在下图中可以看到 的渐进分数和中间分数(灰点)。

对于渐进分数 ,设 。然后,以下重复出现:

。然后,每个向量 对应于等于其斜率系数 的数字。

利用外积 的概念,可以看出(参见下面的解释)

最后一个等式是由于 位于 的不同侧,因此 的外积具有不同的符号。考虑到 的公式如下

注意到 ,因此

解释

正如已经注意到的,,其中 。另一方面,从渐进分数的递推,可以得出

在向量形式中,它重写为

这意味着 共线(即具有相同的斜率系数)。用 计算两个部分的外积,可以得到

得出最终公式

例题 鼻子拉伸算法

每次将 添加到向量 时, 的值都会增加

因此, 向量的最大整数,可以将其添加到 ,而无需更改与 的外积的符号。

换句话说, 是您可以将 添加到 的最大整数次数,而无需跨越 定义的线:

在上面的图片中, 是通过将 重复添加到 而获得的。

当不可能在不跨越 线的情况下将 进一步添加到 时,转到另一侧,重复将 添加到 以获得

此过程生成接近直线的指数较长的向量。

对于这一特性,Boris Delaunay 将生成结果收敛向量的过程称为 鼻子拉伸算法(Nose stretching algorithm)。

如果观察在点 上绘制的三角形,会注意到它的加倍面积是

结合 Pick 定理,这意味着三角形内部没有严格的格点,其边界上的唯一格点是 ,对于所有整数 ,使得 。当连接所有可能的 时,这意味着在由偶数索引和奇数索引收敛向量形成的多边形之间的空间中没有整数点。

这反过来意味着,具有奇数系数的 形成了线 上方 的格点凸包,而具有偶数系数的 形成线 下方 的格点凸包。

定义

这些多边形也被称为 克莱因多边形(Klein polygons),以费利克斯·克莱因(Felix Klein)的名字命名,他首次提出了对连续分数的几何解释。

例题

既然已经介绍了最重要的事实和概念,那么是时候深入研究具体的例题了。

线下凸包

找到格点 的凸包,使得

解答

如果我们考虑无界集合 ,则上凸包将由线 本身给出。

然而,在附加约束 的情况下,最终需要偏离直线以保持适当的凸包。

,则对于整数 ,在 之后的外壳上的第一个 格点是

然而, 不能是下一个格点,因为 大于

为了到达外壳中的下一个格点,应该到达点 ,该点与 相差最小,同时保持

为凸包中的最后一个当前点。然后,下一点 是这样的: 尽可能接近线 。换句话说, 根据 最大化

这样的点位于 以下的格点的凸包上。换句话说, 必须是 的下中间分数。

也就是说,对于某些奇数 的形式为

要找到这样的 ,可以遍历所有可能的 ,从最大的一个开始,并对 使用 ,这样

时,条件 由中间分数的性质保持。

并且 成立,因为已经耗尽了从 获得的半收敛,因此 大于

现在,可以将 添加到 次,然后再超过 ,之后将尝试下一个中间分数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// returns [ah, ph, qh] such that points r[i]=(ph[i], qh[i]) constitute upper
// convex hull of lattice points on 0 <= x <= N and 0 <= y <= r * x, where r =
// [a0; a1, a2, ...] and there are ah[i]-1 integer points on the segment between
// r[i] and r[i+1]
auto hull(auto a, int N) {
  auto [p, q] = convergents(a);
  int t = N / q.back();
  vector ah = {t};
  vector ph = {0, t * p.back()};
  vector qh = {0, t * q.back()};

  for (int i = q.size() - 1; i >= 0; i--) {
    if (i % 2) {
      while (qh.back() + q[i - 1] <= N) {
        t = (N - qh.back() - q[i - 1]) / q[i];
        int dp = p[i - 1] + t * p[i];
        int dq = q[i - 1] + t * q[i];
        int k = (N - qh.back()) / dq;
        ah.push_back(k);
        ph.push_back(ph.back() + k * dp);
        qh.push_back(qh.back() + k * dq);
      }
    }
  }
  return make_tuple(ah, ph, qh);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# returns [ah, ph, qh] such that points r[i]=(ph[i], qh[i]) constitute upper convex hull
# of lattice points on 0 <= x <= N and 0 <= y <= r * x, where r = [a0; a1, a2, ...]
# and there are ah[i]-1 integer points on the segment between r[i] and r[i+1]
def hull(a, N):
    p, q = convergents(a)
    t = N // q[-1]
    ah = [t]
    ph = [0, t*p[-1]]
    qh = [0, t*q[-1]]
    for i in reversed(range(len(q))):
        if i % 2 == 1:
            while qh[-1] + q[i-1] <= N:
                t = (N - qh[-1] - q[i-1]) // q[i]
                dp = p[i-1] + t*p[i]
                dq = q[i-1] + t*q[i]
                k = (N - qh[-1]) // dq
                ah.append(k)
                ph.append(ph[-1] + k * dp)
                qh.append(qh[-1] + k * dq)
    return ah, ph, qh

Timus - Crime and Punishment

您将得到整数 。查找 ,使 达到最大值。

解答

在这个问题中有 ,因此可以用 来解决。但是,有一个 解决方案包含连分数。

为了方便起见,通过替换 来反转 的方向,因此需要找到点 ,使得 是可能的最大值。每个 的最佳 值为

为了更一般地对待它,编写一个函数,该函数在 上找到最佳点。

这个问题的核心解决方案思想基本上重复了前面的问题,但不是使用下中间分数来偏离直线,而是使用上中间分数来接近直线,而不跨越直线,也不违反 。不幸的是,与前一个问题不同,您需要确保在靠近 线时不会越过该线,因此在计算中间分数的系数 时应牢记这一点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# (x, y) such that y = (A*x+B) // C,
# Cy - Ax is max and 0 <= x <= N.
def closest(A, B, C, N):
    # y <= (A*x + B)/C <=> diff(x, y) <= B
    def diff(x, y):
        return C*y-A*x
    a = fraction(A, C)
    p, q = convergents(a)
    ph = [B // C]
    qh = [0]
    for i in range(2, len(q) - 1):
        if i % 2 == 0:
            while diff(qh[-1] + q[i+1], ph[-1] + p[i+1]) <= B:
                t = 1 + (diff(qh[-1] + q[i-1], ph[-1] + p[i-1]) - B - 1) // abs(diff(q[i], p[i]))
                dp = p[i-1] + t*p[i]
                dq = q[i-1] + t*q[i]
                k = (N - qh[-1]) // dq
                if k == 0:
                    return qh[-1], ph[-1]
                if diff(dq, dp) != 0:
                    k = min(k, (B - diff(qh[-1], ph[-1])) // diff(dq, dp))
                qh.append(qh[-1] + k*dq)
                ph.append(ph[-1] + k*dp)
    return qh[-1], ph[-1]

def solve(A, B, N):
    x, y = closest(A, N % A, B, N // A)
    return N // A - x, y

June Challenge 2017 - Euler Sum

计算 ,其中 是自然对数的底,

解答

此和等于格点 的数量,使得

在构造了 以下的点的凸包之后,可以使用 Pick 定理计算这个数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// sum floor(k * x) for k in [1, N] and x = [a0; a1, a2, ...]
int sum_floor(auto a, int N) {
  N++;
  auto [ah, ph, qh] = hull(a, N);

  // The number of lattice points within a vertical right trapezoid
  // on points (0; 0) - (0; y1) - (dx; y2) - (dx; 0) that has
  // a+1 integer points on the segment (0; y1) - (dx; y2).
  auto picks = [](int y1, int y2, int dx, int a) {
    int b = y1 + y2 + a + dx;
    int A = (y1 + y2) * dx;
    return (A - b + 2) / 2 + b - (y2 + 1);
  };

  int ans = 0;
  for (size_t i = 1; i < qh.size(); i++) {
    ans += picks(ph[i - 1], ph[i], qh[i] - qh[i - 1], ah[i - 1]);
  }
  return ans - N;
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# sum floor(k * x) for k in [1, N] and x = [a0; a1, a2, ...]
def sum_floor(a, N):
    N += 1
    ah, ph, qh = hull(a, N)

    # The number of lattice points within a vertical right trapezoid
    # on points (0; 0) - (0; y1) - (dx; y2) - (dx; 0) that has
    # a+1 integer points on the segment (0; y1) - (dx; y2).
    def picks(y1, y2, dx, a):
        b = y1 + y2 + a + dx
        A = (y1 + y2) * dx
        return (A - b + 2) // 2 + b - (y2 + 1)

    ans = 0
    for i in range(1, len(qh)):
        ans += picks(ph[i-1], ph[i], qh[i]-qh[i-1], ah[i-1])
    return ans - N

NAIPC 2019 - It's a Mod, Mod, Mod, Mod World

给定 ,计算

解答

如果您注意到 ,则此问题会减少到上一个问题。有了这个事实,总数减少到

然而,将 相加,是我们能够从上一个问题中得出的结果。

1
2
3
void solve(int p, int q, int N) {
  cout << p * N * (N + 1) / 2 - q * sum_floor(fraction(p, q), N) << "\n";
}
1
2
def solve(p, q, N):
    return p * N * (N + 1) // 2 - q * sum_floor(fraction(p, q), N)

Library Checker - Sum of Floor of Linear

给定 ,计算

解答

这是迄今为止技术上最麻烦的问题。

可以使用相同的方法来构造线 以下的点的全凸包。

已经知道如何解决 的问题。此外,已经知道如何构造这个凸包,直到 段上的这条线的最近格点(这在上面的「罪与罚」问题中完成)。

现在应该注意到,一旦到达了离直线最近的点,就可以假设直线实际上通过了最近的点。因为在实际直线和稍微向下移动以通过最近点的直线之间, 上没有其他格点。

也就是说,要在 上的线 下方构造全凸包,可以将其构造到与 的线最近的点,然后继续,就像该线通过该点一样,重用用于构造 的凸包的算法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# hull of lattice (x, y) such that C*y <= A*x+B
def hull(A, B, C, N):
    def diff(x, y):
        return C*y-A*x
    a = fraction(A, C)
    p, q = convergents(a)
    ah = []
    ph = [B // C]
    qh = [0]

    def insert(dq, dp):
        k = (N - qh[-1]) // dq
        if diff(dq, dp) > 0:
            k = min(k, (B - diff(qh[-1], ph[-1])) // diff(dq, dp))
        ah.append(k)
        qh.append(qh[-1] + k*dq)
        ph.append(ph[-1] + k*dp)

    for i in range(1, len(q) - 1):
        if i % 2 == 0:
            while diff(qh[-1] + q[i+1], ph[-1] + p[i+1]) <= B:
                t = (B - diff(qh[-1] + q[i+1], ph[-1] + p[i+1])) // abs(diff(q[i], p[i]))
                dp = p[i+1] - t*p[i]
                dq = q[i+1] - t*q[i]
                if dq < 0 or qh[-1] + dq > N:
                    break
                insert(dq, dp)

    insert(q[-1], p[-1])

    for i in reversed(range(len(q))):
        if i % 2 == 1:
            while qh[-1] + q[i-1] <= N:
                t = (N - qh[-1] - q[i-1]) // q[i]
                dp = p[i-1] + t*p[i]
                dq = q[i-1] + t*q[i]
                insert(dq, dp)
    return ah, ph, qh

OKC 2 - From Modular to Rational

有一个有理数 ,即 。您可以询问几个素数 的值。恢复

这个问题等价于:查找 中,使 最小的

解答

根据中国剩余定理,要求结果模化几个素数与要求其模化其乘积是相同的。因此,在不丧失一般性的情况下,假设知道余数模足够大的数

对于给定的余数 ,可能有几种可能的解决方案 。然而,如果 都是解,那么它也认为 。假设 ,则意味着 至少为

题面有 ,因此,如果 最多都是 的话,那么差额最多为 。对于 ,这意味着具有 的解 作为有理数是唯一的。

因此,问题归结为,给定 ,找到任何 ,使得

这实际上与找到 是相同的,该 提供了可能的最小

对于 ,这意味着需要找到一对 ,使得 是可能的最小值。

由于 是常量,可以除以它,并进一步将其重新表述为求解 ,这样 是可能的最小值。

就连分数而言,这意味着 的最佳丢番图近似值,并且仅检查 的下中间分数就足够了。

1
2
3
4
5
6
7
8
# find Q that minimizes Q*r mod m for 1 <= k <= n < m 
def mod_min(r, n, m):
    a = fraction(r, m)
    p, q = convergents(a)
    for i in range(2, len(q)):
        if i % 2 == 1 and (i + 1 == len(q) or q[i+1] > n):
            t = (n - q[i-1]) // q[i]
            return q[i-1] + t*q[i]

习题

本页面主要译自博文 Continued fractions,版权协议为 CC-BY-SA 4.0。