Умножение каждого элемента одного массива на каждый элемент другого массива и сортировка нового очень большого массива - PullRequest
2 голосов
/ 28 апреля 2019

Отказ от ответственности Это упражнение моего курса, а не из продолжающегося конкурса.

Описание проблемы

Описание проблемы оченьпрямо:

Вам даны два массива, A и B, содержащие n и m элементов соответственно.Числа, которые нужно отсортировать, это Ai * Bj, для 1 <= i <= n и 1 <= j <= m.Проще говоря, каждый элемент первого массива должен быть умножен на каждый элемент второго массива. </p>

Пусть C - результат этой сортировки, являющийся неубывающей последовательностью элемента.Выведите сумму каждого десятого элемента этой последовательности, то есть C1 + C11 + C21 + ....

1 <= n, m <= 6000 </p>

1 <= Ai,Bj <= 40000 </p>

Ограничение памяти: 512 МБ

Ограничение времени: 2 секунды

Мое решение пока

Первое использованиеJava, используя Arrays.sort, учитывая наибольшее n, m.Нам нужно отсортировать массив размером 36000000. Затем пройти через каждый десятый элемент в массиве, чтобы получить сумму.Это проходит 23 тестовых случая, а остальные получили TLE.

Затем я переключаюсь на C ++, также использую встроенный метод сортировки, и результат немного лучше, проходит 29 тестовых примеров.

Мое наблюдение

С учетом этого ввода

4 4
7 1 4 9
2 7 8 11

Если мы сначала отсортируем два массива A и B, затем умножим их вместе, мы получим

2 8 14 18 7 28 49 63 8 32 56 72 11 44 77 99

, который является массивом с m отсортированными подмассивами.Но я не мог придумать ни одного хорошего решения, чтобы объединить все эти отсортированные подмассивы в O (mn) или где-то вокруг этого.Или нам нужно взглянуть на проблему под другим углом. Существуют ли какие-либо специальные свойства, связанные с умножением всех элементов двух массивов вместе?

Обновление 1: - с использованием MinHeap - недостаточно быстро,[TLE]

Обновление 2: - с использованием k способов слияния - все еще недостаточно быстро.[TLE]

Обновление 3: - Я забыл упомянуть о диапазоне элементов в A и B, поэтому я только что обновил его.

Обновление4: - Основа сортировки по радикалу 256 [Принято]

Заключение

Из этой проблемы я узнал больше о сортировке в целом и некоторую полезную информацию о сортировкес библиотеками на Java и C ++.

  • Встроенные методы сортировки в C ++, такие как std :: sort, не стабильны, потому что это в основном быстрая сортировка, но когда формат данных не подходит для быстрой сортировки, тогда она переключается на сортировку слиянием, нов общем, это самый быстрый встроенный вид C ++ (помимо qsort, stable_sort).

  • Для Java существует 3 типа сортировки, один с Arrays.sort (примитив []), который использует сортировку слиянием под капотом, Arrays.sort (Object []), которыйиспользует Timsort и Collections.sort, который в основном вызывает Arrays.sort для выполнения своих тяжелых задач обработки.

Большое спасибо @rcgldr за его основополагающий код сортировки 256 C ++, он работает как чемпионпри худшем случае 6000 * 6000 элементов максимальное время выполнения составляет 1,187 с.

  • Интересно, что std :: sort C ++ не удался только в 3 последних тестовых случаях, он отлично работает с вводом размера6000 * 3000.

Ответы [ 2 ]

1 голос
/ 28 апреля 2019

объединить все эти отсортированные подмассивы в O (mn)

Продукты имеют размер <2 ^ 31, поэтому достаточно 32-битных целых чисел и будет работать основа 256 сортировки по основанию,Для суммы каждого 10-го элемента может потребоваться 64 бита.</p>

Обновление - вы не упомянули ограничение памяти 256 МБ в ваших комментариях, я только что заметил это.Размер входного массива составляет 6000 *6000* 4 = 137,33 МБ.Выделите рабочий массив, равный половине размера исходного массива (округленный в большую сторону: work_size = (1 + original_size) / 2), в худшем случае 3000 * 6000 элементов (<210 МБ общего необходимого пространства).Рассматривайте исходный массив (product) как две половины и используйте радикальную сортировку для сортировки двух половинок исходного массива.Переместите нижнюю отсортированную половину в рабочий массив, затем объедините рабочий массив с верхней половиной исходного массива обратно в исходный массив.В моей системе (Intel 3770K 3,5 ГГц, Win 7 Pro 64 бит), 2 радикальных сортировки займут менее 0,4 секунды (~ 0,185 секунды каждая), а одноразовое объединение 3000 * 6000 целых чисел займет около 0,16 секунды, что меньше, чем0,6 секунды для сортировки.При таком подходе нет необходимости сортировать A или B перед выполнением умножения. </p>

Разрешено ли использовать регистры SIMD / xmm для умножения внешнего произведения на A и B (A ox B)?

Пример кода C ++ для базовой сортировки 256 оснований:

//  a is input array, b is working array
uint32_t * RadixSort(uint32_t * a, uint32_t *b, size_t count)
{
size_t mIndex[4][256] = {0};            // count / index matrix
size_t i,j,m,n;
uint32_t u;
    for(i = 0; i < count; i++){         // generate histograms
        u = a[i];
        for(j = 0; j < 4; j++){
            mIndex[j][(size_t)(u & 0xff)]++;
            u >>= 8;
        }       
    }
    for(j = 0; j < 4; j++){             // convert to indices
        m = 0;
        for(i = 0; i < 256; i++){
            n = mIndex[j][i];
            mIndex[j][i] = m;
            m += n;
        }       
    }
    for(j = 0; j < 4; j++){             // radix sort
        for(i = 0; i < count; i++){     //  sort by current lsb
            u = a[i];
            m = (size_t)(u>>(j<<3))&0xff;
            b[mIndex[j][m]++] = u;
        }
        std::swap(a, b);                //  swap ptrs
    }
    return(a);
}

Можно использовать сортировку слиянием, но она медленнее.Предполагая, что m> = n, тогда обычная двухсторонняя сортировка слиянием потребует O (mn ⌈log2 (n) ⌉), чтобы отсортировать n отсортированных прогонов, каждый из которых имеет размер m.В моей системе сортировка 6000 прогонов 6000 целых чисел занимает около 1,7 секунды, и я не знаю, сколько времени займет матричное умножение.

Использование кучи или другой формы очереди с приоритетами только увеличит накладные расходы.,Обычная двухсторонняя сортировка слиянием была бы быстрее, чем k-way сортировка слиянием с кучей.

В системе с 16 регистрами, 8 из которых используются в качестве рабочих и конечных индексов или указателей на прогоны, 4 способаСортировка слиянием (без кучи), вероятно, будет немного быстрее (около 15%), это то же самое общее количество операций, 1,5-кратное число сравнений, но 0,5-кратное число перемещений, что немного более удобно для кэша.

1 голос
/ 28 апреля 2019

Ключ к вашему ответу лежит в ваших наблюдениях ...

Если мы сначала отсортируем два массива A и B, а затем умножим их вместе, мы получим 2 8 14 18 7 28 49 63 8 32 56 72 11 44 77 99 который является массивом с m отсортированные подмассивы.

Таким образом, существует n последовательностей данных, которые сортируются, и проблема заключается в том, чтобы использовать их для генерации ответа.

Подсказка 1: Вы можете решить эту проблему, используя очередь приоритетов. Количество элементов в очереди будет таким же, как количество отсортированных списков, которые создаются.

С

#include <vector>
#include <algorithm>
#include <random>
#include <queue>

С учетом следующих структур (C ++)

// helper to catch every tenth element.
struct Counter {
    int mCount;
    double mSum;
    Counter() : mCount(0), mSum(0) {}
    void push_back(int val)
    {
        if (mCount++ % 10 == 0)
        {
            mSum += val;
        }
    }
    double sum() { return mSum; }
};

// Storage in the priority queue for each of the sorted results.
struct Generator {
    int i_lhs;
    int i_rhs;
    int product;
    Generator() : i_lhs(0), i_rhs(0), product(0) {}
    Generator(size_t lhs, size_t rhs, int p) : i_lhs(lhs), i_rhs(rhs), product(p)
    {
    }
 };

// comparitor to get lowest value product from a priority_queue
struct MinHeap
{
    bool operator()(const Generator & lhs, const Generator & rhs)
    {
        if (lhs.product > rhs.product) return true;
        return false;
    }
};

Я измерил ....

double Faster(std::vector<int> lhs, std::vector<int>  rhs)
{
    Counter result;
    if (lhs.size() == 0 || rhs.size() == 0) return 0;

    std::sort(lhs.begin(), lhs.end());
    std::sort(rhs.begin(), rhs.end());
    if (lhs.size() < rhs.size()) {
        std::swap(lhs, rhs);
    }
    size_t l = 0;
    size_t r = 0;
    size_t lhs_size = lhs.size();
    size_t rhs_size = rhs.size();
    std::priority_queue<Generator, std::vector< Generator >, MinHeap > queue;
    for (size_t i = 0; i < lhs_size; i++) {
        queue.push(Generator(i, 0, lhs[i] * rhs[0]));
    }
    Generator curr;
    while (queue.size()) {
        curr = queue.top();
        queue.pop();
        result.push_back(curr.product);
        curr.i_rhs++;
        if( curr.i_rhs < rhs_size ){
            queue.push(Generator(curr.i_lhs, curr.i_rhs, lhs[curr.i_lhs] * rhs[curr.i_rhs]));
        }
    }
    return result.sum();
 }

Быстрее, чем следующая наивная реализация

double Naive(std::vector<int> lhs, std::vector<int>  rhs)
{
    std::vector<int> result;
    result.reserve(lhs.size() * rhs.size());
    for (size_t i = 0; i < lhs.size(); i++) {
        for (size_t j = 0; j < rhs.size(); j++) {
            result.push_back(lhs[i] * rhs[j]);
        }
    }
    std::sort(result.begin(), result.end());
    Counter aCount;
    for (size_t i = 0; i < result.size(); i++) {
        aCount.push_back(result[i]);
    }
    return aCount.sum();
}

Сортировка входных векторов происходит намного быстрее, чем выходной вектор. Для каждой строки мы создаем генератор, который будет перебирать все столбцы. Текущий продукт добавляется в качестве значения приоритета в очередь, и как только у нас есть все созданные генераторы, мы читаем их из очереди.

Затем, если для каждого генератора остается еще один столбец, мы добавляем его обратно в очередь. Это из наблюдения, что на выходе предварительно отсортированного входа было m подмассивов размера n. Очередь содержит все текущие минимальные значения для каждого подмассива, и наименьшее из этого набора является наименьшим оставшимся из всего списка. Когда генератор удаляется и повторно добавляется, он гарантирует, что значение top является следующим наименьшим элементом результатов.

Цикл по-прежнему равен O (нм), поскольку каждый генератор создается один раз, считывая наименьшее значение O (1), а вставляя в очередь O (log n). Что мы делаем один раз для каждой строки, поэтому O (nm * log n + nm), что упрощается до O (nm log n).

Наивным раствором является O (нм log nm).

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...