[初学者笔记]从 求区间内完全平方数 中学到的
[初学者笔记]从 求区间内完全平方数 中学到的
在Hanker rank上遇到一个求区间内完全平方数的题目,具体题目要求如下:
- 要求:输入两个正整数A, B,求[A, B]内完全平方数的数量。( 1 ≤ A ≤ B ≤ 10^9 )
测试时输入有极大的数,如:385793959 712365911 ,因此要用long类型存储。
正在学C++,所以用C++实现,一开始用了特别笨的算法,但是在改进这个笨算法的时候也学到了很多,就把过程记录下来吧。
C/C++中测量程序运行时间的方法
要比较不同算法的效率,可以通过比较不同算法得出结果的运行时间来衡量。
使用标准库<time.h>/
#include<iostream>
#include<ctime>
void main()
{
clock_t start,finish;
double totaltime;
start=clock();
/*这里是要计时的代码*/
finish=clock();
totaltime=(double)(finish-start)/CLOCKS_PER_SEC;
cout<<"该段程序花费时间:"<<totaltime<<"s. "<<endl;
}
另外在这里还提到了其他的计时方法,可供参考。
解决本题的算法与改进过程
算法1:
之前做了许多用循环解决的题,一上来就想了个最笨的办法:从a到b依次寻找开平方后为整数的数。
int count = 0;
for (long i = a;i <= b;i++) {
if (sqrt(i) == (long)sqrt(i)) {
count++;
}
}
当输入的数都比较小的时候,这样运算看不出慢,但当数很大时,因为要验证每一个数开方后是否是整数,这种算法算一个区间就要很长时间,于是思考改进方法。
改进1:
上面这种算法要算两次sqrt,效率比较低,可以改为算一次开方和一次乘法。
int count = 0;
for (long i = a;i <= b;i++) {
long root = (long)sqrt(i);
if (root*root == i) {
count++;
}
}
改进2:
继续改进,因为尾数不是1, 4, 9, 6, 5的整数一定不是完全平方数,先进行一次取余运算并判断,可省去一半多的开方运算,改进后:
int count = 0;
for (long i = a;i <= b;i++) {
int tail = i % 10;
if (tail == 0 ||tail == 1 || tail == 4 || tail == 5 || tail == 6 || tail == 9 )
{
long root = (long)sqrt(i);
if (root*root == i) {
count++;
}
}
}
改进3:
但上面这样还是很慢,就思考能不能从sqrt()上改进,于是搜索了很多开方的算法。发现开方计算的程序实现一般有二分法(binary search)、牛顿迭代法(Newton Raphson Method)等算法。
- 二分法:
float bsqrt(long n) {
float low = 0.0;
float high = (float)n + 1;
while ((high - low) > 0.00001f) {
float mid = (low + high) / 2;
if (mid*mid < n) {
low = mid;
}
else {
high = mid;
}
}
return low;
}
- 牛顿迭代法:
float SqrtByNewton(long x)
{
float guess = x/2.0f;
while (abs(guess*guess - x) > 0.00001f){
guess = (guess + x / guess) / 2.0f;
}
return guess;
}
牛顿迭代法在求方程根时的应用十分广泛,且原理简单易懂,值得研究。
- 一种据传是卡马克在Quake III中使用的一个计算平方根倒数(inverse square root)的算法,其中的0x5f3759df十分经典。稍加修改可以用来求平方根。
float Q_sqrt(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 number*y;
}
- 还有人测试过各种开方算法的速度,并与标准库中的sqrt()进行过比较,链接在此。
其中最快的一种是调用硬件fsqrt指令的算法,比标准库中的要快4倍多:
double inline __declspec (naked) __fastcall sqrt14(double n){
_asm fld qword ptr[esp + 4]
_asm fsqrt
_asm ret 8
}
所以这次改进就选择了最快的开方算法 sqrt14() 函数。
算法2:
针对本题目还有一种特殊的方法:
每个完全平方数 x 都可以展开为 x = 1 + 3 + 5 + 7 + 9 + ... + (2n-1) 即奇数等差数列前n项和的形式。原因:等差数列 1 3 5 ... 2n-1 的前n项和 S = n(1+2n-1)/2 = n^2 ,倒推即可得。所以验证一个数是否为完全平方数,可以将此数减去1、3、5 … 直到得数为0或者负数,如果为0则说明这个数是完全平方数,如果为负数则说明不是。如果限制不允许进行开方计算,则可以使用此方法。
bool isroot(long n) {
for (long odds = 1; ;odds += 2){
n -= odds;
if (n < 0)
return false;
else if (n == 0)
return true;
}
}
算法3:
这个题目实际也就是初中水平,一行代码就可以解决:(a、b 表示区间范围)
cout << (int)(floor(sqrt(b)) - ceil(sqrt(a)) + 1) << endl;
分别计算区间两端数的平方根,ceil返回不小于x的下一个整数,floor返回小于或等于x的最大整数。
例如区间是3到10,开平方后是1.732到3.162,所以取2到3,3减2再加上端点的1,答案就是2。无论数的范围有多大,都只用一步就算得出来。
当然,一上来就该用这个办法的。
测试结果
#1 | #1.1 | #1.2 | #1.3 | #2 | #3 | |
---|---|---|---|---|---|---|
100 1000000 | 0.075s | 0.041s | 0.031s | 0.011s | 1.322s | 0s |
100 10000000 | 0.764s | 0.417s | 0.308s | 0.103s | 41.203s | 0s |
385793959 712365911 | 27.222s | 15.042s | 10.934s | 3.728s | N/A | 0s |
可见当输入的数逐渐变大时,上述改进是有效果的。#2算法由于要从n减到0,因此当数变大时速度明显变慢,最后一次输入几分钟都出不来结果。而用#3算法,则几乎不用耗费时间。
有时候连续做题做多了思维也会僵化,所以花在思考题目上的时间还是应该更多一点。