看大黑书里提到了幂运算可以通过二分优化,今个用上了.
递归思路
对幂运算进行对分.
利用递归执行代码很简单.
typedef long long ll
ll pow_V0(ll a , ll n)
{
if (!n)
return 1;
else if (n % 2 == 1)
return a * pow(a, n-1);
else {
ll temp = pow(a, n/2); //不能省去,要让q_pow()只执行一次
return temp * temp;
}
}
优化
为了避免temp的出现,我们还可以这样列.
其不同之处是把平方留在函数实参里面,先平方再计算次方,也就是在函数调用之前就进行平方,自然不存在平方调用两次函数的问题.
留意到 n 为奇数时,此处 a ^ {n-1} 要再回到 n 为偶数的情况,似乎有一点浪费资源,不如:
ll pow_V1(ll a , ll n)
{
if (!n)
return 1;
else if (n % 2 == 1)
return a * pow_V1(a * a, n/2);
else
return pow_V1(a * a, n/2);
}
与2相关的运算还可以用速度更快的位运算代替:
ll pow_V2(ll a , ll n)
{
if (!n)
return 1;
else if (n & 1)
return a * pow_V2(a * a, n>>1);
else
return pow_V2(a * a, n>>1);
}
错误修改
我们刚刚的第一段 V0 程序中先用函数得到 a 的 n/2 次方,再将其平方,平方时为不重复调用需要一个temp变量。那不如异想天开一下:用 pow(X , 2) 代替平方就不需要额外变量了。
这样明显是舍近求远毫无必要,但我们应当注意到此处暗藏着的一个递归书写的严重错误。
return pow(pow(a, n/2), 2);
当 n 为 2 时,pow() 中将会同样调用一个以 2 为第二参数的 pow() (上式),这意味着得到 pow(X , 2) 需要它本身的结果。这样程序会产生一个无限循环直至崩溃。
使用递归时要注意递归链靠近末端处,是否会这样原地转圈。
循环思路
递归确实好写,但栈空间占用大,我们尝试将其改写成循环。
ll pow_V3(ll a , ll n)
{
ll ret = 1;
while (n)
{
if (n & 1)
ret *= a;
a *= a; //将底数平方
n >>= 1;
}
return ret;
}
我们把次方数 n 用二进制表示出来(以5为例):
a
5
=
a
101
b
=
(
1
∗
a
1
b
)
∗
(
0
∗
a
10
b
)
∗
(
1
∗
a
100
b
)
=
a
1
∗
a
4
a ^ {5} = a ^ {101b} \\ = (1*a^{1b})*(0*a^{10b})*(1*a^{100b}) \\ = a^{1}*a^{4}
a5=a101b=(1∗a1b)∗(0∗a10b)∗(1∗a100b)=a1∗a4
从右到左每处理一位二进制,将底数 a 平方,这样处理下一个二进制位时(二进制位的 “1” 代表的数翻倍),如果该位是 “1”,ret 相应也会乘上的也是 次方数翻倍 的 a。
超出范围
求幂运算中很可能会碰到结果及其巨大的情况,导致数据溢出。
如计算2的64次方,即使是long long类型也顶不住,得到的结果是0。(次方数再继续增加也只是计算 0 ^ {n} )
我们需要对计算结果进行取模,且只能在每一次运算后都取一次模
ll pow_V4(ll a , ll n)
{
ll ret = 1;
while (n)
{
if (n & 1)
ret *= a % MOD;
a *= a % MOD;
n >>= 1;
}
return ret;
}
如果是针对类型大小的范围取模,位运算也可以用来取模(屏蔽二进制高位)
ret *= a & 4294967296 -1
ret *= a & 0X100000000 -1