Перейти до основного вмісту

Дерево Штерна–Броко та послідовності Фарея

Дерево Штерна–Броко

Дерево Штерна–Броко — це елегантна конструкція для представлення множини всіх додатних дробів. Воно було незалежно відкрите німецьким математиком Моріцом Штерном у 1858 році та французьким годинникарем Ашилем Броко у 1861 році. Утім, деякі джерела приписують це відкриття давньогрецькому математику Ератосфену.

Коли підходить цей алгоритм?
  • Чи працюєш із дробами (pq\frac{p}{q}) та потрібен їх упорядкований нескоротний перелік або пошук?
  • Чи задачу можна звести до бінарного пошуку дробу за предикатом «xy<pq\frac{x}{y} < \frac{p}{q}»? (якщо потрібне саме розкладання дробу — див. ланцюгові дроби)
  • Чи допустима логарифмічна глибина спуску — або потрібен крок run-length, щоб уникнути O(p+q)O(p+q)?

Конструкція починається на нульовій ітерації з двох дробів

01,10 \frac{0}{1}, \frac{1}{0}

де варто зауважити, що друга величина строго кажучи не є дробом, але її можна трактувати як нескоротний дріб, що представляє нескінченність.

На кожній наступній ітерації ми розглядаємо всі сусідні дроби ab\frac{a}{b} і cd\frac{c}{d} та вставляємо між ними їхню медіанту a+cb+d\frac{a+c}{b+d}.

Перші кілька ітерацій виглядають так:

01,11,1001,12,11,21,1001,13,12,23,11,32,21,31,10 \begin{array}{c} \dfrac{0}{1}, \dfrac{1}{1}, \dfrac{1}{0} \\ \dfrac{0}{1}, \dfrac{1}{2}, \dfrac{1}{1}, \dfrac{2}{1}, \dfrac{1}{0} \\ \dfrac{0}{1}, \dfrac{1}{3}, \dfrac{1}{2}, \dfrac{2}{3}, \dfrac{1}{1}, \dfrac{3}{2}, \dfrac{2}{1}, \dfrac{3}{1}, \dfrac{1}{0} \end{array}

Продовжуючи цей процес до нескінченності, ми покриваємо всі додатні дроби. Крім того, усі дроби будуть унікальними та нескоротними. Нарешті, дроби також з'являтимуться у порядку зростання.

Перш ніж доводити ці властивості, покажемо саме візуалізацію дерева Штерна–Броко, а не представлення у вигляді списку. Кожен дріб у дереві має двох нащадків. Кожен нащадок є медіантою найближчого предка зліва та найближчого предка справа.

Stern-Brocot tree

Доведення

Упорядкованість. Довести впорядкованість просто. Зауважимо, що медіанта двох дробів завжди лежить між цими дробами

aba+cb+dcd \frac{a}{b} \le \frac{a+c}{b+d} \le \frac{c}{d}

за умови, що

abcd. \frac{a}{b} \le \frac{c}{d}.

Обидві нерівності легко показати, переписавши дроби зі спільними знаменниками.

Оскільки на нульовій ітерації порядок зростаючий, він зберігатиметься на кожній наступній ітерації.

Нескоротність. Щоб довести це, ми покажемо, що для будь-яких двох сусідніх дробів ab\frac{a}{b} і cd\frac{c}{d} виконується

bcad=1. bc - ad = 1.

Пригадаймо, що діофантове рівняння з двома змінними ax+by=cax+by=c має розв'язок тоді й лише тоді, коли cc кратне gcd(a,b)\gcd(a,b). У нашому випадку це означає, що gcd(a,b)=gcd(c,d)=1\gcd(a,b) = \gcd(c,d) = 1, що ми й хочемо показати.

Очевидно, що на нульовій ітерації bcad=1bc - ad = 1. Лишається показати, що медіанти зберігають цю властивість.

Припустимо, що наші два сусідні дроби задовольняють bcad=1bc - ad = 1. Після додавання медіанти до списку

ab,a+cb+d,cd \frac{a}{b}, \frac{a+c}{b+d}, \frac{c}{d}

нові вирази набувають вигляду

b(a+c)a(b+d)=1c(b+d)d(a+c)=1\begin{align} b(a+c) - a(b+d) &= 1 \\ c(b+d) - d(a+c) &= 1 \end{align}

що, використовуючи bcad=1bc-ad=1, легко показати як істинне.

Звідси ми бачимо, що ця властивість завжди зберігається, а отже, усі дроби нескоротні.

Наявність усіх дробів. Це доведення тісно пов'язане з пошуком розташування дробу в дереві Штерна–Броко. З властивості впорядкованості ми маємо, що ліве піддерево дробу містить лише дроби, менші за батьківський дріб, а праве піддерево містить лише дроби, більші за батьківський дріб. Це означає, що ми можемо шукати дріб, обходячи дерево від кореня: ідучи ліворуч, якщо ціль менша за дріб, і праворуч, якщо ціль більша.

Виберемо довільний додатний цільовий дріб xy\frac{x}{y}. Він очевидно лежить між 01\frac{0}{1} і 10\frac{1}{0}, тож єдиний спосіб, у який цей дріб може бути відсутнім у дереві, — це якщо для досягнення його знадобиться нескінченна кількість кроків.

Якби це було так, то на всіх ітераціях ми мали б

ab<xy<cd \frac{a}{b} \lt \frac{x}{y} \lt \frac{c}{d}

що (використовуючи той факт, що ціле z>0    z1z \gt 0 \iff z \ge 1) можна переписати як

bxay1cydx1.\begin{align} bx - ay &\ge 1 \\ cy - dx &\ge 1. \end{align}

Тепер помножимо першу нерівність на c+dc+d, а другу — на a+ba+b і додамо їх, отримавши

(c+d)(bxay)+(a+b)(cydx)a+b+c+d. (c+d)(bx - ay) + (a+b)(cy - dx) \ge a+b+c+d.

Розкривши це і використавши раніше показану властивість bcad=1bc-ad=1, ми отримуємо, що

x+ya+b+c+d. x+y \ge a+b+c+d.

А оскільки на кожній ітерації принаймні одна з величин a,b,c,da,b,c,d зростатиме, процес пошуку дробу міститиме не більше ніж x+yx+y ітерацій. Це суперечить припущенню, що шлях до xy\frac{x}{y} був нескінченним, а отже, xy\frac{x}{y} має бути частиною дерева.

Алгоритм побудови дерева

Щоб побудувати будь-яке піддерево дерева Штерна–Броко, достатньо знати лівого і правого предка. На першому рівні лівим і правим предками є 01\frac{0}{1} і 10\frac{1}{0} відповідно. Використовуючи їх, ми обчислюємо медіанту і спускаємося на один рівень глибше, причому медіанта замінює правого предка в лівому піддереві, і навпаки.

Цей псевдокод намагається побудувати все нескінченне дерево:

void build(int a = 0, int b = 1, int c = 1, int d = 0, int level = 1) {
int x = a + c, y = b + d;

... вивести поточний дріб x/y на поточному рівні дерева

build(a, b, x, y, level + 1);
build(x, y, c, d, level + 1);
}

Алгоритм пошуку дробу

Алгоритм пошуку вже описано у доведенні того, що всі дроби з'являються в дереві, але ми повторимо його тут. Цей алгоритм є алгоритмом бінарного пошуку. Спочатку ми стоїмо в корені дерева і порівнюємо нашу ціль з поточним дробом. Якщо вони однакові, ми завершили роботу і зупиняємо процес. Якщо наша ціль менша, ми переходимо до лівого нащадка, інакше переходимо до правого нащадка.

Ось реалізація, яка повертає шлях до заданого дробу pq\frac{p}{q} у вигляді послідовності символів 'L' і 'R', що означають перехід до лівого і правого нащадка відповідно. Ця послідовність символів однозначно визначає всі додатні дроби і називається системою числення Штерна–Броко.

string find(int p, int q) {
int pL = 0, qL = 1;
int pR = 1, qR = 0;
int pM = 1, qM = 1;
string res;
while(pM != p || qM != q) {
if(p * qM < pM * q) {
res += 'L';
tie(pR, qR) = {pM, qM};
} else {
res += 'R';
tie(pL, qL) = {pM, qM};
}
tie(pM, qM) = pair{pL + pR, qL + qR};
}
return res;
}

Ірраціональним числам у системі числення Штерна–Броко відповідають нескінченні послідовності символів. Уздовж нескінченного шляху до ірраціонального числа алгоритм знаходитиме скоротні дроби з поступово зростаючими знаменниками, які дають дедалі кращі наближення ірраціонального числа. Тож, узявши префікс цієї нескінченної послідовності, можна досягти наближень із будь-якою бажаною точністю. Це застосування є важливим у годинникарстві, що й пояснює, чому дерево було відкрите саме в цій галузі.

Зауважте, що для дробу pq\frac{p}{q} довжина отриманої послідовності може бути аж O(p+q)O(p+q), наприклад, коли дріб має вигляд p1\frac{p}{1}. Це означає, що наведений вище алгоритм не слід використовувати, якщо така складність не є прийнятною!

На щастя, наведений вище алгоритм можна вдосконалити так, щоб гарантувати складність O(log(p+q))O(\log (p+q)). Для цього зауважимо, що якщо поточними граничними дробами є pLqL\frac{p_L}{q_L} і pRqR\frac{p_R}{q_R}, то, зробивши aa кроків праворуч, ми переходимо до дробу pL+apRqL+aqR\frac{p_L + a p_R}{q_L + a q_R}, а зробивши aa кроків ліворуч, ми переходимо до дробу apL+pRaqL+qR\frac{a p_L + p_R}{a q_L + q_R}.

Тому замість того, щоб робити кроки L чи R по одному, ми можемо зробити kk кроків в одному й тому самому напрямку за раз, після чого перемкнемося на рух в інший напрямок, і так далі. У такий спосіб ми можемо знайти шлях до дробу pq\frac{p}{q} як його кодування довжинами серій (run-length encoding).

Оскільки напрямки таким чином чергуються, ми завжди знатимемо, який з них узяти. Тож для зручності ми можемо представити шлях до дробу pq\frac{p}{q} як послідовність дробів

p0q0,p1q1,p2q2,,pnqn,pn+1qn+1=pq\frac{p_0}{q_0}, \frac{p_1}{q_1}, \frac{p_2}{q_2}, \dots, \frac{p_n}{q_n}, \frac{p_{n+1}}{q_{n+1}} = \frac{p}{q}

таку, що pk1qk1\frac{p_{k-1}}{q_{k-1}} і pkqk\frac{p_k}{q_k} є межами інтервалу пошуку на kk-му кроці, починаючи з p0q0=01\frac{p_0}{q_0} = \frac{0}{1} і p1q1=10\frac{p_1}{q_1} = \frac{1}{0}. Тоді після kk-го кроку ми переходимо до дробу

pk+1qk+1=pk1+akpkqk1+akqk,\frac{p_{k+1}}{q_{k+1}} = \frac{p_{k-1} + a_k p_k}{q_{k-1} + a_k q_k},

де aka_k — додатне ціле число. Якщо ви знайомі з ланцюговими дробами, ви впізнаєте, що послідовність piqi\frac{p_i}{q_i} — це послідовність підхідних дробів числа pq\frac{p}{q}, а послідовність [a1;a2,,an,1][a_1; a_2, \dots, a_{n}, 1] представляє ланцюговий дріб числа pq\frac{p}{q}.

Це дозволяє знайти кодування довжинами серій шляху до pq\frac{p}{q} способом, який повторює алгоритм обчислення представлення дробу pq\frac{p}{q} у вигляді ланцюгового дробу:

auto find(int p, int q) {
bool right = true;
vector<pair<int, char>> res;
while(q) {
res.emplace_back(p / q, right ? 'R' : 'L');
tie(p, q) = pair{q, p % q};
right ^= 1;
}
res.back().first--;
return res;
}

Утім, цей підхід працює лише тоді, коли ми вже знаємо pq\frac{p}{q} і хочемо знайти його місце в дереві Штерна–Броко.

На практиці часто буває так, що pq\frac{p}{q} заздалегідь невідомий, але ми можемо для конкретного xy\frac{x}{y} перевірити, чи xy<pq\frac{x}{y} < \frac{p}{q}.

Знаючи це, ми можемо емулювати пошук на дереві Штерна–Броко, підтримуючи поточні межі pk1qk1\frac{p_{k-1}}{q_{k-1}} і pkqk\frac{p_k}{q_k} та знаходячи кожне aka_k за допомогою бінарного пошуку. Тоді алгоритм стає трохи технічнішим і потенційно має складність O(log2(x+y))O(\log^2(x+y)), якщо тільки формулювання задачі не дозволяє знайти aka_k швидше (наприклад, використовуючи floor від якогось відомого виразу).

Послідовність Фарея

Послідовність Фарея порядку nn — це відсортована послідовність дробів між 00 і 11, знаменники яких не перевищують nn.

Ці послідовності названо на честь англійського геолога Джона Фарея, який у 1816 році висунув припущення, що будь-який дріб у послідовності Фарея є медіантою своїх сусідів. Це було доведено дещо пізніше Коші, але незалежно від обох математик Арос дійшов майже того самого висновку ще у 1802 році.

Послідовності Фарея мають багато цікавих властивостей самі по собі, але зв'язок із деревом Штерна–Броко є найочевиднішим. Насправді послідовності Фарея можна отримати, обрізаючи гілки дерева.

З алгоритму побудови дерева Штерна–Броко ми отримуємо алгоритм для послідовностей Фарея. Починаємо зі списку дробів 01,10\frac{0}{1}, \frac{1}{0}. На кожній наступній ітерації вставляємо медіанту лише тоді, коли знаменник не перевищує nn. У певний момент список перестане змінюватися, і шукану послідовність Фарея буде знайдено.

Довжина послідовності Фарея

Послідовність Фарея порядку nn містить усі елементи послідовності Фарея порядку n1n-1, а також усі нескоротні дроби зі знаменником nn, але останнє — це просто функція Ейлера φ(n)\varphi(n). Тож довжина LnL_n послідовності Фарея порядку nn дорівнює

Ln=Ln1+φ(n) L_n = L_{n-1} + \varphi(n)

або, що еквівалентно, розгорнувши рекурсію, ми отримуємо

Ln=1+k=1nφ(k). L_n = 1 + \sum_{k=1}^n \varphi(k).

Відеоматеріали