将一个数组的每个元素乘以另一个数组的每个元素,并对新的非常大的数组进行排序

时间:2019-04-28 06:33:54

标签: java c++ algorithm sorting

免责声明 这是我的课程的练习,而不是正在进行的比赛。

问题描述

问题描述非常简单:

您将获得两个数组A和B,分别包含n和m个元素。对于1 <= i <= n和1 <= j <= m,您需要排序的数字为Ai * Bj。简而言之,第一个数组的每个元素都应与第二个数组的每个元素相乘。

让C作为这种排序的结果,是元素的非递减序列。打印此序列的每十分之一的总和,即C1 + C11 + C21 +...。

1 <= n,m <= 6000

1 <= Ai,Bj <= 40000

内存限制:512MB

时间限制:2秒

到目前为止我的解决方案

首先,我使用Java,使用Arrays.sort,给出最大的n,m。我们将需要对一个大小为3600万的数组进行排序。然后遍历数组中的每十分之一以求和。通过了23个测试用例,其余的获得了TLE。

然后我切换到C ++,也使用内置的sort方法,结果通过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(primitive []),它在后台使用了合并排序;另一种是Arrays.sort(Object []),它使用了Timsort和Collections.sort。基本上是调用Arrays.sort来完成繁重的处理工作。

非常感谢@rcgldr提供的基于radix排序的256 C ++代码,它的工作原理类似于冠军,更糟糕的情况是6000 * 6000个元素,最大运行时间为1.187s。

  • 有趣的是,C ++的std :: sort仅在最后三个最大的测试用例中失败,它在输入大小为6000 * 3000的情况下可以正常工作。

2 个答案:

答案 0 :(得分:1)

答案的线索在于观察...

  

如果我们先对两个数组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的子数组。队列保存每个子数组的所有m个当前最小值,而该集合中的最小值是整个列表中最小的剩余值。删除并重新添加生成器后,它会确保top值是结果的下一个最小项。

循环仍然是O(nm),因为每个生成器都创建一次,读取的最小值是O(1),并且插入到队列中的是O(log n)。我们每行执行一次,所以O(nm * log n + nm)简化为O(nm log n)。

天真的溶液是O(nm log nm)。

我从上述解决方案中发现的性能瓶颈是插入队列的成本,为此我提高了性能,但我不认为algorithm通常“快得多”

答案 1 :(得分:1)

  

将所有这些排序的子数组合并到O(mn)

乘积小于2 ^ 31,因此32位整数就足够了,并且基数排序基数256将起作用。每10个项目的总和可能需要64位。

更新-您在评论中没有提到256 MB的内存限制,我只是注意到了这一点。输入数组大小为6000 * 6000 * 4 = 137.33MB。分配一个工作数组,大小是原始数组大小的一半(向上舍入:work_size =(1 + original_size)/ 2),最坏的情况是3000 * 6000个元素(需要的总空间少于210MB)。将原始(乘积)数组视为两半,并使用基数排序对原始数组的两半进行排序。将排序后的下半部分移到工作数组中,然后将工作数组与原始数组的上半部分合并回原始数组中。在我的系统(Intel 3770K 3.5 ghz,Win 7 Pro 64位)上,两种基数排序将花费不到0.4秒(每种时间约〜0.185秒),并且一次合并3000 * 6000整数将花费约0.16秒,少于排序部分为0.6秒。使用这种方法,在进行乘法运算之前无需对A或B进行排序。

是否允许使用SIMD / xmm寄存器对A和B(A o.x B)进行乘积运算?

基本256基数排序的示例C ++代码:

//  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,则传统的2路合并排序将使用O(mn log2(n)⌉)来排序n个排序的游程,每个游程的大小为m。在我的系统上,对6000个6000个整数的运行进行排序大约需要1.7秒,而且我不知道矩阵相乘会花费多长时间。

使用堆或其他形式的优先级队列只会增加开销。常规的2向合并排序比带堆的k向合并排序要快。

在具有16个寄存器的系统上,其中8个用作工作索引和结束索引或运行指针,四向合并排序(无堆)可能会快一点(大约15%),并且总数相同操作数,比较数1.5倍,但移动数0.5倍,这对缓存更友好。