awesome-everything EN
↑ Обратно к восхождению

Алгоритмы с нуля

Читаем ограничения

Суть Прежде чем решать задачу, посмотри на лимиты входа. Узнай правило (10⁸ операций в секунду), стандартное соответствие размера входа требуемой сложности и как ограничения исключают невозможные подходы.
◷ 16 min

Ты уже знаешь нотацию Big-O и семь классов сложности. Но как выбрать, какая сложность нужна для задачи? Ответ: прочитай ограничения входа. Перед тем как написать хоть одну строку кода, условие задачи говорит тебе лимиты. Эти лимиты указывают прямо на класс сложности, к которому ты должен стремиться. Этот урок учит тебя читать этот сигнал и использовать его, чтобы исключить невозможные подходы ещё до начала.

Цель

После этого урока ты сможешь прочитать ограничения задачи (особенно n, размер входа), оценить, сколько операций ты можешь позволить (~10⁸ в секунду), соединить границы входа с требуемыми классами сложности (например n ≤ 10⁶ → O(n) или O(n log n)) и использовать это соответствие, чтобы решить, какой алгоритмический подход пытаться.

Идея

Золотое правило: компьютеры делают примерно 10⁸ (100 миллионов) простых операций в секунду.

Большинство онлайн-судей (LeetCode, Codeforces и т. д.) дают 1–2 секунды на тест. Значит, ты можешь позволить себе около 10⁸ – 2×10⁸ операций. Это твой бюджет времени.

Соответствие: размер входа n говорит тебе, какая сложность нужна.

Вот стандартная таблица, которую используют соревнующиеся программисты:

Размер входа nВозможная сложностьПример
n ≤ 10–12O(n!) или O(2ⁿ)Перебор всех подмножеств или перестановок
n ≤ 20–25O(2ⁿ)Рекурсивный поиск, битовая маска
n ≤ 500O(n³)Тройной цикл, алгоритм Флойда–Уоршелла
n ≤ 5 000O(n²)Вложенный цикл, сортировка пузырьком, простая ДП
n ≤ 10⁶O(n log n) или O(n)Сортировка слиянием, двоичный поиск, один цикл
n ≥ 10⁷O(n) или O(log n)Однопроходный скан, логарифмический поиск

Как её использовать: работай в обратном направлении от ограничения.

Когда ты видишь «n ≤ 10⁶», ты знаешь, что:

  • O(n²) невозможна — это будет 10¹² операций, далеко за пределами бюджета.
  • O(n log n) возможна — это ~2×10⁷ операций, хорошо в пределах бюджета.
  • O(n) также возможна — ~10⁶ операций, тривиально.

Так что если n ≤ 10⁶, твой алгоритм должен быть по крайней мере такой же быстрый, как O(n log n). Медленные подходы (как O(n²)) исключены, прежде чем ты начал.

Соответствие ограничение → подход — это мост от анализа к проектированию.

Вместо «я не знаю, с чего начать», ограничение говорит тебе: «Тебе нужен O(n) скан ИЛИ O(n log n) разделяй-и-властвуй (например двоичный поиск или сортировка слиянием)». Это не полное решение, но это сильно сужает пространство поиска.

Почему ограничения важны: реальный пример.

Предположим, задача спрашивает: «Посчитай пары (i, j), где i < j и arr[i] + arr[j] = target».

Твой первый инстинкт может быть: «Вложенный цикл. Для каждого i просканируй все j > i». Это O(n²).

Но потом ты читаешь ограничение: n ≤ 100 000.

O(n²) будет 10¹⁰ операций. Timeout. Исключено.

Значит, ты должен использовать O(n log n) или O(n). Ограничение заставляет тебя думать иначе. Один подход: отсортируй и используй два указателя (O(n log n)). Другой: используй хеш-множество (O(n) в среднем). Оба остаются в пределах бюджета. Идея O(n²), несмотря на её простоту, больше не вариант.

Замечание: константы важны, но правило верно.

Точный порог варьируется:

  • Современные процессоры могут делать ~10⁸ – 10⁹ простых операций в секунду.
  • Соревнующиеся программисты часто используют 10⁸ как безопасную оценку.
  • Сложные операции (выделение памяти, рекурсивный оверхед) могут понизить порог.
  • Очень простые операции (битовые сдвиги, сравнения) могут поднять его выше.

Но таблица выше, основанная на 10⁸ операций/сек, это то, на что опирается сообщество. Она работает.

Код

Давайте посмотрим, как ограничения меняют алгоритм, который ты бы написал.

Сценарий 1: n ≤ 10

Задача: «Найди максимальную сумму любого подмножества массива».

Ограничение: n ≤ 10.

Подход: Перебор. Попробуй все 2^n подмножеств. Это 2^10 = 1024 операции. Мгновенно.

function maxSubsetSum(arr) {
  let max = 0;
  // Пройди все 2^n подмножеств, используя битовую маску
  for (let mask = 0; mask < (1 << arr.length); mask++) {
    let sum = 0;
    for (let i = 0; i < arr.length; i++) {
      if (mask & (1 << i)) sum += arr[i];
    }
    max = Math.max(max, sum);
  }
  return max;  // O(2^n)
}

При n ≤ 10 это решение O(2^n) хорошо. Ты не пытаешься быть умным. Ограничение даёт тебе разрешение на перебор.


Сценарий 2: n ≤ 1 000

Задача: «Найди самую длинную общую подпоследовательность (LCS) двух строк».

Ограничение: n ≤ 1 000 (длина каждой строки).

Подход: Динамическое программирование. Построй таблицу 2D размером n × n. Это 10⁶ операций (заполнение таблицы). Возможно.

function lcs(s1, s2) {
  const n = s1.length, m = s2.length;
  const dp = Array(n + 1).fill(0).map(() => Array(m + 1).fill(0));
  
  for (let i = 1; i <= n; i++) {
    for (let j = 1; j <= m; j++) {
      if (s1[i - 1] === s2[j - 1]) {
        dp[i][j] = dp[i - 1][j - 1] + 1;
      } else {
        dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
      }
    }
  }
  return dp[n][m];  // O(n²)
}

При n ≤ 1 000 O(n²) безопасна. Если бы n было 10⁶, это не сработало бы.


Сценарий 3: n ≤ 10⁶

Задача: «Посчитай, сколько элементов в массиве больше среднего значения».

Ограничение: n ≤ 10⁶.

Подход: Один проход. Вычисли сумму (O(n)), вычисли среднее, посчитай (O(n)). Итого: O(n). ~10⁶ операций. Мгновенно.

function countAboveAverage(arr) {
  const sum = arr.reduce((a, b) => a + b, 0);
  const avg = sum / arr.length;
  return arr.filter(x => x > avg).length;  // O(n)
}

При n ≤ 10⁶ требуется один проход. O(n²) это 10¹² операций — невозможно. O(n log n) это ~2×10⁷ операций — излишне для этой задачи (хотя всё ещё приемлемо). O(n) идеален.


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

Вывод
 
Пошаговый разбор

Давайте проследим, как ограничение исключает варианты. Представь, что ты читаешь задачу:

Условие задачи: «Дан массив размером n, найди, существует ли пара элементов, чья сумма равна target».

Теперь ты читаешь ограничение: «1 ≤ n ≤ 100 000».

1 Ограничение: n ≤ 100 000
2 Бюджет: ~10^8 операций
3 Работай в обратном направлении, чтобы найти нужную сложность...

Ограничение не говорит тебе, какой O(n log n) алгоритм использовать, но оно говорит, что всё медленнее не сработает. Это бесценно.

Сложность

Вот полная справочная таблица, с объяснением:

nВозможная сложностьТипичные алгоритмыПочему
≤ 10–12O(n!), O(2ⁿ)Перебор перестановок, подмножеств, backtracking2^12 = 4096 ops; n! ≤ 10^6 ops
≤ 20O(2ⁿ)Битовая маска ДП, перечисление подмножеств2^20 ≈ 10^6 ops
≤ 100–300O(n³)Тройной вложенный цикл, Флойд–Уоршеллn³ ≈ 10^7 ops для n=300
≤ 1 000–5 000O(n²)Вложенный цикл, простая ДП, сортировка пузырькомn² ≈ 10^7 ops для n=3000
≤ 100 000O(n log n)Сортировка слиянием, quicksort, разделяй-и-властвуйn log n ≈ 1.7M ops для n=100K
≤ 1 000 000O(n)Однопроходный скан, хеш-множество, жадный алгоритм, BFS/DFSn ≈ 10^6 ops
≥ 10^7O(log n), O(1)Двоичный поиск, хеш-поиск, таблицы поискаlog n ≈ 30 ops для n=10^9

Заметь логарифмические скачки. Разница между n ≤ 1 000 и n ≤ 10⁶ заставляет тебя перейти от O(n²) к O(n log n). Разница между n ≤ 10⁶ и n ≤ 10⁹ заставляет тебя от O(n) к O(log n).

input size n operations O(1) O(log n) O(n) O(n log n) O(n²) O(2ⁿ)

Ось x не линейна — она показывает, как порог смещается. Маленькие ограничения позволяют квадратичный; большие ограничения требуют линейный или логарифмический.

Практика 0 / 5

В задаче написано: 'n ≤ 10'. Какая сложность безопасна?

В задаче написано: 'n ≤ 1 000 000'. Алгоритм O(n²) сделает примерно сколько операций?

Ограничение: n ≤ 100. Какая сложность *невозможна*?

Ты решаешь задачу с n ≤ 5 000. Наивный перебор O(n³). Стоит ли его пытаться?

Ограничение: n ≤ 10⁶. Какой алгоритм *требуется*?

Граничные случаи

Константы имеют значение. Правило «10⁸ операций в секунду» это оценка. Плотный цикл с битовыми сдвигами может работать на 10⁹ ops/sec. Функция с промахами кэша и malloc() может работать на 10⁷ ops/sec. Соревнующиеся программисты корректируют: если ограничение плотное (n ≤ 1 000 000 и time limit 1 секунда), они могут избежать O(n log n) и стремиться к O(n). Если ограничение щедрое и time limit 2 секунды, они могут использовать алгоритм O(n log n) с большими константами.

Частая ошибка

Путаница между time limit и бюджетом операций. Time limit это 1–2 секунды. Но не все операции занимают одинаковое время. Сравнение занимает ~1 нс. Выделение памяти занимает ~100 нс. Рекурсивный оверхед накапливается. Когда ты видишь «1 секунда», думай «10⁸ простых операций», а не «у меня неограниченное время писать сложный код». Соревнующиеся программисты это помнят и предпочитают простые, плотные циклы рекурсивным или сложным подходам.

Проверь себя
Викторина

Почему соревнующиеся программисты всегда читают ограничения в первую очередь?

Итог

Ограничения это сигнал, который говорит, какой алгоритм попытаться.

Ключевые выводы:

  1. Правило большого пальца: Компьютеры делают ~10⁸ простых операций в секунду. Большинство судей дают 1–2 секунды. Значит, у тебя есть бюджет ~10⁸ – 2×10⁸ операций.

  2. Соответствие: Размер входа n говорит, какая сложность возможна:

    • n ≤ 10–12 → O(2ⁿ) или O(n!) хорошо
    • n ≤ 20–25 → O(2ⁿ) хорошо
    • n ≤ 500 → O(n³) хорошо
    • n ≤ 5 000 → O(n²) хорошо
    • n ≤ 10⁶ → O(n log n) требуется
    • n ≥ 10⁷ → O(n) или O(log n) требуется
  3. Как её использовать: Перед тем как писать код, вычисли стоимость своей первой идеи. Если она не вписывается в бюджет, исключи её и проектируй новый подход.

  4. Ограничение не предложение; это барьер. Алгоритм O(n²) для n = 10⁶ будет timeout. Никакая оптимизация это не исправит — ты должен использовать более быстрый алгоритм.

  5. Работай в обратном направлении: Прочитай ограничение, выбери самую быструю возможную сложность, потом проектируй алгоритм, который её достигает. Ограничение сужает пространство поиска и спасает тебя от тупиков.

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

Продолжить восхождение ↑Мышление и сложность: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.