什么是循环不变式?


268

我正在阅读CLRS的“算法简介”。在第二章中,作者提到了“循环不变式”。什么是循环不变式?




万一有人想根据循环不变性的概念解决实际的算法编码问题,请在HackerRank上参考问题。他们还提到插入排序问题只是为了详细说明概念。
RBT

也可以参考此处的注释以进行理论理解。
RBT

Answers:


345

简而言之,循环不变式是某个谓词(条件),对于循环的每次迭代都适用。例如,让我们看一个简单的for循环,如下所示:

int j = 9;
for(int i=0; i<10; i++)  
  j--;

在此示例中,(对于每次迭代)都是正确的i + j == 9。一个较弱的不变式也是如此 i >= 0 && i <= 10


29
这是一个很好的例子。很多时候,当我听到讲师描述循环不变式时,它简直就是“循环条件”或类似的东西。您的示例表明不变性可能更多。
布莱恩S'7

77
我看不到一个很好的例子,因为循环不变性应该是循环的目标... CLRS使用它来证明排序算法的正确性。对于插入排序,假设循环使用i进行迭代,则在每个循环的末尾,对数组进行排序,直到第i个元素为止。
冲突

5
是的,这个例子没有错,但还不够。我支持@Clash,因为循环不变应该代表目标,而不仅仅是目标本身。
杰克

7
@Tomas Petricek-当循环终止时,i = 10,j = -1;因此,您给出的较弱的不变示例可能不正确(?)
Raja 2012年

7
尽管我同意上面的评论,但我对这个答案表示赞同,因为...此处未定义目标。定义任何适合的目标,这个例子很好。
Flavius

119

我喜欢这个非常简单的定义:(来源

循环不变性是一个条件[在程序变量中],在循环的每次迭代之前和之后立即必须为真。(请注意,这在迭代过程中并没有说明其真实性或虚假性。)

就其本身而言,循环不变性并没有太大作用。但是,给定适当的不变式,可以将其用于帮助证明算法的正确性。CLRS中的简单示例可能与排序有关。例如,让您的循环不变式类似于在循环开始i时对该数组的第一个条目进行排序。如果可以证明这确实是一个循环不变式(即在每次循环迭代之前和之后都成立),则可以使用它来证明排序算法的正确性:在循环结束时,仍然满足循环不变式,而计数器i是数组的长度。因此,对第一个i条目进行排序意味着对整个数组进行排序。

一个甚至更简单的示例:循环不变式,正确性和程序派生

我了解循环不变性的方式是作为系统的,正式的程序推理工具。我们只发表一个专注于证明正确的陈述,我们称其为循环不变式。这组织了我们的逻辑。尽管我们也可以就某种算法的正确性进行非正式的辩论,但使用循环不变性会迫使我们非常仔细地思考,并确保我们的推理是密封的。


10
应该指出的是,“在每次迭代之后立即”包括在循环终止之后-无论循环如何终止。
罗伯特·S·巴恩斯

非常感谢您的回答!最大的收获是使该循环不变的目的是帮助证明算法的正确性。其他答案仅关注什么是循环不变式!
Neekey

39

有很多人在处理循环和不变式时没有立即意识到的一件事。他们在循环不变式和条件循环(控制循环终止的条件)之间感到困惑。

人们指出,循环不变性必须为真

  1. 在循环开始之前
  2. 在循环的每次迭代之前
  3. 循环终止后

(尽管在循环体中它可能暂时为false)。 另一方面,循环条件 在循环终止后必须为false,否则循环将永远不会终止。

因此,循环不变性和循环条件必须是不同的条件。

二进制循环是复杂循环不变性的一个很好的例子。

bsearch(type A[], type a) {
start = 1, end = length(A)

    while ( start <= end ) {
        mid = floor(start + end / 2)

        if ( A[mid] == a ) return mid
        if ( A[mid] > a ) end = mid - 1
        if ( A[mid] < a ) start = mid + 1

    }
    return -1

}

因此,循环条件似乎很简单-当start> end时,循环终止。但是为什么循环正确?证明其正确性的循环不变式是什么?

不变的是逻辑语句:

if ( A[mid] == a ) then ( start <= mid <= end )

该语句是逻辑重言式- 在我们试图证明的特定循环/算法的上下文中,它始终是正确。并且它提供了有关循环终止后正确性的有用信息。

如果返回是因为我们在数组中找到了元素,那么该语句显然是正确的,因为if A[mid] == athen a在数组中,并且mid必须在开始和结束之间。如果循环终止,因为start > end再不可能有数量,使得start <= mid mid <= end,因此我们知道这种说法A[mid] == a肯定是假的。但是,因此,总体逻辑语句在null方面仍然是正确的。(在逻辑上,if(false)then(something)语句始终为true。)

现在,当循环终止时,我对循环条件一定为假的看法又如何呢?看起来当在数组中找到元素时,循环终止时循环条件为真!实际上不是,因为隐含的循环条件确实存在,while ( A[mid] != a && start <= end )但是由于隐含了第一部分,因此我们缩短了实际测试的时间。无论循环如何终止,此条件在循环后显然为假。


可以肯定的是,将逻辑语句用作循环不变式,因为无论条件如何,所有逻辑语句都可以始终为真。
激进分子

事实并非如此奇怪我想,既然没有保证,a存在于A。非正式地这将是“如果该键a存在于阵列中,它必须之间发生startend包容性的”。那么它遵循如果A[start..end]是空的,即a不存在于A.
scanny

33

先前的答案已经很好地定义了循环不变式。

以下是CLRS的作者如何使用循环不变性证明插入排序的正确性

插入排序算法(如Book中所述):

INSERTION-SORT(A)
    for j ← 2 to length[A]
        do key ← A[j]
        // Insert A[j] into the sorted sequence A[1..j-1].
        i ← j - 1
        while i > 0 and A[i] > key
            do A[i + 1] ← A[i]
            i ← i - 1
        A[i + 1] ← key

在这种情况下,循环不变式: 始终对子数组[1至j-1]进行排序。

现在让我们检查一下并证明算法正确。

初始化:在第一次迭代之前,j = 2。因此,子数组[1:1]是要测试的数组。由于它只有一个元素,因此被排序。因此,满足不变性。

维护:可以通过在每次迭代后检查不变量来轻松地验证这一点。在这种情况下很满意。

终止这是我们将证明算法正确性的步骤。

当循环终止时,j = n + 1的值。再次满足循环不变式。这意味着应该对Sub-array [1至n]进行排序。

这就是我们要对算法进行的处理。因此我们的算法是正确的。


1
同意...终止声明在这里是如此重要。
Gaurav Aradhye,2015年

18

除了所有好的答案之外,我猜Jeff Edmonds的《如何思考算法》中的一个很好的例子 可以很好地说明这一概念:

例1.2.1“ Find-Max两指算法”

1)规范:输入实例由元素列表L(1..n)组成。输出包含一个索引i,以使L(i)具有最大值。如果有多个具有相同值的条目,则返回其中任何一个。

2)基本步骤:确定两指方法。您的右手指顺着列表。

3)进度度量:进度度量是您的右手指沿着列表的距离。

4)循环不变式:循环不变式指出您的左手指指向迄今为止您的右手指遇到的最大条目之一。

5)主要步骤:每次迭代,您都将右手指向下移动列表中的一个条目。如果您的右手指现在指向的入口大于左手指的入口,则将左手指移动到与右手指同在的位置。

6)取得进步:您取得进步是因为您的右手指移动了一个条目。

7)保持循环不变:您知道已经按照以下方式维护了循环不变。对于每个步骤,新的左手指元素为Max(旧的左手指元素,新元素)。根据循环不变性,这是Max(Max(shorter list),new element)。从数学上讲,这是Max(更长的列表)。

8)建立循环不变式:您最初通过将两个手指指向第一个元素来建立循环不变式。

9)退出条件:当右手指完成遍历列表后,您就完成了。

10)结局:最后,我们知道问题已解决如下。通过退出条件,您的右手指已遇到所有条目。通过循环不变性,您的左手指指向这些最大值。返回此条目。

11)终止和运行时间:所需时间是列表长度的恒定倍。

12)特殊情况:检查存在多个具有相同值的条目或n = 0或n = 1时会发生什么。

13)编码和实现细节:...

14)形式证明:算法的正确性来自上述步骤。


我认为这个答案确实将“手指”放在了不变式的直觉上:)。
scanny

6

应该注意的是,当循环变量被认为是表示变量之间重要关系的断言时,循环不变变量可以帮助设计迭代算法,这些关系必须在每次迭代开始时以及循环终止时为真。如果这样的话,那么计算就在有效之路上。如果为假,则算法失败。


5

在这种情况下,不变表示在每次循环迭代中的某个点必须为真的条件。

在合同编程中,不变性是在调用任何公共方法之前和之后必须满足的条件(通过合同)。


4

不变的意义永远不变

这里的循环不变式表示“循环中变量发生的变化(递增或递减)没有改变循环条件,即条件满足”,因此循环不变式的概念已经出现


2

循环不变属性是在循环执行的每个步骤中都保持的条件(例如,对于循环,while循环等)

这对于循环不变证明是必不可少的,在循环证明中,如果在执行的每个步骤中都具有该循环不变属性,则能够证明算法正确执行。

为了使算法正确,循环不变式必须保持在:

初始化(开始)

维护(之后的每一步)

终止(完成时)

这用于评估一堆东西,但是最好的例子是用于加权图遍历的贪婪算法。为了使贪心算法产生最优解(整个图形上的路径),它必须以尽可能低的权重路径达到连接所有节点的目的。

因此,循环不变性是所采用的路径具有最小的权重。在一开始,我们还没有添加任何的边缘,所以这个属性为true(这是不假,在这种情况下)。在每个步骤中,我们都遵循最低的权重边缘(贪婪的步骤),因此我们再次采用最低的权重路径。在结束,我们已经找到了最低加权路径,所以我们的财产也是如此。

如果某个算法没有做到这一点,我们可以证明它不是最优的。


1

很难跟踪循环的情况。在没有达到目标行为的情况下不会终止或终止的循环是计算机编程中的常见问题。循环不变量帮助。循环不变式是关于程序中变量之间关系的正式声明,它在循环运行之前(建立不变式)成立,并在循环的底部(每次循环保持不变)再次成立。 )。这是在代码中使用循环不变式的一般模式:

... //此处循环不变量必须为true
while(TEST CONDITION){
//循环的顶部
...
// 循环的底部
//此处的循环不变量必须为真
}
//终止+循环不变量=目标
...
在循环的顶部和底部之间,可能正在朝着达到循环的目标迈进。这可能会干扰(使错误)不变式。循环不变式的意义在于保证在每次重复循环主体之前都将恢复不变式。这有两个优点:

工作不会以复杂的,依赖数据的方式进行到下一个阶段。每个循环都独立于所有循环通过循环,其中不变的作用是将循环绑定在一起成为一个有效的整体。循环工作的理由简化为每次循环都恢复不变的理由。这将循环的复杂整体行为分解为小的简单步骤,每个步骤都可以单独考虑。循环的测试条件不是不变式的一部分。这就是使循环终止的原因。您分别考虑两件事:为什么循环永远终止,以及为什么循环终止时达到其目标。如果每次通过循环都靠近满足终止条件,则循环将终止。通常很容易确保这一点:例如 将计数器变量逐步递增直到达到固定上限。有时,终止合同背后的推理更为困难。

应该创建循环不变式,以便在达到终止条件且不变式为true时,可以达到目标:

不变量+终止=>目标
需要练习创建简单且相关的不变量,这些变量捕获除终止以外的所有目标达成情况。最好使用数学符号来表示循环不变式,但是当这导致过度复杂的情况时,我们将依靠清晰的散文和常识。


1

抱歉,我没有评论权限。

就像你提到的@Tomas Petricek

一个更弱的不变性也成立,那就是i> = 0 && i <10(因为这是连续条件!)”

循环不变式如何?

我希望我没有错,据我所知[1],循环不变性在循环开始(初始化)时将是正确的,在每次迭代之前和之后(维护)都将是正确的,在循环之后也将是正确的循环的终止(Termination)。但是,在最后一次迭代之后,i变为10。因此,条件i> = 0 && i <10变为假,并终止了循环。它违反了循环不变式的第三个属性(Termination)。

[1] http://www.win.tue.nl/~kbuchin/teaching/JBP030/notebooks/loop-invariants.html


我的猜测是这样,因为循环实际上并没有在这些条件下执行。
muiiu

0

循环不变式是一个数学公式,例如(x=y+1)。在该示例中,xy表示一个循环中的两个变量。考虑到这些变量在整个代码执行过程中的行为变化,几乎不可能测试所有to xyvalue并查看它们是否产生任何错误。可以说x是整数。整数可以在内存中保留32位空间。如果超过该数量,则会发生缓冲区溢出。因此,我们需要确保在代码执行期间,它永远不会超出该空间。为此,我们需要了解一个显示变量之间关系的通用公式。毕竟,我们只是试图了解程序的行为。


0

简而言之,它是一个LOOP条件,在每个循环迭代中都成立:

for(int i=0; i<10; i++)
{ }

这样我们可以说我的状态是 i<10 and i>=0



-1

在线性搜索中(按照书中给出的练习),我们需要在给定数组中找到值V。

它很简单,就是从0 <= k <长度扫描数组并比较每个元素。如果找到V,或者扫描达到数组长度,则终止循环。

根据我对上述问题的理解-

循环不变式(初始化): 在k-1个迭代中找不到V。第一次迭代,这将是-1,因此我们可以说在位置-1找不到V

维护性: 在下一次迭代中,在k-1中找不到的V成立

终止: 如果在k位置找到V或k到达数组的长度,则终止循环。

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.