Ланцюгові дроби
Ланцюговий дріб — це представлення дійсного числа у вигляді особливої збіжної послідовності раціональних чисел. Вони корисні у змагальному програмуванні, бо їх легко обчислювати і їх можна ефективно використовувати для пошуку найкращого можливого раціонального наближення відповідного дійсного числа (серед усіх чисел, знаменник яких не перевищує заданого значення).
Окрім того, ланцюгові дроби тісно пов'язані з алгоритмом Евкліда, що робить їх корисними в низці задач теорії чисел.
Представлення ланцюговим дробом
Нехай і . Тоді вираз
називається представленням раціонального числа ланцюговим дробом і коротко позначається як .
Приклад
Нехай . Існує два способи представити його у вигляді ланцюгового дробу:
Можна довести, що будь-яке раціональне число можна представити ланцюговим дробом рівно способами:
Понад те, довжина такого ланцюгового дробу оцінюється як для .
Міркування, що стоять за цим, стануть зрозумілими, щойно ми заглибимося в деталі побудови ланцюгового дробу.
Нехай — цілочислова послідовність така, що . Нехай . Тоді вираз
називається представленням ірраціонального числа ланцюговим дробом і коротко позначається як .
Зауважимо, що для і цілого виконується .
Інше важливе спостереження: коли і коли .
У наведеному вище означенні раціональні числа називаються підхідними дробами числа .
Відповідно, окремий називається -м підхідним дробом числа .
Приклад
Розглянемо . Індукцією можна довести, що , де — послідовність Фібоначчі, визначена як , і . З формули Біне відомо, що
де — золотий перетин, а . Отже,
Зауважимо, що в цьому конкретному випадку альтернативним способом знайти було б розв'язати рівняння
Нехай . Числа для називаються напівпідхідними дробами.
Зазвичай ми називатимемо (напів)підхідні дроби, що більші за , верхніми (напів)підхідними дробами, а ті, що менші за , — нижніми (напів)підхідними дробами.
На додаток до підхідних дробів, означимо повні частки як .
Відповідно, окремий ми називатимемо -ю повною часткою числа .
З наведених вище означень можна зробити висновок, що для .
Розглядаючи як формальний алгебраїчний вираз і дозволяючи довільні дійсні числа замість , ми отримуємо
Зокрема, . З іншого боку, ми можемо виразити як
що означає, що ми можемо обчислити і з .
Послідовність визначена коректно, доки , що трапляється лише тоді, коли — раціональне число.
Отже, представлення ланцюговим дробом однозначно визначене для будь-якого ірраціонального числа .
Реалізація
У фрагментах коду ми здебільшого вважатимемо ланцюгові дроби скінченними.
З перехід до виглядає так:
З цього виразу наступна повна частка отримується як
Для це означає, що
Отже, обчислення представлення ланцюговим дробом для повторює кроки алгоритму Евкліда для і .
З цього також випливає, що для . Отже, підхідні дроби завжди нескоротні.
- C++
- Python
- TypeScript
- Go
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;
}
def fraction(p, q):
a = []
while q:
a.append(p // q)
p, q = q, p % q
return a
function fraction(p: number, q: number): number[] {
const a: number[] = [];
while (q) {
a.push(Math.floor(p / q));
[p, q] = [q, p % q];
}
return a;
}
func fraction(p, q int) []int {
a := []int{}
for q != 0 {
a = append(a, p/q)
p, q = q, p%q
}
return a
}
Ключові результати
Щоб дати певну мотивацію для подальшого вивчення ланцюгових дробів, наведемо зараз кілька ключових фактів.
Рекурентне співвідношення
Для підхідних дробів виконується таке рекурентне співвідношення, що дозволяє їх швидко обчислювати:
де і .
Відхилення
Відхилення від загалом можна оцінити як
Помноживши обидві частини на , отримуємо альтернативну оцінку:
З наведеного вище рекурентного співвідношення випливає, що зростає принаймні так само швидко, як числа Фібоначчі.
На малюнку нижче ви можете побачити візуалізацію того, як підхідні дроби наближаються до :
зображено синьою пунктирною лінією. Непарні підхідні дроби наближаються до нього зверху, а парні — знизу.
Ґратчасті оболонки
Розглянемо опуклі оболонки точок над і під прямою .
Непарні підхідні дроби є вершинами верхньої оболонки, тоді як парні підхідні дроби є вершинами нижньої оболонки.
Усі цілочислові вершини на оболонках отримуються як такі, що
для цілого . Іншими словами, множина ґратчастих точок на оболонках відповідає множині напівпідхідних дробів.
На малюнку нижче ви можете побачити підхідні дроби та напівпідхідні дроби (проміжні сірі точки) числа .
Найкращі наближення
Нехай — дріб, що мінімізує за умови для деякого .
Тоді є напівпідхідним дробом числа .
Останній факт дозволяє знаходити найкращі раціональні наближення , перевіряючи його напівпідхідні дроби.
Нижче ви знайдете подальше пояснення, а також трохи інтуїції та інтерпретації цих фактів.
Підхідні дроби
Розгляньмо детальніше підхідні дроби, означені раніше. Для його підхідними дробами є
\begin{gather} r_0=[a_0],\r_1=[a_0, a_1],\ \dots,\ r_k=[a_0, a_1, \dots, a_k]. \end{gather}
Підхідні дроби — основне поняття ланцюгових дробів, тож важливо вивчити їхні властивості.
Для числа його -й підхідний дріб можна обчислити як
де — континуанта, багатовимірний многочлен, означений як
Отже, є зваженим медіантом дробів і .
Для узгодженості означають два додаткові підхідні дроби і .
Детальне пояснення
Чисельник і знаменник можна розглядати як багатовимірні многочлени від :
З означення підхідних дробів,
Звідси випливає . Це дає співвідношення
Спочатку і , тож
Для узгодженості зручно означити і та формально вважати, що і .
З чисельного аналізу відомо, що визначник довільної тридіагональної матриці
можна обчислити рекурсивно як . Порівнюючи його з , отримуємо прямий вираз
Цей многочлен також відомий як континуанта через його тісний зв'язок із ланцюговим дробом. Континуанта не зміниться, якщо послідовність на головній діагоналі обернути. Це дає альтернативну формулу для її обчислення:
Реалізація
Ми обчислюватимемо підхідні дроби як пару послідовностей і :
- C++
- Python
- TypeScript
- Go
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);
}
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
function convergents(a: number[]): [number[], number[]] {
const p: number[] = [0, 1];
const q: number[] = [1, 0];
for (const it of a) {
p.push(p[p.length - 1] * it + p[p.length - 2]);
q.push(q[q.length - 1] * it + q[q.length - 2]);
}
return [p, q];
}
func convergents(a []int) ([]int, []int) {
p := []int{0, 1}
q := []int{1, 0}
for _, it := range a {
p = append(p, p[len(p)-1]*it+p[len(p)-2])
q = append(q, q[len(q)-1]*it+q[len(q)-2])
}
return p, q
}
Дерева ланцюгових дробів
Існує два основні способи об'єднати всі можливі ланцюгові дроби в корисні деревоподібні структури.
Дерево Штерна–Броко
Дерево Штерна–Броко — це двійкове дерево пошуку, що містить усі різні додатні раціональні числа.
Дерево загалом виглядає так:
Дроби і "віртуально" зберігаються відповідно зліва і справа дерева.
Тоді дріб у вузлі є медіантом двох дробів і над ним.
Рекурентне співвідношення означає, що представлення ланцюговим дробом кодує шлях до у дереві. Щоб знайти , треба зробити кроків праворуч, кроків ліворуч, кроків праворуч і так далі аж до .
Батьком тоді є дріб, отриманий кроком назад у напрямку, що використовувався останнім.
Іншими словами, це коли і коли .
Отже, дітьми є і .
Проіндексуймо дерево Штерна–Броко. Кореневій вершині присвоюється індекс . Тоді для вершини індекс її лівої дитини присвоюється зміною провідного біта з на , а для правої дитини він присвоюється зміною провідного біта з на :
При такому індексуванні представлення раціонального числа ланцюговим дробом задає кодування довжин серій його двійкового індексу.
Для його індекс дорівнює , а його кодування довжин серій, якщо розглядати біти у порядку зростання, дорівнює .
Інший приклад — , який має індекс , а його кодування довжин серій справді дорівнює .
Варто зазначити, що дерево Штерна–Броко насправді є декартовим деревом. Тобто це двійкове дерево пошуку за , але це купа і за , і за .
Вам дано і . Який дріб менший?
Розв'язок
Припустимо поки що, що і ірраціональні, а їхні представлення ланцюговими дробами задають нескінченний спуск у дереві Штерна–Броко.
Як ми вже згадували, у цьому представленні позначає кількість поворотів праворуч у спуску, позначає кількість послідовних поворотів ліворуч і так далі. Тому, коли ми порівнюємо і , якщо , ми просто переходимо до порівняння і . Інакше, якщо ми на правих спусках, нам слід перевірити, чи , а якщо ми на лівих спусках, нам слід перевірити, чи , щоб визначити, чи .
Іншими словами, для ірраціональних і буде тоді й лише тоді, коли при лексикографічному порівнянні.
Тепер, формально використовуючи як елемент представлення ланцюговим дробом, можна емулювати ірраціональні числа і , тобто елементи, які менші (більші) за , але більші (менші) за будь-яке інше дійсне число. Зокрема, для один із цих двох елементів можна емулювати як , а інший — як .
Який із них відповідає , а який — , можна визначити за парністю або порівнявши їх як ірраціональні числа.
- Python
# перевіряємо, чи a < b, припускаючи, що a[-1] = b[-1] = infty і a != b
def less(a, b):
a = [(-1)**i*a[i] for i in range(len(a))]
b = [(-1)**i*b[i] for i in range(len(b))]
return a < b
# [a0; a1, ..., ak] -> [a0, a1, ..., ak-1, 1]
def expand(a):
if a: # порожнє a = inf
a[-1] -= 1
a.append(1)
return a
# повертає a-eps, a+eps
def pm_eps(a):
b = expand(a.copy())
a.append(float('inf'))
b.append(float('inf'))
return (a, b) if less(a, b) else (b, a)
Вам дано . Знайдіть раціональне число таке, що є лексикографічно найменшим і .
Розв'язок
У термінах дерева Штерна–Броко це означає, що нам потрібно знайти LCA дробів і . Завдяки зв'язку між деревом Штерна–Броко і ланцюговим дробом цей LCA приблизно відповідатиме найбільшому спільному префіксу представлень ланцюговими дробами для і .
Отже, якщо і — ірраціональні числа, то LCA дорівнює .
Для раціональних і один із них може бути самим LCA, що змусило б нас розбирати випадки. Щоб спростити розв'язок для раціональних і , можна використати представлення ланцюговими дробами чисел і , яке було виведене в попередній задачі.
- Python
# знаходить лексикографічно найменше (q, p)
# таке, що p0/q0 < p/q < p1/q1
def middle(p0, q0, p1, q1):
a0 = pm_eps(fraction(p0, q0))[1]
a1 = pm_eps(fraction(p1, q1))[0]
a = []
for i in range(min(len(a0), len(a1))):
a.append(min(a0[i], a1[i]))
if a0[i] != a1[i]:
break
a[-1] += 1
p, q = convergents(a)
return p[-1], q[-1]
GCJ 2019, Round 2 - New Elements: Part 2
Вам дано пар додатних цілих чисел . Потрібно знайти пару додатних цілих чисел таку, що є строго зростаючою послідовністю.
Серед таких пар знайдіть лексикографічно мінімальну.
Розв'язок
Переформулюємо умову: має бути додатним для всіх , де і .
Серед таких рівнянь маємо чотири значущі групи для :
- можна ігнорувати, оскільки ми шукаємо .
- давало б "IMPOSSIBLE" як відповідь.
- , . Такі обмеження еквівалентні .
- , . Такі обмеження еквівалентні .
Нехай — найбільший з четвертої групи, а — найменший з третьої групи.
Тепер задача така: за даними знайти дріб такий, що лексикографічно найменший і .
- Python
def solve():
n = int(input())
C = [0] * n
J = [0] * n
# p0/q0 < y/x < p1/q1
p0, q0 = 0, 1
p1, q1 = 1, 0
fail = False
for i in range(n):
C[i], J[i] = map(int, input().split())
if i > 0:
A = C[i] - C[i-1]
B = J[i] - J[i-1]
if A <= 0 and B <= 0:
fail = True
elif B > 0 and A < 0: # y/x > (-A)/B if B > 0
if (-A)*q0 > p0*B:
p0, q0 = -A, B
elif B < 0 and A > 0: # y/x < A/(-B) if B < 0
if A*q1 < p1*(-B):
p1, q1 = A, -B
if p0*q1 >= p1*q0 or fail:
return 'IMPOSSIBLE'
p, q = middle(p0, q0, p1, q1)
return str(q) + ' ' + str(p)
Дерево Калкіна–Вілфа
Дещо простіший спосіб організувати ланцюгові дроби в двійкове дерево — це дерево Калкіна–Вілфа.
Дерево загалом виглядає так:
У корені дерева розташоване число . Тоді для вершини з числом її дітьми є і .
На відміну від дерева Штерна–Броко, дерево Калкіна–Вілфа не є двійковим деревом пошуку, тож його не можна використовувати для виконання раціонального бінарного пошуку.
У дереві Калкіна–Вілфа безпосереднім батьком дробу є коли і в іншому випадку.
Для дерева Штерна–Броко ми використовували рекурентне співвідношення для підхідних дробів. Щоб провести зв'язок між ланцюговим дробом і деревом Калкіна–Вілфа, нам слід пригадати рекурентне співвідношення для повних часток. Якщо , то .
З іншого боку, якщо ми багаторазово переходимо від до його батька в дереві Калкіна–Вілфа коли , ми опинимося в . Якщо продовжувати так робити, ми опинимося в , потім і так далі. Звідси можемо вивести, що:
- Коли , безпосереднім батьком у дереві Калкіна–Вілфа є .
- Коли і , його безпосереднім батьком є .
- А коли і , його безпосереднім батьком є .
Відповідно, дітьми є
- , що є ,
- , що є для і для .
Варто зауважити, що якщо ми пронумеруємо вершини дерева Калкіна–Вілфа в порядку пошуку в ширину (тобто корінь має номер , а діти вершини мають індекси і відповідно), то індекс раціонального числа в дереві Калкіна–Вілфа буде таким самим, як у дереві Штерна–Броко.
Отже, числа на однакових рівнях дерева Штерна–Броко і дерева Калкіна–Вілфа однакові, але їхній порядок різниться через перестановку з обертанням бітів.
Збіжність
Для числа і його -го підхідного дробу виконується така формула:
Зокрема, це означає, що
і
Звідси можемо зробити висновок, що
Остання нерівність зумовлена тим, що і загалом розташовані по різні боки від , тож
Детальне пояснення
Щоб оцінити , почнемо з оцінки різниці між сусідніми підхідними дробами. За означенням,
Замінивши і у чисельнику їхніми рекурентними співвідношеннями, отримуємо
тож чисельник завжди є взятим зі знаком мінус чисельником . Він, своєю чергою, дорівнює для
тож
Це дає альтернативне представлення як часткової суми нескінченного ряду:
З рекурентного співвідношення випливає, що монотонно зростає принаймні так само швидко, як числа Фібоначчі, тож
завжди визначене коректно, оскільки відповідний ряд завжди збігається. Варто зауважити, що залишковий ряд
має той самий знак, що й , через те, наскільки швидко спадає . Отже, з парними індексами наближаються до знизу, тоді як з непарними індексами наближаються до нього зверху:
З цього малюнка ми бачимо, що
тож відстань між і ніколи не більша за відстань між і :
Вам дано . Знайдіть такі, що .
Розв'язок
Хоча цю задачу зазвичай розв'язують розширеним алгоритмом Евкліда, існує простий і прямолінійний розв'язок із ланцюговими дробами.
Нехай . Вище було доведено, що . Підставивши і замість і , отримуємо
де . Якщо ділиться на , то розв'язком є і .
- Python
# повертає (x, y) таке, що Ax+By=C
# припускає, що таке (x, y) існує
def dio(A, B, C):
p, q = convergents(fraction(A, B))
C //= A // p[-1] # ділимо на gcd(A, B)
t = (-1) if len(p) % 2 else 1
return t*C*q[-2], -t*C*p[-2]
Дробово-лінійні перетворення
Ще одне важливе поняття для ланцюгових дробів — це так звані дробово-лінійні перетворення.
Дробово-лінійне перетворення — це функція така, що для деяких .
Композиція дробово-лінійних перетворень і сама є дробово-лінійним перетворенням:
Обернене до дробово-лінійного перетворення також є дробово-лінійним перетворенням:
DMOPC '19 Contest 7 P4 - Bob and Continued Fractions
Вам дано масив додатних цілих чисел . Потрібно відповісти на запитів. Кожен запит — обчислити .
Розв'язок
Ми можемо розв'язати цю задачу за допомогою дерева відрізків, якщо вміємо конкатенувати ланцюгові дроби.
Загалом справедливо, що .
Позначимо . Зауважимо, що . У цих позначеннях виконується
Отже, задача зводиться до обчислення
Композиція перетворень асоціативна, тож у кожному вузлі дерева відрізків можна обчислити композицію перетворень у його піддереві.
Нехай . Обчисліть представлення ланцюговим дробом числа для .
Це дозволяє обчислювати і для будь-якого .
Розв'язок
Як ми зазначили вище, , тож .
Отже, послідовно додаючи , і так далі, ми зможемо обчислити
Оскільки оборотна, вона також монотонна за . Тому для будь-якого виконується, що лежить між і .
Понад те, для воно дорівнює . Отже, лежить між і . Коли вони рівні, вони також рівні .
Зауважимо, що . Знаючи , ми можемо скомпонувати з поточним перетворенням і продовжувати додавати , і так далі, шукаючи нові заокруглення вниз, які збігатимуться, з яких ми зможемо вивести і так далі, доки не відновимо всі значення .
Нехай і . Обчисліть представлення ланцюговими дробами чисел і .
Розв'язок
Ідея тут схожа на попередню задачу, але замість слід розглянути білінійне дробове перетворення .
Замість ви змінюватимете поточне перетворення як або .
Потім ви перевіряєте, чи , і якщо вони всі збігаються, ви використовуєте це значення як у результуючому дробі та змінюєте перетворення як
Ланцюговий дріб називається періодичним, якщо для деякого .
Ланцюговий дріб називається зрештою періодичним, якщо , де періодичний.
Для виконується , тож . Існує загальний зв'язок між періодичними ланцюговими дробами та квадратними рівняннями. Розглянемо таке рівняння:
З одного боку, це рівняння означає, що представлення ланцюговим дробом періодичне з періодом .
З іншого боку, використовуючи формулу для підхідних дробів, це рівняння означає, що
Тобто є дробово-лінійним перетворенням самого себе. З рівняння випливає, що є коренем рівняння другого степеня:
Подібне міркування виконується для ланцюгових дробів, що зрештою періодичні, тобто для . Справді, з першого рівняння виводимо, що , а з другого рівняння — що , де і — дробово-лінійні перетворення. Тому,
Можна далі довести (і це вперше зробив Лагранж), що для довільного квадратного рівняння з цілими коефіцієнтами його розв'язок є зрештою періодичним ланцюговим дробом.
Знайдіть ланцюговий дріб числа , де і не є точним квадратом.
Розв'язок
Для -ї повної частки числа загалом виконується
Тому,
Помноживши чисельник і знаменник на , ми позбудемося у знаменнику, тож повні частки матимуть вигляд
Знайдемо , припускаючи, що відоме.
Передусім, . Тоді,
Отже, якщо позначити , то виконуватиметься
\begin{align}x_{k+1} &=& z_k t_k, \ y_{k+1} &=& -y_k z_k, \ z_{k+1} &=& t_k^2 - y_k^2 n.\end{align}
Приємне в такому представленні те, що якщо ми скоротимо на їхній найбільший спільний дільник, результат буде унікальним. Тому ми можемо використати його, щоб перевірити, чи поточний стан уже повторювався, а також щоб дізнатися, на якому попередньому індексі був цей стан.
Нижче наведено код для обчислення представлення ланцюговим дробом числа :
- Python
# обчислюємо ланцюговий дріб числа sqrt(n)
def sqrt(n):
n0 = math.floor(math.sqrt(n))
x, y, z = 1, 0, 1
a = []
def step(x, y, z):
a.append((x * n0 + y) // z)
t = y - a[-1]*z
x, y, z = -z*x, z*t, t**2 - n*x**2
g = math.gcd(x, math.gcd(y, z))
return x // g, y // g, z // g
used = dict()
for i in range(n):
used[x, y, z] = i
x, y, z = step(x, y, z)
if (x, y, z) in used:
return a
Використовуючи ту саму функцію step, але інші початкові , і , можна обчислити його для довільного .
Tavrida NU Akai Contest - Continued Fraction
Вам дано і , не є точним квадратом. Нехай , знайдіть для .
Розв'язок
Після обчислення періоду можна обчислити за допомогою бінарного піднесення до степеня дробово-лінійного перетворення, породженого представленням ланцюговим дробом. Щоб знайти результуюче перетворення, ви стискаєте період розміру в одне перетворення і повторюєте його разів, після чого вручну комбінуєте його з рештою перетворень.
- Python
x, k = map(int, input().split())
mod = 10**9+7
# компонуємо (A[0]*x + A[1]) / (A[2]*x + A[3]) і (B[0]*x + B[1]) / (B[2]*x + B[3])
def combine(A, B):
return [t % mod for t in [A[0]*B[0]+A[1]*B[2], A[0]*B[1]+A[1]*B[3], A[2]*B[0]+A[3]*B[2], A[2]*B[1]+A[3]*B[3]]]
A = [1, 0, 0, 1] # (x + 0) / (0*x + 1) = x
a = sqrt(x)
T = len(a) - 1 # період a
# застосовуємо ak + 1/x = (ak*x+1)/(1x+0) до (Ax + B) / (Cx + D)
for i in reversed(range(1, len(a))):
A = combine([a[i], 1, 1, 0], A)
def bpow(A, n):
return [1, 0, 0, 1] if not n else combine(A, bpow(A, n-1)) if n % 2 else bpow(combine(A, A), n // 2)
C = (0, 1, 0, 0) # = 1 / 0
while k % T:
i = k % T
C = combine([a[i], 1, 1, 0], C)
k -= 1
C = combine(bpow(A, k // T), C)
C = combine([a[0], 1, 1, 0], C)
print(str(C[1]) + '/' + str(C[3]))
Геометрична інтерпретація
Нехай для підхідного дробу . Тоді виконується таке рекурентне співвідношення:
Нехай . Тоді кожен вектор відповідає числу, що дорівнює його кутовому коефіцієнту .
З поняттям псевдоскалярного добутку можна показати (див. пояснення нижче), що
Остання рівність зумовлена тим, що і лежать по різні боки від , тож псевдоскалярні добутки і з мають різні знаки. З огляду на , формула для тепер виглядає так:
Зауважимо, що , тож
Пояснення
Як ми вже зазначали, , де . З іншого боку, з рекурентного співвідношення для підхідних дробів виводимо, що
У векторній формі це переписується як
що означає, що і колінеарні (тобто мають однаковий кутовий коефіцієнт). Узявши псевдоскалярний добуток обох частин з , отримуємо
що дає остаточну формулу
Щоразу, коли ви додаєте до вектора , значення збільшується на .
Отже, — це максимальна ціла кількість векторів , які можна додати до без зміни знаку векторного добутку з .
Іншими словами, — це максимальна ціла кількість разів, скільки ви можете додати до , не перетинаючи пряму, задану :
На малюнку вище отримується багаторазовим додаванням до .
Коли вже неможливо далі додавати до без перетину прямої , ми переходимо на інший бік і багаторазово додаємо до , щоб отримати .
Ця процедура породжує експоненційно довші вектори, що наближаються до прямої.
За цю властивість процедуру породження послідовних векторів підхідних дробів Борис Делоне назвав алгоритмом витягування носа.
Якщо ми поглянемо на трикутник, побудований на точках , і , ми помітимо, що його подвоєна площа дорівнює
У поєднанні з теоремою Піка це означає, що немає ґратчастих точок строго всередині трикутника, а єдиними ґратчастими точками на його межі є і для всіх цілих таких, що . Якщо об'єднати це для всіх можливих , це означає, що немає цілих точок у просторі між многокутниками, утвореними векторами підхідних дробів з парними і непарними індексами.
Це, своєю чергою, означає, що з непарними коефіцієнтами утворюють опуклу оболонку ґратчастих точок з над прямою , тоді як з парними коефіцієнтами утворюють опуклу оболонку ґратчастих точок з під прямою .
Ці многокутники також відомі як многокутники Клейна, названі на честь Фелікса Клейна, який першим запропонував цю геометричну інтерпретацію ланцюгових дробів.
Приклади задач
Тепер, коли найважливіші факти та поняття було введено, час заглибитися в конкретні приклади задач.
Знайдіть опуклу оболонку ґратчастих точок таких, що і для .
Розв'язок
Якби ми розглядали необмежену множину , верхня опукла оболонка задавалася б самою прямою .
Однак з додатковим обмеженням нам зрештою довелося б відхилитися від прямої, щоб підтримувати коректну опуклу оболонку.
Нехай , тоді перші ґратчастих точок на оболонці після — це для цілого .
Однак не може бути наступною ґратчастою точкою, оскільки більше за .
Щоб дістатися до наступних ґратчастих точок на оболонці, нам слід дістатися до точки , яка відхиляється від на найменший відступ, зберігаючи при цьому .
Нехай — остання поточна точка опуклої оболонки. Тоді наступна точка така, що і якомога ближча до прямої . Іншими словами, максимізує за умов і .
Такі точки лежать на опуклій оболонці ґратчастих точок під . Іншими словами, має бути нижнім напівпідхідним дробом .
Тобто має вигляд для деякого непарного числа і .
Щоб знайти такий , ми можемо перебрати всі можливі , починаючи з найбільшого, і використати для такого, що .
З умова зберігатиметься завдяки властивостям напівпідхідних дробів.
А виконуватиметься, тому що ми вже вичерпали напівпідхідні дроби, отримані з , тож більше за .
Тепер, коли ми можемо додати до разів, перш ніж перевищимо , після чого ми спробуємо наступний напівпідхідний дріб.
- C++
- Python
- TypeScript
- Go
// повертає [ah, ph, qh] такі, що точки r[i]=(ph[i], qh[i]) утворюють верхню опуклу оболонку
// ґратчастих точок на 0 <= x <= N і 0 <= y <= r * x, де r = [a0; a1, a2, ...]
// і на відрізку між r[i] та r[i+1] є ah[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);
}
# повертає [ah, ph, qh] такі, що точки r[i]=(ph[i], qh[i]) утворюють верхню опуклу оболонку
# ґратчастих точок на 0 <= x <= N і 0 <= y <= r * x, де r = [a0; a1, a2, ...]
# і на відрізку між r[i] та r[i+1] є ah[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
// повертає [ah, ph, qh] такі, що точки r[i]=(ph[i], qh[i]) утворюють верхню опуклу оболонку
// ґратчастих точок на 0 <= x <= N і 0 <= y <= r * x, де r = [a0; a1, a2, ...]
// і на відрізку між r[i] та r[i+1] є ah[i]-1 цілих точок
function hull(a: number[], N: number): [number[], number[], number[]] {
const [p, q] = convergents(a);
let t = Math.floor(N / q[q.length - 1]);
const ah = [t];
const ph = [0, t * p[p.length - 1]];
const qh = [0, t * q[q.length - 1]];
for (let i = q.length - 1; i >= 0; i--) {
if (i % 2) {
while (qh[qh.length - 1] + q[i - 1] <= N) {
t = Math.floor((N - qh[qh.length - 1] - q[i - 1]) / q[i]);
const dp = p[i - 1] + t * p[i];
const dq = q[i - 1] + t * q[i];
const k = Math.floor((N - qh[qh.length - 1]) / dq);
ah.push(k);
ph.push(ph[ph.length - 1] + k * dp);
qh.push(qh[qh.length - 1] + k * dq);
}
}
}
return [ah, ph, qh];
}
// повертає (ah, ph, qh) такі, що точки r[i]=(ph[i], qh[i]) утворюють верхню опуклу оболонку
// ґратчастих точок на 0 <= x <= N і 0 <= y <= r * x, де r = [a0; a1, a2, ...]
// і на відрізку між r[i] та r[i+1] є ah[i]-1 цілих точок
func hull(a []int, N int) ([]int, []int, []int) {
p, q := convergents(a)
t := N / q[len(q)-1]
ah := []int{t}
ph := []int{0, t * p[len(p)-1]}
qh := []int{0, t * q[len(q)-1]}
for i := len(q) - 1; i >= 0; i-- {
if i%2 == 1 {
for qh[len(qh)-1]+q[i-1] <= N {
t = (N - qh[len(qh)-1] - q[i-1]) / q[i]
dp := p[i-1] + t*p[i]
dq := q[i-1] + t*q[i]
k := (N - qh[len(qh)-1]) / dq
ah = append(ah, k)
ph = append(ph, ph[len(ph)-1]+k*dp)
qh = append(qh, qh[len(qh)-1]+k*dq)
}
}
}
return ah, ph, qh
}
Вам дано цілі числа , і . Знайдіть і такі, що і якомога більше.
Розв'язок
У цій задачі виконується , тож її можна розв'язати за . Однак існує розв'язок за із ланцюговими дробами.
Для нашої зручності ми обернемо напрямок , зробивши заміну , так що тепер нам потрібно знайти точку таку, що , і якомога більше. Оптимальне для кожного має значення .
Щоб розглядати це загальніше, ми напишемо функцію, що знаходить найкращу точку на і .
Основна ідея розв'язку в цій задачі по суті повторює попередню задачу, але замість використання нижніх напівпідхідних дробів для відхилення від прямої ви використовуєте верхні напівпідхідні дроби, щоб наблизитися до прямої, не перетинаючи її та не порушуючи . На жаль, на відміну від попередньої задачі, вам потрібно переконатися, що ви не перетинаєте пряму , наближаючись до неї, тож слід це пам'ятати при обчисленні коефіцієнта напівпідхідного дробу .
- Python
# (x, y) таке, що y = (A*x+B) // C,
# Cy - Ax максимальне і 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
Обчисліть , де — число Ейлера і .
Розв'язок
Ця сума дорівнює кількості ґратчастих точок таких, що і .
Після побудови опуклої оболонки точок під цю кількість можна обчислити за допомогою теореми Піка:
- C++
- Python
- TypeScript
- Go
// сума floor(k * x) для k з [1, N] і x = [a0; a1, a2, ...]
int sum_floor(auto a, int N) {
N++;
auto [ah, ph, qh] = hull(a, N);
// Кількість ґратчастих точок усередині вертикальної прямокутної трапеції
// на точках (0; 0) - (0; y1) - (dx; y2) - (dx; 0), що має
// a+1 цілих точок на відрізку (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;
}
# сума floor(k * x) для k з [1, N] і x = [a0; a1, a2, ...]
def sum_floor(a, N):
N += 1
ah, ph, qh = hull(a, N)
# Кількість ґратчастих точок усередині вертикальної прямокутної трапеції
# на точках (0; 0) - (0; y1) - (dx; y2) - (dx; 0), що має
# a+1 цілих точок на відрізку (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
// сума floor(k * x) для k з [1, N] і x = [a0; a1, a2, ...]
function sum_floor(a: number[], N: number): number {
N++;
const [ah, ph, qh] = hull(a, N);
// Кількість ґратчастих точок усередині вертикальної прямокутної трапеції
// на точках (0; 0) - (0; y1) - (dx; y2) - (dx; 0), що має
// a+1 цілих точок на відрізку (0; y1) - (dx; y2).
const picks = (y1: number, y2: number, dx: number, a: number): number => {
const b = y1 + y2 + a + dx;
const A = (y1 + y2) * dx;
return Math.floor((A - b + 2) / 2) + b - (y2 + 1);
};
let ans = 0;
for (let i = 1; i < qh.length; i++) {
ans += picks(ph[i - 1], ph[i], qh[i] - qh[i - 1], ah[i - 1]);
}
return ans - N;
}
// сума floor(k * x) для k з [1, N] і x = [a0; a1, a2, ...]
func sumFloor(a []int, N int) int {
N++
ah, ph, qh := hull(a, N)
// Кількість ґратчастих точок усередині вертикальної прямокутної трапеції
// на точках (0; 0) - (0; y1) - (dx; y2) - (dx; 0), що має
// a+1 цілих точок на відрізку (0; y1) - (dx; y2).
picks := func(y1, y2, dx, a int) int {
b := y1 + y2 + a + dx
A := (y1 + y2) * dx
return (A-b+2)/2 + b - (y2 + 1)
}
ans := 0
for i := 1; i < len(qh); i++ {
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
За даними , і обчисліть .
Розв'язок
Ця задача зводиться до попередньої, якщо зауважити, що . З цим фактом сума зводиться до
Однак сумування для від до — це те, що ми вміємо з попередньої задачі.
- C++
- Python
- TypeScript
- Go
void solve(int p, int q, int N) {
cout << p * N * (N + 1) / 2 - q * sum_floor(fraction(p, q), N) << "\n";
}
def solve(p, q, N):
return p * N * (N + 1) // 2 - q * sum_floor(fraction(p, q), N)
function solve(p: number, q: number, N: number): number {
return (p * N * (N + 1)) / 2 - q * sum_floor(fraction(p, q), N);
}
func solve(p, q, N int) int {
return p*N*(N+1)/2 - q*sumFloor(fraction(p, q), N)
}
Library Checker - Sum of Floor of Linear
За даними , , і обчисліть .
Розв'язок
Це найтехнічніше клопітна задача дотепер.
Можна використати той самий підхід і побудувати повну опуклу оболонку точок під прямою .
Ми вже знаємо, як розв'язати це для . Понад те, ми вже знаємо, як побудувати цю опуклу оболонку аж до найближчої до цієї прямої ґратчастої точки на відрізку (це робиться в задачі "Crime and Punishment" вище).
Тепер слід зауважити, що щойно ми досягли найближчої до прямої точки, ми можемо просто вважати, що пряма насправді проходить через найближчу точку, оскільки немає інших ґратчастих точок на між справжньою прямою і прямою, зміщеною трохи нижче, щоб проходити через найближчу точку.
Тобто, щоб побудувати повну опуклу оболонку під прямою на , ми можемо побудувати її аж до найближчої до прямої точки на , а потім продовжити так, ніби пряма проходить через цю точку, повторно використовуючи алгоритм побудови опуклої оболонки з :
- Python
# оболонка ґратчастих (x, y) таких, що 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
Існує раціональне число таке, що . Ви можете запитати значення за модулем для кількох простих чисел . Відновіть .
Еквівалентне формулювання: Знайдіть , що дає мінімум для .
Розв'язок
За китайською теоремою про остачі запитування результату за модулем кількох простих чисел те саме, що запитування його за модулем їхнього добутку. Через це без втрати загальності вважатимемо, що ми знаємо остачу за модулем достатньо великого числа .
Може існувати кілька можливих розв'язків для за даної остачі . Однак, якщо і обидва є розв'язками, то також виконується . Припускаючи, що , це означає, що становить принаймні .
В умові нам сказали, що , тож якщо і , і не перевищують , то різниця не перевищує . Для це означає, що розв'язок з єдиний як раціональне число.
Отже, задача зводиться до такого: за даним за модулем знайти будь-яке таке, що і .
Це по суті те саме, що знайти , яке дає мінімально можливе для .
Для це означає, що нам потрібно знайти пару таку, що і мінімально можливе.
Оскільки стале, ми можемо поділити на нього і далі переформулювати це як: знайти таке, що і мінімально можливе.
У термінах ланцюгових дробів це означає, що є найкращим діофантовим наближенням до , і достатньо перевірити лише нижні напівпідхідні дроби .
- Python
# знаходимо Q, що мінімізує Q*r mod m для 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]
Задачі для практики
- UVa OJ - Continued Fractions
- ProjectEuler+ #64: Odd period square roots
- Codeforces Round #184 (Div. 2) - Continued Fractions
- Codeforces Round #201 (Div. 1) - Doodle Jump
- Codeforces Round #325 (Div. 1) - Alice, Bob, Oranges and Apples
- POJ Founder Monthly Contest 2008.03.16 - A Modular Arithmetic Challenge
- 2019 Multi-University Training Contest 5 - fraction
- SnackDown 2019 Elimination Round - Election Bait
- Code Jam 2019 round 2 - Continued Fraction