Тернарний пошук
Нам задано функцію , яка є унімодальною на відрізку . Під унімодальною функцією ми розуміємо одну з двох можливих поведінок функції:
-
Функція спочатку строго зростає, досягає максимуму (в одній точці або на відрізку), а потім строго спадає.
-
Функція спочатку строго спадає, досягає мінімуму, а потім строго зростає.
У цій статті ми розглядатимемо перший сценарій. Другий сценарій повністю симетричний першому.
Завдання полягає в тому, щоб знайти максимум функції на відрізку .
- Функція унімодальна (строго зростає до екстремуму, потім строго спадає) і ви шукаєте її максимум або мінімум?
- Функція лише монотонна, а ви шукаєте точку переходу чи конкретне значення, а не екстремум? (якщо так → Бінарний пошук)
- Точку екстремуму можна знайти лише через порівняння значень , без аналітичної похідної? (якщо похідна відома й потрібна швидка збіжність → Метод Ньютона)
Алгоритм
Розглянемо будь-які 2 точки та на цьому відрізку: . Обчислимо значення функції в точках та , тобто знайдемо та . Тепер ми отримуємо один із трьох варіантів:
-
Шуканий максимум не може бути розташований ліворуч від , тобто на відрізку , оскільки або обидві точки та , або лише належать області, де функція зростає. У будь-якому разі це означає, що максимум треба шукати на відрізку .
-
Ця ситуація симетрична до попередньої: максимум не може бути розташований праворуч від , тобто на відрізку , і простір пошуку звужується до відрізка .
-
Бачимо, що або обидві ці точки належать області, де значення функції максимальне, або перебуває в області зростання, а — в області спадання (тут ми скористалися строгістю зростання/спадання функції). Отже, простір пошуку звужується до . Щоб спростити код, цей випадок можна об'єднати з будь-яким із попередніх.
Таким чином, спираючись на порівняння значень у двох внутрішніх точках, ми можемо замінити поточний відрізок новим, коротшим відрізком . Повторно застосовуючи описану процедуру до відрізка, ми можемо отримати як завгодно короткий відрізок. Зрештою його довжина стане меншою за певну наперед задану константу (точність), і процес можна зупинити. Це числовий метод, тож ми можемо вважати, що після цього функція досягає свого максимуму в усіх точках останнього відрізка . Без втрати загальності ми можемо повернути значення .
Ми не накладали жодних обмежень на вибір точок та . Цей вибір визначатиме швидкість збіжності й точність реалізації. Найпоширеніший спосіб — вибрати точки так, щоб вони ділили відрізок на три рівні частини. Отже, маємо
Якщо та вибрати ближче одне до одного, швидкість збіжності трохи зросте.
Аналіз часу роботи
Це можна уявити так: щоразу після обчислення функції в точках та ми, по суті, відкидаємо приблизно одну третину відрізка — або ліву, або праву. Отже, розмір простору пошуку становить від початкового.
Застосувавши основну теорему про рекурентні співвідношення, ми отримуємо шукану оцінку складності.
Випадок цілочислових аргументів
Якщо приймає цілочисловий параметр, відрізок стає дискретним. Оскільки ми не накладали жодних обмежень на вибір точок та , на коректність алгоритму це не впливає. та так само можна вибрати так, щоб вони ділили на 3 приблизно рівні частини.
Відмінність виникає в критерії зупинки алгоритму. Тернарний пошук доведеться зупинити, коли , бо в цьому випадку ми вже не можемо вибрати та так, щоб вони відрізнялися одне від одного, а також від та , і це може спричинити нескінченний цикл. Щойно , треба перевірити решту кандидатів , щоб знайти точку, яка дає максимальне значення .
Метод золотого перерізу
У деяких випадках обчислення може бути доволі повільним, але зменшити кількість ітерацій неможливо через проблеми з точністю. На щастя, можна обчислювати лише один раз на кожній ітерації (окрім першої).
Щоб зрозуміти, як це зробити, перегляньмо ще раз спосіб вибору та . Припустимо, що ми вибираємо та на так, що , де — деяка константа. Щоб зменшити обсяг обчислень, ми хочемо вибрати таке , щоб на наступній ітерації одна з нових точок обчислення , збігалася з або , і тоді ми зможемо повторно використати вже обчислене значення функції.
Тепер припустимо, що після поточної ітерації ми поклали . Тоді точка задовольнятиме . Ми хочемо, щоб ця точка збігалася з , тобто .
Помноживши обидві частини рівності на , ми отримуємо . Зауважимо, що та . Підставивши це й помноживши на , ми отримуємо таке рівняння:
Це добре відоме рівняння золотого перерізу. Розв'язавши його, отримуємо . Оскільки має бути додатним, маємо . Застосувавши ту саму логіку до випадку, коли ми кладемо і хочемо, щоб збігалося з , ми отримуємо те саме значення . Отже, якщо вибрати та , на кожній ітерації ми можемо повторно використати одне зі значень , обчислених на попередній ітерації.
Реалізація
- C++
- Python
- TypeScript
- Go
double ternary_search(double l, double r) {
double eps = 1e-9; //встановіть граничну похибку тут
while (r - l > eps) {
double m1 = l + (r - l) / 3;
double m2 = r - (r - l) / 3;
double f1 = f(m1); //обчислює значення функції в m1
double f2 = f(m2); //обчислює значення функції в m2
if (f1 < f2)
l = m1;
else
r = m2;
}
return f(l); //повертає максимум f(x) на [l, r]
}
def ternary_search(l: float, r: float) -> float:
eps = 1e-9 # встановіть граничну похибку тут
while r - l > eps:
m1 = l + (r - l) / 3
m2 = r - (r - l) / 3
f1 = f(m1) # обчислює значення функції в m1
f2 = f(m2) # обчислює значення функції в m2
if f1 < f2:
l = m1
else:
r = m2
return f(l) # повертає максимум f(x) на [l, r]
function ternarySearch(l: number, r: number): number {
const eps = 1e-9; // встановіть граничну похибку тут
while (r - l > eps) {
const m1 = l + (r - l) / 3;
const m2 = r - (r - l) / 3;
const f1 = f(m1); // обчислює значення функції в m1
const f2 = f(m2); // обчислює значення функції в m2
if (f1 < f2)
l = m1;
else
r = m2;
}
return f(l); // повертає максимум f(x) на [l, r]
}
func ternarySearch(l, r float64) float64 {
const eps = 1e-9 // встановіть граничну похибку тут
for r-l > eps {
m1 := l + (r-l)/3
m2 := r - (r-l)/3
f1 := f(m1) // обчислює значення функції в m1
f2 := f(m2) // обчислює значення функції в m2
if f1 < f2 {
l = m1
} else {
r = m2
}
}
return f(l) // повертає максимум f(x) на [l, r]
}
Тут eps — це фактично абсолютна похибка (без урахування похибок через неточне обчислення функції).
Замість критерію r - l > eps ми можемо вибрати фіксовану кількість ітерацій як критерій зупинки. Кількість ітерацій слід вибирати так, щоб забезпечити потрібну точність. Зазвичай у більшості задач для програмування гранична похибка становить , тож 200 — 300 ітерацій є достатніми. Крім того, кількість ітерацій не залежить від значень та , тож кількість ітерацій відповідає потрібній відносній похибці.
Задачі для практики
- Codeforces - New Bakery
- Codechef - Race time
- Hackerearth - Rescuer
- Spoj - Building Construction
- Codeforces - Weakness and Poorness
- LOJ - Closest Distance
- GYM - Dome of Circus (D)
- UVA - Galactic Taxes
- GYM - Chasing the Cheetahs (A)
- UVA - 12197 - Trick or Treat
- SPOJ - Building Construction
- Codeforces - Devu and his Brother
- Codechef - Is This JEE
- Codeforces - Restorer Distance
- TIMUS 1058 Chocolate
- TIMUS 1436 Billboard
- TIMUS 1451 Beerhouse Tale
- TIMUS 1719 Kill the Shaitan-Boss
- TIMUS 1913 Titan Ruins: Alignment of Forces