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

Тернарний пошук

Нам задано функцію f(x)f(x), яка є унімодальною на відрізку [l,r][l, r]. Під унімодальною функцією ми розуміємо одну з двох можливих поведінок функції:

  1. Функція спочатку строго зростає, досягає максимуму (в одній точці або на відрізку), а потім строго спадає.

  2. Функція спочатку строго спадає, досягає мінімуму, а потім строго зростає.

У цій статті ми розглядатимемо перший сценарій. Другий сценарій повністю симетричний першому.

Завдання полягає в тому, щоб знайти максимум функції f(x)f(x) на відрізку [l,r][l, r].

Коли підходить цей алгоритм?
  • Функція унімодальна (строго зростає до екстремуму, потім строго спадає) і ви шукаєте її максимум або мінімум?
  • Функція лише монотонна, а ви шукаєте точку переходу чи конкретне значення, а не екстремум? (якщо так → Бінарний пошук)
  • Точку екстремуму можна знайти лише через порівняння значень ff, без аналітичної похідної? (якщо похідна відома й потрібна швидка збіжність → Метод Ньютона)

Алгоритм

Розглянемо будь-які 2 точки m1m_1 та m2m_2 на цьому відрізку: l<m1<m2<rl < m_1 < m_2 < r. Обчислимо значення функції в точках m1m_1 та m2m_2, тобто знайдемо f(m1)f(m_1) та f(m2)f(m_2). Тепер ми отримуємо один із трьох варіантів:

  • f(m1)<f(m2)f(m_1) < f(m_2)

    Шуканий максимум не може бути розташований ліворуч від m1m_1, тобто на відрізку [l,m1][l, m_1], оскільки або обидві точки m1m_1 та m2m_2, або лише m1m_1 належать області, де функція зростає. У будь-якому разі це означає, що максимум треба шукати на відрізку [m1,r][m_1, r].

  • f(m1)>f(m2)f(m_1) > f(m_2)

    Ця ситуація симетрична до попередньої: максимум не може бути розташований праворуч від m2m_2, тобто на відрізку [m2,r][m_2, r], і простір пошуку звужується до відрізка [l,m2][l, m_2].

  • f(m1)=f(m2)f(m_1) = f(m_2)

    Бачимо, що або обидві ці точки належать області, де значення функції максимальне, або m1m_1 перебуває в області зростання, а m2m_2 — в області спадання (тут ми скористалися строгістю зростання/спадання функції). Отже, простір пошуку звужується до [m1,m2][m_1, m_2]. Щоб спростити код, цей випадок можна об'єднати з будь-яким із попередніх.

Таким чином, спираючись на порівняння значень у двох внутрішніх точках, ми можемо замінити поточний відрізок [l,r][l, r] новим, коротшим відрізком [l,r][l^\prime, r^\prime]. Повторно застосовуючи описану процедуру до відрізка, ми можемо отримати як завгодно короткий відрізок. Зрештою його довжина стане меншою за певну наперед задану константу (точність), і процес можна зупинити. Це числовий метод, тож ми можемо вважати, що після цього функція досягає свого максимуму в усіх точках останнього відрізка [l,r][l, r]. Без втрати загальності ми можемо повернути значення f(l)f(l).

Ми не накладали жодних обмежень на вибір точок m1m_1 та m2m_2. Цей вибір визначатиме швидкість збіжності й точність реалізації. Найпоширеніший спосіб — вибрати точки так, щоб вони ділили відрізок [l,r][l, r] на три рівні частини. Отже, маємо

m1=l+(rl)3m_1 = l + \frac{(r - l)}{3} m2=r(rl)3m_2 = r - \frac{(r - l)}{3}

Якщо m1m_1 та m2m_2 вибрати ближче одне до одного, швидкість збіжності трохи зросте.

Аналіз часу роботи

T(n)=T(2n/3)+O(1)=Θ(logn)T(n) = T({2n}/{3}) + O(1) = \Theta(\log n)

Це можна уявити так: щоразу після обчислення функції в точках m1m_1 та m2m_2 ми, по суті, відкидаємо приблизно одну третину відрізка — або ліву, або праву. Отже, розмір простору пошуку становить 2n/3{2n}/{3} від початкового.

Застосувавши основну теорему про рекурентні співвідношення, ми отримуємо шукану оцінку складності.

Випадок цілочислових аргументів

Якщо f(x)f(x) приймає цілочисловий параметр, відрізок [l,r][l, r] стає дискретним. Оскільки ми не накладали жодних обмежень на вибір точок m1m_1 та m2m_2, на коректність алгоритму це не впливає. m1m_1 та m2m_2 так само можна вибрати так, щоб вони ділили [l,r][l, r] на 3 приблизно рівні частини.

Відмінність виникає в критерії зупинки алгоритму. Тернарний пошук доведеться зупинити, коли (rl)<3(r - l) < 3, бо в цьому випадку ми вже не можемо вибрати m1m_1 та m2m_2 так, щоб вони відрізнялися одне від одного, а також від ll та rr, і це може спричинити нескінченний цикл. Щойно (rl)<3(r - l) < 3, треба перевірити решту кандидатів (l,l+1,,r)(l, l + 1, \ldots, r), щоб знайти точку, яка дає максимальне значення f(x)f(x).

У деяких випадках обчислення f(x)f(x) може бути доволі повільним, але зменшити кількість ітерацій неможливо через проблеми з точністю. На щастя, можна обчислювати f(x)f(x) лише один раз на кожній ітерації (окрім першої).

Щоб зрозуміти, як це зробити, перегляньмо ще раз спосіб вибору m1m_1 та m2m_2. Припустимо, що ми вибираємо m1m_1 та m2m_2 на [l,r][l, r] так, що rlrm1=rlm2l=φ\frac{r - l}{r - m_1} = \frac{r - l}{m_2 - l} = \varphi, де φ\varphi — деяка константа. Щоб зменшити обсяг обчислень, ми хочемо вибрати таке φ\varphi, щоб на наступній ітерації одна з нових точок обчислення m1m_1', m2m_2' збігалася з m1m_1 або m2m_2, і тоді ми зможемо повторно використати вже обчислене значення функції.

Тепер припустимо, що після поточної ітерації ми поклали l=m1l = m_1. Тоді точка m1m_1' задовольнятиме rm1rm1=φ\frac{r - m_1}{r - m_1'} = \varphi. Ми хочемо, щоб ця точка збігалася з m2m_2, тобто rm1rm2=φ\frac{r - m_1}{r - m_2} = \varphi.

Помноживши обидві частини рівності rm1rm2=φ\frac{r - m_1}{r - m_2} = \varphi на rm2rl\frac{r - m_2}{r - l}, ми отримуємо rm1rl=φrm2rl\frac{r - m_1}{r - l} = \varphi\frac{r - m_2}{r - l}. Зауважимо, що rm1rl=1φ\frac{r - m_1}{r - l} = \frac{1}{\varphi} та rm2rl=rl+lm2rl=11φ\frac{r - m_2}{r - l} = \frac{r - l + l - m_2}{r - l} = 1 - \frac{1}{\varphi}. Підставивши це й помноживши на φ\varphi, ми отримуємо таке рівняння:

φ2φ1=0\varphi^2 - \varphi - 1 = 0

Це добре відоме рівняння золотого перерізу. Розв'язавши його, отримуємо 1±52\frac{1 \pm \sqrt{5}}{2}. Оскільки φ\varphi має бути додатним, маємо φ=1+52\varphi = \frac{1 + \sqrt{5}}{2}. Застосувавши ту саму логіку до випадку, коли ми кладемо r=m2r = m_2 і хочемо, щоб m2m_2' збігалося з m1m_1, ми отримуємо те саме значення φ\varphi. Отже, якщо вибрати m1=l+rl1+φm_1 = l + \frac{r - l}{1 + \varphi} та m2=rrl1+φm_2 = r - \frac{r - l}{1 + \varphi}, на кожній ітерації ми можемо повторно використати одне зі значень f(x)f(x), обчислених на попередній ітерації.

Реалізація

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]
}

Тут eps — це фактично абсолютна похибка (без урахування похибок через неточне обчислення функції).

Замість критерію r - l > eps ми можемо вибрати фіксовану кількість ітерацій як критерій зупинки. Кількість ітерацій слід вибирати так, щоб забезпечити потрібну точність. Зазвичай у більшості задач для програмування гранична похибка становить 106{10}^{-6}, тож 200 — 300 ітерацій є достатніми. Крім того, кількість ітерацій не залежить від значень ll та rr, тож кількість ітерацій відповідає потрібній відносній похибці.

Задачі для практики

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