从自幂数计算谈算法优化方法

第一章  前言


1.1  参考文献

       郭继展,郭勇,苏辉《程序算法与技巧精选》,机械工业出版社,2008年5月第一版,ISBN 978-7-111-23816-4,第7.3节:求自幂数——用数组预作乘法提高速度100倍。


1.2  写此文章的目的

       (1)想用自己的语言总结书上的内容;

       (2)用自己的笔记本电脑测试下各个方法间,效率差距究竟有多大;

       (3)用自己的风格写一遍代码,加深对知识的理解,也方便以后自己复习。


1.3  自幂数定义

        http://b.baidu.cn/view/8059450.htm

1.4  文章概述

       我会先在第二章讨论各种算法的效率问题,并贴所有源代码供读者验证,也对结果进行截图方便不愿跑代码的懒读者快速读完本篇文章。在第三章写一段计算所有自幂数的小程序。

       PS:本来排版很好的,涉及到英文的书写时,博客给我自动换行换到难看死了……


第二章  算法之间的差距


2.1  最原始的思路

       把pow(10,x-1)至pow(10,x)内的所有数遍历一遍,来求x位自幂数,。
#include 
#include 
#include 
using namespace std;

clock_t start__;
#define tic start__=clock()
#define toc cout<<(clock()-start__)*1000/CLOCKS_PER_SEC<<"ms\n"

void zms1(int x)	// 自幂数拼音首字母:zms,后缀+数字用于区别不同算法的zms函数
{
	int l=pow(10,x-1),r=pow(10,x),i,i0,j,t,a;
	for(i=l; i

从自幂数计算谈算法优化方法_第1张图片


2.2  用各个位的分别穷举代替一个值的穷举

       因为算法涉及到一个值各个位上的操作,每次需要进行a=i0%10; i0/=10;等操作。以七位自幂数为例,我们不妨穷举7个位上的每个值来求解。当然,这样的弊端是,函数的清晰度会降低,也无法像zms1一样具有通用性——可以通过输入参数x,求任意x位的自幂数。优点是降低循环内部的操作次数,效率会提升。我单写一个七位自幂数的函数zms2来验证。

#include 
#include 
#include 
using namespace std;

clock_t start__;
#define tic start__=clock()
#define toc cout<<(clock()-start__)*1000/CLOCKS_PER_SEC<<"ms\n"

void zms2()
{
	int i,j,k,l,m,n,o;	//用来穷举各个位的值的变量,即值本身为ijklmno
	for(i=1;i<10;i++) for(j=0;j<10;j++) for(k=0;k<10;k++)
		for(l=0;l<10;l++) for(m=0;m<10;m++) for(n=0;n<10;n++) for(o=0;o<10;o++)
		{
			if(pow(i,7)+pow(j,7)+pow(k,7)+pow(l,7)+pow(m,7)+pow(n,7)+pow(o,7)
				==1000000*i+100000*j+10000*k+1000*l+100*m+10*n+o)
				cout<


       zms2中,判断一个数是否为自幂数的方法如if语句中所写的式子,非常清晰易懂。而zms1中方法换汤不换药,在数学里,要判断两个表达式f(x)与g(x)是否相等,可以定义h(x)=f(x)-g(x),如果h(x)恒等于0,则f(x)=g(x)。此处道理相同,相当于用t来存储右式1000000*i+100000*j+10000*k+1000*l+100*m+10*n+o减去左式pow(i,7)+pow(j,7)+pow(k,7)+pow(l,7)+pow(m,7)+pow(n,7)+pow(o,7)的值,如果结果为t==0,则为七位自幂数。在后续的程序中,我们主要使用zms1的自幂数判断方法,用这种方法才能方便的存储中间计算值和进行回溯运算。
       再回到zms1和zms2的效率分析上,zms2因为少了对原值各个位的拆解运算,效率提高了1倍多。

2.3  事先存储中间运算量

       再仔细分析语句:pow(i,7)+pow(j,7)+pow(k,7)+pow(l,7)+pow(m,7)+pow(n,7)+pow(o,7)==1000000*i+100000*j+10000*k+1000*l+100*m+10*n+o,因为i,j...o只能取0~9的值,在计算pow(?,7)方面,只有10种运算——pow(0,7),pow(1,7),pow(2,7)...pow(9,7)。这意味着什么呢?在zms2进行的7*(1千万-1百万)=6300万次运算中,跑的7次幂部分,其实只有10种结果!相当于一套10道题的试卷,让你重复做630万次!!!在pow(10,?)中,也是一样的道理。
       于是,我们就想到,不如把所有的中间计算值存储在一个矩阵A中,每次运算时,直接引用a[i][j]的值即可。
       我们把zms2中的:pow(i,7)+pow(j,7)+pow(k,7)+pow(l,7)+pow(m,7)+pow(n,7)+pow(o,7)==1000000*i+100000*j+10000*k+1000*l+100*m+10*n+o改写为1000000*i-pow(i,7)+100000*j-pow(j,7)+10000*k-pow(k,7)+1000*l-pow(l,7)+100*m-pow(m,7)+10*n-pow(n,7)+o-pow(o,7)==0,将改写的式子分成7部分,推导出公式,存储在a[i][j]。由于说清楚此处的道理需要画表等繁琐步骤,笔者也很懒,不在此处展开,读者看代码自己慢慢琢磨领悟。抓住问题的本质—— 用矩阵存储中间运算量的思想,这才是最重要的。

#include 
#include 
#include 
using namespace std;

clock_t start__;
#define tic start__=clock()
#define toc cout<<(clock()-start__)*1000/CLOCKS_PER_SEC<<"ms\n"

void zms3()
{
	int i,j,k,l,m,n,o;
	int a[7][10];	// 存储中间计算量

	for(i=0;i<7;i++) for(j=0;j<10;j++)
		a[i][j] = j*pow(10,6-i) - pow(j,7);	// 推导出公式后赋值

	for(i=1;i<10;i++) for(j=0;j<10;j++) for(k=0;k<10;k++)
		for(l=0;l<10;l++) for(m=0;m<10;m++) for(n=0;n<10;n++) for(o=0;o<10;o++)
		{
				if(a[0][i]+a[1][j]+a[2][k]+a[3][l]+a[4][m]+a[5][n]+a[6][o]==0)
				cout<


       可以发现,此时程序的效率已经相当满意了。但考虑到这仅仅是七位自幂数,我们最大将会算到十位自幂数。所以到了这里我们不能骄傲自满,还要再思考能不能继续优化算法。
       优化到这里,七位自幂数已经没有挑战性啦!我们直接挑战九位自幂数吧!
#include 
#include 
#include 
using namespace std;

clock_t start__;
#define tic start__=clock()
#define toc cout<<(clock()-start__)*1000/CLOCKS_PER_SEC<<"ms\n"

void zms4()
{
	int i,j,k,l,m,n,o,p,q;
	int a[9][10];	// 存储中间计算量

	for(i=0;i<9;i++) for(j=0;j<10;j++)
		a[i][j] = j*pow(10,8-i) - pow(j,9);	// 推导出公式后赋值

	for(i=1;i<10;i++) for(j=0;j<10;j++) for(k=0;k<10;k++) for(l=0;l<10;l++) for(m=0;m<10;m++) 
		for(n=0;n<10;n++) for(o=0;o<10;o++) for(p=0;p<10;p++) for(q=0;q<10;q++)
		{
				if(a[0][i]+a[1][j]+a[2][k]+a[3][l]+a[4][m]+a[5][n]+a[6][o]+a[7][p]+a[8][q]==0)
				cout<

从自幂数计算谈算法优化方法_第2张图片

2.4  每重循环有固定规律运算,采用回溯算法

       仍然观察zms3中,if内的条件表达式:a[0][i]+a[1][j]+a[2][k]+a[3][l]+a[4][m]+a[5][n]+a[6][o]==0,发现进入最后一重循环,遍历o的0~910种情况时,左式中"a[0][i]+a[1][j]+a[2][k]+a[3][l]+a[4][m]+a[5][n]"整项表达式的值都是不变的!即每一重循环内的,都是提供一个固定的加数a[第几重循环-1][该重循环此次遍历的值]。因此,在进入下一重循环前,每重循环都可以事先加好该数,将左式的值统一存储在变量t上,最后判断t==0即可。
       当然,对七位自幂数再做优化已经没有意义了(看不到优化带来的效果有多显著),用时很可能变成"0ms",我们来改进九位自幂数的函数zms4。同时引入变量c来统计一共求了多少个解。同时,为了不至于有太多缩进,我把代码的排版写的很“奇异”,我个人倒是很喜欢这种“奇异”的排版形式,我认为它很清晰的展示了这是一个回溯算法。

#include 
#include 
#include 
using namespace std;

clock_t start__;
#define tic start__=clock()
#define toc cout<<(clock()-start__)*1000/CLOCKS_PER_SEC<<"ms\n"

void zms5()
{
	int x = 9,c=0;	// x代表是几位自幂数,c用于统计解的个数
	int i,j,k,l,m,n,o,p,q;
	int t,a[9][10];	// 存储中间计算量

	for(i=0;i

从自幂数计算谈算法优化方法_第3张图片
  
       效率又提升了2秒多。

2.5  进一步分析问题,减少不必要的循环进入

       仔细分析问题,和我们存储中间变量的矩阵A,我们使用a[x-1][]行,来存储最末尾的值(记为b),b-pow(b,x),我们要探讨的自幂数范围是1~10,即x是1~10内的整数,简单推导下,就可以知道,无论b取0~9中的任何值,b-pow(b,x)都是非正数,所以在进入最后一重循环,加上a[x-1][b]之前,如果t的值已经是负数,就没必要进入最后一重循环了——肯定不是解。
       这步优化,有些读者可能会觉得带来的效益很小。其实在自幂数问题中,效益相当之大!因为pow(某位的值,x)的值是均匀的,与某位的值所在的位置无关,且都不小,到了8,9,10次幂,值的增长也非常快。而pow(10,某位所在的位置对应的权指数)的值随遍历的各个位从左到右,越来越小。
       我想表达的意思是,在自幂数问题中,t在进入最后一重循环前,有相当多的值是负的!在10次幂中比例在80%以上!用zms5的方法跑十位自幂数,需要80秒,但加入此优化后只需6秒!
       这里先简单的在zms5中加上一句if语句,看看效率变化。

#include 
#include 
#include 
using namespace std;

clock_t start__;
#define tic start__=clock()
#define toc cout<<(clock()-start__)*1000/CLOCKS_PER_SEC<<"ms\n"

void zms6()
{
	int x = 9,c=0;	// x代表是几位自幂数,c用于统计解的个数
	int i,j,k,l,m,n,o,p,q;
	int t,a[9][10];	// 存储中间计算量

	for(i=0;i

从自幂数计算谈算法优化方法_第4张图片
       
       九位自幂数再进入最后一重循环前,t值为负的比例还不大。故运算仅快了20%多。
       在计算十位自幂数时,中间运算量将会超出32位整型的数值范围,所以必须使用__int64来定义t和a。这样也会造成效率的降低。
       九位自幂数改用__int64时,代码及结果如下:

#include 
#include 
#include 
using namespace std;

clock_t start__;
#define tic start__=clock()
#define toc cout<<(clock()-start__)*1000/CLOCKS_PER_SEC<<"ms\n"

void zms7()
{
	int x = 9,c=0;	// x代表是几位自幂数,c用于统计解的个数
	int i,j,k,l,m,n,o,p,q;
	__int64 t,a[9][10];	// 存储中间计算量

	for(i=0;i

从自幂数计算谈算法优化方法_第5张图片

       到此,关于效率的问题讨论完毕。

第三章  自幂数计算小程序

       在这里,贴出1~10位自幂数运算的完整程序(在笔者借助参考文献,所能达到的最优的算法),由于是很有规律的套用多重循环,我也想过能不能用一个递归函数写一个通用算法,但貌似不太容易,在我的水平内做不到,且估计效率会大打折扣。就很蹩脚的为10种自幂数写了10段多重循环。不过很有规律,代码编写中复制粘贴即可……

#include 
#include 
#include 
using namespace std;

clock_t start__;
#define tic start__=clock()
#define toc cout<<(clock()-start__)*1000/CLOCKS_PER_SEC<<"ms\n"

int main()
{
	void zms(int);
	int x;
	while(cout<<"请输入要计算几位自幂数:",cin>>x)
	{
		if(x<1||x>10)
		{
			cout << "输入非法,请重输\n";
			continue;
		}
		zms(x);
		cout << endl;
	}
	return 0;
}

void zms(int x)
{// 输入参数值只能为[1,10]的整数
	int c = 0;	 // 统计解的个数
	int i,j,k,l,m,n,o,p,q,r;// 各个位的数字枚举变量
	__int64 t,a[10][10]; //10位自幂数需要用int64,与int差别是9位自幂数计算5秒变8秒

	tic;
	switch(x)
	{
	case 1: cout << "一位自幂数(独身数):" << endl; break;
	case 2: cout << "二位自幂数(无):" << endl; break;
	case 3: cout << "三位自幂数(水仙花数):" << endl; break;
	case 4: cout << "四位自幂数(四叶玫瑰数):" << endl; break;
	case 5: cout << "五位自幂数(五角星数):" << endl; break;
	case 6: cout << "六位自幂数(六合数):" << endl; break;
	case 7: cout << "七位自幂数(北斗七星数):" << endl; break;
	case 8: cout << "八位自幂数(八仙数):" << endl; break;
	case 9: cout << "九位自幂数(九九重阳数):" << endl; break;
	case 10: cout << "十位自幂数(十全十美数):" << endl; break;
	}

	for(i=0;i

你可能感兴趣的:(C/C++)