Поиск в отсортированном массиве
Найдём 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.
Условие A[i] ≥ x на отсортированном массиве имеет вид «ложь, ложь, …, истина, истина». Ищем первый индекс, где оно истинно. Если таких элементов нет, возвращаем n — позицию сразу после массива. Так lower bound определён и для отсутствующего x.
Ваш шаг: left = 0, right = 10, mid = 5, A[mid] = 8. Как обновить границы?
Здесь и в основном коде используется [left, right): сначала left = 0, right = n, цикл left < right. Подходящий mid сохраняем через right = mid; неподходящий исключаем через left = mid + 1. Ответ n допустим.
Предварительная сортировка не бесплатна. Для одного поиска в неупорядоченном массиве обычно достаточно O(n)-прохода. Сортировать имеет смысл, когда данные уже упорядочены или запросов много: стоимость подготовки тогда оплачивается один раз.
Идея деления пополам проста, но ошибки обычно возникают в границах. Зафиксируем точный смысл левой и правой границы, чтобы из него следовали и обновления, и условие остановки.
Один шаблон: [left,right) и инвариант
Этот раздел откроется после предыдущих проверок.
Продолжить: Поиск в отсортированном массиве →После сравнения A[mid]=7 с целью 8 можно исключить и сам mid, и всё слева: left = mid+1. Если A[mid]=10, этот индекс ещё может оказаться первой подходящей позицией, поэтому right = mid. Именно эти два действия выполняет модель.
Храним полуинтервал ещё не проверенных позиций [left,right). Все индексы меньше left заведомо содержат значения < x; все действительные индексы начиная с right содержат значения ≥ x. Ответ-граница находится между left и right включительно, в том числе может быть n.
left = 0; right = n
while left < right:
mid = left + (right-left)//2
if A[mid] >= x:
right = mid
else:
left = mid+1
return leftright не входит в неизвестный полуинтервал. mid всегда является допустимым индексом.
Ваш шаг: left = 0, right = 10, mid = 5, A[mid] = 8. Как обновить границы?
Здесь и в основном коде используется [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
Смысл [left,right) теперь одинаков в модели и коде. Изменим только строгость проверки и получим вторую полезную границу.
Границы и ближайшие элементы
Этот раздел откроется после предыдущих проверок.
Продолжить: Поиск в отсортированном массиве →В [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.
Ваш шаг: left = 0, right = 10, mid = 5, A[mid] = 8. Как обновить границы?
Здесь и в основном коде используется [left, right): сначала left = 0, right = n, цикл left < right. Подходящий mid сохраняем через right = mid; неподходящий исключаем через left = mid + 1. Ответ n допустим.
| Что ищем | Условие good | Ответ |
|---|---|---|
| Первый A[i] ≥ x | A[mid] ≥ x | lower |
| Первый A[i] > x | A[mid] > x | upper |
| Последний A[i] ≤ x | Сначала ищем upper | upper−1, если upper>0 |
| Есть ли x | lower<n и A[lower]=x | Да / нет |
≥ на > в одном месте и не сменить ожидаемый ответ — типичная ошибка двоичного поиска.Границы помогают искать и соседей. Индекс последнего элемента меньше 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).
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)fn bound(a: &[i64], x: i64, upper: bool) -> usize {
let (mut left, mut right) = (0,a.len());
while left < right {
let mid = left + (right-left)/2;
let good = if upper { a[mid] > x } else { a[mid] >= x };
if good { right=mid; } else { left=mid+1; }
}
left
}
fn main() {
let data=[1,3,3,5,8,8,8,10];
let lo=bound(&data,8,false); let hi=bound(&data,8,true);
println!("{} {} {}",lo,hi,hi-lo);
}4 7 3
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Сколько предложений укладывается в бюджет?
После сортировки n цен каждый из q запросов решается upper bound. При n = q = 100 000 получается O(n log n + q log n), а не 10¹⁰ сравнений перебором. Большое значение бюджета не увеличивает число уровней поиска по массиву.
Во всех этих запросах граница находилась среди индексов массива. Но самому алгоритму массив не нужен: достаточно уметь проверять монотонное условие для выбранного кандидата. Перенесём поиск на числовое значение ответа.
Двоичный поиск по ответу
Этот раздел откроется после предыдущих проверок.
Продолжить: Поиск в отсортированном массиве →Сначала простой вопрос: нужно накопить не менее 12 монет, а каждый день добавляет 3. Для 2,3,4,5 дней ответы «нет, нет, да, да». Первое подходящее число дней равно 4. После первого «да» ответ снова не станет «нет» — это и есть свойство, которое позволяет делить диапазон пополам.
Пусть требуется минимальное значение параметра, при котором задачу можно выполнить. Вместо прямого построения оптимума спросим: можно ли выполнить задачу при фиксированном X? Если увеличение X не превращает возможное в невозможное, получаем тот же переход «нет → да».
Монотонный предикат (Monotone predicate) — Булева функция, у которой на упорядоченной области сначала все false, затем все true (или наоборот).
Рассмотрим n прямоугольников a×b без поворотов, которые размещаются рядами и столбцами в квадрате. При стороне 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 не гарантирует ответ, поиск способен вернуть неверную границу.
Нужно 6 прямоугольников 2×3 без поворота. Меняйте сторону квадрата, выберите ответ и проверьте число мест.
Поворот прямоугольников не используется. Предикат 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)).
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))fn square_side(w: u64, h: u64, n: u64) -> u128 {
assert!((1..=1_000_000_000).contains(&w));
assert!((1..=1_000_000_000).contains(&h));
assert!((1..=1_000_000_000).contains(&n));
let (w,h,n)=(w as u128,h as u128,n as u128);
let (mut left,mut right)=(0,w.max(h)*n);
while left<right {
let mid=left+(right-left)/2;
if (mid/w)*(mid/h)>=n { right=mid; } else { left=mid+1; }
}
left
}
fn main() { println!("{}", square_side(3,2,12)); }9
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Минимальный размер, скорость или лимит нагрузки
Задачи о вместимости, расписании и разбиении часто допускают дешёвую проверку фиксированного лимита. Сначала проверьте на малых входах, что увеличение лимита не ухудшает допустимость. Для отрицательных весов привычная жадная проверка разбиения может оказаться неверной.
Мы заменили оптимизацию проверкой допустимости. Этот же приём особенно полезен, когда ответ определяется максимумом нескольких величин: проверка общего ограничения может оказаться проще прямой минимизации.
Минимакс и задача о встрече
Этот раздел откроется после предыдущих проверок.
Продолжить: Поиск в отсортированном массиве →Два участника стоят в 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]. Все должны выбрать одну и ту же точку, поэтому нужно пересечение всех отрезков.
Для проверки достаточно найти самую правую левую границу L и самую левую правую границу R. Если L ≤ R, любой p из [L, R] подходит; если L > R, общего места нет. Один проход по участникам стоит O(n).
[-12.00, -4.00][-4.20, 10.20][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, поэтому формат вывода не заменяет оценку ошибки.
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}")fn meeting_time(people: &[(f64,f64)]) -> f64 {
assert!(!people.is_empty() && people.iter().all(|&(_,v)|v>0.0));
let feasible=|t:f64| {
let left=people.iter().map(|&(x,v)|x-v*t).fold(f64::NEG_INFINITY,f64::max);
let right=people.iter().map(|&(x,v)|x+v*t).fold(f64::INFINITY,f64::min);
left<=right
};
let (mut left,mut right)=(0.0,1.0);
while !feasible(right) { right*=2.0; }
for _ in 0..100 {
let mid=(left+right)/2.0;
if feasible(mid) { right=mid; } else { left=mid; }
}
right
}
fn main() { println!("{:.6}",meeting_time(&[(7.0,1.0),(1.0,2.0),(3.0,1.0)])); }2.000000
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Общее место встречи
При 60 000 участниках проверка 100 кандидатов времени — около 6 миллионов обработок отрезков. Перебор всех координат до 10⁹ не нужен. Учитывайте требуемую абсолютную или относительную ошибку и используйте достаточно точный тип.
В отличие от стороны квадрата, время встречи — вещественный ответ. У него нет следующего целого: обновление left=mid+1 потеряло бы часть допустимых времён. Уточним, как вместо дискретного сужения задать требуемую точность и число итераций.
Двоичный поиск по вещественным числам
Этот раздел откроется после предыдущих проверок.
Продолжить: Поиск в отсортированном массиве →Пусть ответ лежит на [0,10], а нужна абсолютная ошибка не более 0.001. После 14 делений ширина 10/2¹⁴≈0.0006104: любой конец находится от ответа не дальше этой ширины в точной арифметике. Средняя точка даёт границу ошибки вдвое меньше. Количество знаков после запятой само по себе не доказывает требуемую точность.
Теперь границы ограничивают вещественный ответ: left ≤ ответ ≤ right, правая граница заведомо допустима. Вместо целого left=mid+1 используем left=mid: прибавление единицы потеряло бы возможные дробные ответы. Если mid допустим, по-прежнему уменьшаем right до mid. Для ответа на самой левой границе left тоже может быть допустим.
В точной арифметике каждый двоичный шаг уменьшает ширину интервала вдвое. Если исходные границы L и R известны, можно заранее вычислить число шагов, достаточное для требуемой абсолютной ошибки ε.
Из (R−L)/2ᵏ ≤ ε получаем k ≥ log₂((R−L)/ε). Например, для начальной ширины 10⁹ и ε = 10⁻⁶ теоретически достаточно 50 шагов. В модели ниже видно, как быстро сужается интервал.
Точная арифметическая ширина уменьшается вдвое. В машинной арифметике дальнейшее сужение в итоге ограничено точностью типа.
Реальные float и f64 имеют конечную точность. На больших значениях соседние представимые числа могут отстоять друг от друга больше нужной абсолютной ошибки; при многократном сужении середина может совпасть с границей. Фиксированное число итераций предотвращает бесконечный цикл, но не устраняет ошибку самой проверки.
Сверяйтесь с условием задачи: абсолютная ошибка ограничивает расстояние до ответа, относительная — расстояние в масштабе его величины. Если требуется допустимое значение параметра, возвращайте проверенную правую границу. Число знаков после запятой при выводе должно сохранять уже полученную точность, а не подменять её.
ε = 1e-3. Чтобы ширина стала не больше ε, достаточно 14 делений: 10 / 2^14 = 6.104e-4. Середина отличается от ответа не более чем на половину ширины в точной арифметике; правый конец — не более чем на ширину.
До сих пор поиск держался на переходе монотонного условия. Иногда такой проверки не видно, зато известна форма функции стоимости. Тогда применяется другой приём с отдельным условием корректности — тернарный поиск.
Тернарный поиск минимума
Этот раздел откроется после предыдущих проверок.
Продолжить: Поиск в отсортированном массиве →У функции f(x)=(x−1)²+1 на [−3,3] значения 17,5,1,5 в точках −3,−1,1,3: слева от 1 функция убывает, справа растёт. Две внутренние точки позволяют понять, с какой стороны минимума лежит более дорогая часть. Сначала рассмотрите рисунок и переключите функцию на две впадины.
На [−3,3] значение убывает до x=1, затем растёт. Сравнение внутренних точек позволяет сохранить минимум.
Пусть функция сначала строго убывает, затем строго возрастает; между этими участками может быть плоская область глобального минимума. Например, 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Каждый шаг сохраняет хотя бы один минимум и оставляет не больше 2/3 предыдущей ширины. Для достижения абсолютной точности ε требуется O(log((R−L)/ε)) шагов. Если вычисление f стоит O(n), эту стоимость нужно умножить на число шагов и учесть два вычисления за шаг.
Слова «сначала не возрастает, затем не убывает» без дополнительных оговорок недостаточны для стандартной обработки равенства: плоский участок выше минимума может скрывать направление спуска. В рассматриваемом шаблоне требуем строгий ход вне области глобального минимума. Унимодальность необходимо доказать по условию задачи, а не заключить по нескольким точкам графика.
Для целочисленного аргумента удобнее остановиться, когда осталось несколько кандидатов, и перебрать их. Если же можно надёжно проверять знак изменения f(mid+1)−f(mid), часто подходит двоичный поиск по этому знаку. Выбор определяется тем, какое свойство функции удалось доказать.
Минимум унимодальной функции
Ищем минимум (x−u)²+1 на [−4,6]. Для этой функции выпуклость доказывается непосредственно, поэтому сравнение двух внутренних значений позволяет отбросить треть. Делаем 100 итераций вместо проверки равенства f64-границ. Возвращаем значение функции, а не обещание точно представить координату минимума.
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}")fn minimum_value(u: f64) -> f64 {
let f=|x:f64| (x-u)*(x-u)+1.0;
let (mut left,mut right)=(-4.0,6.0);
for _ in 0..100 {
let m1=left+(right-left)/3.0;
let m2=right-(right-left)/3.0;
if f(m1)<=f(m2) { right=m2; } else { left=m1; }
}
f((left+right)/2.0)
}
fn main() { println!("{:.6}",minimum_value(1.4)); }1.000000
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Один параметр стоимости
При доказанной выпуклости функция затрат по координате или настройке имеет одну область минимума. Если вычисление f стоит O(n), учтите его на каждой итерации: тернарный шаг вычисляет два значения. Нельзя заменить произвольную оптимизацию тернарным поиском без доказательства формы функции.
Двоичный и тернарный поиск похожи сужением интервала, но сохраняют разные утверждения: первый — положение перехода предиката, второй — наличие минимума. Сведём эти условия в общую схему выбора метода и проверки реализации.
Выбор метода и практические ошибки
Этот раздел откроется после предыдущих проверок.
Продолжить: Поиск в отсортированном массиве →Перед написанием поиска сформулируйте, что означает ответ. Для массива это может быть первая позиция ≥ x, для задачи оптимизации — минимальный допустимый параметр, для функции — точка минимума. Разные формулировки требуют разных условий, даже если код везде содержит две границы и середину.
| Задача | Метод | Ключевое условие |
|---|---|---|
| Первый элемент ≥ x | Двоичный поиск | Отсортированный массив |
| Минимальное допустимое целое x | Поиск по ответу | Монотонная проверка |
| Нулевое значение монотонной функции | Вещественный бинарный поиск | Известно изменение знака / предиката |
| Минимум унимодальной функции | Тернарный поиск | Сначала убывает, потом возрастает |
Затем докажите, почему отбрасываемая часть не содержит нужного ответа, и почему оставшийся интервал строго уменьшается. Для целых проверьте пустой диапазон, один кандидат, повторы и ответ на краю. Для вещественных добавьте оценку погрешности и ограничения арифметики.
Наконец, посчитайте не только число шагов, но и стоимость каждого. q запросов в готовом массиве дают O(q log n). Линейная проверка в поиске по ответу даёт O(n log R). Вещественный поиск зависит от исходного диапазона и требуемой ошибки, а не от количества цифр, которые удобно напечатать.
Почему тернарный поиск не всегда лучше двоичного?
| Ошибочная строка | Что ломается | Исправление для [left,right) |
|---|---|---|
| while left <= right | Лишнее чтение при схлопнувшихся границах | while left < right |
| left = mid при A[mid]<x | Один кандидат не исключается: зависание | left = mid+1 |
| right = mid−1 при A[mid]≥x | Теряется подходящий mid | right = mid |
| good = A[mid]>x для lower | Пропускаются повторы x | good = A[mid]>=x |
После справочника применим эту схему к числовым ограничениям и задачам для тренировки. Цель практики — уметь самостоятельно получить предикат и инвариант, а затем выбрать подходящий шаблон поиска.
Справочник терминов
Справочник собирает определения из разобранных разделов. Используйте поиск, если нужно восстановить термин; затем переходите к оценке ограничений и задачам для самостоятельного решения.
Общий справочник всех лекций ↗
Найдено: 13 из 13
Ограничения и выбор алгоритма
Этот раздел откроется после предыдущих проверок.
Продолжить: Поиск в отсортированном массиве →Перебор массива: до 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. Это обнаруживает ошибки проверки раньше, чем тест на одном большом входе.
Практика: 3 уровня
Этот раздел откроется после предыдущих проверок.
Продолжить: Поиск в отсортированном массиве →Разминка перед внешними задачами
Напишите поиск точного значения, границу и подсчёт повторов. Черновики и зачёт общие с проверками глав; откройте подсказки по одной после своей попытки.
Основное применение и испытание
После разминки решите Search Insert Position и Sqrt(x), затем Interesting Drink и Koko Eating Bananas. Задачу, соединяющую вещественный поиск и проверку пересечения, оставьте как дополнительное испытание.
Примените разобранные приёмы самостоятельно. Для каждой задачи сначала сформулируйте требования к ответу и предложите простое решение. Затем подставьте максимальные ограничения, оцените время и память и решите, какое улучшение требуется. Подсказки с идеей и оценкой раскрываются отдельно.
Уровни задают порядок практики в курсе, а не единую шкалу платформ. Если уровень отличается от отметки LeetCode, это указано в карточке. Для Codeforces используется оценка курса.
Лёгкие
Три задачи для закрепления основных приёмов.
- LeetCode · 35 · Easy
Search Insert Position
Вернуть lower bound даже при отсутствии точного совпадения.
Ограничения. До 10⁴ различных отсортированных чисел; требуется O(log n).
Идея и оценка сложности
Ищите первую позицию ≥ target. Ответ может быть n: это вставка после всех элементов. Проверьте target меньше минимума, больше максимума и массив из одного элемента. Не обращайтесь к найденной позиции, пока не проверили её допустимость.
- LeetCode · 69 · Easy
Sqrt(x)
Найти последнюю целую хорошую границу без вещественного округления.
Ограничения. 0 ≤ x ≤ 2³¹−1; встроенное возведение в дробную степень запрещено.
Идея и оценка сложности
Монотонен предикат s² ≤ x. Чтобы не переполнить промежуточный тип, используйте широкий тип или s ≤ x/s с отдельной обработкой нуля. Время O(log(x+1)), память O(1). Проверьте полный квадрат и соседние значения.
- Codeforces · 706B
Interesting drink
Свести количество доступных цен к upper bound после одной сортировки.
Ограничения. n и q до 10⁵; цены до 10⁵, бюджеты запросов до 10⁹.
Идея и оценка сложности
Число цен ≤ бюджета равно индексу первого значения > бюджета. Общая стоимость O(n log n+q log n), а не O(nq). Крайние бюджеты и повторяющиеся цены проверяют правильный выбор верхней границы.
Средние
Две задачи на сочетание идей и анализ ограничений.
- LeetCode · 875 · Medium
Koko Eating Bananas
Разделить поиск минимальной скорости и проверку допустимости.
Ограничения. До 10⁴ куч, размеры и число часов до 10⁹; часов не меньше числа куч.
Идея и оценка сложности
При скорости k суммируйте ceil(pile/k). Этот предикат стоит O(n) и монотонен, поэтому общий поиск O(n log maxPile). Сумма может не помещаться в 32 бита; остановитесь, когда она уже превышает h. Не выполняйте округление через float.
- Codeforces · 782B
The Meeting Place Cannot Be Changed
Проверять пересечение достижимых интервалов и точность вещественного ответа.
Ограничения. До 60 000 участников; координаты и положительные скорости до 10⁹; ошибка до 10⁻⁶ по правилу условия.
Идея и оценка сложности
Для t вычислите максимум левых и минимум правых границ. Непустое пересечение означает допустимость, которая сохраняется при росте t. Поиск с фиксированным числом итераций даёт O(n · iterations). Гарантируйте начальную верхнюю границу и печатайте достаточно знаков.
Тяжёлая
Одна задача для переноса идеи на новую постановку.
- 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)), суммы хранятся в широком типе. Отрицательные значения нарушили бы это обоснование.