Pull to refresh

Comments 10

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

vector<int> replaceNonCoprimes(vector<int>& nums) {
  ...
  nums.erase(nums.begin() + k, nums.end());
  return std::move(nums);
}

Memory: Beats 99.62%

Так ведь я об этом написал, и код работает со входным массивом nums :) Поправлю форматирование - вместо маркированного списка поставлю заголовки, чтобы в глаза бросалось.

  • Способ 1:

NumsVector result;
// копия вектора

  • Способ 2

Сожмем исходный массив так, что нули окажутся в хвосте, а хвост отстрижем
...
nums[w++] = nums[r];
...
nums.resize(w);

Ваш erase тот же, что мой resize. Я проверил на leetcode: erase работает не выигрывает у resize в памяти. А вот явный вызов std::move(nums) дает Memory 119.64 MB Beats 99.57%. Компилятор часто сам соображает, где оптимизировать возврат с помощью move, но здесь почему-то явный вызов сэкономил память.

Явный std::move еще и ускорил решение - чудеса да и только.

Спасибо за находку :)

Вообще странно модифицировать аргумент функции и при этом возвращать его. А ещё страннее сделать move.

Просто так устроены задачи на LeetCode - входной массив передают по ссылке и просят вернуть результат - ответ задачи. Не всегда это массив, но когда да, можно стереть входной массив и вернуть :)

Понятно. Я так и предположил.

Компилятор не может здесь ничего "оптимизировать" (как минимум без flto), в случае с копированием вектора (без std::move), вы обязаны создать копию, а вектор-аргумент останется не-пустым (и естественно должен указывать на другую память, иначе при вызове деструкторов случился бы "double-free"), в случае с std::move вы перекидываете указатели на уже выделенную память, а вектор аргумент содержит nullptr-ы. Если например тестирующая обертка логгирует size у тестового вектора после вызова нашей функции - то и flto никак не поможет.

Я бы сказал это архетипичный пример мув-семантики, недоступный до её введения в стандарт. До С++11 приходилось делать так:

    vector<int> replaceNonCoprimes(vector<int>& nums) {
        vector<int> ans;
        ans.swap(nums);
        ...
        return ans;
    }

Работает полностью аналогично, и тут как раз срабатывает корректная оптимизация NRVO (обязательная с C++17), и кстати с таким подходом std::move будет только всё портить.

Строчка с nums.erase только отвлекает, она тут не причём. Главное что мы не просто используем входную память для своих вычислений, а то что мы не выделяем никакую другую.

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

Как же сложно. Вот все решение:

    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 последних элемента если надо. Но это надо делать в цикле, потому что новое объединенное число изменилось же, оно опять может нарушить инвариант.

Если a b c идут подряд и a, b взимнопросты, а c и b нет, при замене (c , b) -> на gcd проверять взаимнопростоту с a не надо. gcd точно делит b, значит, тоже не будет иметь общих делителей с a. После этого задача становится однопроходной и тривиальной.

Там замена (c , b) -> lcm, а оно имеет общие делители с "а", если gcd(a, c) > 1

My bad, прочитал невнимательно

Sign up to leave a comment.

Articles