Двоичный и тернарный поиск

Инварианты границ, поиск ответа, вещественная точность и унимодальные функции

01

Поиск в отсортированном массиве

Найдём 8 в [1,3,5,7,8,10,13]. Средняя ячейка содержит 7: первые четыре значения не подходят. В оставшейся части среднее равно 10: всё правее него слишком велико. Осталась ячейка с 8. За три сравнения исключили почти весь массив; попробуйте выбирать следующую половину самостоятельно в модели.

В предыдущих лекциях мы получали упорядоченный массив. Теперь воспользуемся его порядком. Если A отсортирован по возрастанию и средний элемент меньше x, то все элементы левее тоже меньше x. Одно сравнение позволяет исключить сразу половину кандидатов, а не одну позицию.

Для массива [1, 3, 3, 8, 10] поиск значения 3 может вернуть любую из двух равных ячеек. Однако в задачах часто требуется первая позиция, место для вставки или количество вхождений. Удобнее сразу искать определённую границу, а проверку равенства делать после.

Lower bound (Lower bound) — Индекс первого элемента массива, который не меньше x; если такого нет, возвращается n.

lower_bound(A, x) = min { i ∈ [0,n] : i=n или A[i] ≥ x }

Условие A[i] ≥ x на отсортированном массиве имеет вид «ложь, ложь, …, истина, истина». Ищем первый индекс, где оно истинно. Если таких элементов нет, возвращаем n — позицию сразу после массива. Так lower bound определён и для отсутствующего x.

Lower bound и upper bound

Ваш шаг: left = 0, right = 10, mid = 5, A[mid] = 8. Как обновить границы?

1[0]
3[1]
3[2]
5[3]
8[4]
8[5]
8[6]
10[7]
13[8]
17[9]

1[0]
3[1]
3[2]
5[3]
8[4]
8[5]
8[6]
10[7]
13[8]
17[9]
left
0
right (не включён)
10
Полуинтервал [0, 10); ищем первый элемент ≥ 8.
Шаг 1 / 6

Здесь и в основном коде используется [left, right): сначала left = 0, right = n, цикл left < right. Подходящий mid сохраняем через right = mid; неподходящий исключаем через left = mid + 1. Ответ n допустим.

Пустой массивПри left=right=0 цикл left < right сразу пропускается. Возвращается 0 — допустимая позиция вставки; читать A[0] нельзя.

Предварительная сортировка не бесплатна. Для одного поиска в неупорядоченном массиве обычно достаточно O(n)-прохода. Сортировать имеет смысл, когда данные уже упорядочены или запросов много: стоимость подготовки тогда оплачивается один раз.

Идея деления пополам проста, но ошибки обычно возникают в границах. Зафиксируем точный смысл левой и правой границы, чтобы из него следовали и обновления, и условие остановки.

Примените алгоритм к новому примеру

Задания раздела

Ищем 8 в [1,3,5,7,8,10,13], A[mid]=7 при mid=3. Что исключаем?

lower_bound([2,4,9],10): введите индекс.

Попыток: 0
02

Один шаблон: [left,right) и инвариант

Этот раздел откроется после предыдущих проверок.

Продолжить: Поиск в отсортированном массиве →

После сравнения A[mid]=7 с целью 8 можно исключить и сам mid, и всё слева: left = mid+1. Если A[mid]=10, этот индекс ещё может оказаться первой подходящей позицией, поэтому right = mid. Именно эти два действия выполняет модель.

Храним полуинтервал ещё не проверенных позиций [left,right). Все индексы меньше left заведомо содержат значения < x; все действительные индексы начиная с right содержат значения ≥ x. Ответ-граница находится между left и right включительно, в том числе может быть n.

Основной шаблон lower_bound: [left,right)
left = 0; right = n
while left < right:
    mid = left + (right-left)//2
    if A[mid] >= x:
        right = mid
    else:
        left = mid+1
return left

right не входит в неизвестный полуинтервал. mid всегда является допустимым индексом.

Lower bound и upper bound

Ваш шаг: left = 0, right = 10, mid = 5, A[mid] = 8. Как обновить границы?

1[0]
3[1]
3[2]
5[3]
8[4]
8[5]
8[6]
10[7]
13[8]
17[9]

1[0]
3[1]
3[2]
5[3]
8[4]
8[5]
8[6]
10[7]
13[8]
17[9]
left
0
right (не включён)
10
Полуинтервал [0, 10); ищем первый элемент ≥ 8.
Шаг 1 / 6

Здесь и в основном коде используется [left, right): сначала left = 0, right = n, цикл left < right. Подходящий mid сохраняем через right = mid; неподходящий исключаем через left = mid + 1. Ответ n допустим.

Если mid подходит, right=mid сохраняет его как возможный ответ; иначе left=mid+1 исключает уже проверенную ячейку. На каждом шаге число неизвестных позиций строго уменьшается. При left=right их нет: left — первая подходящая позиция или n. Это доказывает и корректность, и завершение.

Для пустого массива left=right=0 — ни одного чтения. Для [8] и x=8 выполняется right=0; для x=9 выполняется left=1. Чтобы проверить точное наличие значения, после поиска нужны обе проверки: left < n и A[left] == x. Время O(log(n+1)), дополнительная память O(1).

Необязательное углубление: фиктивные границы −1 и n
Другой корректный инвариант хранит последний неподходящий l и первый подходящий r. Начало l=−1, r=n; цикл r−l>1; m=l+(r−l)//2; при A[m]≥x делаем r=m, иначе l=m; возвращаем r. A[−1] и A[n] не читаются. Этот вариант меняет смысл границ и строку обновления l; используйте его отдельно после освоения основного шаблона.

Смысл [left,right) теперь одинаков в модели и коде. Изменим только строгость проверки и получим вторую полезную границу.

Примените алгоритм к новому примеру

Задания раздела

Для [left,right) выберите обновление.

Пустой массив: left=right=0. Что вернёт поиск?

[8], x=9: чему равен left после шага?

Попыток: 0
03

Границы и ближайшие элементы

Этот раздел откроется после предыдущих проверок.

Продолжить: Поиск в отсортированном массиве →

В [1,3,3,8,8] число 3 занимает индексы 1 и 2. Первая позиция ≥3 равна 1, первая позиция >3 равна 3. Это lower_bound и upper_bound. В обоих случаях начальные границы 0,n, цикл left<right, обновления right=mid и left=mid+1; меняется только условие good.

Lower bound и upper bound

Ваш шаг: left = 0, right = 10, mid = 5, A[mid] = 8. Как обновить границы?

1[0]
3[1]
3[2]
5[3]
8[4]
8[5]
8[6]
10[7]
13[8]
17[9]

1[0]
3[1]
3[2]
5[3]
8[4]
8[5]
8[6]
10[7]
13[8]
17[9]
left
0
right (не включён)
10
Полуинтервал [0, 10); ищем первый элемент ≥ 8.
Шаг 1 / 6

Здесь и в основном коде используется [left, right): сначала left = 0, right = n, цикл left < right. Подходящий mid сохраняем через right = mid; неподходящий исключаем через left = mid + 1. Ответ n допустим.

Что ищемУсловие goodОтвет
Первый A[i] ≥ xA[mid] ≥ xlower
Первый A[i] > xA[mid] > xupper
Последний A[i] ≤ xСначала ищем upperupper−1, если upper>0
Есть ли xlower<n и A[lower]=xДа / нет

[1,3,3,8,8], x=3. Введите lower bound (индексы с нуля).

Для того же массива и x=3 введите upper bound.

Не путать строгостьЗаменить ≥ на > в одном месте и не сменить ожидаемый ответ — типичная ошибка двоичного поиска.

Границы помогают искать и соседей. Индекс последнего элемента меньше x равен lower−1, но он существует только при lower > 0. Первый элемент больше x находится на upper, но обращаться к нему можно только при upper < n. Возвращённая позиция вставки сама по себе не гарантирует существования элемента.

В коде Python и Rust ниже два поиска отличаются только строгостью сравнения. Попробуйте заранее определить результат для пустого массива, x меньше минимума и x больше максимума — эти случаи проверяют, действительно ли вы понимаете смысл возвращаемого индекса.

Обе границы и число вхождений

Один шаблон ищет первую позицию, где предикат стал истинным. Lower bound использует ≥ x, upper bound — > x. Ответ n допустим, обращения к a[n] нет. Разность границ равна числу вхождений. Время O(log(n + 1)), память O(1).

Обе границы и число вхождений / Python
def bound(a: list[int], x: int, upper: bool = False) -> int:
    left, right = 0, len(a)
    while left < right:
        mid = left + (right - left) // 2
        good = a[mid] > x if upper else a[mid] >= x
        if good: right = mid
        else: left = mid + 1
    return left

data = [1, 3, 3, 5, 8, 8, 8, 10]
lo, hi = bound(data, 8), bound(data, 8, True)
print(lo, hi, hi - lo)
Ожидаемый вывод
4 7 3

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Сколько предложений укладывается в бюджет?

После сортировки n цен каждый из q запросов решается upper bound. При n = q = 100 000 получается O(n log n + q log n), а не 10¹⁰ сравнений перебором. Большое значение бюджета не увеличивает число уровней поиска по массиву.

Во всех этих запросах граница находилась среди индексов массива. Но самому алгоритму массив не нужен: достаточно уметь проверять монотонное условие для выбранного кандидата. Перенесём поиск на числовое значение ответа.

Напишите lower_bound(a, x): первый индекс с a[i] ≥ x в отсортированном списке. Если подходящего значения нет, верните len(a). Реализуйте двоичное сужение границ.

Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

Открыто 0 из 3

    5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

    Тест 1 lower_bound([1, 3, 3, 8], 3)Ожидается: 1
    Тест 2 lower_bound([], 7)Ожидается: 0
    Тест 3 lower_bound([1, 3], 9)Ожидается: 2

    Дополнительные тесты: 6. Проверяются границы и повторы.

    Попыток: 0
    04

    Двоичный поиск по ответу

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Поиск в отсортированном массиве →

    Сначала простой вопрос: нужно накопить не менее 12 монет, а каждый день добавляет 3. Для 2,3,4,5 дней ответы «нет, нет, да, да». Первое подходящее число дней равно 4. После первого «да» ответ снова не станет «нет» — это и есть свойство, которое позволяет делить диапазон пополам.

    Пусть требуется минимальное значение параметра, при котором задачу можно выполнить. Вместо прямого построения оптимума спросим: можно ли выполнить задачу при фиксированном X? Если увеличение X не превращает возможное в невозможное, получаем тот же переход «нет → да».

    Монотонный предикат (Monotone predicate) — Булева функция, у которой на упорядоченной области сначала все false, затем все true (или наоборот).

    Рассмотрим n прямоугольников a×b без поворотов, которые размещаются рядами и столбцами в квадрате. При стороне x в ряд помещается ⌊x/a⌋ прямоугольников, а рядов помещается ⌊x/b⌋. Требуется вместить не меньше n штук.

    feasible(x) ⇔ ⌊x/a⌋ · ⌊x/b⌋ ≥ n

    Если сторона x подходит, любая большая тоже подходит. Ищем первую допустимую целую сторону тем же циклом left<right: при fits(mid) делаем right=mid, иначе left=mid+1. Начинаем с left=0 и заведомо достаточной right=max(a,b)·n: в такой квадрат точно поместится ряд из n прямоугольников. Для другой проверки верхнюю границу можно искать удвоением. Если right не гарантирует ответ, поиск способен вернуть неверную границу.

    Сначала предположите: хватит ли стороны X?

    Нужно 6 прямоугольников 2×3 без поворота. Меняйте сторону квадрата, выберите ответ и проверьте число мест.

    Прямоугольники в квадрате
    left
    0
    right
    36
    Проверили сторону
    0
    Вмещает
    0
    0 заведомо мало, верхняя граница гарантированно достаточна.
    Шаг 1 / 7

    Поворот прямоугольников не используется. Предикат floor(s/w) · floor(s/h) ≥ n монотонен.

    Одна проверка здесь занимает O(1) машинных арифметических операций, поэтому время — O(log R), где R — найденная верхняя граница. В других задачах проверка может просматривать n элементов: тогда общая оценка будет O(n log R). Название «двоичный поиск» само по себе не задаёт полную сложность.

    ПереполнениеПри больших целых floor(x/a)*floor(x/b) может переполнять тип. Можно сравнивать с требуемым количеством через безопасное деление или применять целые произвольной точности.

    Python использует целые произвольной точности, а пример Rust считает в u128 при явно ограниченных w,h,n. Даже если границы поиска помещаются в i64, произведение двух частных может оказаться больше. В общем случае используйте подходящий широкий тип или безопасное сравнение через деление.

    Поиск стороны квадрата по ответу

    w, h и n положительны и не превосходят 10⁹. Предикат монотонен: увеличение стороны не уменьшает число помещающихся прямоугольников. Начальная верхняя граница max(w,h)·n достаточна. В Rust вычисления идут в u128: промежуточное произведение частных может превышать i64. Повороты не разрешены. Сложность O(log(max(w,h)·n)).

    Поиск стороны квадрата по ответу / Python
    def square_side(w: int, h: int, n: int) -> int:
        assert 1 <= w <= 10**9 and 1 <= h <= 10**9 and 1 <= n <= 10**9
        left, right = 0, max(w, h) * n
        while left < right:
            mid = left + (right - left) // 2
            if (mid // w) * (mid // h) >= n: right = mid
            else: left = mid + 1
        return left
    
    print(square_side(3, 2, 12))
    Ожидаемый вывод
    9

    В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

    Минимальный размер, скорость или лимит нагрузки

    Задачи о вместимости, расписании и разбиении часто допускают дешёвую проверку фиксированного лимита. Сначала проверьте на малых входах, что увеличение лимита не ухудшает допустимость. Для отрицательных весов привычная жадная проверка разбиения может оказаться неверной.

    Мы заменили оптимизацию проверкой допустимости. Этот же приём особенно полезен, когда ответ определяется максимумом нескольких величин: проверка общего ограничения может оказаться проще прямой минимизации.

    Напишите fits(x, a, b, n): помещается ли не меньше n прямоугольников a×b без поворотов в сетке внутри квадрата x×x? a,b,n > 0, x ≥ 0. Верните bool.

    Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

    Открыто 0 из 3

      5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

      Тест 1 fits(6, 2, 3, 6)Ожидается: True
      Тест 2 fits(5, 2, 3, 6)Ожидается: False
      Тест 3 fits(0, 1, 1, 1)Ожидается: False

      Дополнительные тесты: 4. Проверяются границы и повторы.

      Попыток: 0
      05

      Минимакс и задача о встрече

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Поиск в отсортированном массиве →

      Два участника стоят в 0 и 10 и идут со скоростью 1. За t=4 они достигают [−4,4] и [6,14]: встреча невозможна. При t=5 интервалы соприкасаются в точке 5 — оба придут одновременно. При t=6 мест встречи уже много. Сначала подвигайте время в модели, затем сравните случай нескольких участников.

      Пусть участники находятся в точках xᵢ на прямой и движутся со скоростями vᵢ > 0. Нужно выбрать место встречи p так, чтобы последний участник пришёл как можно раньше. Для фиксированной точки p время встречи равно maxᵢ |p−xᵢ|/vᵢ, а искать нужно минимум этого выражения по p.

      Вместо выбора точки зафиксируем время t и спросим, существует ли хотя бы одно общее место встречи к этому моменту. Участник i за это время достигает любого места из отрезка [xᵢ−vᵢt, xᵢ+vᵢt]. Все должны выбрать одну и ту же точку, поэтому нужно пересечение всех отрезков.

      feasible(t) ⇔ maxᵢ(xᵢ − vᵢt) ≤ minᵢ(xᵢ + vᵢt)

      Для проверки достаточно найти самую правую левую границу L и самую левую правую границу R. Если L ≤ R, любой p из [L, R] подходит; если L > R, общего места нет. Один проход по участникам стоит O(n).

      Могут ли все встретиться к моменту t?
      Участник 1[-12.00, -4.00]
      Участник 2[-4.20, 10.20]
      Участник 3[5.40, 24.60]

      Пересечение: [5.40, -4.00]. Левая граница больше правой: встретиться пока нельзя.

      Увеличение времени расширяет каждый интервал достижимости. В алгоритме по этому предикату выполняют двоичный поиск времени.

      При увеличении t каждый отрезок только расширяется, поэтому непустое пересечение уже не исчезнет. Получен монотонный предикат для двоичного поиска по времени. При k итерациях полная работа равна O(kn), а не O(k). Положительные скорости гарантируют существование конечного подходящего времени; нулевые скорости потребовали бы отдельного разбора.

      Минимальное время встречи

      За время t участник i достигает отрезка [xᵢ−vᵢt, xᵢ+vᵢt]. Все могут встретиться тогда и только тогда, когда пересечение непусто. Для непустого набора, положительных скоростей и конечных разумных координат сначала удваиваем верхнюю границу, затем делаем 100 итераций. Проверка стоит O(n); точность ограничена f64, поэтому формат вывода не заменяет оценку ошибки.

      Минимальное время встречи / Python
      def meeting_time(people: list[tuple[float, float]]) -> float:
          assert people and all(v > 0 for _, v in people)
          def feasible(t: float) -> bool:
              return max(x-v*t for x,v in people) <= min(x+v*t for x,v in people)
          left, right = 0.0, 1.0
          while not feasible(right): right *= 2
          for _ in range(100):
              mid = (left + right) / 2
              if feasible(mid): right = mid
              else: left = mid
          return right
      
      print(f"{meeting_time([(7.0,1.0),(1.0,2.0),(3.0,1.0)]):.6f}")
      Ожидаемый вывод
      2.000000

      В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

      Общее место встречи

      При 60 000 участниках проверка 100 кандидатов времени — около 6 миллионов обработок отрезков. Перебор всех координат до 10⁹ не нужен. Учитывайте требуемую абсолютную или относительную ошибку и используйте достаточно точный тип.

      В отличие от стороны квадрата, время встречи — вещественный ответ. У него нет следующего целого: обновление left=mid+1 потеряло бы часть допустимых времён. Уточним, как вместо дискретного сужения задать требуемую точность и число итераций.

      Примените алгоритм к новому примеру

      Задания раздела

      Два участника в 0 и 10 со скоростью 1. Возможна встреча к t=4?

      При каком минимальном целом t они впервые могут встретиться?

      Попыток: 0
      06

      Двоичный поиск по вещественным числам

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Поиск в отсортированном массиве →

      Пусть ответ лежит на [0,10], а нужна абсолютная ошибка не более 0.001. После 14 делений ширина 10/2¹⁴≈0.0006104: любой конец находится от ответа не дальше этой ширины в точной арифметике. Средняя точка даёт границу ошибки вдвое меньше. Количество знаков после запятой само по себе не доказывает требуемую точность.

      Теперь границы ограничивают вещественный ответ: left ≤ ответ ≤ right, правая граница заведомо допустима. Вместо целого left=mid+1 используем left=mid: прибавление единицы потеряло бы возможные дробные ответы. Если mid допустим, по-прежнему уменьшаем right до mid. Для ответа на самой левой границе left тоже может быть допустим.

      В точной арифметике каждый двоичный шаг уменьшает ширину интервала вдвое. Если исходные границы L и R известны, можно заранее вычислить число шагов, достаточное для требуемой абсолютной ошибки ε.

      после k шагов: длина интервала ≤ (R−L)/2ᵏ

      Из (R−L)/2ᵏ ≤ ε получаем k ≥ log₂((R−L)/ε). Например, для начальной ширины 10⁹ и ε = 10⁻⁶ теоретически достаточно 50 шагов. В модели ниже видно, как быстро сужается интервал.

      Вещественный двоичный поиск: √2
      left
      0.0000000000
      right
      2.0000000000
      Ширина
      2.00e+0
      √2 принадлежит [0, 2].
      Шаг 1 / 16

      Точная арифметическая ширина уменьшается вдвое. В машинной арифметике дальнейшее сужение в итоге ограничено точностью типа.

      Реальные float и f64 имеют конечную точность. На больших значениях соседние представимые числа могут отстоять друг от друга больше нужной абсолютной ошибки; при многократном сужении середина может совпасть с границей. Фиксированное число итераций предотвращает бесконечный цикл, но не устраняет ошибку самой проверки.

      Численная ошибкаДля близких чисел их среднее может совпасть с одной из границ из-за округления; количество итераций разумно ограничивать. Важна и надёжность сравнения при вычислении условия.

      Сверяйтесь с условием задачи: абсолютная ошибка ограничивает расстояние до ответа, относительная — расстояние в масштабе его величины. Если требуется допустимое значение параметра, возвращайте проверенную правую границу. Число знаков после запятой при выводе должно сохранять уже полученную точность, а не подменять её.

      Выберите точность на [0,10]

      ε = 1e-3. Чтобы ширина стала не больше ε, достаточно 14 делений: 10 / 2^14 = 6.104e-4. Середина отличается от ответа не более чем на половину ширины в точной арифметике; правый конец — не более чем на ширину.

      Из-за округления mid == left, но right-left ещё больше ε. Что исправит бесконечный цикл?

      До сих пор поиск держался на переходе монотонного условия. Иногда такой проверки не видно, зато известна форма функции стоимости. Тогда применяется другой приём с отдельным условием корректности — тернарный поиск.

      Примените алгоритм к новому примеру

      Задания раздела

      Ширина 10. Сколько двоичных делений нужно для ширины ≤0.001?

      mid округлился в left. Как избежать зависания?

      Попыток: 0
      07

      Тернарный поиск минимума

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Поиск в отсортированном массиве →

      У функции f(x)=(x−1)²+1 на [−3,3] значения 17,5,1,5 в точках −3,−1,1,3: слева от 1 функция убывает, справа растёт. Две внутренние точки позволяют понять, с какой стороны минимума лежит более дорогая часть. Сначала рассмотрите рисунок и переключите функцию на две впадины.

      Проверьте форму функции до поиска

      На [−3,3] значение убывает до x=1, затем растёт. Сравнение внутренних точек позволяет сохранить минимум.

      Почему обычный тернарный поиск нельзя обосновать для (x²−4)² на [−3,3]?

      Пусть функция сначала строго убывает, затем строго возрастает; между этими участками может быть плоская область глобального минимума. Например, f(x) = (x−3)²+2 имеет единственный минимум при x = 3. Мы умеем вычислять f, но не используем её производную.

      Унимодальная функция (Unimodal function) — Для поиска минимума — функция, которая сначала не возрастает, затем не убывает. В стандартном тернарном шаблоне ниже дополнительно требуем строгое убывание и возрастание вне области глобального минимума.

      Возьмём две внутренние точки m₁ = l+(r−l)/3 и m₂ = r−(r−l)/3. Если f(m₁) < f(m₂), минимум не может быть правее m₂: иначе на всём этом участке функция ещё убывала бы. При обратном неравенстве отбрасываем левую треть. Для указанной формы функции при равенстве можно оставить среднюю часть; сохранение одной из двух соседних частей тоже оставляет хотя бы один минимум.

      Тернарный поиск на вещественном интервале
      repeat K times:
          m1 = l + (r-l)/3
          m2 = r − (r-l)/3
          if f(m1) <= f(m2): r = m2
          else: l = m1
      return (l+r)/2
      Тернарный поиск: минимум параболы
      Середина интервала
      1.00000
      Точный минимум модели
      1.4
      Минимум выпуклой параболы на интервале [−4, 6].
      Шаг 1 / 26

      Каждый шаг сохраняет хотя бы один минимум и оставляет не больше 2/3 предыдущей ширины. Для достижения абсолютной точности ε требуется O(log((R−L)/ε)) шагов. Если вычисление f стоит O(n), эту стоимость нужно умножить на число шагов и учесть два вычисления за шаг.

      после k шагов: длина интервала ≤ (2/3)ᵏ (R−L)
      Условие применимостиБез унимодальности выбрасывать треть интервала некорректно: в отброшенной части может находиться другой, меньший локальный минимум.

      Слова «сначала не возрастает, затем не убывает» без дополнительных оговорок недостаточны для стандартной обработки равенства: плоский участок выше минимума может скрывать направление спуска. В рассматриваемом шаблоне требуем строгий ход вне области глобального минимума. Унимодальность необходимо доказать по условию задачи, а не заключить по нескольким точкам графика.

      Для целочисленного аргумента удобнее остановиться, когда осталось несколько кандидатов, и перебрать их. Если же можно надёжно проверять знак изменения f(mid+1)−f(mid), часто подходит двоичный поиск по этому знаку. Выбор определяется тем, какое свойство функции удалось доказать.

      Минимум унимодальной функции

      Ищем минимум (x−u)²+1 на [−4,6]. Для этой функции выпуклость доказывается непосредственно, поэтому сравнение двух внутренних значений позволяет отбросить треть. Делаем 100 итераций вместо проверки равенства f64-границ. Возвращаем значение функции, а не обещание точно представить координату минимума.

      Минимум унимодальной функции / Python
      def minimum_value(u: float) -> float:
          def f(x: float) -> float: return (x-u)**2+1
          left, right = -4.0, 6.0
          for _ in range(100):
              m1 = left + (right-left)/3
              m2 = right - (right-left)/3
              if f(m1) <= f(m2): right = m2
              else: left = m1
          return f((left+right)/2)
      
      print(f"{minimum_value(1.4):.6f}")
      Ожидаемый вывод
      1.000000

      В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

      Один параметр стоимости

      При доказанной выпуклости функция затрат по координате или настройке имеет одну область минимума. Если вычисление f стоит O(n), учтите его на каждой итерации: тернарный шаг вычисляет два значения. Нельзя заменить произвольную оптимизацию тернарным поиском без доказательства формы функции.

      Двоичный и тернарный поиск похожи сужением интервала, но сохраняют разные утверждения: первый — положение перехода предиката, второй — наличие минимума. Сведём эти условия в общую схему выбора метода и проверки реализации.

      Примените алгоритм к новому примеру

      Задания раздела

      Какая форма позволяет обосновать тернарный поиск минимума?

      На целой оси осталось четыре кандидата. Что делать?

      Попыток: 0
      08

      Выбор метода и практические ошибки

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Поиск в отсортированном массиве →

      Перед написанием поиска сформулируйте, что означает ответ. Для массива это может быть первая позиция ≥ x, для задачи оптимизации — минимальный допустимый параметр, для функции — точка минимума. Разные формулировки требуют разных условий, даже если код везде содержит две границы и середину.

      ЗадачаМетодКлючевое условие
      Первый элемент ≥ xДвоичный поискОтсортированный массив
      Минимальное допустимое целое xПоиск по ответуМонотонная проверка
      Нулевое значение монотонной функцииВещественный бинарный поискИзвестно изменение знака / предиката
      Минимум унимодальной функцииТернарный поискСначала убывает, потом возрастает

      Затем докажите, почему отбрасываемая часть не содержит нужного ответа, и почему оставшийся интервал строго уменьшается. Для целых проверьте пустой диапазон, один кандидат, повторы и ответ на краю. Для вещественных добавьте оценку погрешности и ограничения арифметики.

      Наконец, посчитайте не только число шагов, но и стоимость каждого. q запросов в готовом массиве дают O(q log n). Линейная проверка в поиске по ответу даёт O(n log R). Вещественный поиск зависит от исходного диапазона и требуемой ошибки, а не от количества цифр, которые удобно напечатать.

      Почему тернарный поиск не всегда лучше двоичного?
      Тернарный поиск не требует вычислять производную, но использует две оценки f за итерацию и сужает интервал до 2/3. Если знак производной можно надёжно вычислить, бинарный поиск по этому знаку может быть эффективнее.
      Ошибочная строкаЧто ломаетсяИсправление для [left,right)
      while left <= rightЛишнее чтение при схлопнувшихся границахwhile left < right
      left = mid при A[mid]<xОдин кандидат не исключается: зависаниеleft = mid+1
      right = mid−1 при A[mid]≥xТеряется подходящий midright = mid
      good = A[mid]>x для lowerПропускаются повторы xgood = A[mid]>=x

      Ошибка 1: что заменить в while left <= right для [left,right)?

      left, right = 0, len(a)
      while left <= right:
          mid = (left + right) // 2
          # читаем a[mid]

      Ошибка 2: A[mid] < x, код делает left = mid. Как исправить?

      Ошибка 3: нужен первый ≥ x, а good = A[mid] > x. Что заменить?

      После справочника применим эту схему к числовым ограничениям и задачам для тренировки. Цель практики — уметь самостоятельно получить предикат и инвариант, а затем выбрать подходящий шаблон поиска.

      Примените алгоритм к новому примеру

      Задания раздела

      Условие цикла для [left,right)?

      При A[mid]<x ошибочно написали left=mid. Исправьте.

      Нужен lower_bound, но good=A[mid]>x. Как исправить?

      Попыток: 0
      А—Я

      Справочник терминов

      Справочник собирает определения из разобранных разделов. Используйте поиск, если нужно восстановить термин; затем переходите к оценке ограничений и задачам для самостоятельного решения.

      Общий справочник всех лекций ↗

      Найдено: 13 из 13

      Двоичный поиск / Binary searchЛогарифмическое сужение области ответа по монотонному условию.
      Lower bound / Lower boundПервый индекс i с A[i]≥x.
      Upper bound / Upper boundПервый индекс i с A[i]>x.
      Инвариант / InvariantСвойство, сохраняющееся после каждого шага.
      Фиктивная граница / Sentinel boundaryЛогическая точка за массивом, не требующая обращения к памяти.
      Монотонный предикат / Monotone predicateИстина меняется вдоль оси только один раз.
      Двоичный поиск по ответу / Binary search on answerПоиск граничного параметра по проверке допустимости.
      Минимакс / MinimaxМинимизация максимума некоторого семейства функций.
      Feasibility check / Проверка допустимостиОтвет, достижимо ли решение при фиксированном параметре.
      Вещественный двоичный поиск / Real binary searchБинарное сужение непрерывного интервала до нужной точности.
      Унимодальная функция / Unimodal functionНа интервале сначала убывает, затем возрастает (нестрого).
      Тернарный поиск / Ternary searchПоиск экстремума унимодальной функции сравнением двух внутренних точек.
      Абсолютная погрешность / Absolute errorМодуль разницы между приближением и истинным ответом.
      09

      Ограничения и выбор алгоритма

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Поиск в отсортированном массиве →
      Стоимость проверки × число проверок

      Перебор массива: до 10 000 000 000 сравнений. После подготовки: около 1 700 000 сравнений плюс O(n log n) на сортировку.

      Один поиск по ответу при O(n)-проверке: около 3 000 000 обработок элементов. Большое значение элемента не увеличивает число шагов поиска по индексам массива.

      Перейдём от доказательства поиска к бюджету операций. Для целого ответа отдельно оценим ширину пространства R и стоимость одной проверки C. Общая оценка — O(C log(R + 1)). В уже готовом массиве R определяется числом позиций, а C = O(1); при поиске по ответу проверка может быть значительно дороже.

      Когда число до миллиарда не требует миллиарда шагов

      Для целой границы в диапазоне до 10⁹ достаточно около 30 сужений. Если каждый раз проверяется n = 200 000 объектов, это около 6 миллионов обработок объектов. Но ввод n элементов уже стоит Ω(n); получить их из файла за O(log n) невозможно.

      При n = q = 100 000 одна сортировка и q запросов дают O(n log n + q log n), а повторный линейный поиск — до 10¹⁰ сравнений. После сортировки по диапазону цен уже не нужно перебирать весь возможный бюджет.

      Правая граница и переполнение

      В задаче упаковки прямоугольников грубая граница max(w,h)·n может достигать 10¹⁸, а произведение числа строк и столбцов — быть ещё больше. В Rust выбирайте широкий промежуточный тип или сравнение через деление. Беззнаковое left = −1 не выражает фиктивную границу; используйте знаковый тип либо шаблон [left,right).

      Вещественный ответ

      Из начальной ширины W и требуемой ширины ε следует k ≥ log₂(W/ε). Это рассуждение в точной арифметике; машинная погрешность проверки добавляет отдельные ограничения. Для задачи о встрече при положительных скоростях проверка стоит O(n), а 100 итераций при 60 000 участниках — порядка 6 миллионов обработок интервалов.

      Тернарный поиск требует унимодальности. Если функция считается за O(n), каждое из двух вычислений учитывается в бюджете. На целой оси сужайте до нескольких кандидатов и проверяйте их явно; не ожидайте, что вещественное округление даст точный целый минимум.

      ПРОВЕРКА ПРЕДИКАТА

      Сравните с полным перебором на малых входах

      Сгенерируйте маленькую задачу, переберите все допустимые ответы и сравните минимум с найденной границей. Отдельно проверьте, что после первого true предикат никогда не возвращается к false. Это обнаруживает ошибки проверки раньше, чем тест на одном большом входе.

      Примените алгоритм к новому примеру

      Задания раздела

      n=q=100000 цен и запросов. Какой план выбрать?

      Одна проверка смотрит 200000 элементов; выполнено 30 проверок. Сколько обработок?

      Попыток: 0
      10

      Практика: 3 уровня

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Поиск в отсортированном массиве →

      Разминка перед внешними задачами

      Напишите поиск точного значения, границу и подсчёт повторов. Черновики и зачёт общие с проверками глав; откройте подсказки по одной после своей попытки.

      Напишите binary_search(a,x): индекс первого точного вхождения x в отсортированном массиве либо −1. Используйте двоичное сужение [left,right) и проверку наличия после цикла.

      Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

      Открыто 0 из 3

        5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

        Тест 1 binary_search([1, 3, 5, 8, 10], 8)Ожидается: 3
        Тест 2 binary_search([], 8)Ожидается: -1
        Тест 3 binary_search([1, 3, 5], 4)Ожидается: -1

        Дополнительные тесты: 4. Проверяются границы и повторы.

        Попыток: 0

        Напишите lower_bound(a, x): первый индекс с a[i] ≥ x в отсортированном списке. Если подходящего значения нет, верните len(a). Реализуйте двоичное сужение границ.

        Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

        Открыто 0 из 3

          5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

          Тест 1 lower_bound([1, 3, 3, 8], 3)Ожидается: 1
          Тест 2 lower_bound([], 7)Ожидается: 0
          Тест 3 lower_bound([1, 3], 9)Ожидается: 2

          Дополнительные тесты: 6. Проверяются границы и повторы.

          Попыток: 0

          Напишите count_occurrences(a,x) для отсортированного массива: два двоичных поиска границ и число вхождений. Встроенные count/bisect не используйте.

          Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

          Открыто 0 из 3

            5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

            Тест 1 count_occurrences([1, 3, 3, 8, 8], 3)Ожидается: 2
            Тест 2 count_occurrences([], 3)Ожидается: 0
            Тест 3 count_occurrences([1, 3, 8], 4)Ожидается: 0

            Дополнительные тесты: 4. Проверяются границы и повторы.

            Попыток: 0

            Основное применение и испытание

            После разминки решите Search Insert Position и Sqrt(x), затем Interesting Drink и Koko Eating Bananas. Задачу, соединяющую вещественный поиск и проверку пересечения, оставьте как дополнительное испытание.

            Примените разобранные приёмы самостоятельно. Для каждой задачи сначала сформулируйте требования к ответу и предложите простое решение. Затем подставьте максимальные ограничения, оцените время и память и решите, какое улучшение требуется. Подсказки с идеей и оценкой раскрываются отдельно.

            Уровни задают порядок практики в курсе, а не единую шкалу платформ. Если уровень отличается от отметки LeetCode, это указано в карточке. Для Codeforces используется оценка курса.

            01

            Лёгкие

            Три задачи для закрепления основных приёмов.

            1. LeetCode · 35 · Easy

              Search Insert Position

              Вернуть lower bound даже при отсутствии точного совпадения.

              Ограничения. До 10⁴ различных отсортированных чисел; требуется O(log n).

              Идея и оценка сложности

              Ищите первую позицию ≥ target. Ответ может быть n: это вставка после всех элементов. Проверьте target меньше минимума, больше максимума и массив из одного элемента. Не обращайтесь к найденной позиции, пока не проверили её допустимость.

            2. LeetCode · 69 · Easy

              Sqrt(x)

              Найти последнюю целую хорошую границу без вещественного округления.

              Ограничения. 0 ≤ x ≤ 2³¹−1; встроенное возведение в дробную степень запрещено.

              Идея и оценка сложности

              Монотонен предикат s² ≤ x. Чтобы не переполнить промежуточный тип, используйте широкий тип или s ≤ x/s с отдельной обработкой нуля. Время O(log(x+1)), память O(1). Проверьте полный квадрат и соседние значения.

            3. Codeforces · 706B

              Interesting drink

              Свести количество доступных цен к upper bound после одной сортировки.

              Ограничения. n и q до 10⁵; цены до 10⁵, бюджеты запросов до 10⁹.

              Идея и оценка сложности

              Число цен ≤ бюджета равно индексу первого значения > бюджета. Общая стоимость O(n log n+q log n), а не O(nq). Крайние бюджеты и повторяющиеся цены проверяют правильный выбор верхней границы.

            02

            Средние

            Две задачи на сочетание идей и анализ ограничений.

            1. LeetCode · 875 · Medium

              Koko Eating Bananas

              Разделить поиск минимальной скорости и проверку допустимости.

              Ограничения. До 10⁴ куч, размеры и число часов до 10⁹; часов не меньше числа куч.

              Идея и оценка сложности

              При скорости k суммируйте ceil(pile/k). Этот предикат стоит O(n) и монотонен, поэтому общий поиск O(n log maxPile). Сумма может не помещаться в 32 бита; остановитесь, когда она уже превышает h. Не выполняйте округление через float.

            2. Проверять пересечение достижимых интервалов и точность вещественного ответа.

              Ограничения. До 60 000 участников; координаты и положительные скорости до 10⁹; ошибка до 10⁻⁶ по правилу условия.

              Идея и оценка сложности

              Для t вычислите максимум левых и минимум правых границ. Непустое пересечение означает допустимость, которая сохраняется при росте t. Поиск с фиксированным числом итераций даёт O(n · iterations). Гарантируйте начальную верхнюю границу и печатайте достаточно знаков.

            03

            Тяжёлая

            Одна задача для переноса идеи на новую постановку.

            1. LeetCode · 410 · Hard

              Split Array Largest Sum

              Обосновать жадную проверку лимита суммы и монотонность ответа.

              Ограничения. До 1000 неотрицательных значений до 10⁶; частей k ≤ min(50,n).

              Идея и оценка сложности

              Для лимита S последовательно набирайте максимально длинную допустимую часть. Проверьте, что понадобится не больше k частей; из-за неотрицательности и k≤n можно дополнительно разрезать до ровно k. Ищите S от max(nums) до sum(nums). Стоимость O(n log(sum+1)), суммы хранятся в широком типе. Отрицательные значения нарушили бы это обоснование.

            Примените алгоритм к новому примеру

            Задания раздела

            Выберите метод.

            Почему перед поиском минимальной скорости нужно доказать монотонность?

            Попыток: 0

            ПРОДОЛЖИТЬ ОБСУЖДЕНИЕ

            Промпт для ИИ

            Скопируйте промпт с текстом лекции в чат с ИИ, чтобы обсудить непонятные места. В него входят дополнительные материалы, псевдокод и примеры на обоих языках.