Я не математик, но люблю решать задачи. Я люблю трудные задачи, которые не знаешь, как решать, а если и знаешь, трудно написать код верно.
Наконец, все работает. Остаются черновики, которые выбросить жалко. Выброшу лишнее с черновика и оставлю конспект, который и через годы напомнит решение.
Говорят "У человека феноменальная память - он помнит все". Он записывает. Не помните, что делали три дня назад? Ведите дневник, а не покупайте "таблетки для памяти".
Задача
Дан массив положительных целых чисел. Сделать так, чтобы каждые два соседних числа оказались взаимно просты. Заменить два соседних числа 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
и записать
Число p
- простое, когда p
делится только на 1
и p
.
Разложить число a
на простые множители значит записать
где - простые числа, а степени
- положительные целые. При этом
- простые числа не повторяются.
Найдем наибольший общий делитель чисел 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)
.
Решение задачи
Движемся по массиву слева направо, ищем два смежных числа, которые взаимно не просты - 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]
, пока они взаимно не просты.

Теперь сходим влево - заменяем числа 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]](https://habrastorage.org/r/w780/webt/pg/k0/7o/pgk07owgozmcjncxvbwdb7qeqs0.png)
Затем переходим к следующему числу i = i + 1
и снова объединяем с непростыми соседями. Продолжаем, пока не дойдем до конца массива.
Код на C++
Напишем и проверим код на leetcode.com.
Задача предлагает массив vector<int> nums
, но мы хотим удалять числа посреди массива, а vector
справляется с этим плохо, потому что хранит числа в памяти непрерывно. Вот так работает метод vector::erase()
:
-
Способ 1 - запросить новый блок памяти, копировать элементы, кроме удаленного:
-
Способ 2 - сдвинуть элементы после удаленного влево на один:
Превратим массив в двусвязный список - он удаляет элементы из середины быстрее.

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
знает, где ближайший сосед слева. Так не придется ходить по массиву и искать соседние числа среди нулей.
Индекс right
подскажет функции mergeRight
, где ближайший сосед справа. Массив не содержит нулей правее right
- туда программа еще не добралась.



Уберем нули из массива, прежде чем отдать массив на проверку. Два способа:
Копируем числа в новый массив, кроме нулей
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; }
}
}
Пишите в комментариях, как бы вы еще сэкономили память или ускорили код, или предлагайте другие решения.
Комментарии (7)
wataru
02.10.2025 20:15Как же сложно. Вот все решение:
vector<int> replaceNonCoprimes(vector<int>& nums) { int n = 0; for (int x : nums) { nums[n++] = x; int d; while (n > 1 && (d = gcd(nums[n-1], nums[n-2])) > 1) { nums[n-2] = nums[n-2] / d * nums[n-1]; --n; } } nums.resize(n); return nums; }
Собираем ответ среди первых n ячеек имеющегося массива nums. Поддерживаем инваринт, что там все соседние числа взаимнопросты. Каждое число добавялем туда в конец. Инвариант может быть нарушен только между двумя последними числами. Поэтому проверяем и объединяем 2 последних элемента если надо. Но это надо делать в цикле, потому что новое объединенное число изменилось же, оно опять может нарушить инвариант.
gchebanov
Вместо того что бы выделять новый вектор под ответ, можно отдать уже выделенную память, раз уж нам разрешили менять аргумент.
sa2304 Автор
Так ведь я об этом написал, и код работает со входным массивом nums :) Поправлю форматирование - вместо маркированного списка поставлю заголовки, чтобы в глаза бросалось.
Способ 1:
Способ 2
Ваш
erase
тот же, что мойresize
. Я проверил на leetcode:erase
работает не выигрывает уresize
в памяти. А вот явный вызовstd::move(nums)
даетMemory 119.64 MB Beats 99.57%
. Компилятор часто сам соображает, где оптимизировать возврат с помощьюmove
, но здесь почему-то явный вызов сэкономил память.Явный
std::move
еще и ускорил решение - чудеса да и только.Спасибо за находку :)
sci_nov
Вообще странно модифицировать аргумент функции и при этом возвращать его. А ещё страннее сделать move.
sa2304 Автор
Просто так устроены задачи на LeetCode - входной массив передают по ссылке и просят вернуть результат - ответ задачи. Не всегда это массив, но когда да, можно стереть входной массив и вернуть :)
sci_nov
Понятно. Я так и предположил.
gchebanov
Компилятор не может здесь ничего "оптимизировать" (как минимум без flto), в случае с копированием вектора (без
std::move
), вы обязаны создать копию, а вектор-аргумент останется не-пустым (и естественно должен указывать на другую память, иначе при вызове деструкторов случился бы "double-free"), в случае сstd::move
вы перекидываете указатели на уже выделенную память, а вектор аргумент содержитnullptr
-ы. Если например тестирующая обертка логгирует size у тестового вектора после вызова нашей функции - то и flto никак не поможет.Я бы сказал это архетипичный пример мув-семантики, недоступный до её введения в стандарт. До С++11 приходилось делать так:
Работает полностью аналогично, и тут как раз срабатывает корректная оптимизация NRVO (обязательная с C++17), и кстати с таким подходом
std::move
будет только всё портить.Строчка с
nums.erase
только отвлекает, она тут не причём. Главное что мы не просто используем входную память для своих вычислений, а то что мы не выделяем никакую другую.Ну и если нам разрешают менять аргумент функции, значит и разрешают сделать его пустым, украв его память. И естественно что выкинув из решения аллокацию на полмегабайта оно станет быстрее.