SB树
SB 树 Stern-Brocot 树是一个二叉搜索树,它包含了全部非负有理数,是存储分数(或二元组)的高效数据结构(?
建树
初始时你有一个三元组
然后依次往下递归建树,令当前节点为
显然没有人喜欢看式子,我们来给出 SB 树的前 4 行:
当然也有可能是你在 oi-wiki 上看到的这个:
这个只是把相邻三元组两边重复的分数合一块了。
性质
单调性
注意到
所以每一层的分数都是单调的。
最简性
首先注意到每个三元组的任意两个相邻分数
那么这个最简性如何证明?考虑反证,假设有一个分数
但
完全性
首先众所周知,如果
于是当且仅当
假设存在一个
故不存在不在 SB 树里的分数。
说实话这几个性质除了单调性基本没什么用,看看得了。
基础操作
查找一个分数
观察建树过程,你会发现从一个节点
于是从根节点开始,先一路往右跳,跳到头记录最左分数,再往左跳,跳到头记录最右分数,一路往下走。假设是这样的一个数列
我们往前面加上初始三元组两端的
这个可能不好懂,你考虑
此时我们再往左跳
所以最右端的分数就是
向左跳再向右本质上是上述过程翻转,所以也是一样的。
然后注意到这个递推式子刚好和连分数的形式相符,于是
可以把连分数列出来观察。
注意到我们求的节点是三元组的中间分数,设其为
一个分数的连分数是用辗转相除法得到的,所以得到连分数形式就可以得到路径,复杂度
可以观察代码:
struct frac {
int a, b;
frac(int _a = 0, int _b = 0) : a(_a), b(_b) {}
frac operator + (const frac &oth) const { return {a + oth.a, b + oth.b}; }
frac operator + (const int &oth) const { return {a + oth * b, b}; }
frac operator - (const int &oth) const { return {a - b * oth, b}; }
frac operator * (const int &oth) const { return {a * oth, b * oth}; }
bool operator != (const frac &oth) const { return a != oth.a || b != oth.b; }
frac inv() {
frac ans = {b, a};
return ans;
}
};
struct path {
char c;
int n;
};
vector<path> fnd(frac x) {
int to = x.a > x.b; // 大于 1 就往右,否则往左
if (!to) x.inv();
vector<path> ans;
while (x.b) {
int k = x.a / x.b;
ans.pb({(to ? 'R' : 'L'), k});
x = x - k, x = x.inv(), to ^= 1;
}
if (ans.size()) {
if (ans.back().n == 1) ans.pop_back();
else ans.back().n--;
} // 连分数的最后一位是 1,需要处理
return ans;
}
获得一个分数
根据刚才推的连分数递推即可。
frac get(vector<path> x) {
frac p = {0, 1}, q = {1, 0};
for (path i : x) {
if (i.c == 'L') q = p * i.n + q;
else p = p + q * i.n;
}
return p + q;
}
最佳有理近似
最佳有理近似的定义:
对于实数
而在 SB 树上,每一个结点的所有祖先都是它的最佳有理近似,理由是由于一条路径是一个连分数展开,故选取连分数的一段前缀显然是最接近原分数的。
所以求最佳有理近似就是求这个结点的一个祖先。
frac anc(frac x, int k) { // 第 k 个祖先(第 k 个最佳有理近似)
vector<path> p = fnd(x), ans;
int dep = 0;
for (path a : p) {
dep += a.n;
if (dep >= k) {
a.n -= dep - k, ans.pb(a);
break;
}
ans.pb(a);
}
if (dep >= k) return get(ans);
return {-1, 0}; // 无解
}
子树覆盖范围
从全集
int cross(frac x, frac y) { return x.b * y.a - x.a * y.b; }
pair<frac, frac> rge(frac x) {
frac l = {0, 1}, r = {1, 0};
int to = x.a > x.b; // 大于 1 往右,否则往左
while (x != l + r) {
if (to) {
int t = (cross(l, x) - 1) / cross(x, r); // 算能一直收缩几次,-1 是因为不能取等
l = l + r * t;
}
else {
int t = (cross(x, r) - 1) / cross(l, x);
r = r + l * t;
}
to ^= 1;
}
return {l, r};
}
范围最优分数
以最小分母为例,设当前区间为
若
否则,令
注意,如果
否则区间在
复杂度
frac best(frac a, frac b) {
int k = a.a / a.b;
if ((k + 1) * b.b < b.a) return {k + 1, 1};
a = a - k, b = b - k;
frac ans;
if (a.a == 0) ans = {b.b / b.a + 1, 1};
else ans = best(b.inv(), a.inv());
return ans.inv() + k;
}
推荐习题
P1797 【模板】 Stern-Brocot 树(LCA 可以转换成查找分数后在路径上做操作然后转回分数)
P1298 最接近的分数(最佳有理近似)
P5179 Fraction(范围最优分数)
P13117 [GCJ 2019 #2] New Elements: Part 2(推推式子然后范围最优分数)
P8058 [BalkanOI 2003] Farey 序列(等价于 SB 树的每一层,SB 树上二分)
P17463 Iridescence / PYTRIP4(SB 树枚举互质数对)