Подготовка к алгоритмическим задачам

Решето Эратосфена: эффективный поиск простых чисел
Почему этот алгоритм важен?
Решето Эратосфена позволяет оценить:
- 🧮 Фундаментальные принципы работы с простыми числами
- ⚡ Эффективность оптимизированных алгоритмов
- 💾 Компромиссы между памятью и вычислениями
- 📊 Асимптотическую сложность и её практическое значение
Постановка задачи
Формулировка:
Найти все простые числа в диапазоне от 2 до N.
Примеры:
// Первый пример Input: n = 10 Output: [2, 3, 5, 7] // Все простые числа от 2 до 10 // Второй пример Input: n = 20 Output: [2, 3, 5, 7, 11, 13, 17, 19] // Все простые числа от 2 до 20
Принцип работы алгоритма
Решето Эратосфена основано на простом наблюдении:
Если p — простое число, то все числа вида k×p, где k > 1, являются составными.
Алгоритм работает следующим образом:
- Создаём список чисел от 2 до N
- Начинаем с первого простого числа p = 2
- Отмечаем все числа, кратные p, как составные
- Находим следующее неотмеченное число — оно простое
- Повторяем шаги 3-4 до тех пор, пока p² ≤ N
Подходы к решению
Решение 1: Классическое решето Эратосфена
function sieveOfEratosthenes(n) { // Создаём массив, где все элементы изначально помечены как простые const isPrime = new Array(n + 1).fill(true); isPrime[0] = isPrime[1] = false; // 0 и 1 не являются простыми // Проходим по всем числам до sqrt(n) for (let p = 2; p * p <= n; p++) { // Если число p простое, отмечаем его кратные как составные if (isPrime[p]) { for (let i = p * p; i <= n; i += p) { isPrime[i] = false; } } } // Собираем все простые числа в массив const primes = []; for (let i = 2; i <= n; i++) { if (isPrime[i]) { primes.push(i); } } return primes; }
Анализ решения:
✅ Простая реализация, соответствующая математическому описанию
✅ Эффективная временная сложность O(N log log N)
✅ Оптимизация: начинаем отмечать кратные с p² вместо 2p
❌ Требует O(N) дополнительной памяти
Решай алгоритмические задачи как профи

Решение 2: Оптимизация по памяти 💾
function segmentedSieve(n) { const limit = Math.floor(Math.sqrt(n)); const primes = sieveOfEratosthenes(limit); // Найдём простые числа до sqrt(n) const result = [...primes]; // Копируем начальные простые числа // Обрабатываем сегменты размером sqrt(n) const segmentSize = limit; for (let low = limit + 1; low <= n; low += segmentSize) { // Определяем границы текущего сегмента const high = Math.min(low + segmentSize - 1, n); // Локальное решето для текущего сегмента const segment = new Array(high - low + 1).fill(true); // Отмечаем составные числа в текущем сегменте for (const prime of primes) { // Находим первое кратное простого числа в текущем сегменте let firstMultiple = Math.ceil(low / prime) * prime; if (firstMultiple < low) { firstMultiple += prime; } // Отмечаем все кратные как составные for (let j = firstMultiple; j <= high; j += prime) { segment[j - low] = false; } } // Собираем простые числа из текущего сегмента for (let i = 0; i < segment.length; i++) { if (segment[i]) { result.push(low + i); } } } return result; }
Особенности:
✅ Эффективно работает с большими диапазонами
✅ Значительно снижает требования к памяти O(√N)
✅ Хорошо работает с кешем процессора благодаря локальности данных
❌ Более сложная реализация
Решение 3: Битовая оптимизация 🔢
function bitSieve(n) { // Используем битовый массив вместо булевого // Каждый бит представляет нечётное число: 3, 5, 7, ... const size = Math.floor((n - 1) / 2); const sieve = new Uint8Array(Math.ceil(size / 8)); function setBit(k) { const bit = k % 8; const byte = Math.floor(k / 8); sieve[byte] |= (1 << bit); } function getBit(k) { const bit = k % 8; const byte = Math.floor(k / 8); return (sieve[byte] & (1 << bit)) !== 0; } // 2 — единственное чётное простое число const primes = [2]; // Итерируемся только по нечётным числам for (let p = 3; p <= n; p += 2) { const index = Math.floor((p - 3) / 2); if (!getBit(index)) { primes.push(p); // Отмечаем кратные как составные if (p * p <= n) { for (let i = p * p; i <= n; i += 2 * p) { const markIndex = Math.floor((i - 3) / 2); setBit(markIndex); } } } } return primes; }
Характеристики:
✅ Использует в 8 раз меньше памяти по сравнению с булевым массивом
✅ Обрабатывает только нечётные числа, что сокращает вдвое количество операций
✅ Эффективно для очень больших диапазонов
❌ Сложнее для понимания и отладки
Сравнение производительности
Реализация | Временная сложность | Пространственная сложность | Оптимальна для |
---|---|---|---|
Классическая | O(N log log N) | O(N) | Небольших диапазонов |
Сегментированная | O(N log log N) | O(√N) | Больших диапазонов |
Битовая | O(N log log N) | O(N/16) | Минимального использования памяти |
Расширенные применения
Модификация для нахождения простых делителей
function primeFactors(n) { const primes = sieveOfEratosthenes(Math.floor(Math.sqrt(n))); const factors = []; for (const prime of primes) { while (n % prime === 0) { factors.push(prime); n /= prime; } } if (n > 1) { factors.push(n); // Если осталось простое число больше sqrt(n) } return factors; }
Нахождение простых чисел в заданном диапазоне [L, R]
function primesInRange(l, r) { // Находим простые числа до sqrt(r) const limit = Math.floor(Math.sqrt(r)); const smallPrimes = sieveOfEratosthenes(limit); // Создаём решето для диапазона [L, R] const size = r - l + 1; const isPrime = new Array(size).fill(true); // Отмечаем составные числа for (const prime of smallPrimes) { // Находим первое кратное prime в диапазоне [L, R] const start = Math.ceil(l / prime) * prime; for (let i = Math.max(start, prime * prime); i <= r; i += prime) { isPrime[i - l] = false; } } // Особые случаи для 0 и 1 if (l <= 1 && r >= 1) { isPrime[1 - l] = false; } if (l <= 0 && r >= 0) { isPrime[0 - l] = false; } // Формируем результат const primes = []; for (let i = 0; i < size; i++) { if (isPrime[i]) { primes.push(l + i); } } return primes; }
Практические применения 🌟
-
Криптография:
- Генерация больших простых чисел для RSA
- Создание криптографических ключей
-
Теория чисел:
- Проверка гипотез о распределении простых чисел
- Исследование свойств специальных простых чисел
-
Алгоритмы факторизации:
- Основа для более сложных алгоритмов факторизации
- Полезно при факторизации малых чисел
-
Олимпиадное программирование:
- Решение задач, связанных с простыми числами
- Оптимизация скорости работы программ
Оптимизации и рекомендации 💡
-
Для диапазонов < 10⁷:
- Используйте классическое решето с битовой оптимизацией
- Проверяйте только нечётные числа (кроме 2)
-
Для диапазонов > 10⁷:
- Применяйте сегментированное решето
- Ограничьте максимальный размер сегмента для оптимальной работы с кешем
-
Дополнительные оптимизации:
- Проверяйте только простые числа до √N
- Пропускайте кратные 2 и 3, используя колёсную факторизацию
Типичные ошибки
- 🤦♂️ Неправильная обработка границ диапазонов
- 😅 Переполнение памяти при работе с большими числами
- 🔄 Избыточная проверка чисел, которые уже отмечены как составные
- 🧮 Отсутствие оптимизации начального шага (p * p вместо 2 * p)
Эффективная реализация
function optimizedSieve(n) { if (n < 2) return []; if (n === 2) return [2]; // Работаем только с нечётными числами, экономя половину памяти const size = Math.floor(n / 2); const sieve = new Uint8Array(Math.ceil(size / 8)); // Функции для работы с битовым массивом const setBit = (k) => { sieve[k >>> 3] |= 1 << (k & 7); }; const getBit = (k) => { return (sieve[k >>> 3] & (1 << (k & 7))) !== 0; }; const sqrtN = Math.sqrt(n) | 0; for (let i = 1; i <= (sqrtN >>> 1); i++) { if (!getBit(i)) { // 2*i + 1 — текущее простое число const p = 2 * i + 1; // Отмечаем все нечётные кратные p for (let j = i + p; j < size; j += p) { setBit(j); } } } // Формируем список простых чисел const primes = [2]; // 2 — единственное чётное простое число for (let i = 1; i < size; i++) { if (!getBit(i)) { primes.push(2 * i + 1); } } return primes; }
Заключение
Решето Эратосфена представляет собой:
- 🧮 Один из древнейших и наиболее эффективных алгоритмов в истории математики
- ⚡ Яркий пример того, как простая идея может привести к оптимальному решению
- 💾 Демонстрацию компромисса между временем работы и использованием памяти
- 🌟 Фундаментальный алгоритм с широким спектром современных применений
Несмотря на свой возраст (более 2000 лет), решето Эратосфена остаётся непревзойдённым по простоте и эффективности алгоритмом для нахождения всех простых чисел в заданном диапазоне, подтверждая ценность классических алгоритмов в современной компьютерной науке.