杂耍算法

来源:互联网 发布:淘宝联盟返利低 编辑:程序博客网 时间:2024/04/30 23:18

关于编程珠玑中习题2.3的一点思考

分类: 编程珠玑 104人阅读 评论(0) 收藏 举报

出处:http://www.cnblogs.com/HappyAngel/archive/2011/01/16/1936905.html

    这两天看到编程珠玑第二章,关于习题2.3中说到杂耍算法执行gcd(i,n)次后即可停止,这里我想了很久为什么?书中提到的Swap Sections解决了我的疑惑,在明白为什么的时候真的 “啊哈”了一下,原来这样,感觉证明非常巧妙,不敢独享,所以复述如下。

problem:

   rotate a one-dimensional vector of n elements left by i positions. For instance, with n=8 and i=3, the vector abcdefgh is rotated to defghabc.

   这个问题可以有三个巧妙的算法来解决(关于这三个算法的代码,我自己实现了一下,附在文后),这在Swap Sections都提到了,包括神奇的反转算法、递归交换算法以及下面这个杂耍算法:

one solution is:

   move x[0] to the temporary t, then move x[i] to x[0], x[2i] to x[i], and so on, until we come back to taking an element from x[0], at which point we instead take the element from t and stop the process.

If that process didn't move all the elements , then we start over at x[1], and continue until we move all the elements.

   这个算法在执行gcd(i,n)次后就停止了,为什么?

先来了解一点数论知识(一下内容摘自《初等数论》,潘承洞著):

1 同余:

  设m不等于0, 若m|a-b,即 a-b=km, 则称m为模,a同余于b模m,以及b是a对模m的剩余。记作 a≡b(mod m)。

2 同余类

  对给定的模m,有且恰有m个不同的模m的同余类,他们是

  0 mod m,1 mod m,…,(m-1)mod m。

3 完全剩余系

  一组数y1,y2,…,ys称为是模m的完全剩余系,如果对任意的a有且仅有一个yj是a对模m的剩余,即a同余于yj模m。由此可见0,1,2,…,m-1是一个完全剩余系。因此,如果m个数是两两不同余的,那么这m个数便是完全剩余系。

 

基于以上知识,我们可以证明这样一个事实,即如果i和n互质的话,那么序列:

0 i mod n 2i mod n 3i mod n …. (n-1)*i mod n

就包括了集合{0,1,2 … n-1}的所有元素。(下一个元素(n)*i mod n 又是0)

为什么?

证明:

   由于0,1,2,…,n-1本身是一个完全剩余系,即它们两两互不同余。设此序列为Xi(0<=i<=n-1),可得下式:

Xi≠Xj (mod n), 注:这里由于不能打出不同余字符因此用不等于替代

由于i与n是互质的,所以 Xi*i ≠i*Xj (mod n),这里由于不能打出不同余字符因此用不等于替代

因此i*Xi是m个互不同余数,那么可断定它们是完全剩余系。得证。

有了上面的结论,那么如果i和n互质,下面的赋值过程便能完成所有值的赋值(设数组为X[0..n-1],长度为n):

   t = X[0]

   X[0] = X[i mod n]

   X[i mod n] = X[2i mod n]

   …….

   X[(n-2)*i mod n] = X[(n-1)*i mod n]

   X[ (n-1)*i mod n] = t。

   因为以上操作已经把包括{0,1,…,n-1}所有元素放到了最终位置上,每次完成一个元素的放置。

   根据以上我们得到了一个中间结论,如果i和n互质,我们就可以一次完成。那么如果i和n不是互质的呢?

   自然的想法是利用我们已经得到的结论,让i和n互质,即让i’ = i/(gcd(i,n)),n’ = n/(gcd(i,n))。这样便构造了一对互质的数, i’和n’。这意味着把整个数组的每g=gcd(i,n)个元素组成块,如下所示:

   这样,根据已得结论,我们可以一次获得最终答案,因为i’和n’互质,由于我们的单位是块元素,所以,必须要g次来完成块的移动,每次相当于把g中的一个元素移到最终位置上。所以总共需要g次移动,算法终止。□

   整个证明过程最巧妙的地方在于对i和n进行处理的时候,以及处理之后转换成块元素的这个地方,感觉很巧妙,这样的证明绝对秒杀什么陪集数目的说法,回味无穷。

三个算法的代码:

[cpp] view plaincopyprint?
  1. /* 
  2.      programming pearls: chapter2 
  3.   
  4.      Answer for Rotating a one-dimensional vector of n elements left by i positions. 
  5.   
  6.      method1: process 杂耍算法 
  7.      method2: process2 交换算法 
  8.      method3: process1 反转算法 
  9.   */  
  10.    
  11.  #include <iostream>  
  12.    
  13.   using namespace std;  
  14.    
  15.   const int num = 10;  
  16.   int a[num] = {0,1,2,3,4,5,6,7,8,9};  
  17.    
  18.   //answer 1  
  19.  /* 
  20.      parameter 1: the vector to be rotated 
  21.      par 2: the num of steps to rotate towards left  
  22.   */  
  23.    
  24.   //assume m and n are not zero  
  25.   int gcd(int m, int n)  
  26.  {  
  27.      while(m != n)  
  28.      {  
  29.          if(m > n)  
  30.              m -= n;  
  31.          else  
  32.              n -= m;  
  33.      }  
  34.      return m;  
  35.  }  
  36.    
  37.   //jungle code, method 1  
  38.   void process(int* a, int i)  
  39.  {  
  40.      int n,m;  
  41.      for(int j=0; j < gcd(i,num); j++)  
  42.      {  
  43.          n = a[j];  
  44.          for(m=j+i; m!=j; m=(m+i)%num)  
  45.          {  
  46.              a[(m-i+num)%num] = a[m];  
  47.          }  
  48.          m = (m-i+num)%num;  
  49.           a[m] = n;  
  50.      }  
  51.  }  
  52.    
  53.    
  54.   void reverse(int m, int n)  
  55.  {  
  56.      int mid = (m+n)/2;  
  57.      int temp;  
  58.   //    int j;  
  59.     
  60.      for(int i=m, j=n; i <= mid; i++,j--)  
  61.      {  
  62.          temp = a[i];  
  63.          a[i] = a[j];  
  64.          a[j] = temp;  
  65.      }  
  66.  }  
  67.    
  68.   //methond 3  反转算法  
  69.   void process1(int* a, int i)  
  70.  {  
  71.      reverse(0,i-1);  
  72.      reverse(i,num-1);  
  73.      reverse(0,num-1);  
  74.  }  
  75.    
  76.    
  77.   //method 2  交换算法  
  78.   //swap a[i..i+m-1] with a[j..j+m-1]  
  79.   void Swap(int*a, int i, int j, int m)  
  80.  {  
  81.      for(int l=0; l < m; l++)  
  82.      {  
  83.          int temp = a[i+l];  
  84.          a[i+l] = a[j+l];  
  85.          a[j+l] = temp;  
  86.      }  
  87.  }  
  88.    
  89.   //n remains the beginning of the rightmost section to be  
  90.   // swapped. use variables i and j to denote the lengths of the  
  91.   // sections still to be swapped.  
  92.   //loop invariant:  
  93.   //    m                n-i                   n                     n+j           p-1   
  94.   //    |already swapped|swap with b[n..n+j-1]|swap with b[n-i..n-1]|already swapped|  
  95.   void process2(int* a, int l)  
  96.  {  
  97.      if(l==0 || l==num)  
  98.      {  
  99.          return;  
  100.      }  
  101.    
  102.      int n = l;  
  103.      int j = num-l;  
  104.      int i = l;  
  105.      while(i != j)//当交换的两边长度相等时终止,最后再将相等的两边交换即可  
  106.       {  
  107.          if(i > j)  
  108.          {  
  109.              Swap(a,n-i,n,j);  
  110.              i -= j;  
  111.          }  
  112.          else if(i < j)  
  113.          {  
  114.              Swap(a, n-i, n+j-i,i);  
  115.              j -= i;  
  116.          }  
  117.      }  
  118.      Swap(a,n-i,n,i);  
  119.  }  
  120.    
  121.   int main()  
  122.  {  
  123.      process2(a,8);  
  124.      //process1(a,9);  
  125.     
  126.      for(int i=0; i < num; i++)  
  127.          cout<<a[i]<<" ";  
  128.      cout<<endl;  
  129.        
  130.      int r;  
  131.      cin>>r;  
  132.      return 0;  
  133.  }  
  134.  

    杂耍算法

    分类: 编程珠玑 119人阅读 评论(0) 收藏 举报

    目录(?)[+]

    出处:http://www.cnblogs.com/solidblog/archive/2012/07/15/2592009.html

    1.前言

    我的第一篇文章:编程珠玑(一):前言 && 位图排序,从发布以来到目前为止已经被浏览了超过一千次。

    有几个朋友进行了回复,都是给予支持和鼓励的。在此,对这些朋友表示感谢!

    学习本来就是一件需要耐心和毅力的事情,各种滋味只有同道中人才能理解。

    同时,学习也是一件能够带给快乐和成就感的事情:

        当你搞懂一个算法、理解一个原理时候,其中的喜悦不亚于盛夏的冷饮,寒冬的暖炉。

     多谢朋友们,我会继续努力的,为了自己也为了对某个人的承诺!

    2.问题描述

              请将一个具有n个元素的一维向量x向左旋转i个位置。例如,假设n=8, i=3,那么向量abcdefgh旋转之后得到向量defghabc。

    3.解决思路

    1. 普通方法
      步骤如下:
      1)设法将向量x中的前i个元素复制到一个临时数组中
      2)将余下的n-i个元素左移i个位置
      3)将前i个元素从临时数组中复制回x中的后面位置
      简单的实现了一下,代码如下:
    [cpp] view plaincopyprint?
    1. View Code   
    2.  #include <stdio.h>  
    3.    
    4.  void Rotate(char *a, int length, int i)  
    5.  {  
    6.      char tmp[10];  
    7.      int step = i % length;  
    8.      if (step == 0)  
    9.      {  
    10.          return;  
    11.      }  
    12.    
    13.      int j = 0;  
    14.    
    15.      for (j = 0; j < step; j++)  
    16.      {  
    17.          tmp[j] = a[j];  
    18.      }  
    19.      tmp[step] = '\0';  
    20.    
    21.      for (j = step; j < length; j++)  
    22.      {  
    23.          a[j -step] = a[j];  
    24.      }  
    25.    
    26.    
    27.      j = 0;  
    28.      while((a[length - step + j] = tmp[j]) != '\0')  
    29.      {  
    30.          j++;  
    31.      }  
    32.  }  
    33.    
    34.  void main()  
    35.  {  
    36.      char a[9] = "abcdefgh";  
    37.      Rotate(a,8,3);  
    38.      printf("%s",a);  
    39.  }  
    1. 不足:
      这个方案使用了i个额外的位置,这使得它太浪费空间了。

    2. 杂耍算法
      先上解决方法,详细分析我们稍后奉上
      步骤如下:

      1)先将x[0]移到临时变量t中
      2)将x[i]移动到x[0]中,x[2i]移动到x[i]中,依次类推
      3)将x中的所有下标都对n取模,直到我们又回到从x[0]中提取元素。不过这时我们从t中提取元素,结束。
      代码如下:
    [cpp] view plaincopyprint?
    1. #include<stdio.h>  
    2.   
    3. //使用辗转相除法求最大公约数  
    4. int gcd(int a, int b)  
    5. {  
    6.     if (a % b == 0)  
    7.     {  
    8.         return b;  
    9.     }  
    10.     else  
    11.     {  
    12.         return gcd(b, a%b);  
    13.     }  
    14. }  
    15.   
    16. //杂耍算法  
    17. void rotate(char* a,int n,int i)  
    18. {  
    19.     int step = gcd(n,i);  
    20.     for(int j = 0; j < step; j++)  
    21.     {  
    22.         int temp = a[j];  
    23.         int current = j;  
    24.         while(1)  
    25.         {  
    26.             int next= (current + i) % n;  
    27.             if(next== j)   
    28.             {  
    29.                 break;  
    30.             }  
    31.             a[current] = a[next];  
    32.             current= next;  
    33.         }  
    34.         a[current] = temp;  
    35.     }  
    36. }  
    37.   
    38. int main()  
    39. {  
    40.     char a[9] = "abcdefgh";  
    41.     rotate(a,8,3);  
    42.   
    43.     printf("%s\n",a);  
    44.     return 0;  
    45. }  

    3、说实话,这个算法理解起来还是有一定的难度的,想要把原理搞懂还是需要下点功夫的。
    下面就让我们一起来分析一下这个算法吧,准备好了吗,我们开始了!

    两个整数的最大公约数是能够同时整除它们的最大的正整数。辗转相除法基于如下原理:

    两个整数的最大公约数等于其中较小的数和两数的差的最大公约数。

    我们来看一下这个原理的证明:

       设两数为a、b(b<a),用gcd(a,b)表示a,b的最大公约数。
    r=a mod b,r为a除以b以后的余数,辗转相除法即是要证明gcd(a,b)=gcd(b,r)。

        证明步骤如下:

    1)令c=gcd(a,b),则设a=mc,b=nc

    2)r = a mod b,所以r = a - k*b = mc - k*nc = (m - kn) * c。即,r = (m - kn) * c

    3)由r = (m - kn) * c 可知:c也是r的因数

    4)可以肯定m - kn与n互质(why?)

       假设他们不互质,必然存在大于1的整数d,使得m-kn = xd, n = yd。那么,m = xd + kyd = (x + ky)d

       那么,a = mc = (x + ky)dc , b = nc = ydc 。=> a,b的最大公约数为dc,而不是c。

    5)既然m - kn与n互质,所以c = gcd(r,b)。

    结论,gcd(a,b)=gcd(b,r)

    (辗转相除法更详细的描述请参照百度百科:http://baike.baidu.com/view/255668.htm。

    5.杂耍算法

    杂耍算法中如下两点我无法理解:

    1.为什么程序要循环执行gcd(i,n)次
    2.这个算法是通过什么样的机制就让所有位置上的元素都移动到了预期的位置

    程序的走向我是明白的,但是核心算法始终不得其解。
    最终还是需要借助网络,搜到了园子内一位朋友写的文章(
    关于编程珠玑中习题2.3的一点思考
    看完以后,有种豁然开朗的感觉,在此多谢这个朋友了。

    不过,那篇文章中有些概念的细节讲的不是太清楚,我也是借助网络才有了更清晰的了解。
    再次,我来总结个精简吧的吧,写的不好还请各位包涵!

    先从几个概念开始:

    1. 同余
      如果两个整数(a,b)除以同一个整数m,如果得到相同的余数k。则称a,b对于模m同余。
       记作a ≡ b (mod m)
    2. 同余类
      所谓同余类是指以某一特定的整数(如m)为模,按照同余的方式对全体整数进行的分类。
      对给定的模m,有且恰有m个不同的模m的同余类。它们是:
         0 mod m,1 mod m,…,(m-1)mod m。
    3. 完全剩余类
      通过2)我们知道,所有的整数以m为模可以划分为m个没有交集的集合。
      从每个集合中取一个整数组成一个集合,则这个集合中的m个整数就不存在同余的整数,这个集合就叫做完全剩余类。

    了解了以上三个概念后,我们能够得出如下的结论:

    如果i和n互质,那么序列:
        0   i mod n     2i mod n    3i mod n    …… (n-1)*i mod n
    就包括了集合{0,1,2,……n-1}的所有元素 

    我们为什么会有这样的结论呢,下面来证明一下

    前提条件

       对于模n来说,序列0,1,2,……,n-1本身就是一个完全剩余类(即他们之间两两互不同余)

    证明步骤

    1)从此序列中任取两个数字xi,xj(0 <= i,j <= n-1),则有Xi≠Xj (mod n),   注:这里由于不能打出不同余字符因此用不等于替代

    2)由于i和n是互质的,所以 xi * i ≠ xj * i(mod n)
        =》这就说明,xi从0开始一直取值到n-1,得到的序列0 * i,1 * i,2 *i,……(n-1)*n就是一个完全剩余类。即集合0,1,2,……n-1}

    有了以上的结论,如果i和n互质,下面的赋值过程便能完成所有位置的值的移动:
        t = X[0]
        X[0] = X[i mod n]
        X[i mod n] = X[2i mod n]
        …….
        X[(n-2)*i mod n] = X[(n-1)*i mod n]
        X[ (n-1)*i mod n] = t
    以上的赋值操作,赋值操作符的两边都得到了一个完全剩余类,也就是说所有的0 ~ n-1的所有位置都被移动过了
    请注意第二个操作,X[0] = X[i mod n]。
        该操作决定了整体的导向,该操作将i mod n位置的值移动到了最开始的位置。
        由于i,2i,……之间的偏移量是相同的,所以整个操作实际上就是讲序列向左移动i个位置(超过了开始位置的接到最右边去)

    那么如果i和n不互质呢?

    自然的想法是利用我们已经得到的结论,让i和n互质,即让i’ = i/(gcd(i,n)),n’ = n/(gcd(i,n))。

    这样便构造了一对互质的数, i’和n’。这意味着把整个数组的每g=gcd(i,n)个元素组成块。

    由于我们的单位是块元素,每次相当于把g中的一个元素移到最终位置上,所以总共需要g次移动。