【问题标题】:Effective combination algorithm to pass the time limit有效的组合算法通过时间限制
【发布时间】:2014-06-30 08:38:11
【问题描述】:

我正在做一个练习编程问题,并被困在通过这个问题的时间限制。是否有另一种有效的算法来通过时间限制?

我已经对数组进行了排序,但我认为这仍然没有多大帮助。

这是我的伪代码:

sort boxA
sort boxB
for i in boxA:
      for j in boxB:
          if i+j < value:
              break
          else if i+j > value:
              count+=1
print the count
set count = 0

问题是要输出多少个boxA+boxB的组合大于等于这个值。

输入:

5 3 1200                #number of boxA | number of boxB | value
100 110 160 750 1030    #number of boxA
400 500 500             #number of boxB

输出:

5

说明: 有五种方式组合 boxA 和 boxB 使得 value >= value

1. 750 + 500
2. 750 + 500
3. 1030 + 400
4. 1030 + 500
5. 1030 + 500

BoxA 和 boxB 的列表中可以有 500000 个项目。我认为这种测试用例对我的算法进行了时间限制。

你能展示另一种有效的算法来解决这个问题吗?谢谢你。

【问题讨论】:

  • 您考虑过使用&lt;algorithm&gt; 吗?
  • 是的,我已经使用 std::sort 从最大值对数组进行排序。
  • 您可以使用二等分 B 框将复杂度从 O(nm) 降低到 O(nlog m)
  • &lt;algorithm&gt; 中还有很多其他的useful functions
  • 如果对 boxA 中的每个元素进行排序,则无需遍历 boxB 中的所有元素。如果它们是排序的,那么你知道 boxA[i+1]+boxB[j]>=boxA[i]+boxB[j]。您可以利用这些知识来优化内部循环。

标签: c++ algorithm


【解决方案1】:

对于 A 中每个带有a 项目的盒子,B 中的盒子数量加上 a,大于或等于某个值 d 将等于具有数量的盒子的数量大于 (d - a) 的项目。

所以,首先对数组B进行排序,然后对于A中的每个值为x的框,使用二分查找从B中的哪个索引开始查找框中的项目大于或等于到 d - x。添加最终结果(n - 索引),其中 n 是 B 中的项目数。

时间复杂度为 O(m log n)

例子:

我们有两个数组 A 是 {1,5,9,2,4,5},B 是 {1,3,3,4,5,6,7,8};

例如,我们想找到总和大于 7 的两个框。

所以,对于 A 中的每个元素

1 -> 我们使用二分查找来查找 B 中大于或等于 (7 - 1) 的最小元素的索引,该索引现在位于索引 5 处,因此我们将 (8 - 5) 添加到结果中(使用8 是 B) 中的元素个数。

5 -> 我们需要在 B 中找到 (7 - 5) -> 搜索后我们有索引 1 -> 将 (8 - 2) 添加到结果中。

...

【讨论】:

  • 如果你对 A 进行排序,那么在 B 中对 d-x 的二分搜索不必考虑整个 B:你只需要在B 的子范围 [0, last_binary_search_result]。
  • @j_random_hacker 是的,即使我们可以从 A 的最大元素到最小元素,这样可以提前终止程序。但是,额外的成本是 O(n logn),所以这取决于,我猜测:)
  • 我还是对这个算法有点困惑。你能给我举个例子吗?谢谢!
  • 排序数组B需要O(n log n)时间,所以技术上整个运行时间是O((m + n) log n),或者O(max(m, n) log n)如果你喜欢这样写的话。
  • 如果想再优化一点,在这个算法中总是选择较大的数组作为数组A(即先对较小的数组进行排序,然后对较大的数组进行循环,二分查找较小的数组)。
【解决方案2】:

您可以使用类似以下的内容:Live example

std::size_t Count(std::vector<int>& A, std::vector<int>& B, int N)
{
    std::vector<int>& a = A.size() < B.size() ? A : B;
    std::vector<int>& b = A.size() < B.size() ? B : A;

    std::sort(b.begin(), b.end());
    std::size_t res = 0;
    for (int e : a)
    {
        auto it = std::lower_bound(b.begin(), b.end(), N - e);
        res += std::distance(it, b.end());
    }
    return res;
}

【讨论】:

    【解决方案3】:

    我假设您可以对两个数组进行排序,并且可以向前或向后遍历每个数组。

    sort boxA
    sort boxB
    let a = first number in boxA    // the smallest number in the set
    let b = last number in boxB     // the largest
    let total = 0
    let subtotal = 0
    
    while a exists
    {
        while (b exists) and (a + b >= value)
        {
            let b = previous number in boxB   // "Move" the boxB iterator one place
                                              // toward the start of the array.
            let subtotal = subtotal + 1   // Now subtotal is the number of times
                                          // we have "moved" the boxB iterator since
                                          // the algorithm started to execute.
        }
    
        // Now subtotal is the number of numbers b in boxB such that a + b >= value.
    
        total += subtotal
        let a = next number in boxA
    }
    
    print total
    

    如果boxAboxB 包含重复条目(相同的数值不止一次),“下一个数字”表示“数字的下一个副本”,而不是“下一个唯一数字”,与“上一个数字”类似号”。

    我本可以将while a exists ... let a = next number in boxA 写成for a in boxA,但我想强调该算法处理boxA 的方式与处理boxB 的方式之间的关系:它迭代一个一次通过boxA(正向)和一次通过boxB(反向)同时

    特别是,与典型的嵌套循环控制结构不同,对于来自boxA 的每个新值,我们确实将迭代器设置在boxB 上“回到起点”。 相反,在算法的整个执行过程中,“内循环”只能迭代与boxB 中的数字一样多的次数。 因此,算法的运行时间是对两个数组进行排序所花费的时间 加上一个额外的 O(n),其中 n 是较大数组的大小。 当然,如果算法接收到未排序的数组,由于排序的原因,最坏情况的成本是 O(n log n),但它仍然比需要额外 O(n log n) 步骤的算法更快(通过常数因子)数组排序后。 如果我们假设数组已经排序(出于其他原因),那么算法运行时间为 O(n)。

    更新:

    正如 cmets 中所指出的,如果您首先知道 boxA 中有多少项目(对于某些数据结构很容易在 O(1) 时间内确定——尤其是通常的“数组”)——使用的语言),您可以添加逻辑,当boxB 中的所有数字都已被访问时,将打破boxA 循环。不要重复额外的remaining_size_of_A 次,只需执行 total += remaining_size_of_A * subtotal。 (注意此时subtotal等于boxB的大小。) 这可以节省几个步骤。

    【讨论】:

    • 你的算法时间复杂度是 O(nlogn + mlogm + (n + m))。内循环结束后,您需要使用一些数学公式。 total += number of item left in a * subtotal 所以时间复杂度会降低到 O(nlogn + mlogm + min(n,m))
    • 是的,如果我提前知道boxA 中的项目数,你是对的,我可以跳过 Theta(max(m,n)-min(n,m)) 步骤结尾。但是,我写的内容适用于任何双向数据结构,例如双向链表。 (即使是单链表,我只需要另一个 O(n) 步骤来制作它的反向副本。)
    • 糟糕,从我之前的评论中划掉“Theta(max(m,n)-min(n,m))”。 boxAboxB 的大小不能确定哪个迭代器首先终止——重要的是数组中包含的数字的值。我们可以有 1000 个 boxA 元素和 10 个 boxB 元素,但仍然永远不会到达 boxB 的开头,因为经过排序后,(last in boxA) + boxB[5] &lt; value
    【解决方案4】:

    您需要分而治之的策略。假设范围 [b1, e1) 和 [b2, e2) 已排序,则以下过程可以完成工作。

    #include <iterator>
    
    template<class It1, class It2, class T>
    size_t do_work(It1 b1, It1 e1, It2 b2, It2 e2, T const t){
        if (b1 == e1 || b2 == e2) return 0;
    
        auto const n1 = std::distance(b1, e1);
        auto const n2 = std::distance(b2, e2);
        if (n1 > n2) return do_work(b2, e2, b1, e1, t);// always divide the shorter sequence
    
        auto const l11 = n1 / 2, l12 = n1 - l11;
        auto const l21 = n2 / 2, l22 = n2 - l21;
        auto const m1 = std::next(b1, l11);
        auto const m2 = std::next(b2, l21);
    
        if (*m1 + *m2 > t){
            return do_work(b1, m1, b2, e2, t) + do_work(m1, e1, b2, m2, t) + l12 * l22;
        }
        else{
            auto const _m1 = std::next(m1);
            return do_work(b1, _m1, std::next(m2), e2, t) + do_work(_m1, e1, b2, e2, t);
        }
    }
    

    如果It1和It2是随机访问迭代器,时间复杂度约为O(n log2(n)),其中n = max(n1, n2)。

    【讨论】:

    • 我希望你的意思是 O(n log n) 而不是 O(n^log2(n))。如果真的是后者,请尽可能快地远离算法。
    • :-) 我想可能是这样。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-11-30
    • 2012-11-28
    • 1970-01-01
    • 2021-10-11
    • 1970-01-01
    • 1970-01-01
    • 2012-05-08
    相关资源
    最近更新 更多