[笔记]Stern-Brocot 树

给你四个正整数 a,b,c,d ,求一个最简分数 pq 满足 ab<pq<cd

若有多组解,输出 q 最小的一组,若仍有多组解,输出 p 最小的一组。

Stern-Brocot 树

首先引入分数逼近。这里的分数逼近是指用用一个分数来逼近另一个分数,使得误差趋于零。例如,假设需要逼近的分数为 rs,有分数 uv>rs。那么有以下结论:

rsr+us+vuv

具体等号能不能取到记不清了,不过不影响。结论很好证明,下面证一下。


r+us+vrs 做减法,得到 r+us+vrs=(r+u)sr(s+v)s(s+v)=usvrs(s+v)

因为 rs<uv,两边同时乘以 sv,得 vr<us,即 usvr>0

又因为 s(s+v)>0,所以 usvrs(s+v)>0。证毕。

注意上面结论和证明成立的条件是 u,v,s,r>0


接下来引入 Stern-Brocot 树这个概念。

Stern-Brocot 树可以维护所有的正分数。这一点可以被我们用来解决这道题目。

首先介绍一下 Stern-Brocot 树。这个树由 0110 两个分数开始。10 不大好定义,暂且把它当做 +。将这两个分数作为源节点

接下来,像我们刚才讨论的分数逼近,将 0110 的分子分母分别相加,得到另外一个分数 11。这个分数确实在 0110 之间。11 被成为第 1 层迭代后的节点。

同样的,将 1101,10 分别进行操作,得到两个分数,称为第二次迭代。

所以我们得到了 Stern-Brocot 树的构建基础:将 abcd 分子分母分别相加,得到 a+cb+d 作为下一轮迭代的节点。

例如,进行三次操作后,这棵树就会变成这样:

01,11,1001,12,11,21,1001,13,12,23,11,32,21,31,10

注意,某些节点(就是第 i 层存在,第 i+1 层也存在的节点),实际上在第 i+1 层是不会出现的。只是为了方便比较加了上去。

可以看到,第三层的第二个分数 13 就是左右两边两个数分子分母分别相加的和。第四个,第六个和第八个以此类推。

下面是来自 OI-wiki 的一张图。

Stern-Brocot树

Stern-Brocot树

刚才所提到的不存在的节点就是虚线相连的那些节点。可以看到,这棵树具有二叉结构。因此在这棵树上搜索只需要花费 O(log2n) 的时间。非常优秀。

关于最简性的证明可以看 OI-wiki 上的解释。这里不再赘述。


对于这道题,显然可以在 Stern-Brocot 树上二分来求解。具体的,如果当前结果 xy 在左端点 AB 的左边,则向右递归,反之亦然。于是可以写出这样的代码:

void solve(int a = 0, int b = 1, int c = 1, int d = 0) {
	int x = a + c, y = b + d;
	long double now = (long double)x / y;
	long double L = (long double)A / B, R = (long double)C / D;
	if (now > L && now < R) {
		ans = {x, y}; return;
	}
	if (now <= L) solve(x, y, c, d);
	else solve(a, b, x, y);
}

交上去以后发现只有 60 分。说明我们需要继续优化算法。

如果把递归时的路径打印出来,我们发现可能会连续地向左(向右)递归很多次。这很不好,因为浪费了许多时间。那么是否可以用较短的复杂度计算出接下来需要连续向左(向右)递归多少次呢?

答案是可以的。假设当前的递归函数是 (a,b,c,d),当前分数 xy=a+cb+d。假设 AB<xy<CD,这是最好的,可以直接输出了。但是如果 xyAB,显然需要向右递归。假设向右递归的次数为 t,那么 x+cty+dtAB。解一下这个不等式:

x+cty+dtAB(x+ct)B(y+dt)AxB+cBtyA+dAttyAxBcBdA

同理,如果 xyCD,那么需要连续向左递归的次数 tyCxDaDbC

如此,我们用 O(1) 的时间求出了连续向左(向右)递归的次数。


代码

#include <algorithm>
#include <cstdio>
#include <iostream>

using namespace std;

using PII = pair<int, int>;
PII ans;
int A, B, C, D;

void solve(int a = 0, int b = 1, int c = 1, int d = 0) {
    int x = a + c, y = b + d;
    long double now = (long double)x / y;
    long double L = (long double)A / B, R = (long double)C / D;
    if (now > L && now < R) {
        ans = {x, y}; return;
    }
    if (now <= L) {
        int t = (int)(y * A - x * B) / (c * B - d * A);
        solve(x + c * t, y + d * t, c, d);
    }
    else {
        int t = (int)(y * C - x * D) / (a * D - b * C);
        solve(a, b, x + a * t, y + b * t);
    }
}
signed main() {
    while (scanf("%d%d%d%d", &A, &B, &C, &D) != EOF) {
        solve();
        printf("%d/%d\n", ans.first, ans.second);
    }
    return 0;
}

简短精炼的代码后面有个小坑:别忘了用 long double

最后留个 Stern-Brocot 树的练习题:P1298 最接近的分数。

posted @   Link-Cut-Y  阅读(52)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
点击右上角即可分享
微信分享提示