[初学者笔记]从 求区间内完全平方数 中学到的

[初学者笔记]从 求区间内完全平方数 中学到的

Hanker rank上遇到一个求区间内完全平方数的题目,具体题目要求如下:

  • 要求:输入两个正整数A, B,求[A, B]内完全平方数的数量。( 1 ≤ A ≤ B ≤ 10^9 )

测试时输入有极大的数,如:385793959 712365911 ,因此要用long类型存储。

正在学C++,所以用C++实现,一开始用了特别笨的算法,但是在改进这个笨算法的时候也学到了很多,就把过程记录下来吧。

C/C++中测量程序运行时间的方法

要比较不同算法的效率,可以通过比较不同算法得出结果的运行时间来衡量。

使用标准库<time.h>/中的clock()函数,计时精度为ms级。使用方法举例如下:

#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算法,则几乎不用耗费时间。

有时候连续做题做多了思维也会僵化,所以花在思考题目上的时间还是应该更多一点。

posted @ 2017-01-16 14:44  叶空Frank  阅读(554)  评论(0编辑  收藏  举报