算法设计与分析基础系列(减治法)--快速幂

 转载文章,原文章来源于算法设计与分析基础系列(减治法)--快速幂

 (欢迎关注微信公众号,会定期更新内容)

============================================================

前言

本文内容基于书籍"算法设计与分析基础"(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,如果读者对编程竞赛有过一定的研究和学习的话,对快速幂应该会比较熟悉,其中一个常见应用是配合"费马小定理"来求解逆元,在后续的文章里我们会再进行更加详细的讨论。

欢迎大家多多转载并关注后续更新,如果有其他算法相关的问题也欢迎留言讨论~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值