Поиск минимума – базовая процедура обработки данных, встречающаяся в сортировках, оптимизационных задачах, статистических агрегатах и во множестве подзадач ЕГЭ. Несмотря на простоту формулировки, корректная и эффективная реализация требует явной формализации инвариантов, аккуратной работы с индексами и понимания нижних границ по числу сравнений. В данной статье излагаются: формальная модель и доказательство корректности алгоритма линейного поиска минимума, нижняя граница n−1 сравнений, инженерные соглашения (инициализация, обработка пустого массива, стабильность при равенствах, работа с типами), продвинутые варианты (минимум и его индекс, минимум по предикату/проекцией, блочная и параллельная редукция), мини-шпаргалка формул, типовые ошибки и пять разборов «в стиле ЕГЭ».
Задача и обозначения
Дан массив (последовательность)
A : {1,2,…,n} → X
где X – упорядоченное множество (обычно целые или вещественные числа). Требуется вычислить
min(A) = argmin_{x∈Im(A)} x
и (часто) индекс
i_min ∈ {1,…,n} : A[i_min] = min(A).
Если минимум достигается несколько раз, фиксируем соглашение: возвращается минимальный индекс (первая встреча минимума). Это важно для детерминизма.
Модель вычислений
Алгоритм опирается на сравнения элементов по порядку ≤. Стоимость – количество сравнений и присваиваний. Для несортированного массива без дополнительной информации любая корректная стратегия требует не менее n−1 сравнений.
Инвариант цикла (основа доказательства)
Рассмотрим классический однопроходный алгоритм:
Инициализация:
i_min := 1; m := A[1]
Итерации по k от 2 до n:
если A[k] < m тогда m := A[k]; i_min := k
Результат: (m, i_min).
Инвариант: после обработки префикса A[1..k] значение m равно min(A[1..k]), а i_min – индексу первого вхождения этого минимума.
Доказательство – по индукции: база k=1 очевидна; шаг – корректен, т.к. рассматриваются два исчерпывающих случая: A[k] < m (обновление) и A[k] ≥ m (сохранение минимума и его первого индекса).
Завершимость и частные случаи
Алгоритм делает конечное число шагов (ровно n−1 сравнений). Для n=1 возвращает (A[1],1). Для n=0 (пустой массив) минимум не определён; требуется согласованная политика (исключение, специальное значение, булев флаг успеха)
Нижняя граница по числу сравнений
Для произвольного несортированного массива в сравнительной модели любой корректный алгоритм поиска минимума обязан выполнить минимум n−1 сравнений. Идея доказательства через дерево решений: у каждого элемента, кроме минимального, должен быть установлен факт «он не минимальный» посредством хотя бы одного прямого сравнения с текущим кандидатом. Не сравнив элемент ни с одним меньшим или равным, нельзя исключить его из статуса минимума. Отсюда оптимальность линейного алгоритма.
Итерационный вариант (возврат значения и индекса)
Псевдокод (1-индексация):
function argmin(A[1..n]):
require n ≥ 1
i_min := 1
m := A[1]
for k := 2 to n do
if A[k] < m then
m := A[k]
i_min := k
end if
end for
return (m, i_min)
Сложность:
сравнения: n − 1
присваивания (в худшем): ≤ 2·(# обновлений)
время: Θ(n) память: Θ(1)
Стабильность: при A[k] = m минимум и индекс не меняются → возвращаем первое вхождение.
Рекурсивный вариант (разделяй и властвуй)
Идея: минимум префикса – минимум минимума половин.
function argmin_dc(A[l..r]):
if l = r then return (A[l], l)
mid := ⌊(l + r)/2⌋
(m1, i1) := argmin_dc(A[l..mid])
(m2, i2) := argmin_dc(A[mid+1..r])
if m1 ≤ m2 then return (m1, i1) else return (m2, i2)
Сложность: T(n) = 2T(n/2) + O(1) = Θ(n) сравнений; стек Θ(log n). Преимущество – готовый каркас для параллельной редукции.
Минимум по предикату/проекции
Нередко нужен минимум по функции f(x) (например, модуль, расстояние, ключ структуры):
if f(A[k]) < f(m) then m := A[k]
Чтобы не вычислять f дважды, запоминают значение f(m) и обновляют его вместе с m. Это соглашение об инварианте: храним пару (m, fm) где fm = f(m).
Безопасная работа с типами
Для вещественных чисел избегайте сравнения «почти равных»: используйте A[k] < m - ε при потребности в строгом порядке, где ε – числовой допуск.
Для целых – проверяйте диапазоны и не допускайте переполнения при вычислении проекций f(x) (например, x*x для больших x).
Нижняя граница сравнений:
∀ корректных алгоритмов min: comparisons ≥ n − 1
Линейный алгоритм:
T(n) = Θ(n), S(n) = Θ(1), сравнения = n − 1
Инвариант префикса:
после обработки A[1..k]: m = min(A[1..k]) и i_min – первый индекс минимума
Разделяй и властвуй (сложность):
T(n) = 2T(n/2) + O(1) = Θ(n)
глубина стека = Θ(log n)
Параллельная редукция (идеальная глубина):
время при p → ∞ ~ O(log n), работа – Θ(n)
Упражнение 1. «Формальный инвариант»
Условие. Докажите корректность следующего фрагмента для массива A[1..n], n ≥ 1:
m := A[1]; i := 1
for k := 2 to n do
if A[k] < m then
m := A[k]; i := k
end if
end for
Решение. Инвариант: после итерации k выполняется m = min(A[1..k]), i – индекс первого вхождения минимума. База k=1 тривиальна. Переход: если A[k] < m, обновление устанавливает минимум на A[k]; иначе m остаётся минимальным на A[1..k−1], следовательно, и на A[1..k]. После k=n получаем глобальный минимум.
Упражнение 2. «Сколько сравнений?»
Условие. Сколько сравнений выполнит алгоритм из упражнения 1 на массиве длины n? Докажите, что это оптимально.
Решение. Цикл выполняет сравнение A[k] < m для каждого k = 2..n – всего n−1. Оптимальность следует из нижней границы (§2.3): чтобы исключить каждый из n−1 не-минимальных элементов, нужно не менее одного сравнения для каждого.
Упражнение 3. «Минимум по модулю с индексом»
Условие. Требуется найти элемент с минимальным модулем и его индекс. При равенстве модулей выбрать наименьший индекс. Приведите корректный однопроходный алгоритм.
Решение.
m := A[1]; fm := |A[1]|; i := 1
for k := 2 to n do
fk := |A[k]|
if fk < fm then
fm := fk; m := A[k]; i := k
end if
// при fk = fm ничего не делаем → сохраняется первый индекс
end for
return (m, i)
Инвариант: fm = min{|A[1]|,…,|A[k]|}, i – первый индекс достижения. Сложность – Θ(n).
Упражнение 4. «Найдите и исправьте ошибку»
Условие. Дан фрагмент:
m := +∞; i := 0
for k := 1 to n do
if A[k] <= m then
m := A[k]; i := k
end if
end for
Покажите, почему он возвращает последнее вхождение минимума, и исправьте код так, чтобы возвращалось первое.
Решение. Из-за <= при равенстве A[k] = m происходит «перезапись» индекса на большее значение. Исправление – строгое сравнение и корректная инициализация:
require n ≥ 1
m := A[1]; i := 1
for k := 2 to n do
if A[k] < m then
m := A[k]; i := k
end if
end for
Упражнение 5. «Два минимума за один проход»
Условие. За один проход по массиву A[1..n] найти минимум и второй минимум (второй – строго больше первого). Если второго нет – сообщить об этом. Сформулируйте инварианты и дайте алгоритм.
Решение. Храним (m1, m2) – текущие минимум и второй минимум. Инициализация: упорядочить первые два элемента. Инвариант: m1 – минимальный из просмотренных, m2 – минимальный среди тех, что строго больше m1.
require n ≥ 1
m1 := +∞; m2 := +∞
for k := 1 to n do
x := A[k]
if x < m1 then
m2 := m1
m1 := x
else if (x > m1) and (x < m2) then
m2 := x
end if
end for
if m2 = +∞ then "второго минимума нет" else return (m1, m2)
Сложность Θ(n). Корректность следует из инвариантов; сравнения – до 2(n−1) в худшем, что асимптотически оптимально.
1. Python (возврат значения и индекса)
from typing import Sequence, Tuple
def argmin(a: Sequence[float]) -> Tuple[float, int]:
n = len(a)
if n == 0:
raise ValueError("empty sequence")
m = a[0]
i_min = 0
for k in range(1, n):
if a[k] < m: # строгое сравнение – первое вхождение
m = a[k]
i_min = k
return m, i_min
2. Идея «редукции» (параллельная обработка блоков)
Разбить массив на p блоков, в каждом найти локальный (m,i), затем выполнить редукцию по p тройкам (m, i, offset) – глубина O(log p), работа Θ(n).
Обработан случай n=0 (сигнал ошибки/флаг).
Инициализация от первого элемента (m := A[1]; i := 1).
Используется строгое сравнение < для стабильности (первое вхождение).
Возвращается индекс вместе со значением.
Нет выходов за границы, цикл начинается с k=2.
При варианте «по модулю/проекции» кэшируется f(m).
Для «двух минимумов» соблюдён инвариант m2 > m1.
Почему без дополнительных знаний нельзя выполнить меньше n−1 сравнений при поиске минимума?
Сформулируйте и докажите инвариант корректности линейного алгоритма mini.
Чем отличается возвращение первого вхождения минимума от последнего и как это отражается в условии?
Как модифицировать алгоритм для минимума по функции f(x) без двойного вычисления f?
Каков порядок памяти у рекурсивного алгоритма «разделяй и властвуй» и чем он полезен?
Поиск минимального элемента – каноническая «маленькая» задача, через которую отрабатываются инварианты, корректность циклов, нижние границы и инженерная дисциплина работы с данными. Линейный проход с n−1 сравнениями – оптимален в сравнительной модели; расширения (минимум по предикату, два минимума, параллельная редукция) естественно вырастают из той же логики инвариантов. Для ЕГЭ умение строго обосновать правильность, аккуратно работать с индексами и оценивать сложность – прямой вклад в баллы как в задачах на программирование, так и в теоретических вопросах.