如何愚弄“尝试一些测试用例”启发式算法:看起来正确但实际上不正确的算法


105

为了尝试测试某个问题的算法是否正确,通常的出发点是尝试在多个简单的测试案例上手动运行该算法-在一些示例问题实例(包括一些简单的“拐角案例”)上进行尝试”。这是一种很好的启发式方法:这是一种快速清除算法中许多错误尝试并了解为什么该算法无效的好方法。

但是,在学习算法时,有些学生很想停在这里:如果他们的算法在一些示例上都能正常工作,包括他们认为可以尝试的所有极端情况,那么他们得出的结论是算法必须正确。总是有一个学生问:“如果我可以在几个测试用例上尝试一下,为什么我需要证明我的算法正确?”

那么,如何欺骗“尝试一堆测试用例”启发式方法?我正在寻找一些很好的例子来表明这种启发式方法是不够的。换句话说,我正在寻找一个或多个算法的示例,这些示例从表面上看可能是正确的,并且在任何人都可能想出的所有小输入上输出正确答案,但是该算法实际上在哪里不起作用。也许该算法恰好在所有小输入上都能正常工作,仅对大输入失败,或者仅对具有异常模式的输入失败。

具体来说,我在寻找:

  1. 一种算法。该缺陷必须在算法级别上。我不是在寻找实现错误。(例如,该示例至少应与语言无关,并且该缺陷应与算法问题有关,而不是与软件工程或实现问题有关。)

  2. 有人可能会提出的一种算法。伪代码应该看起来至少看起来是正确的(例如,混淆或明显可疑的代码不是一个好例子)。如果这是某些学生在尝试解决作业或考试问题时真正想到的一种算法,则可加分。

  3. 通过概率较高的合理手动测试策略的算法。手动尝试一些小型测试用例的人应该不太可能发现该缺陷。例如,“手动模拟十二个小型测试用例的QuickCheck”应该不太可能揭示该算法不正确。

  4. 优选地,确定性算法。我已经看到许多学生认为“手动尝试一些测试用例”是检查确定性算法是否正确的合理方法,但是我怀疑大多数学生不会认为尝试几个测试用例是验证概率的好方法算法。对于概率算法,通常没有办法判断任何特定输出是否正确。而且您手头不足以对输出分布进行任何有用的统计检验。因此,我宁愿专注于确定性算法,因为它们更清楚地了解了学生的误解。

我想教导证明算法正确的重要性,并且希望使用一些这样的示例来帮助激发正确性的证明。我希望那些相对简单并且可以被大学生使用的例子;需要重型机械或大量数学/算法背景的示例不太有用。另外,我也不想使用“非自然”的算法;虽然构造一些怪异的人工算法来欺骗启发式方法可能很容易,但是如果看起来很不自然,或者构造了明显的后门只是为了欺骗这种启发式方法,那么它可能不会使学生信服。有什么好的例子吗?


2
我喜欢您的问题,这也与我前几天在数学上看到的一个非常有趣的问题有关,该问题与用大常数反驳猜想有关。您可以在这里
ZeroUltimax 2014年

1
经过进一步的挖掘,我发现了两种几何算法。
ZeroUltimax 2014年

@ZeroUltimax是的,不保证任何3个非共线点的中心点都在内部。快速的补救方法是在最左侧和最右侧之间的线上获得一个pt。还有问题吗?
通知2014年

对于我来说,这个问题的前提似乎有点奇怪,我很难理解,但是我认为这归结为所描述的算法设计过程从根本上来说是一个问题。即使对于那些不“停在那里”的学生来说,这也注定了。1>编写算法,2>考虑/运行测试用例,3a>停止或3b>证明正确。第一步几乎被识别输入类的问题域。极端情况和算法本身由此产生。(续)
Mindor先生2014年

1
您如何正式将实现错误与有缺陷的算法区分开来?我对您的问题很感兴趣,但与此同时,我对您所描述的情况似乎更多是规则而不是例外感到不安。许多人确实测试了他们实现的东西,但他们通常仍然存在错误。最受欢迎的答案的第二个例子就是这样的错误。
babou 2014年

Answers:


70

我认为一个常见的错误是使用贪婪算法,这并不总是正确的方法,但在大多数测试案例中都可以使用。

例如:硬币面额,和若干,表达作为的总和:■利用尽可能少的硬币越好。 n n d id1,,dknndi

天真的方法是先使用最大可能的硬币,然后贪婪地产生这样的金额。

例如,对于值的硬币,和 将给予贪婪正确答案为之间的所有数字和 除了编号。5 1 1 14 10 = 6 + 1 + 1 + 1 + 1 = 5 + 565111410=6+1+1+1+1=5+5


10
这确实是一个很好的例子,特别是学生经常犯错误。为了看到算法失败,不仅需要选择特定的硬币组,而且还需要选择特定的值。
拉斐尔

2
另外,我想说的是,在这个例子中,学生也经常会得到错误的证明(炫耀一些幼稚的论点,而这些幼稚的论点在仔细检查中会失败),因此在这里可以学到很多。
拉斐尔

2
老式的英国硬币系统(在1971年十进制之前)有一个很好的例子。一种计算四个先令的贪婪算法将使用半冠冕(2.5先令),一先令硬币和六便士(1/2先令)。但是最佳解决方案使用两个弗罗林(每个先令2先令)。
Mark Dominus

1
确实,在很多情况下,贪婪算法似乎是合理的,但不起作用-另一个示例是最大二分匹配。另一方面,在某些示例中,贪婪算法似乎不起作用,但它确实起作用:最大生成树。
jkff

62

我立即想起了R. Backhouse的一个例子(这可能已经写在他的书中)。显然,他分配了一个编程作业,学生必须编写一个Pascal程序来测试两个字符串的相等性。一个学生上交的程序之一是:

issame := (string1.length = string2.length);

if issame then
  for i := 1 to string1.length do
    issame := string1.char[i] = string2.char[i];

write(issame);

现在,我们可以使用以下输入来测试程序:

“大学”“大学” 真;好

“课程”“课程” 是;好

“”“” 是;好

“大学”“课程” 错误;好

“讲座”“课程” 错误;好

“精确”“精确” 错误,可以

所有这些似乎都很有希望:也许程序确实可以工作。但是,使用“ pure”和“ true”进行更仔细的测试会发现错误的输出。实际上,如果字符串具有相同的长度和相同的最后一个字符,则程序会说“ True”!

但是,测试非常彻底:我们使用了长度不同的字符串,长度相等但内容不同的字符串,甚至是相等的字符串。此外,学生甚至测试并执行了每个分支。您不能真的认为测试在这里很粗心-鉴于该程序确实非常简单,可能很难找到足够彻底地进行测试的动力和能量。


另一个可爱的例子是二进制搜索。在TAOCP中,Knuth说:“尽管二进制搜索的基本思想相对简单,但细节可能令人吃惊”。显然,Java的二进制搜索实现中的一个错误十年来未被发现。这是一个整数溢出错误,只有在输入足够大的情况下才会显示出来。Bentley在《Programming Pearls》一书中还介绍了二进制搜索实现的棘手细节。

底线:仅通过测试就很难确定二进制搜索算法是正确的。


9
当然,从源头上来看,该缺陷是显而易见的(如果您之前写过类似的东西)。
拉斐尔

3
即使纠正了示例程序中的简单缺陷,字符串也会带来很多有趣的问题!字符串反转是经典的-做到这一点的“基本”方法是简单地反转字节。然后编码开始起作用。然后代孕(通常两次)。问题是,当然,没有简单的方法可以正式证明您的方法是正确的。
2014年

6
也许我完全误解的问题,但是这似乎是在一大败笔实现,而不是在一个缺陷算法本身。
Mindor先生2014年

8
@ Mr.Mindor:您如何分辨程序员是否写下了正确的算法然后错误地实现了它,还是写下了错误的算法然后如实地实现了(我犹豫要说“正确”!)
Steve Jessop

1
@wabbit这值得商bat。对您来说显而易见的事情对于一年级学生而言可能并不明显。
Juho

30

我遇到的最好的例子是素数测试:

输入:自然数p,p!= 2
输出:pa prime是否?
算法:计算2 **(p-1)mod p。如果结果= 1,则p是质数,否则p不是质数。

除了很少的反例外,这几乎适用于每个数字,而且实际上需要一台机器在现实的时间内找到反例。第一个反例是341,反例的密度实际上随着p的增加而减小,尽管大约是对数的。

不仅可以使用2作为幂的基础,还可以通过使用其他增加的小质数作为基础来改进算法,以防先前的质数返回1。此外,该方案还有反例,即Carmichael数,虽然很少见


Fermat素数检验是一个概率检验,因此您的后置条件不正确。
Femaref 2014年

5
的确,这是一个概率测试,但答案很好地显示了(更普遍的)概率算法被误认为是精确算法是错误的根源。关于Carmichael号码的
vzn 2014年

2
这是一个很好的示例,但有一个局限性:对于我所熟悉的素数测试的实际使用,即非对称密码密钥生成,我们使用概率算法!这些数字太大,无法进行精确测试(如果不是,那么它们将不适合加密,因为可以在现实时间内通过蛮力找到密钥)。
吉尔斯2014年

1
正是由于这些原因,您提到的限制才是实际的,而不是理论上的,并且密码系统(例如RSA)中的主要测试会遭受罕见/极不可能发生的故障,再次强调了示例的重要性。即在实践中,有时这种限制是不可避免的。存在用于素数测试的P时间算法,例如AKS,但是对于实践中使用的“较小”数字来说,它们花费的时间太长。
vzn

如果您不仅用2 p进行测试,而且用 p对50个不同的随机值2≤a <p进行测试,那么大多数人会知道它是概率性的,但是失败的可能性很小,以致您的计算机出现故障的可能性更大错误的答案。对于2 p,3 p,5 p和7 p,故障已经非常罕见。
gnasher729

21

这是谷歌代表按照我参加的一个惯例向我扔的东西。它是用C编码的,但是可以在使用引用的其他语言中使用。很抱歉,必须在[cs.se]上进行编码,但这是唯一的说明。

swap(int& X, int& Y){
    X := X ^ Y
    Y := X ^ Y
    X := X ^ Y
}

即使给定x和y的任何值,该算法也可以使用。但是,如果将其称为swap(x,x),它将无法正常工作。在这种情况下,x最终为0。现在,这可能不满足您的需要,因为您可以以某种方式证明该操作在数学上是正确的,但仍然会忽略这种边缘情况。


1
在欠缺的C竞赛中使用了这个技巧来产生有缺陷的RC4实现。再次阅读该文章,我只是注意到此黑客可能是@DW提交的
CodesInChaos

7
这个缺陷确实很细微-但是,该缺陷是特定于语言的,因此,它并不是算法中的真正缺陷。这是实施中的缺陷。可以提出其他一些语言古怪的例子,这些例子可以很容易地掩盖细微的缺陷,但这并不是我真正想要的(我正在寻找算法抽象级别的东西)。无论如何,此缺陷都不是证明价值的理想证明;除非您已经考虑过使用别名,否则当您写出正确性的“证明”时,您可能最终会忽略相同的问题。
DW

这就是为什么我对此获得如此高的投票感到惊讶的原因。
ZeroUltimax 2014年

2
@DW取决于如何定义算法的模型。如果下降到显式的内存引用级别(而不是假定没有共享的通用模型),这就是算法缺陷。该缺陷并不是真正针对特定语言的,它会以任何支持共享内存引用的语言出现。
吉尔斯2014年

16

一整类算法本来就很难测试:伪随机数生成器。您不能测试单个输出,而必须使用统计手段调查(许多)输出系列。根据测试的内容和方式,您很可能会错过非随机特征。

兰杜(RANDU)是一个著名的例子,它发生了严重错误。它通过了当时可用的审查-无法考虑后续输出的元组的行为。三元组已经显示出许多结构:

基本上,测试并不能涵盖所有用例:尽管RANDU的一维使用(可能多数情况下)很好,但它不支持使用它来采样三维点(以这种方式)。

正确的伪随机采样是一项棘手的事情。幸运的是,那里现在有功能强大的测试套件,例如dieharder,专门用于将我们知道的所有统计信息扔给拟议的生成器。够了吗

公平地说,我不知道您可以为PRNG证明什么。


2
一个很好的例子,但是实际上通常没有办法证明任何PRNG没有缺陷,只有无限的弱测试和强测试层次。实际上,从任何严格的意义上证明一个人是“随机的”大概是无法决定的(虽然已经证明了这一点)。
vzn 2014年

1
这是一个很难测试的好主意,但是RNG也很难证明。PRNG不太容易实现错误,甚至不能正确指定。像diehard这样的测试对于某些用途来说是好的,但是对于加密货币,您可以通过diehard并仍然被嘲笑。没有“经过验证的安全” CSPRNG,您最好的办法是证明如果CSPRNG损坏了,那么AES也是如此。
吉尔斯2014年

@Gilles我不是想进入加密货币领域,只是统计上的随机性(我认为两者有很多正交的要求)。我应该在答案中说清楚吗?
拉斐尔

1
加密随机性暗含统计随机性。据我所知,除了信息理论随机性的理想概念(并与在确定性图灵机上实现的PRNG概念相矛盾)之外,两者都没有数学上的形式定义。统计随机性是否具有“必须独立于我们将对其进行测试的分布”之外的正式定义?
吉尔斯2014年

1
@vzn:可以用许多可能的方式定义随机数字序列的含义,但是一个简单的方法就是“大的Komolgorov复杂度”。在这种情况下,很容易表明确定随机性是不确定的。
科迪2014年

9

二维局部最大值

n×nA

(i,j)A[i,j]

A[i,j+1],A[i,j1],A[i1,j],A[i+1,j]A

0134323125014013

那么每个加粗的单元格都是局部最大值。每个非空数组至少具有一个局部最大值。

O(n2)

AXXA(i,j)X(i,j)(i,j)

AXAX(i,j)A

AA

(i,j)AA(i,j)

该算法在递归调用自身n2×n2A(i,j)

T(n)n×nT(n)=T(n/2)+O(n)T(n)=O(n)

因此,我们证明了以下定理:

O(n)n×n

还是我们?


T(n)=O(nlogn)T(n)=T(n/2)+O(n)

2
这是一个美丽的例子!我喜欢它。谢谢。(我终于弄清楚了该算法的缺陷。从时间戳上可以了解我花了多长时间的下限。我太尴尬了,无法透露实际时间。:-)
DW

1
O(n)

8

这些是素数示例,因为它们很常见。

(1)SymPy中的原始性。 发行1789年。在著名的网站上进行了错误的测试,直到10 ^ 14之后才失败。尽管此修复程序是正确的,但它只是修补漏洞而不是重新考虑问题。

(2)Perl中的原始性6. Perl6添加了is-prime,它使用了许多具有固定基数的MR测试。有一些已知的反例,但是由于默认的测试数量很大(因此,通过降低性能来掩盖真正的问题),它们的数量很大。这将很快解决。

(3)FLINT中的原始性。 自固定以来,n_isprime()对于复合材料返回true。基本上与SymPy相同。使用SPRP-2伪素数的Feitsma / Galway数据库到2 ^ 64,我们现在可以测试它们。

(4)Perl的Math :: Primality。 is_aks_prime损坏。此序列似乎与许多AKS实现类似-许多代码要么偶然工作(例如,在第1步中丢失了,最终通过尝试划分完成了整个工作),要么对于较大的示例却不起作用。不幸的是,AKS太慢了,很难测试。

(5)Pari的2.2之前版本为is_prime。 Math :: Pari票。它使用10个随机基进行MR测试(启动时具有固定种子,而不是每次调用时都具有GMP的固定种子)。它将告诉您,每1M个呼叫中有9个是质数。如果选择正确的数字,您可能会使其经常失败,但是数字变得稀疏,因此在实践中不会显示太多。此后,他们改变了算法和API。

没错,但这是概率测试的经典之作:例如,您给出mpz_probab_prime_p个回合?如果我们给它进行5次回合,那肯定看起来效果很好-数字必须通过210费马基础测试,然后再通过5个预选的Miller-Rabin基础测试。在3892757297131(使用GMP 5.0.1或6.0.0a)之前,您将找不到反例,因此您必须进行大量测试才能找到它。但是在2 ^ 64下有成千上万的反例。因此,您一直在增加数字。多远?有对手吗?正确答案有多重要?您是否将随机基准与固定基准混淆?您知道您将得到什么输入大小吗?

1016

这些很难正确测试。我的策略包括明显的单元测试,边缘情况,在其他软件包之前或在其他软件包中看到的失败示例,可能的情况下与已知数据库的测试(例如,如果执行单个base-2 MR测试,则可以减少计算上的不可行测试2 ^ 64个数字以测试大约3200万个数字),最后,许多随机测试使用另一个软件包作为标准。最后一点适用于诸如素数之类的函数,其中有一个相当简单的输入和一个已知的输出,但是很多任务是这样的。我用它来查找我自己的开发代码中的缺陷以及比较包中的偶然性问题。但是鉴于无限的输入空间,我们无法测试所有内容。

至于证明正确性,这是另一个素数示例。BLS75方法和ECPP具有素证的概念。基本上,在他们忙于进行搜索以找到适用于其证明的值之后,他们可以以已知格式输出它们。然后,可以编写一个验证程序,或由其他人编写。与创建相比,它们运行起来非常快,现在(1)这两段代码都不正确(因此,为什么您更喜欢其他程序员作为验证者),或者(2)证明思想背后的数学错误。#2总是可能的,但是这些通常是由多个人发布和审阅的(在某些情况下,很容易让您逐步了解自己)。

相比之下,AKS,APR-CL,试验划分或确定性Rabin检验之类的方法除“素数”或“复合”外均不产生任何输出。在后一种情况下,我们可能有一个可以验证的因子,但是在前一种情况下,我们只剩下这一点输出了。程序运行正常吗?不知道。

重要的是,不仅要在几个玩具示例上测试软件,还要在算法的每个步骤中都经过一些示例,然后说:“给出此输入,我处于这种状态是否有意义?”


1
其中许多看起来像是(1)实现错误(底层算法是正确的,但未正确实现),这很有趣,但不是这个问题的重点,或者(2)故意的,有意识的选择来选择一些速度很快,并且大多数情况下都可以运行,但是失败的可能性很小(对于使用一个随机基数或几个固定/随机基数进行测试的代码,我希望选择这样做的人知道他们正在进行性能折衷)。
DW

您的观点是正确的-正确的算法+错误不是重点,尽管讨论和其他示例也将它们混为一谈。这个领域已经成熟,猜想适用于少量数字,但不正确。对于第(2)点,某些情况是正确的,但我的示例#1和#3并不是这种情况-认为算法是正确的(这5个基数给出了10 ^ 16以下数字的可靠结果),随后发现事实并非如此。
DanaJ

这不是伪素数测试的基本问题吗?
asmeurer 2014年

asmeurer,是的,在我的第二篇以及随后的讨论中。但是#1和#3都是使用具有已知碱基的Miller-Rabin给出低于阈值的确定性正确结果的情况。因此,在这种情况下,“算法”(宽松地使用术语来匹配OP)是不正确的。#4并不是一个可能的主要测试,但是正如DW所指出的那样,该算法可以正常工作,这只是实现上的困难。我将其包括在内是因为它会导致类似的情况:需要进行测试,在您说它可行之前,您除了简单的示例还走了多远?
DanaJ 2014年

您的某些帖子似乎适合该问题,而有些则不适合(参见@DW的评论)。请删除无法回答问题的示例(和其他内容)。
拉斐尔

7

Fisher-Yates-Knuth混排算法是一个(实际的)示例,本网站的一位作者对此发表了评论

该算法生成给定数组的随机排列,如下所示:

 // To shuffle an array a of n elements (indices 0..n-1):
  for i from n − 1 downto 1 do
       j ← random integer with 0 ≤ j ≤ i
       exchange a[j] and a[i]

ij0ji

“天真”算法可以是:

 // To shuffle an array a of n elements (indices 0..n-1):
  for i from n − 1 downto 1 do
       j ← random integer with 0 ≤ j ≤ n-1
       exchange a[j] and a[i]

从所有可用元素中选择要交换的元素。但是,这会产生排列偏差的采样(有些表示过多等)。

实际上,可以使用简单的(或幼稚的)计数分析来提出费舍尔-叶茨-努斯混洗。

nn!=n×n1×n2..nn1

验证混洗算法是否正确(有偏差)的主要问题是由于统计原因,需要大量样本。我在上面链接的关于编码恐怖的文章准确地解释了这一点(以及实际测试)。


1
参见此处,了解随机播放算法的示例正确性证明。
拉斐尔

5

我见过的最好的例子(读:我最受伤害的事情)与collat​​z猜想有关。我当时参加了编程比赛(第一名获得500美元奖金),其中一个问题是找出两个数字达到相同数字所需的最小步数。当然,解决方案是交替使用每个步骤,直到它们都达到以前看到的效果为止。我们给了一个数字范围(我认为它在1到1000000之间),并告诉我们collat​​z猜想已经得到验证,直到2 ^ 64,所以我们得到的所有数字最终都收敛于1。我使用32位整数,但是要执行这些步骤。事实证明,在1到1000000(约17万个)之间存在一个晦涩的数字,这将导致32位整数在适当的时间溢出。实际上,这些数字在2 ^ 31以下非常罕见。我们测试了系统中的巨大数字(大于1000000),以“确保”不会发生溢出。结果是我们只是未测试的一个较小的数字导致了溢出。因为我用“ int”代替“ long”,所以我只获得了300美元的奖金,而不是500美元的奖金。


5

背包0/1的问题是,几乎所有的学生认为是贪婪算法解决的。如果您先前曾在贪婪算法起作用的Knapsack问题版本中展示过一些贪婪解决方案,则这种情况会更经常发生。

对于这些问题,我应该在课堂上展示Knapsack 0/1(动态编程)的证明,以消除任何疑问,以及贪婪的问题版本。实际上,这两种证明都不是简单的,学生可能会发现它们很有帮助。此外,在CLRS 3ed第16章第425-427页中对此进行了评论。

问题: 小偷抢商店,可以将最大重量的W放入背包。有n个项目,第i个项目重wi,价值vi。小偷应该带些什么?最大化他的收益

背包0/1问题:设置相同,但是物品可能不会分成较小的部分,因此小偷可能会决定拿走或离开它(二元选择),但可能不会拿走一小部分。

您可以从学生那里得到一些想法或算法,这些想法或算法遵循与贪婪版本问题相同的想法,即:

  • 计算袋子的总容量,并尽可能多地放置最有价值的对象,然后重复此方法,直到不能再放置更多对象为止,因为袋子已经满了,或者没有重量相等的东西放在袋子内。
  • 另一种错误的方式是思考:放置较轻的物品,然后按照最高到最低的价格放置这些物品。
  • ...

对您有帮助吗?实际上,我们知道硬币问题是背包问题的版本。但是,在背包问题的森林中还有更多示例,例如,关于背包2D的情况(当您想用砍伐的木材制作家具时非常有用,我在我所在城市的本地人那里看到过),这是非常普遍的,贪婪也在这里起作用,但不是。


贪婪已被接受,但特别是背包问题很适合设置一些陷阱。
拉斐尔

3

一个常见的错误是错误地实施改组算法。参见有关维基百科的讨论。

n!nn(n1)n


1
这是一个很好的错误,但不是愚弄测试用例启发式的一个很好的例子,因为测试实际上并不适用于混洗算法(它是随机的,因此您将如何对其进行测试?使测试用例失败意味着什么,以及您怎么看输出结果呢?)
DW

您当然可以对其进行统计测试。均匀的随机性远非“输出中可能发生的任何事情”。如果说一个模拟骰子的程序连续给您100 3个,您会不会感到怀疑?
Per Alexandersson

再一次,我说的是学生的“尝试手工测试案例”的启发式方法。我已经看到很多学生认为这是检查确定性算法是否正确的合理方法,但是我怀疑他们不会认为这是测试混洗算法是否正确的好方法(由于混洗算法是随机的,因此无法判断任何特定输出是否正确;在任何情况下,您都无法手动编写足够多的示例来进行任何有用的统计检验)。因此,我并不认为改组算法将有助于消除常见的误解。
DW

1
@PerAlexandersson:即使您仅生成一个随机播放,使用n> 2080的MT也不是真正随机的。现在与期望值的偏差将很小,因此您可能不在乎...但这适用于即使您所产生的收益远少于期间(正如asmeurer指出的那样)。
2014年

2
Nikos M.更为详尽的答案似乎已使这个答案过时了?
拉斐尔

2

将统计功能引入标准库的Pythons PEP450可能很有趣。作为拥有在Python标准库中计算方差的函数的理由的一部分,作者Steven D'Aprano写道:

def variance(data):
        # Use the Computational Formula for Variance.
        n = len(data)
        ss = sum(x**2 for x in data) - (sum(data)**2)/n
        return ss/(n-1)

通过临时测试,以上内容似乎是正确的:

>>> data = [1, 2, 4, 5, 8]
>>> variance(data)
  7.5

但是,向每个数据点添加常量不应更改方差:

>>> data = [x+1e12 for x in data]
>>> variance(data)
  0.0

并且方差永远不能为负:

>>> variance(data*100)
  -1239429440.1282566

问题是关于数字以及精度如何丢失。如果要获得最大的精度,则必须以某种方式对操作进行排序。天真的实现会导致不正确的结果,因为不精确度太大。那是我在大学的数字课程所要解决的问题之一。


1
n1

2
@Raphael:虽然公平地说,但是对于浮点数据,选择的算法众所周知是一个较差的选择。

2
这不仅仅是关于数字的运算的实现以及精度的损失方式。如果要获得最大的精度,则必须以某种方式对操作进行排序。那是我在大学的数字课程所要解决的问题之一。
基督教徒

除了Raphael的准确评论外,此示例的缺点是我认为正确性的证明不会帮助避免这种缺陷。如果您不了解浮点运算的精妙之处,则可能会认为您已经证明了这一点是正确的(通过证明该公式有效)。因此,这不是一个理想的例子来教学生为什么证明自己的算法正确很重要。如果学生看到了这个例子,我的怀疑是他们会改课“浮点数/数值计算的东西很棘手”。
DW

1

尽管这可能与您追求的不完全相同,但毫无疑问,它很容易理解和测试一些小情况,而无需任何其他思考就会导致算法错误。

nn2+n+410<dd divides n2+n+41d<n2+n+41

拟议解决方案

int f(int n) {
   return 1;
}

n=0,1,2,,39n=40

这种“尝试一些小情况并从结果中推论算法”的方法在编程比赛中经常出现(尽管不像这里那样极端),其中压力是想出一种算法,该算法必须(a)易于实现,并且(b )具有快速的运行时间。


5
我不认为这是一个很好的例子,因为很少有人会尝试通过返回1.找到一个多项式的除数
布莱恩小号

1
nn3n

在某种意义上说,这可能是相关的,因为返回除数的常数值(或其他计算)可能是对问题(例如统计问题或未处理算法边缘情况)的错误算法方法的结果。然而,答案需要改写
尼科斯·M.

@NikosM。嘿。我觉得自己在打败一匹老马,但是问题的第二段说:“如果他们的算法在少数示例中都能正常工作,包括他们可以考虑尝试的所有极端情况,那么他们得出的结论是,算法必须是正确的。总是有一个学生问:“如果我可以在几个测试用例上尝试一下,为什么我要证明我的算法正确?”在这种情况下,对于前40个值(远远超过一个学生)可能会尝试),返回1是正确的。在我看来,这就是OP所寻找的东西
Rick Decker 2014年

好的,是的,但是这句话是微不足道的(也许通常是正确的),但不符合问题的实质。仍然需要重新措辞
Nikos M.
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.