本文系转载链接 http://www.cnblogs.com/solidblog/archive/2012/07/15/2592009.html
作者在文中的证明思路清晰,不过我只看懂了辗转相除法的证明,杂耍的证明未看懂,但是仔细模拟杂耍算法的过程会有比较直接的体会。
问题:将大小为n的数组向左移动i位
基本原理:
1)先将x[0]移到临时变量t中
2)将x[i]移动到x[0]中,x[2i]移动到x[i]中,依次类推
3)将x中的所有下标都对n取模,直到我们又回到从x[0]中提取元素。不过这时我们从t中提取元素,结束。
移动过程中不会出现数据丢失的问题,即x[i]已经放到正确的位置后,x[i]就是空位,那么后面的x[2i]便可以赋值给x[i]。问题的关键是理解应该执行几次上面的操作,证明的结果是重复执行i和n的最大公约数gcd(i, n)次,在纸上随便写两个正整数,按照上面的过程走一遍,就会发现,这样做事正确的。接下来是链接作者的描述,其中辗转相除的代码有Bug,已经修改,修改策略可以参考《挑战程序设计竞赛》中的2.6.1节。
#include
//使用辗转相除法求最大公约数
int gcd(int a, int b) //b会不断变小直到0
{
if (b == 0)
return a;
return gcd(b, a%b); //a与b的最大公约数 == 较大数a与较小数b的余数 (如果一开始a小于b,那么此句会进行交换)
}
//杂耍算法
void rotate(char* a,int n,int i)
{
int step = gcd(n,i); //找到重复执行的次数step
for(int j = 0; j < step; j++) //分别以0到step-1作为起点
{
int temp = a[j];
int current = j;
while(1)
{
int next= (current + i) % n;
if(next== j) //如果要提取(向左移动i位)起点值,则退出
{
break;
}
a[current] = a[next]; //向左移动i位
current= next;
}
a[current] = temp; //将起点的值temp赋给最后空出来的位置(完成向左移动i位的操作)
}
}
int main()
{
char a[9] = "abcdefgh";
rotate(a,8,3);
printf("%s\n",a);
return 0;
}
两个整数的最大公约数是能够同时整除它们的最大的正整数。辗转相除法基于如下原理:
两个整数的最大公约数等于其中较小的数和两数的差的最大公约数。
我们来看一下这个原理的证明:
设两数为a、b(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。)
杂耍算法中如下两点我无法理解:
1.为什么程序要循环执行gcd(i,n)次
2.这个算法是通过什么样的机制就让所有位置上的元素都移动到了预期的位置
程序的走向我是明白的,但是核心算法始终不得其解。
最终还是需要借助网络,搜到了园子内一位朋友写的文章(关于编程珠玑中习题2.3的一点思考)
看完以后,有种豁然开朗的感觉,在此多谢这个朋友了。
不过,那篇文章中有些概念的细节讲的不是太清楚,我也是借助网络才有了更清晰的了解。
再次,我来总结个精简吧的吧,写的不好还请各位包涵!
先从几个概念开始:
了解了以上三个概念后,我们能够得出如下的结论:
如果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次移动。