一位朋友要求我分享我过去一段时间偶然发现的东西。原帖取自这里。问题陈述可以在这里找到。基本上是一个算法竞赛的网站。
我遇到了一个算法问题,我使用以下代码解决了这个问题:
double dp[80002][50];
class FoxListeningToMusic {
public:
vector <double> getProbabilities(vector <int> length, int T) {
memset(dp, 0, sizeof(dp));
int n = length.size();
for(int i = 0; i < n; i++)
dp[0][i] = 1.0 / (double)n;
double mul = 1.0 / (double)n;
int idx ;
for(int i = 1; i <= T; i++) {
for(int j = 0; j < n; j++) {
idx = i - length[j];
if(idx >= 0) {
for(int k = 0; k < n; k++)
dp[i][k] += mul * dp[idx][k];
}
else
dp[i][j] += mul;
}
}
}
vector<double> v(n);
for(int i = 0; i < n; i++)
v[i] = dp[T][i];
return v;
}
};
代码是否以正确的答案解决问题并不重要,至少对于我将要讨论的内容而言。事实是我对这段代码有时间限制(这意味着它在某些测试用例中执行了超过 2 秒)。不知何故,因为这里的复杂性是 O(T * length.size() ^ 2),如果我们考虑到问题约束,它变成 2 * 10 8 。然而,有趣的是我测试了我的解决方案,特别是针对时间限制。对于我的解决方案,我使用的情况似乎是“最坏情况”:长度为 50 1s,T = 80000。代码运行了 0.75 秒。这远远低于 2 秒的时间限制。
我说我使用的情况是最坏的情况,因为将执行的指令数量仅取决于内部 for 中的分支条件 idx >= 0。如果这是真的,将再执行一个循环(该循环的复杂度为 O(n))。在另一种情况下,只会执行一个操作 O(1)。正如你所看到的,元素长度越少,这种情况就越多。
尽管有这种推理,但在对以下案例进行测试后,我的问题还是失败了:
length = {1, 1, 1, 1, 3, 3, 3, 3, 1, 3, 3, 2, 3, 2, 3, 3, 1, 2, 3, 1, 2, 3, 2,
1, 3, 1, 1, 1, 2, 3, 2, 3, 2, 2, 1, 3, 1, 1, 3, 1, 3, 1, 3, 2, 3, 1,
1, 3, 2, 76393} T= 77297.
For this case my program runs for 5.204000 seconds.
我的第一个假设是运行时测量的这种意外比率的原因(只要我们应该期望在第一种情况下要执行的处理器指令要少得多)是处理器以某种方式缓存了类似的计算:在我的示例中,计算关于长度的所有元素都是对称的,真正聪明的处理器可以使用它来避免重复相同的指令序列。所以我尝试编写另一个示例:这次在长度数组中使用不同的值:
length = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20,
21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 77943}
T=80000 runs for 0.813000 seconds.
在这个示例之后,我不再能够说出这些时间测量的原因 - 我的第二个示例似乎需要比我失败的测试更多的处理器指令,并且不允许我认为在第一个示例中发生的缓存。实际上我无法定义这种行为的原因,但我很确定它应该与处理器缓存或传送带有关。我非常好奇这些实验在不同芯片组上的表现如何,因此请随时在此处发表评论。
此外,如果有任何人比我更了解硬件并且他/她可以解释这种行为,我们将不胜感激。
在那之前,我应该为自己做一个注释——在估计算法复杂度时不要低估处理器优化。有时,它们似乎会显着降低/增加特定示例的摊销速度。