【问题标题】:Fastest way to find minimal product of 2 array elements containing 200000+ elements找到包含 200000+ 个元素的 2 个数组元素的最小乘积的最快方法
【发布时间】:2020-04-29 06:45:00
【问题描述】:

我有一个数组a[n]。号码n 由我们输入。我需要找到a[i] 和a[j] 的最小乘积,如果:

1) abs(i - j) > k

2) a[i] * a[j] 被最小化

这是我的解决方案(非常天真):

#include <iostream>
using namespace std;
#define ll long long
int main() {
    ll n,k; cin >> n >> k;

    ll a[n]; for(ll i=0;i<n;i++) cin >> a[i];

    ll mn; bool first = true;

    for(ll i=0;i<n;i++) {
        for(ll j=0;j<n;j++) {
            if(i!=j)
            if(abs(i-j) > k) {
                if(first) {
                    mn = a[i]*a[j];
                    first = false;
                } else if(a[i]*a[j] < mn) mn = a[i]*a[j];
            }
        }
    }
    cout << mn << endl;
}

但我想知道是否有更快的方法来找到距离最小的产品?

【问题讨论】:

  • Why should I not #include <bits/stdc++.h>? 和 C++ 仅通过编译器扩展提供 可变长度数组。你为什么不使用std::vector? @Scheff - 排序会破坏原来的“距离”关系。
  • 至少可以去掉检查if (i!=j) if (abs(i - j) &gt; k)。只需在 i + k + 1 处开始内循环:for (ll j = i + k + 1; j &lt; n; ++j)。如果mn 事先被初始化,例如first 的检查也可以被消除。与mn = a[0] * a[k + 1];。 (也许,k 最初应该与 n 核对以防弹。)但它仍然是 O(N²)。这必须更快...
  • @PaulMcKenzie 请在前十个具有索引距离的最小产品中显示一个查询,其中不少于两个有用命中i>(或最大)。
  • @PaulMcKenzie “可能有数百个(如果不是数千个)显示此问题答案的 URL 链接。” -- 请至少分享其中三个网址。
  • 这个问题是从哪里来的?这听起来不像是凭空捏造的东西。如果它来自那些“在线法官”网站之一,我不会感到惊讶。如果是这样,那么在这些网站上可能会针对解决问题进行冗长的讨论,如果不是完整的解决方案的话。

标签: c++ algorithm optimization minimum


【解决方案1】:

假设至少有一对元素满足条件并且其中两个元素的乘法没有溢出,这可以在Theta(n-k)时间和Theta(1)空间最坏和最好的情况下完成,像这样:

auto back_max = a[0];
auto back_min = a[0];
auto best = a[0]*a[k+1];

for(std::size_t i=1; i<n-(k+1); ++i) {
    back_max = std::max(back_max, a[i]);
    back_min = std::min(back_min, a[i]);
    best = std::min(best, std::min(a[i+k+1]*back_max, a[i+k+1]*back_min));
}

return best;

就时间和空间的渐近最坏情况复杂度而言,这是最优的,因为最优乘积可能是a[0] 与任何n-(k+1) 元素的距离至少为k+1,因此至少为n-(k+1)任何解决问题的算法都需要读取整数。


算法背后的思路如下:

最优产品使用a 的两个元素,假设它们是a[r] 和a[s]。不失一般性,我们可以假设 s &gt; r 因为乘积是可交换的。

由于abs(s-r) &gt; k 的限制,这意味着s &gt;= k+1。现在s 可能是满足此条件的每个索引,因此我们迭代这些索引。这是在所示代码中对i 的迭代,但为方便起见,它被k+1 移动(并不重要)。对于每次迭代,我们需要找到以i+k+1为最大索引的最优乘积,并将其与之前的最佳猜测进行比较。

由于距离要求,与i+k+1 配对的可能索引都是小于或等于i 的索引。我们也需要遍历所有这些,但这是不必要的,因为由于产品的单调性,固定i 处的a[i+k+1]*a[j] 与j 的最小值等于min(a[i+k+1]*max(a[j]), a[i+k+1]*min(a[j])) a[j] 上的最小值和最大值都说明了 a[i+k+1] 的两个可能符号或等效的单调性的两个可能方向。)

由于我们在这里优化的 a[j] 值集只是 {a[0], ..., a[i]},它在 i 的每次迭代中仅增长一个元素 (a[i]),我们可以简单地跟踪 max(a[j])如果a[i] 大于或小于先前的最佳值,则通过更新它们来使用单个变量和min(a[j])。这是通过代码示例中的back_max 和back_min 完成的。

迭代的第一步 (i=0) 在循环中被跳过,而是作为变量的初始化执行。

【讨论】:

  • @greybeard 我不需要保留它们,因为a[i+k+1] 的最佳产品的唯一可能候选者是最小值和最大值。
  • 您能解释一下为什么该算法在您的答案中有效吗?
【解决方案2】:

不确定最快。

对于没有i 的更简单的问题,最小乘积是两个最小和最大元素的对的乘积。

所以,(下面的太复杂了,见walnut's answer)
( • 如果 k ≤ n 则拒绝
• 将 minProduct 初始化为 a[0]*a[k+1])

  • 保留两个dynamic minmax data structures upToI 和 beyondIplusK
    以 { } 和 { a[j] | 开头k ≤ j }
  • 对于每个 i 从 0 到 n - k - 1
    • 将 [i] 添加到 upToI
    • 从beyondIplusK中移除a[i+k]
    • 在
      中检查新的最小产品 min(upToI)×min(beyondIplusK), min(upToI)×max(beyondIplusK),
      max(upToI)×min(beyondIplusK) 和 max(upToI)×max(beyondIplusK)

【讨论】:

  • 这应该是最快的,至少在复杂性方面。它是 O(n) 时间和存储。
  • 原解的复杂度为O(N**2),你如何估计你的解的复杂度?
  • O(nlogn) 时间,O(n) 空间(用于合适的 minmax 实现)
  • @greybeard。为什么需要 n*logn 时间。为什么不简单地保留一个包含minUpto、maxUpto、minBeyond、maxBeyond 的 4*n 数组(您可以在两次迭代中创建)?然后,在第三次迭代中,对于每个索引,找到最小可能的乘法。
  • (@smttsp 那将是walnut's solution方向的替代步骤。)
【解决方案3】:

对于“最小幅度”

找到 2 个“最小量级”元素,然后(在找到两个零或搜索整个数组之后)将它们相乘。

对于没有abs(i - j) &gt; k 部分的“最低值”

有3种可能:

  • 两个最高(最小)的负数

  • 两个最低(最小量级)的非负数

  • 最低(最大)负数和最高(最大)非负数

您可以搜索所有 6 个值并找出产品,最后哪个是最好的。

但是;一旦你看到零,你就知道你不需要更多地了解前两种可能性;当你看到一个负数和一个非负数时,你就知道你只关心第三种可能性。

这导致了一个具有 3 个状态的有限状态机——“关心所有 3 种可能性”、“除非看到负数,否则答案为零”和“只关心最后一种可能性”。这可以实现为一组 3 个循环,当(有限状态机的)状态发生变化时,其中 2 个循环跳转到(goto)另一个循环的中间。

具体来说,它可能看起来有点像(未经测试):

   // It could be any possibility

   for(ll i=0;i<n;i++) {
       if(a[i] >= 0) {
            if(a[i] < lowestNonNegative1) {
                lowestNonNegative2 = lowestNonNegative1;
                lowestNonNegative1 = a[i];
            }
            if(lowestNonNegative2 == 0) {
                goto state2;
            }
       } else {
            if(a[i] > highestNegative1) {
                highestNegative2 = highestNegative1;
                highestNegative1= a[i];
            }
            if(lowestNonNegative1 < LONG_MAX) {
                goto state3;
            }
       }
   }
   if(lowestNonNegative2 * lowestNonNegative1 < highestNegative2 * highestNegative1) {
       cout << lowestNonNegative2 * lowestNonNegative1;
   } else {
       cout << highestNegative2 * highestNegative1;
   }
   return;

   // It will be zero, or a negative and a non-negative

   for(ll i=0;i<n;i++) {
state2:
       if(a[i] < 0) {
           goto state3;
       }
   }
   cout << "0";
   return;

   // It will be a negative and a non-negative

   for(ll i=0;i<n;i++) {
state3:
       if(a[i] < lowestNegative) {
           lowestNegative = a[i];
       } else if(a[i] > highestNonNegative) {
           highestNonNegative = a[i];
       }
    }
    cout << lowestNegative * highestNonNegative;
    return;

abs(i - j) &gt; k 部分的“最低值”

在这种情况下,您仍然有 3 种可能性;并且可以使它与相同的“有限状态机的 3 个循环”方法一起工作,但它变得太混乱/丑陋。对于这种情况,一个更好的选择可能是预先扫描数组以确定是否有任何零以及它们是全部为负数还是全部为正数;这样在预扫描之后,您就可以知道答案是否为零,或者选择仅针对特定可能性设计的循环。

【讨论】:

  • 这在哪里解释了索引差异的下限 k?
  • @greybeard:它没有(我错过了那部分) - 需要修改代码以考虑到这一点。
  • 为什么需要两个个零?
  • @TrentP:啊——你是对的。一个零就足以知道答案是 0 还是负数。
猜你喜欢
  • 2021-05-09
  • 1970-01-01
  • 2011-12-09
  • 2019-10-24
  • 2021-12-27
  • 1970-01-01
  • 1970-01-01
  • 2017-02-02
  • 2020-09-06
相关资源
最近更新 更多