每个节点实际对应的分数就是 f(S)=b+da+c。每个节点的矩阵 S 都可以写成一系列矩阵 L 和 R 的乘积,这也可以理解为是由 L 和 R 构成的字符串,表示了从根节点到达它的路径。将所有正的有理数唯一地表示为这样的字符串,这可以看作得到了正有理数的一种表示,故而也称作 Stern–Brocot 数系(Stern–Brocot number system)。
建树实现
建树算法只需要模拟上述过程即可。下面是对前 n 层的 Stern–Brocot 树做中序遍历的代码。
建树
[list2tab]
C++
// In-Order Transversal of Stern-Brocot Tree till Layer N.void build(int n, int a = 0, int b = 1, int c = 1, int d = 0, int level = 1) { if (level > n) return; // Only first n layers. int x = a + c, y = b + d; build(n, a, b, x, y, level + 1); std::cout << x << '/' << y << ' '; // Output the current fraction. build(n, x, y, c, d, level + 1);}
Python
# In-Order Transversal of Stern-Brocot Tree till Layer N.def build(n, a=0, b=1, c=1, d=0, level=1): if level > n: return x, y = a + c, b + d build(n, a, b, x, y, level + 1) print(f"{x}/{y}", end=" ") build(n, x, y, c, d, level + 1)
因为 Stern–Brocot 是二叉搜索树,只需要通过比较当前分数和要寻找的分数的大小关系,就可以确定从根节点到给定分数的路径。将路径上向左子节点移动和向右子节点移动分别记作 L 和 R,则每个路径就对应一个由 L 和 R 构成的字符串,这就是上文提到的有理数在 Stern–Brocot 数系中的表示。求得到达某个有理数的路径的过程就相当于求得该有理数在 Stern–Brocot 数系中的表示。
朴素的分数查找算法的实现如下:
朴素分数查找
[list2tab]
C++
// Locate a given fraction in the Stern-Brocot tree.std::string find(int x, int y, int a = 0, int b = 1, int c = 1, int d = 0) { int m = a + c, n = b + d; if (x == m && y == n) return ""; if (x * n < y * m) { return 'L' + find(x, y, a, b, m, n); } else { return 'R' + find(x, y, m, n, c, d); }}
Python
# Locate a given fraction in the Stern-Brocot tree.def find(x, y, a=0, b=1, c=1, d=0): m = a + c n = b + d if x == m and y == n: return "" if x * n < y * m: return "L" + find(x, y, a, b, m, n) else: return "R" + find(x, y, m, n, c, d)
查找分数的朴素算法效率并不高,但是经过简单的优化就可以得到 O(log(p+q)) 的快速查找算法。优化的关键是将连续的 L 和 R 合并处理。
如果要查找的分数 qp 落入 ba 和 dc 之间,那么连续 t 次向右移动时,右侧边界保持不动,而左侧节点移动到 b+tda+tc 处;反过来,连续 t 次向左移动时,左侧边界保持不动,而右侧节点移动到 tb+dta+c 处。因此,可以直接通过 b+tda+tc<qp 或 qp<tb+dta+c 来确定向右和向左移动的次数。此处取严格不等号,是因为算法移动的是左右端点,而要寻找的分数是作为最后得到的端点的中位分数出现的。
快速分数查找
[list2tab]
C++
// Locate a given fraction in the Stern-Brocot tree.auto find(int x, int y) { std::vector<std::pair<int, char>> res; int a = 0, b = 1, c = 1, d = 0; bool right = true; while (x != a + c || y != b + d) { if (right) { auto t = (b * x - a * y - 1) / (y * c - x * d); res.emplace_back(t, 'R'); a += t * c; b += t * d; } else { auto t = (y * c - x * d - 1) / (b * x - a * y); res.emplace_back(t, 'L'); c += t * a; d += t * b; } right ^= 1; } return res;}
Python
# Locate a given fraction in the Stern-Brocot tree.def find(x, y): res = [] a, b, c, d = 0, 1, 1, 0 right = True while x != a + c or y != b + d: if right: t = (b * x - a * y - 1) // (y * c - x * d) res.append((t, "R")) a += t * c b += t * d else: t = (y * c - x * d - 1) // (b * x - a * y) res.append((t, "L")) c += t * a d += t * b right ^= 1 return res
// Locate a given fraction in the Stern-Brocot tree.auto find(int x, int y) { std::vector<std::pair<int, char>> res; bool right = true; while (y) { res.emplace_back(x / y, right ? 'R' : 'L'); x %= y; std::swap(x, y); right ^= 1; } --res.back().first; return res;}
Python
# Locate a given fraction in the Stern-Brocot tree.def find(x, y): res = [] right = True while y: res.append([x // y, ("R" if right else "L")]) x, y = y, x % y right ^= 1 res[-1][0] -= 1 return res
// Farey Sequence of Order N.void build(int n, int a = 0, int b = 1, int c = 1, int d = 1, bool init = true) { int x = a + c, y = b + d; if (y > n) return; // Only first n layers. if (init) std::cout << "0/1 "; // Output 0/1; build(n, a, b, x, y, false); std::cout << x << '/' << y << ' '; // Output the current fraction. build(n, x, y, c, d, false); if (init) std::cout << "1/1 "; // Output 1/1;}
Python
# Farey Sequence of Order N.def build(n, a=0, b=1, c=1, d=1, init=True): x, y = a + c, b + d if y > n: return if init: print("0/1", end=" ") build(n, a, b, x, y, False) print(f"{x}/{y}", end=" ") build(n, x, y, c, d, False) if init: print("1/1", end=" ")
直接构建 Farey 序列的复杂度是 O(∣Fn∣)=O(n2) 的。
序列长度与分数查找
Farey 序列的长度可以递归求出。相较于 Fn−1,序列 Fn 多出来的分数的分母都是 n,而分子不超过 n 且与 n 互素,因此有: