转载文章,原文章来源于算法设计与分析基础系列(减治法)--快速幂
(欢迎关注微信公众号,会定期更新内容)
============================================================
前言
本文内容基于书籍"算法设计与分析基础"(Introduction to The Design and Analysis of Algorithms,作者Anany Levitin),主要学习和讨论其中的快速幂算法。
对于一个输入规模为n的问题,如果我们能够求得规模更小的问题的解,比如规模为n/2的问题,或者规模为n-c(其中c为常数)的问题,并且能够建立两种规模之间的解的关系,那么就可以考虑减治法。其具体实现方式通常包括,"从上到下"和"从下到上"两种,往往对应"递归"和"循环",我们以计算a^n为例进行说明。
问题描述
输入正整数a和非负整数n,计算a^n。
解答
比较显然的方法是利用递归或者循环,从1开始不断地乘以a,直到完成n次停止,其复杂度为O(n)。
现在我们来考虑更加高效的一种方法,首先需要对a^n进行变形。
如果n是偶数,那么有a^n=a^(n/2)*a^(n/2),我们可以发现,记x=a^(n/2),那么如果我们能够求得x,接着就可以通过一次乘法得到a^n=x*x。
如果n是奇数,那么有a^n=a^((n-1)/2)*a^((n-1)/2)*a,我们依然可以发现,记x=a^((n-1)/2),那么如果我们能够求得x,接着就可以通过两次乘法得到a^n=x*x*a
通过上述我们可以发现,不论n的奇偶性,我们总是可以先求出规模大约减半(n/2或者(n-1)/2)的问题的答案,然后通过有限次数的乘法,就可以求出原问题的答案了。而对于规模为n/2或者(n-1)/2的问题,我们可以根据其奇偶性进一步转化成:先求得规模大约为n/4的问题的答案,然后通过有限次数的乘法来求得规模为n/2的问题的答案。到这里,其实整个过程和递归思想完全一样了,我们直接给出代码如下(为了代码方便,均未考虑是否会溢出的问题,实际使用时需要注意)
int solve(int a, int n){
if(n == 0){ // 递归终止条件,任意正整数的0次方等于1
return 1;
}
int x = solve(a, n / 2); // 先求出a^(n/2),由于"整数除法"的特性,无论n的奇偶性均可以这么做
int r = x * x; // x=a^(n/2)
if(n % 2 == 1){ // 如果n是奇数,那么还要再乘以一次a
r = r * a;
}
return r; // 返回当前规模问题的解
}
上面的递归函数,其实就是一种"从上到下"的实现方式。这里的"上"指的是输入规模较大的问题(对应n),而"下"指的是输入规模较小的问题(对应n/2),为了求解规模为n的问题,我们利用"递归函数"帮助我们先解决规模为n/2的问题(但实际上我们并不知道规模为n/2的问题到底该怎么解,而是直接交给递归函数解决),然后根据规模为n/2的问题的解,再构造出规模为n的问题的解。如果将n比喻成楼层高度的话,那么我们要去处理高度为n的问题时,就调用递归函数解决高度为n/2的问题,然后再根据n/2情况下的解构造出高度为n的问题的解,就像是"从上到下"一样。
与之相对的,我们还有解答2,主要利用循环来实现,对应"从下到上"的思想。
我们将a^n重新变形如下
如果n是偶数,那么a^n=(a^2)^(n/2),我们可以发现,如果记x=a^2,那么就有a^n=x^(n/2),此时的幂指数已经减半从n变成n/2了
如果n是奇数,那么有a^n=a*(a^2)^((n-1)/2),可以发现,如果记x=a^2,那么就有a^n=a*x^((n-1)/2),此时的幂指数依然减半。
通过上述我们可以发现,无论n的奇偶性,我们总是可以先求出x=a^2,然后就等价成了计算x^(n/2)或者x^((n-1)/2)*a的问题。下一步就是继续求出y=x^2=a^4的问题(此时大约是n/4),再下一步就是z=y^2=x^4=a^8(此时大约是n/8),等等以此类推,直到求出a^n。于是,我们可以给出如下代码采用循环实现
int solve(int a, int n){
int r = 1; // 用于存储答案,初始值为1
while(n > 0){ // 只要次幂n还是大于0说明就要继续求解
if(n % 2 == 1){ // n是奇数
r = r * a; // n是奇数时,我们要额外乘一个a
int x = a * a; // 计算x=a*a
a = x; // 下次循环实际是计算x^((n-1)/2),由于每次循环都是用的变量a,所以在下次循环开始前把x的值给a
n = n / 2; // 利用"整数除法"的性质可以直接取n/2
} else { // n是偶数,和奇数情况类似,只是不需要额外乘一次a了
int x = a * a;
a = x;
n = n / 2;
}
}
return r;
}
和"从上到下"的递归实现对应,这里"从下到上"的循环实现中,"上"依然指的是输入规模较大的问题而"下"指的是规模较小的问题。为了求解输入规模为n的问题,我们先处理规模最小的a^2,然后处理规模稍大的a^4直到求出a^n ,并且规模较小的问题我们是明确知道如何求解的(不同于递归思想完全交给“递归函数”解决)。如果还用楼层高度类比的话,就像是"从下到上"那样。
总结
1,上述两类算法,由于每次输入规模都近似减半,因此复杂度是O(logn)的,这种单纯的对数复杂度的算法其实不算很常见的(二分算是一个)。
2,"从上到下"和"从下到上"是两种很重要的思想,在动态规划的题目中也经常出现,一般来说"递归"和"循环"的解法是同时存在的,但实际的题目里,可能其中一种方式会更容易想到也更容易实现一些。
3,如果读者对编程竞赛有过一定的研究和学习的话,对快速幂应该会比较熟悉,其中一个常见应用是配合"费马小定理"来求解逆元,在后续的文章里我们会再进行更加详细的讨论。
欢迎大家多多转载并关注后续更新,如果有其他算法相关的问题也欢迎留言讨论~