首先,让我们参考“暴力”算法。下面我会指出其中的一些问题,但这是一个正确的解决方案。
struct Result
{
size_t i;
size_t j;
int64_t value;
};
Result findBestBruteForce(const vector<int>& a)
{
size_t besti = 0;
size_t bestj = 0;
int64_t bestvalue = INT64_MIN;
for (size_t i = 0; i < a.size(); i++)
{
for (size_t j = i + 1; j < a.size(); j++)
{
int64_t value = (a[i] + (int64_t)a[j]) * (j - i);
if (value > bestvalue)
{
bestvalue = value;
besti = i;
bestj = j;
}
}
}
return { besti, bestj, bestvalue };
}
以上代码的问题在于其时间复杂度为O(N²)。更确切地说,对于外部for循环的N次迭代(其中i从0到N),内部for循环平均会迭代N/2次。如果N很小,这并不是问题。
在我的PC上,打开了全部优化后,当N小于20000时,运行时间不到1秒钟。一旦N接近100000,处理50亿次迭代需要几秒钟。让我们假设预期速率为每秒10亿次操作。如果N为1000000,即OP所概述的最大值,可能需要500秒。这就是N平方算法的本质。
那么我们该如何加快速度呢?这里有一个有趣的观察。假设我们的数组是这样的:
10 5 4 15 13 100 101 6
在上面的外部循环的第一次迭代中,其中i=0,我们将在内部循环的每次迭代中计算以下内容:
for each j: (a[0]+a[j])(j-0)
for each j: (10+a[j])(j-0)
for each j: [15*1, 14*2, 25*3, 23*4, 1000*5, 1010*6, 16*6]
= [15, 28, 75, 92, 5000, 6060, 96]
因此,当
i=0
时,
a[i]=15
,从该集合计算出的最大值为6060。
由于
A[0]
为15,并且我们正在跟踪当前的“最佳”值,因此没有动力再次迭代所有值以获取
i=1
,因为
a[1]==14
小于15。 没有
j
索引可以计算出比已经找到的更大的
(a[1]+a[j])*(j-1)
的值。 因为
(14+a[j])*(j-1)
将始终小于
(15+a[j])*(j-1)
。(假定数组中的所有值均为非负数)。
因此,要进行概括,外循环可以跳过任何
i
索引处的
A[best_i]>A[i]
。 这是对上述代码的一个非常简单的改变:
Result findBestOptimized(const std::vector<int>& a)
{
if (a.size() < 2)
{
return {0,0,INT64_MIN};
}
size_t besti = 0;
size_t bestj = 0;
int64_t bestvalue = INT64_MIN;
int minimum = INT_MIN;
for (size_t i = 0; i < a.size(); i++)
{
if (a[i] <= minimum)
{
continue;
}
for (size_t j = i + 1; j < a.size(); j++)
{
int64_t value = (a[i] + (int64_t)a[j]) * (j - i);
if (value > bestvalue)
{
bestvalue = value;
besti = i;
bestj = j;
minimum = a[i];
}
}
}
return { besti, bestj, bestvalue };
}
上面,我们介绍了在考虑进行完整的内部循环枚举之前,A[i]必须达到的最小值。
我使用编译优化对其进行了基准测试。在一个一百万项的随机数组上,它在不到一秒的时间内运行。
但是...还有另一个优化!
如果内部循环未能找到一个索引j使得value>bestvalue,则我们已经知道当前的A[i]大于minimum。因此,在内部循环结束时,我们可以将minimum增加到A[i]。
现在,我将呈现最终解决方案:
Result findBestOptimizedEvenMore(const std::vector<int>& a)
{
if (a.size() < 2)
{
return { 0,0,INT64_MIN };
}
size_t besti = 0;
size_t bestj = 0;
int64_t bestvalue = INT64_MIN;
int minimum = INT_MIN;
for (size_t i = 0; i < a.size(); i++)
{
if (a[i] <= minimum)
{
continue;
}
for (size_t j = i + 1; j < a.size(); j++)
{
int64_t value = (a[i] + (int64_t)a[j]) * (j - i);
if (value > bestvalue)
{
bestvalue = value;
besti = i;
bestj = j;
}
}
minimum = a[i];
}
return { besti, bestj, bestvalue };
}
我对上述解决方案进行了基准测试,数组大小从N = 100到N = 1000000不等。它可以在不到25毫秒的时间内完成所有迭代。
在上述解决方案中,当数组中的所有项按升序排列时,可能存在O(N²)的最坏情况运行时间。但我认为平均情况应该是O(N lg N)或更好的顺序。如果有人感兴趣,我会稍后进行更多分析。
n < 10^4
下运行的代码粘贴一下? - Nimrod