Задачи по алгоритмам: ищем непростые числа

Я не математик, но люблю решать задачи. Я люблю трудные задачи, которые не знаешь, как решать, а если и знаешь, трудно написать код верно.

Наконец, все работает. Остаются черновики, которые выбросить жалко. Выброшу лишнее с черновика и оставлю конспект, который и через годы напомнит решение.

Говорят «У человека феноменальная память — он помнит все». Он записывает. Не помните, что делали три дня назад? Ведите дневник, а не покупайте «таблетки для памяти».

Задача

Дан массив положительных целых чисел. Сделать так, чтобы каждые два соседних числа оказались взаимно просты. Заменить два соседних числа a и b на наименьшее общее кратное lcm(a, b), когда a и b- взаимно не просты.

Два числа a и b взаимно просты, когда наибольший общий делитель чисел gcd(a, b) равен 1.

Наибольший общий делитель

b — делитель a, когда a делится на b. Пример: 6 делится на 6, 3, 2 и 1, поэтому 6, 3, 2 и 1 — делители 6.

c — общий делитель чисел a и b, когда a делится на c и b делится на c.

Примеры:

12 = 6 * 2 = 3 * 4 = 3 * 2 * 2 18 = 9 * 2 = 3 * 3 * 2 45 = 9 * 5 = 3 * 3 * 5 3 — общий делитель чисел 18, 45. 9 — общий и наибольший делитель чисел 18, 45. 3 — общий и наибольший делитель чисел 12, 18, 45.

Часто говорят «множители числа» вместо «делители».

Поиск наибольшего общего делителя: разложение числа на множители

Разложить число a на множители значит подобрать делители числа a и записать

a = d_1 * d_2 * ... * d_n

Число p — простое, когда p делится только на 1 и p.

Разложить число a на простые множители значит записать

a = p_1^{b_1} * p_2^{b_2} * ... * p_n^{b_n}

где p_1, p_2, ..., p_n — простые числа, а степени b_1, b_2, ..., b_n — положительные целые. При этом p_1 not= p_2 not= p_n — простые числа не повторяются.

Найдем наибольший общий делитель чисел a и b, когда разложим числа на простые множители.

Поиск наибольшего общего делителя: алгоритм Евклида

Большие числа раскладывать на множители утомительно. Алгоритм Евклида поможет найти наибольший общий делитель проще.

| a, когда b = 0 gcd(a, b) = | | gcd(b, a mod b), когда b != 0 int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }

Алгоритм Евклида работает за время O(log n), где n = min(a, b).

Наименьшее общее кратное

Наименьшее общее кратное чисел a и b — наименьшее целое число c, которое делится на a и делится на b. Обозначают lcm(a, b).

{lcm(a, b)} = {a * b over gcd(a, b)}

Решение задачи

Движемся по массиву слева направо, ищем два смежных числа, которые взаимно не просты — 1 < gcd(c, d).

Пусть в массиве [a, b, c, d, e, f] взаимно просты a, b и взаимно просты b, c, а c, d — нет. Заменим числа c и d на c1 = lcm(c, d). Теперь придется снова проверить взаимную простоту c1 и b. Продолжим движение вправо, но запомним, что должны сходить влево. Заменяем числа nums[i], nums[i + 1], пока они взаимно не просты.

f560ecbb1f5ddc41e95a20a75eec37ad

Теперь сходим влево — заменяем числа nums[i — 1], nums[i], пока они взаимно не просты.

Порядок замен не важен — это можно доказать.

Теперь проверим числа nums[i], nums[i + 1] снова, если заменили nums[i — 1], nums[i] хотя бы раз. Повторяем замены вправо и влево, чтобы nums[i] оказался взаимно прост с обоими соседями — слева nums[i — 1] и справа nums[i + 1].

Объединяем смежные числа, которые взаимно не просты с nums[i]
Объединяем смежные числа, которые взаимно не просты с nums[i]

Затем переходим к следующему числу i = i + 1 и снова объединяем с непростыми соседями. Продолжаем, пока не дойдем до конца массива.

Код на C++

Напишем и проверим код на leetcode.com.

Задача предлагает массив vector<int> nums, но мы хотим удалять числа посреди массива, а vector справляется с этим плохо, потому что хранит числа в памяти непрерывно. Вот так работает метод vector::erase():

  • Способ 1 — запросить новый блок памяти, копировать элементы, кроме удаленного:

    24be66d2b1e87b5301e3322891e91847
  • Способ 2 — сдвинуть элементы после удаленного влево на один:

    ca638d98a8a7a6ff80c088089d17a0ba

Превратим массив в двусвязный список — он удаляет элементы из середины быстрее.

f136d736f7e129deb68d57fcb33aa64f

using NumsList = list<int>; using NumsVector = vector<int>; NumsVector replaceNonCoprimes(NumsVector &vec) { NumsList nums{vec.begin(), vec.end()}; // … }

Теперь шагаем по списку слева направо: берем следующее число и объединяем с «непростыми» соседями справа и слева. Функция mergeRight объединяет число с соседом справа, а mergeLeft — слева.

NumsVector replaceNonCoprimes(NumsVector &vec) { // … auto iter = nums.begin(); while (nums.end() != iter) { bool replaced = mergeRight(nums, iter); if (replaced) { do { replaced = mergeRight(nums, iter); } while(replaced); do { replaced = mergeLeft(nums, iter); } while(replaced); } else { ++iter; } } //… }

Затем копируем числа из списка в массив и возвращаем:

using NumsList = list<int>; using NumsVector = vector<int>; NumsVector replaceNonCoprimes(NumsVector &vec) { // … vec = {nums.begin(), nums.end()}; return vec; }

Отправляем на проверку — программа работает.

Медленный и прожорливый код
Медленный и прожорливый код

LeetCode говорит, что программа жрет больше времени и памяти, чем другие.

Простой, но медленный и прожорливый кодusing NumsList = list<int>; bool nonCoprimes(int a, int b) { return 1 < std::gcd(a, b); } bool mergeRight(NumsList &nums, NumsList::iterator &iter) { bool merged = false; if (!nums.empty() && nums.end() != iter) { auto right = next(iter); if (nums.end() != right && nonCoprimes(*iter, *right)) { *iter = std::lcm(*iter, *right); nums.erase(right); merged = true; } } return merged; } bool mergeLeft(NumsList &nums, NumsList::iterator &iter) { bool merged = false; if (!nums.empty() && nums.end() != iter && nums.begin() != iter) { auto left = prev(iter); if (nonCoprimes(*left, *iter)) { *iter = std::lcm(*left, *iter); nums.erase(left); merged = true; } } return merged; } vector<int> replaceNonCoprimes(vector<int>& vec) { NumsList nums{vec.begin(), vec.end()}; auto iter = nums.begin(); while (nums.end() != iter) { bool replaced = mergeRight(nums, iter); if (replaced) { do { replaced = mergeRight(nums, iter); } while(replaced); do { replaced = mergeLeft(nums, iter); } while(replaced); } else { ++iter; } } vec = {nums.begin(), nums.end()}; return vec; }

Оптимизация

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

Не удаляем элементы массива, но стираем — пишем 0. Так мы не используем метод vector::erase(), но теперь массив содержит нули между соседними числами.

mergeLeft
mergeLeft
mergeRight
mergeRight
810c05c687d7c1d6d10f3e49d81ea39b

Запомним части массива, которые разбиваем — пусть mergeLeft знает, где ближайший сосед слева. Так не придется ходить по массиву и искать соседние числа среди нулей.

Индекс right подскажет функции mergeRight, где ближайший сосед справа. Массив не содержит нулей правее right — туда программа еще не добралась.

mergeLeft запоминает новую часть массива
mergeLeft запоминает новую часть массива
mergeLeft забывает часть массива, когда в части не осталось чисел
mergeLeft забывает часть массива, когда в части не осталось чисел
Программа запоминает новую часть массива, когда переходит к следующему числу
Программа запоминает новую часть массива, когда переходит к следующему числу

Уберем нули из массива, прежде чем отдать массив на проверку. Два способа:

  • Копируем числа в новый массив, кроме нулей

NumsVector result; for (const auto &n: nums) { if (0 < n) { result.push_back(n); } }

Пусть программа знает, сколько чисел стерла, тогда знает, сколько памяти нужно:

NumsVector result{nums.size() — merged_count}; int w = 0; for (const auto &n: nums) { if (0 < n) { result[w++] = n; } }

  • Сожмем исходный массив так, что нули окажутся в хвосте, а хвост отстрижем

int w = 0; for (int r = 0; r < nums.size(); ++r) { if (0 < nums[r]) { nums[w++] = nums[r]; } } nums.resize(w);

Сжимаем массив
Сжимаем массив

Экономим еще немного памяти, если используем vector вместо stack:

using SlicesStack = vector<int>;

Скромный и неприхотливый код
Скромный и неприхотливый код

Скромный и неприхотливый кодusing NumsVector = vector<int>; using SlicesStack = vector<int>; NumsVector replaceNonCoprimes(NumsVector &nums) { int merged_count = 0; int i = 0; SlicesStack slices; int right = i + 1; while (i < nums.size() && right < nums.size()) { bool merged = mergeRight(nums, i, right, merged_count); if (merged) { mergeLeft(nums, i, slices, merged_count); } else { if (1 < right — i) { slices.push_back(i); } i = right; right = i + 1; } } if (0 < merged_count) { int w = 0; for (int r = 0; r < nums.size(); ++r) { if (0 < nums[r]) { nums[w++] = nums[r]; } } nums.resize(nums.size() — merged_count); } return nums; } bool coprimes(int a, int b) { return 1 == std::gcd(a, b); } bool mergeRight(NumsVector &nums, int i, int &right, int &merged_count) { bool merged = false; while (right < nums.size() && !coprimes(nums[i], nums[right])) { nums[i] = std::lcm(nums[i], nums[right]); nums[right] = 0; ++right; merged = true; ++merged_count; } return merged; } void mergeLeft(NumsVector &nums, int i, SlicesStack &slices, int &merged_count) { int p = i — 1; while (0 <= p) { if (0 == nums[p]) { if (slices.empty()) { return; } p = slices.back(); } if (!coprimes(nums[p], nums[i])) { if (i — 1 == p) { slices.push_back(p); } nums[i] = std::lcm(nums[p], nums[i]); nums[p] = 0; ++merged_count; p = —slices.back(); if (p < 0 || 0 <= p && 0 == nums[p]) { slices.pop_back(); p = !slices.empty() ? slices.back() : -1; } } else { break; } } }

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

Источник: habr.com

✅ Найденные теги: Задачи, новости
Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Фото сгенерированных лиц: исследование показывает, что люди не могут отличить настоящие лица от сгенерированных
Нейросети построили капитализм за трое суток: 100 агентов Claude заперли…
Скетч: цифровой осьминог и виртуальный мир внутри компьютера с человечком.
Сцена с жестами пальцами, где один жест символизирует "VPN", а другой "KHP".
‼️Paramount купила Warner Bros. Discovery — сумма сделки составила безумные…
Скриншот репозитория GitHub "Claude Scientific Skills" AI для научных исследований.
Структура эффективного запроса Claude с элементами задачи, контекста и референса.
Эскиз и готовая веб-страница платформы для AI-дизайна в современном темном режиме.
ideipro logotyp
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых