【问题标题】:Multiplying every element of one array by every element of another array and sort the new very large array将一个数组的每个元素乘以另一个数组的每个元素并对新的非常大的数组进行排序
【发布时间】:2019-09-17 03:37:08
【问题描述】:

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

问题描述

问题描述很直接:

给定两个数组,A 和 B,分别包含 n 和 m 个元素。您需要排序的数字是 Ai*Bj ,对于 1

令 C 是这种排序的结果,是一个非递减的元素序列。打印这个序列的每第十个元素的总和,即 C1 + C11 + C21 + ... 。

1

1

内存限制:512MB

时间限制:2秒

到目前为止我的解决方案

首先我使用Java,使用Arrays.sort,给定最大的n,m。我们需要对一个大小为 36000000 的数组进行排序。然后遍历数组中的每 10 个元素以获得总和。这通过了 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(primitive[]),它在底层使用归并排序,Arrays.sort(Object[]),它使用 Timsort 和 Collections.sort它基本上调用 Arrays.sort 来完成繁重的处理工作。

非常感谢@rcgldr 的基数排序基数 256 C++ 代码,它在 6000*6000 元素的更坏情况下工作起来就像一个冠军,最大运行时间为 1.187 秒。

  • 有趣的是,C++ 的 std::sort 仅在最后 3 个最大的测试用例中失败,它适用于大小为 6000*3000 的输入。

【问题讨论】:

  • couldn't think of any good solution to merge all of these sorted subarrays in O(n) 你脑子里最想念的事情是什么? (我想我是在问 O(mn log(min(m, n)) 会做吗?
  • (未指定为非负数的元素并不完全有帮助。)
  • @greybeard 他们都是非负数,你有解决方案吗?如果是的话,你能给我一点提示吗?因为只是正常的排序是行不通的,不管排序步骤有多好。
  • (我想提到 O(mn log(min(m, n))) 一个提示;也应该适用于混合符号.)
  • 你的意思是循环遍历数组C,然后在A或B中进行某种二分查找,这样我们就可以知道C排序时那个元素的顺序?跨度>

标签: java c++ algorithm sorting


【解决方案1】:

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

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

更新 - 你没有在你的 cmets 中提到 256MB 的内存限制,我只是注意到了这一点。输入数组大小为 6000*6000*4 = 137.33MB。分配一个大小为原始数组一半的工作数组(向上取整:work_size = (1+original_size)/2),最坏的情况,3000*6000 个元素(所需的总空间小于 210MB)。将原始(产品)数组视为两半,并使用基数排序对原始数组的两半进行排序。将排序后的下半部分移动到工作数组中,然后将工作数组与原始数组的上半部分合并回原始数组。在我的系统上(Intel 3770K 3.5 ghz,Win 7 Pro 64 位),2 个基数排序将花费不到 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 个用作工作和结束索引或运行指针,4 路合并排序(没有堆)可能会快一点(大约 15%),总数相同操作数,1.5 x 比较数,但 0.5 x 移动数,这对缓存更友好。

【讨论】:

  • 谢谢,我会尝试基数排序,这似乎是我最后的希望,因为我已经尝试过 MinHeap 和 K 方式合并。
  • 对不起,我忘记添加 A 和 B 的输入范围,A、B 中的所有元素都是非负数且不超过 40000。
  • 让输入的整数有 d 个数字。基数排序需要 O(d*(n+b)) 时间,其中 b 是表示数字的基础。所以考虑到最坏的情况,运行时间是O(10*(36000000+10)),我觉得不会过去。
  • 我也刚刚更新了有关内存限制和时间限制的信息。鉴于在 C++ 和 Java O(mn logmn) 中使用内置排序的天真解决方案,它超时了。我不认为在这种情况下会更好。
  • 您那里有基数排序基数 256 的有效实现吗?语言无关紧要,因为我以前从未在 base 256 中实现过基数排序,所以看看如何有效地实现它很有用。
【解决方案2】:

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

如果我们先对两个数组 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)。

我从上述解决方案中发现的性能瓶颈是插入队列的成本,我为此提高了性能,但我不认为它比 algorithmically" 快得多。

【讨论】:

  • 谢谢,我刚刚搜索了一下,如何对“k 个排序数组”进行排序。有几种方法,但我也在想一种新的方法,但我不知道是否可能。无论如何,我会先尝试 MinHeap 方法,看看它是否有效。
  • 不够快 :( 与对整个 mxn 数组进行排序没有太大区别。
  • 我已经更新,在尝试了 MinHeap 和 K 方式合并以合并 K 个排序数组之后,它与在 C++ 或 Java 中使用内置排序方法的天真解决方案没有太大区别。
猜你喜欢
  • 2015-08-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多