几种快速平方根算法及其拓展应用
前置芝士
全文均为实数,不考虑复数。
快速幂
微积分入门:1、2、3、4、5。(其实只影响一些细节性的理解)
单精度浮点数在计算机中的储存原理
如图
其中:
符号位(sign)用来表示正负,接下来简写为
指数位(exponent)用来表示该浮点数在二进制下的科学计数法的指数部分。为了能够表示出小数部分,它必须要区分正数和负数,所以会被偏置
尾数位(mantissa)用来存放该数的尾数部分。由于尾数必以
我们举个栗子
用公式写出来,就是类似这样:
的式子,想感受具体转换就点这个或这个。
从上面也能看出,并非所有的浮点数都能毫无精度损失地储存在计算机中,例如
而整数可以视为没有小数部分的浮点数,于是就有了如下表示方法。
正文
还是先贴个图放着 :
二分法
我们将开平方根的结果设为
牛顿迭代法
在这里我们用
随便找一个函数,比如这个:
那么,会有
稍加变形,就有
而且接下来还有
于是你就可以无穷逼近这个根,就像这样,
需要注意的是,假如是求一般的函数,一定要在单调有根的区间内使用该方法,否则会有漏根的情况。同时还要注意极值点,因为该点的
在求平方根时则要变形一下函数,假设你要求
卡马克快速倒数平方根算法
重头戏来了,不过这次先贴上那段来自 Quake3 的著名代码:
float Q_rsqrt( float number )
{
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = * ( long * ) &y; // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
y = * ( float * ) &i;
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
return y;
}
它是用来求
先分析一下为什么这段代码这么高效吧。它几乎只进行了加减和乘,少数用除法的时候都是靠位运算实现的;而在 CPU 中,加减的速度跟位运算相当,乘法的速度比加减慢近 10 倍,除法的速度比加减慢至少 20 倍,且数据类型精数越高,效率越低。
进入正题
假设你要求
看这三行,先忽略中间一行和它的注释。
i = * ( long * ) &y; // evil floating point bit level hacking
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
y = * ( float * ) &i;
它将输入的数的整数单独部分取了出来。
然后再分析一下中间那行哔格爆表的代码。
想一想,函数
所以我们对
我们将它们都视为单精度浮点数,把一开始储存浮点数的式子套进去,就会有
化简一下,则会有
为了进一步推导,我们先来看看近似函数的一些知识。根据泰勒展开,当
所以我们推测
我们可以将
注意看,这个等式的左边正好就是
i = 0x5f3759df - ( i >> 1 ); // what the fuck?
其实就是在求
y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration
// y = y * ( threehalfs - ( x2 * y * y ) ); // 2nd iteration, this can be removed
是在作甚?
回忆一下,浮点运算会丢失部分精度;用近似函数计算也会有误差。再回忆一下上一节的牛顿迭代法,它可以做什么?无限逼近真实值啊!所以这两行就是在作牛顿迭代降低误差!这也是为什么之前可以用近似函数来计算。事实上这个算法的优越性就在于它可以更快地接近函数近似值。于是整个算法是完全的常数复杂度(因为不超过
一开始也说了,这个算法是求
拓展
我们其实可以选择把上面的公式推广,不单单只是求
再套入单精度浮点数的模板,就有
再化简,并带入
又是这个熟悉的形式。但这只是一个近似解,还要做牛顿迭代。
因为你要求
从公式也能看出来,
为了之后称呼方便,我们称这个算法为拓展卡马克算法。
不过这个算法还有拓展的余地:求任何浮点数次幂的值。还是假设我们要求
再将
接下来是更优的那种。
先联想一下普通快速幂,它只能支持整数幂,将这个幂转换成二进制来处理。但浮点数也是可以用二进制表示的。例如
现在,拓展卡马克算法就可以解决浮点数次幂了。
双精度
显然单精度不够用,所以带来双精度版本。
我们先来了解一下双精度浮点数在计算机中的储存方法:
具体规则与双精度相同,只有内存上的差异。 其表示方法如下:
同样地,超长整形也可以视为没有小数部分的双精度浮点数,所以可以如下表示:
接着再在草稿本上演算一下之前的步骤,就能发现:
其余部分一致。也就是说,要想由单精度进化为双精度,只需要在原来的基础上改掉那个奇怪的数字和数据类型!
但是不要忘了精度问题!改成双精度后需要多做几次牛顿迭代。个人推荐 米四达狂喜。
至此,我们终于理解了这数串强大,神秘而又美丽的代码。现在来练练手吧!
我们拿求立方根举例子,把
#include<bits/stdc++.h>
using namespace std;
long double f(double number){//不开long double会#1 WA
long long i;
double y,x2=1.0*number/3;
const double k = 2.0/3;
y = number;
i = *(long long*) &y;
i = 0x2A9F8507753418BC + 1.0*i/3;
y = *(double*) &i;
y = k*y+1.0*x2/(y*y);
y = k*y+1.0*x2/(y*y);
y = k*y+1.0*x2/(y*y);
y = k*y+1.0*x2/(y*y);
return y;
}
long long n;
int main(){
cin>>n;
cout<<(long long)f(n);
return 0;
}
不过你肯定发现了,在牛顿迭代时出现了万恶的除法运算。而且回顾之前的迭代式子
会发现:只要计算的不是形如
后记
笔者第一次写这么复杂的东西,可能有很多细节或错误需要完善,敬请私信。如果以后有时间会补代码上去。
写完翻日报的时候才发现日报里有类似的,幸好他后面没讲原理和拓展算法。不过他前面的牛顿迭代法讲得比我好,看不懂我的请看他的。