zoukankan      html  css  js  c++  java
  • [算法导论]#3 循环不变式

    在面试某手的时候,完成了一个有序链表的合并,之后面试官又要求用循环不变式来证明算法的正确性……循环不变式?这是啥

    后来发现这是算法导论第一章的内容。

    不会=算法导论没看

    分析过程

    必须证明三条性质

    • 初始化:循环的第一次迭代之前,它为真
    • 保持:如果循环的某次迭代之前它为真,那么下次迭代之前它仍为真
    • 终止:在循环终止时,不变式为我们提供了一个有用的性质,该性质有助于证明算法是正确的

    前两步有点类似于数学归纳法,而最后一步其实也很重要,因为算法并不是无穷无尽的,必须要终止。例如二分,终止条件非常重要。

    以插入排序为例

    for(i = 2;i <= n;++i){
        key = a[i];
        j = i - 1;
        while(j > 0 && a[j] > key){
            a[j+1] = a[j];
            j--;
        }
        a[j] = key;
    }
    

    在一个有序的数列里插入一个数,插到正确的位置同时比它大的后移,就是这个算法的思想

    那就按照循环不变式这个算法的正确性

    设循环不变式:([1..i]) 在循环中,依然由原先的([1..i])中的元素组成并保持有序

    • 初始化:很明显在循环之前((i<2))第一个数是单独的,肯定有序
    • 保持:这个算法会将比key大的数都往右移动一个位置,然后将key插入到正确的位置中,这时数组1-i是有序的。
    • 终止:循环结束后,(i=n+1)因此保证了1-n是有序的,每个元素都是a中原先的元素,但是有序了。因此算法正确。

    这个证明不是太数学,而是比较感性的,但也可以说明问题

    以二分为例

    之前说过,一个二分算法的正确性,终止条件很重要

    https://www.cnblogs.com/smallocean/p/11913963.html 这是之前做过二分的笔记

    • 在单调递增序列(a)中查找$geq x $的数中最小的一个
      while(l<r){
          int mid = (l+r)>>1;/*右移运算 相当于除2并且向下取整*/
          if(a[mid]>=x) r=mid;
          else l=mid+1;
      }
      return a[l];
    

    设循环不变式:在每次循环中,如果有答案,一定存在于([l…r])

    • 初始:([l..r])就是整个序列的范围
    • 保持:在单调的序列中,选择的(mid)的这个值,(a[mid]geq x)说明(mid)后面一定没有答案,但是(mid)可能是答案,(a[mid]<x)说明(mid)前面包括(mid)一定没有答案。得证
    • 结束:这个就是证明会不会死循环,涉及到选取(mid)的问题,如果此时(l+1=r)(mid)取到的是(l),这就意味着,如果(mid)是正确答案,那么结果返回(l)是正确的。如果(mid)不是正确答案,那么结果就是(mid+1),也是正确的。

    我们可以用结束的这个性质,如果都没有答案,那么最后一定是(l=n-1,r=n),然后返回(n)。因为(mid)永远不可能取到(r),所以我们可以一开始把范围设置成([1...n+1]),很明显这个也是满足那不定式的。

    另外一种情况也类似。

    • 在单调递增序列(a)中查找(leq x)的数中最大的一个
      while(l<r){
          int mid = (l+r+1)>>1;
          if(a[mid]<=x) l=mid;
          else r=mid-1;
      }
      return a[l];
    

    初始和保持和上面的类似。

    • 结束:也是涉及到选择(mid)的问题,如果此时(l+1=r),这时(mid)取到的是(r),这就意味着,如果(mid)是正确答案,返回(l=mid)时正确的,如果不是,那么结果就是(l)也是正确的。

    有一篇总结很好的文章:https://blog.csdn.net/ltyqljhwcm/article/details/52772002

    以KMP为例

     Next[1] = 0;
        for (int i = 2, j = 0; i <= n; ++i) {
            while (j > 0 && a[i] != a[j + 1]) j = Next[j];
            if (a[i] == a[j + 1]) ++j;
            Next[i] = j;
        }
    

    维持这个不变式:Next[i]含义就是模式串以i结尾的子串([1..i]的后缀)与模式串的前缀能匹配的最长长度

    • 初始:(Next[1] = 0),满足
    • 保持:如果匹配,j++,最长长度即等于[1…i-1]匹配的最长长度加一。满足。如果不匹配,那说明后缀找多了,那肯定要减少后缀的长度,即减小到下一个后缀与前缀匹配的位置,即(Next[j])
    • 结束:(i)(n)就结束了

    kmp真正的证明比较复杂,这里只是用可视化的方式帮助理解代码。

    总结

    总的来说循环不变式是一个很好的思想,能帮助我们证明算法的正确性。前两步的类似数学归纳的方法和终止时候的正确性,其实在平时写代码的时候也会不经意间用到类似的方法来想。这次把这种思路书面化,以后也不会再走很多弯路了。

  • 相关阅读:
    第3次实践作业
    第2次实践作业
    第1次实践作业
    2019 SDN大作业
    第05组 Beta版本演示
    个人作业——软件工程实践总结&个人技术博客
    个人作业——软件评测
    Springboot项目部署到云服务器(Ubuntu 18.04)
    结对第二次作业——某次疫情统计可视化的实现
    结对第一次—疫情统计可视化(原型设计)
  • 原文地址:https://www.cnblogs.com/smallocean/p/12602356.html
Copyright © 2011-2022 走看看