Pull to refresh
35
0
Андрей Шитов @5nw

User

Send message

Немного магии, меняем массив на {3, 5, 7, 2, 2} и оптимизация уже не работает https://godbolt.org/z/adjcPvsf7, по крайней мере на шестнадцатом clang и тринадцатом gcc все компилируется в ту же порнографию
Более того, на всех других массивах, что я пробовал, получается то же самое
Это какой-то специальный массив, подобранный для C vs C++ холиворов?)

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

std::sort компилируется в нулевой оверхед потому что для массивов таких размеров внутри вызывается insertion_sort (2 цикла), который естественно оптимизируется лучше, чем сложный qsort

Вы пытаетесь сравнить теплое с мягким и из этого делаете вывод, что C++ лучше)
Если мне не изменяет склероз, std::sort использует 3 алгоритма внутри: insertion_sort для мелких массивов, собственно qsort, и heap_sort если qsort начинает слишком глубоко проваливаться в рекурсию
Сравнивать его с голым qsort, естественно, нельзя

Решить эту задачу за час вполне реально, у меня получилось за минут 15-20

Достаточно додуматься или знать заранее несколько вещей:

  1. Если хранить несколько цифр в одной переменной (в нашем случае достаточно 4, т. к. 2000 < 10000, и 9999 * 9999 + 9999 влезает в int32), то задача значительно упрощается. Вместо умножения двух многозначных чисел, нам требуется умножать многозначное число на цифру (цифра от 0 до 9999). То есть вместо 10-ной, мы работаем в 10000-ной системе счисления.

  2. Хранить цифры лучше в обратном порядке, чтобы было удобнее добавлять новые. Перед выводом число нужно развернуть и не забыть вывести лидирующие нули перед всеми цифрами, кроме первой.

Вот решение на плюсах, на шарпе скорее всего будет что-то похожее

#include <iostream>
#include <vector>
#include <algorithm>
#include <iomanip>

using namespace std;

int main() {
    vector<int> d = {1};
    d.reserve(2000);

    for (int i = 2; i <= 2000; ++i) {
        int o = 0;
        for (int j = 0; j < d.size(); ++j) {
            o += d[j] * i;
            d[j] = o % 10000;
            o /= 10000;
        }
        if (o > 0) {
            d.push_back(o);
        }
    }

    reverse(d.begin(), d.end());
    cout << d[0];
    for (int i = 1; i < d.size(); ++i) {
        cout << setw(4) << setfill('0') << d[i];
    }
    cout << endl;

    return 0;
}

p.s. Олимпиадник в прошлом
p.p.s. На практике эти знания вряд ли пригодятся

Просто напомню что в Python для этого есть bisect, bisect_left и bisect_right
В результате, получим такое решение первой задачи:


import bisect

n, m = map(int, input().split())
a = list(map(int, input().split()))
b = list(map(int, input().split()))

for v in b:
    left = bisect.bisect_left(a, v)
    if left >= n or a[left] != v:
        print(0)
    else:
        print(left + 1, bisect.bisect_right(a, v))

В целях обучения это конечно не актуально, но на соревновании поможет сэкономить несколько минут

Кстати, диапазон возрастов людей сильно ограничен (к сожалению). Поэтому тут можно отсортировать массив подсчетом за O(N), использование std::sort также вызывает вопросы.
В общем, к сожалению, автор выбрал не самые удачные примеры

Входной файл может быть списком учащихся в стране с болонской системой обучения.

В условии этого не сказано, поэтому рассматриваем выборку как среднестатистическую


Тогда спрашивается, зачем в учебном примере всё это?

Зачем в учебном примере учить применять алгоритм, который здесь явно применять не следует?

Хорошо, оцениваем.
Ну, во-первых, там не O(sqrt(N) + N), а O(ln(N) + N). А логарифм — очень и очень медленно растущая функция. Например, двоичный логарифм 1 000 000 всего около 20, а 1 000 000 000 — около 30. Так что можно сказать, что O(ln(N) + N) = O(N), что кстати верно также по определению асимптотической сложности.
Во-вторых, у нас не абстрактные циферки, а реальный возраст реальных людей. То есть, данные подчиняются некому статистическому распределению. Поскольку в статье ничего не сказано о выборке, будем считать ее среднестатистической. Возьмем распределение по возрастам жителей России за 2017 год http://www.statdata.ru/nasel_pol_vozr. Нам нужен диапазон 21-24, но для простоты возьмем 20-24 (на деле выигрыш будет еще больше). Людей в возрасте 20-24: 7 828 тыс., всего людей 146 804 тыс. Проигрыш copy_if будет составлять 18,8 раз (!), что, извините, слишком много, даже для учебной статьи.
В целом, у вас неплохая статья, но вот пример с copy_if выбран не совсем удачно. Можно, например, сформулировать условие так, что даны два неупорядоченных списка людей, нужно первый отсортировать по возрасту, а из второго выбрать людей с возрастом 21-24 года. Тогда никаких вопросов к использованию copy_if не было бы.

Да, в VC++ (VS 2017, 15.4.1) как раз таки в 1.5 раза


    size_type _Calculate_growth(const size_type _Newsize) const
        {   // given _Oldcapacity and _Newsize, calculate geometric growth
        const size_type _Oldcapacity = capacity();

        if (_Oldcapacity > max_size() - _Oldcapacity / 2)
            {
            return (_Newsize);  // geometric growth would overflow
            }

        const size_type _Geometric = _Oldcapacity + _Oldcapacity / 2;

        if (_Geometric < _Newsize)
            {
            return (_Newsize);  // geometric growth would be insufficient
            }

        return (_Geometric);    // geometric growth is sufficient
        }
Для reserve указывается число элементов, не байт.

Да, конечно, там должно быть что-то вроде v.reserve(4096 / sizeof(decltype(v.front())))


Кроме того, при добавлении в вектор и исчерпании capacity оно увеличивается в 1.5 раз

Странно, где вы видели увеличение в 1.5 раза. Сейчас посмотрел у себя в libstdc++ 8.1 (g++ 8.1) размер увеличивается в 2 раза:


      size_type
      _M_check_len(size_type __n, const char* __s) const
      {
    if (max_size() - size() < __n)
      __throw_length_error(__N(__s));

    const size_type __len = size() + std::max(size(), __n); // <---------------
    return (__len < size() || __len > max_size()) ? max_size() : __len;
      }

В VC++, насколько я помню, также в 2 раза, но это нужно отдельно смотреть

Несколько замечаний.


  1. Если используете потоки и не используете stdio обязательно отключайте синхронизацию с stdio sync_with_stdio, потоки ускорятся в несколько раз:
    int main()
    {
    std::ios::sync_with_stdio(false);
    // ...
    }
  2. Вектор при добавлении элементов может выполнять реаллокацию, что крайне неэффективно. Поэтому если вы знаете, сколько примерно элементов будете обрабатывать, то сделайте v.reserve() с небольшим запасом. Если нет, то можно хотя бы выделить страницу памяти (4096 байт), так как меньше страницы в любом случае не выделится:
    v.reserve(4096);
  3. Используйте cbegin() и cend() вместо begin() и end() там, где это возможно
  4. Сортировать массив сложных структур не особо эффективное занятие, либо определите swap для своей структуры, либо сортируйте указатели. Так как у нас контейнер владеет данными, то вполне подойдет std::unique_ptr:
    std::vector<std::unique_ptr<man>> v;
    // ...
    std::sort(v.begin(), v.end(), [](const auto& m1, const auto& m2){return m1->age < m2->age;});
  5. Вот это также неэффективно:
    std::copy_if(v.begin(), v.end(), std::ostream_iterator<man>(std::cout, "\n"), predicate(20, 25));

    Вектор уже отсортирован, нет необходимости просматривать его целиком. Можно найти начало требуемого диапазона (age > 20) бинарным поиском std::lower_bound и выводить элементы, пока мы не выйдем из диапазона (age < 25). Так можно просмотреть лишь небольшую часть массива.
    С++ это язык написания прежде всего эффективного кода (если эффективность для вас не важна, лучше взять другой, более удобный язык), не нужно гнаться за красотой. Если "красивый" алгоритм из STL делает что-то хуже (применительно к вашей задаче), чем можно сделать "некрасиво" руками, то использовать его не нужно.


Добавил альтернативный вариант реализации статических строк

Все-таки возможно сделать этот вариант хорошо с помощью оператора пользовательского литерала:


template<typename Char, Char ... Chars>
constexpr static_string<Char, Chars ... > operator"" _ss() {
    return static_string<Char, Chars ... >{};
};

constexpr auto str1 = "abc"_ss;
constexpr auto str2 = "def"_ss;
constexpr auto str = str1 + str2 + str1;
std::cout << str << std::endl;

Пожалуй, вынесу это в статью

Обновил статью с учетом замечаний

Если у Вас в проекте это не дает большого выигрыша, то конечно, не следует это использовать. Я уверен, в большинстве случаев constexpr char[] будет достаточно.
P.S. А в Nvidia, например, используют compile-time хэш таблицу
https://www.youtube.com/watch?v=kUbWYdlS9v0&list=PLZN9ZGiWZoZoFa2q0NqD6metQxavT2JYP&index=19

Прошу прощения, прочитал "определять" как "передавать"
В C++ известная проблема со строковыми литералами в шаблонных параметрах, поэтому если так хочется именно в шаблонных параметрах, то придется определять строку как список символов, например так:


template<char ... Chars> struct static_string{};

template<char ... Chars1, char ... Chars2>
constexpr auto static_string_concat(
const static_string<Chars1 ... >& str1,
const static_string<Chars2 ... >& str2) {
    return static_string<Chars1 ..., Chars2 ...>{};
}

template<char ch, char ... Chars>
std::ostream& operator<<(std::ostream& os, const static_string<ch, Chars ...>& str) {
    os << ch << static_string<Chars ... >{};
    return os;
}

std::ostream& operator<<(std::ostream& os, const static_string<>& str) {
    return os;
}

constexpr static_string<'a', 'b', 'c'> str1;
constexpr static_string<'d', 'e', 'f'> str2;
constexpr auto str = static_string_concat(str1, str2);
std::cout << str << std::endl;

Честно говоря, этот вариант я тоже рассматривал, но в итоге ничего толкового из него не вышло

Нет, но можно например использовать такой workaround с помощью хэшей:


template<unsigned long long> constexpr auto greet() {return "Hello, guest!";}
template<> constexpr auto greet<static_string_hash("alice")>() {return "Hello, Alice!";}
template<> constexpr auto greet<static_string_hash("bob")>() {return "Hello, Bob!";}

constexpr auto name = make_static_string("alice");
constexpr auto greeting = greet<name.hash()>();
std::cout << greeting << std::endl;

Как вариант, правда я не уверен, что все операции со строками, которые описаны в статье, возможно реализовать на макросах

В статье есть пример с плагинами, есть директория с плагинами path = "/path/to/plugins/" и два плагина plugin1 = "plugin1.so", plugin2 = "plugin2". Чтобы не писать два раза путь так plugin1_path = "/path/to/plugins/plugin1" и plugin2_path = "/path/to/plugins/plugin2" мы используем конкатенацию plugin1_path = path + "plugin1" и plugin2_path = path + "plugin2"
Да, все полностью статично, то есть конкатенация происходит на этапе компиляции

Information

Rating
Does not participate
Location
Москва, Москва и Московская обл., Россия
Date of birth
Registered
Activity