水仙花数的改进算法

什么是水仙花数?

请参见:http://baike.baidu.com/view/152077.htm

 

在传统算法中,需要花费大量的时间进行计算,一个是进行幂运算,一个是进行数字的拆分判断。

这个算法在两个地方上进行了改进:

1。 幂运算用一个数组缓冲,这样,只有位数变化的时候,才需要刷新一次缓冲,而且是乘法,不是幂运算,

    有了这个缓冲,幂运算就变成取数组元素的代码。

    这个改进的前提是,我们只需要0~9的n次幂,所以个数是固定的

 

2。判断是否水仙花数,用差值比较,而不是直接判断

    原理是这样的:

   假设一个数A=a(N)a(N-1)...a(0) 是一个水仙花数

   如果另一个数B=b(M)b(M-1)....b(0)也是一个水仙花数  其中 a(x)表示A在第x位上的数字,b(x)也是

   那么,因为 A,B的特性,可以得到

  A-B=[a(N)^N+...a(0)^N ] - [b(M)^M+...b(0)^M]                 (式1)

   为什么要搞成这个样子呢? 因为,如果N=M,A与B的大多数位的数值相同,那么我们只需要计算A、B数字不相同的位就可以判断是否水仙花数。

 

   因为 A-Z=A-B+B-C+C....+Y-Y-Z = (A-B)+(B-C)+(C.....-Y)+(Y-Z)

   所以,计算差值的时候,可以把每次的差值累计起来计算。

 

   因此,我们来看,假设循环每次递增为1,那么,90%的情况下,只有1位数值发生了变化。 判断这个数是不是水仙花数的时候,不需要把所有位上的数字的N次方的值相加再和数字本身比较,而是仅仅计算发生了变化的这一位数字的N次方,和上一个水仙花数的对应位的M次方的差,如果(式1)成立,那么这个数就是一个新的水仙花数。而因为只有一位发生了变化,所以(式1)可以简化成

  A-B=a(x)^N-b(x)^M                                                            (式2)

N是A的位数,M是B的位数。

   这样一来,几乎90%的计算只需要做1位的加减法。前面也说过了,“如果N=M,A与B...”,所以在N!=M的情况下,需要从新调整差值。

    因此,编写代码如下,主要的思想是利用每次判断某个数是否水仙花数的时候的计算结果,减轻下一次计算的负担。

package test; public class NarcissusNumber { private byte[] number; private byte[] lastNum; private int maxPos = 0; private boolean isNarc; private long[] powers; private long diff = 0; private byte[] diffDigits = new byte[22]; private long totalDiff; public static void main(String[] args) { int n = 3; long seed = 153; NarcissusNumber num = new NarcissusNumber(); num.init(seed, n); int delta = 1; long t = System.currentTimeMillis(); while (n < 10) { if (num.isNarcissus()) { System.out.println(num.getNumberString()); delta = num.getDigit(0) == 0 ? 1 : (10 - num.getDigit(0)); n = num.add(delta); } else { n = num.add(1); } } System.out.println(String.format("Used %.3f Second", (System .currentTimeMillis() - t) / 1000.0)); } // 关键就是这个函数,每次把差值累计起来,比较变动位的差值和数的差值是不是相等 private int add(int delta) { int x = delta; totalDiff += x; int pos = 0; boolean needCarry = false; // 计算增加的delta后的新数,并统计哪些位置发生了变化(记录在pos中)。大多数情况下只有1位。 do { diffDigits[pos] = number[pos]; number[pos] += x; if (number[pos] > 9) { number[pos] = 0; needCarry = true; x = 1; pos++; } else { needCarry = false; } } while (needCarry); // 判断是否有进位 if (maxPos <= pos) { // if total digit numbers changed, re-calculate the differ // 整个数的长度发生了变化,就是在N!=M的情况下,需要从新调整差值。 diff = 1; for (int i = 0; i < maxPos; i++) { diff -= powers[lastNum[i]]; } maxPos = pos + 1; for (int i = 0; i < 10; i++) { powers[i] *= i; } } else{ // 没有进位,只需要比较发生了变化的位 for (int i = 0; i <= pos; i++) { diff += (powers[number[i]] - powers[diffDigits[i]]); } } // 通过判断差值是否相等来判断是否水仙花数 isNarc = diff == totalDiff; if (isNarc) { for (int i = 0; i < maxPos; i++) { lastNum[i] = number[i]; } diff = 0; totalDiff = 0; } return maxPos; } private int getDigit(int i) { return number[i]; } private String getNumberString() { StringBuffer result = new StringBuffer(maxPos); for (int i = maxPos - 1; i >= 0; i--) { result.append(number[i]); } return result.toString(); } private boolean isNarcissus() { return isNarc; } /** * init all variables with seed * * @param seed * @param n */ private void init(long seed, int n) { lastNum = new byte[21]; number = new byte[21]; maxPos = n; for (int i = 0; i < n; i++) { lastNum[i] = number[i] = (byte) (seed % 10); seed /= 10; } isNarc = true; powers = new long[10]; for (int i = 0; i < 10; i++) { powers[i] = (long) Math.pow(i, n); } totalDiff = 0; }  使用这个程序计算10位以下的水仙花数,结果如下:

153
370
371
407
1634
8208
9474
54748
92727
93084
548834
1741725
4210818
9800817
9926315
24678050
24678051
88593477
146511208
472335975
534494836
912985153
Used 32.929 Second

 

而使用简单的每次判断,则运算时间稍长:

153
370
371
407
1634
8208
9474
54748
92727
93084
548834
1741725
4210818
9800817
9926315
24678050
24678051
88593477
146511208
472335975
534494836
912985153
Used 66.258 Second

 

这个优化的效果在9位以下时,效果不明显,以下是计算时间比较:

计算位数 简单算法时间(秒) 优化算法时间(秒) 循环花费时间(秒)
3  0  0  0
4  0  0  0
5  0.016  0  0
6  0.047  0.031  0.016
7  0.546  0.328  0.219
8  6.032  3.313  2.500
9  66.258  32.929  28.926
10  714.458  330.422  330.167
11  。。。  。。。  ...

上表中的“循环花费时间”是指不做判断,只是对数字作++运算。

例如,位数=9,这一行,简单算法共用时66.258秒,其中循环花费时间28.926秒,那么,用于判断的时间为:66.258-28.926=37.328秒;

而同时,优化算法共用时32.929秒,那么判断的时间为32.929-28.926=4.003秒。

 

所以从这里看,数位越大,循环本身花费的时间越成为性能瓶颈。 如果能够提高循环的效率,比如,每次不是增加1,而是增加10以上的一个数,那么效率还可以提高10倍。

我想了一天,也没想到怎么确定递增的步长。 希望高手们继续研究啊

==========================================

 

2010-10-12

----------------------------------------------------

有研究了一天,终于得到一个增加递增步长的办法。

原理是根据差值计算方法判断一个数是不是水仙花数的时候,从个位数为0开始,如果在10个数范围内有一个水仙花数的话,此时计算的sum[a(x)^n-b(x)^m]与A-B的差值一定是固定的。 因为,在这10个数内,sum[a(x)^n-b(x)^m]只会有x=0的变化,以个位数=4时变成水仙花数为例,假设个数+4是水仙花数,那么新增的差值就是 4^n+4. 所以,如果个位数为0时,

    sum[a(x)^n-b(x)^m]与A-B的差值刚好等于 4^n+4, 也就是  sum[a(x)^n-b(x)^m] - (A-B) = 4^n+4

那么,在个位数变成4后, sum[a(x)^n-b(x)^m]的变化就是

    sum[a(x)^n-b(x)^m] + a(0)^n , 其中a(0)=4,

所以上式变成

    (A-B)+4^n+4+4^n = 4+(A-B)

而同时,A-B的值,B一直没有变化,A从一个个位数=0的数,变成个位数=4的数,那么A-B也就变化了4,也是(A-B)+4

所以新的sum[a(x)^n-b(x)^m]=新的A-B,

所以,可以根据个位数=0的时候计算得到的 差值,直接判断出这10个数内有没有水仙花数。

 

据此修改代码的步长计算方法,运算时间比较如下:

计算位数 简单算法时间(秒) 优化算法时间(秒) 步长优化后
算法时间(秒)

循环花费时间 (秒,步长=1)

3  0  0  0  0
4  0  0  0  0
5  0.016  0  0  0
6  0.047  0.031  0.016  0.016
7  0.546  0.328  0.094  0.219
8  6.032  3.313  0.875  2.500
9  66.258  32.929  8.323  28.926
10  714.458  330.422 84.955  330.167
11  。。。  。。。  ......  ...

 

 

附代码如下

package test; public class NarcissusNumber { private byte[] number; private byte[] lastNum; private int maxPos = 0; private boolean isNarc; private long[] powers; private long diff = 0; private byte[] diffDigits = new byte[22]; private long totalDiff; private int step = 1; public static void main(String[] args) { int n = 3; long seed = 153; NarcissusNumber num = new NarcissusNumber(); num.init(seed, n); int delta = 1; long t = System.currentTimeMillis(); long val = seed; while (n < 11) { if (num.isNarcissus()) { System.out.println(num.getNumberString()); } delta = num.getDelta(); n = num.add(delta); } System.out.println(String.format("Used %.3f Second", (System .currentTimeMillis() - t) / 1000.0)); } private int getDelta() { return step; } private int add(int delta) { int x = delta; totalDiff += x; int pos = 0; boolean needCarry = false; // do the ADD first, count how many digits changed (pos+1) do { diffDigits[pos] = number[pos]; number[pos] += x; if (number[pos] > 9) { number[pos] -= 10; x = 1; needCarry = true; pos++; } else { // number[pos] += x; needCarry = false; } } while (needCarry); // refresh the max digit numbers if (maxPos <= pos) { // if total digit numbers changed, re-calculate the differ diff = 0; for (int i = 0; i < maxPos; i++) { diff -= powers[lastNum[i]]; } maxPos = pos + 1; for (int i = 0; i < 10; i++) { powers[i] *= i; } for (int i = 0; i < maxPos; i++) { diff += powers[number[i]]; } } else { for (int i = 0; i <= pos; i++) { diff += (powers[number[i]] - powers[diffDigits[i]]); } } long b = totalDiff - diff; // System.out.println(getNumberString()+", new diff="+diff+", b="+(b)); // judement if it's a Narcissus Number isNarc = b == 0; if (isNarc) { step = (number[0] == 0 ? 1 : 10 - number[0]); for (int i = 0; i < maxPos; i++) { lastNum[i] = number[i]; } diff = 0; totalDiff = 0; } else { if (number[0] != 0) { step = 1; } else { step = 10; int s = 1; int e = 9; while (s <= e) { int p = (s + e) >> 1; long val = powers[p] - p; if (b == val) { step = p; break; } else if (b < val) { e = p - 1; } else { s = p + 1; } } } } return maxPos; } private String getNumberString() { StringBuffer result = new StringBuffer(maxPos); for (int i = maxPos - 1; i >= 0; i--) { result.append(number[i]); } return result.toString(); } private boolean isNarcissus() { return isNarc; } /** * init all variables with seed * * @param seed * @param n */ private void init(long seed, int n) { lastNum = new byte[21]; number = new byte[21]; maxPos = n; for (int i = 0; i < n; i++) { lastNum[i] = number[i] = (byte) (seed % 10); seed /= 10; } isNarc = true; powers = new long[10]; for (int i = 0; i < 10; i++) { powers[i] = (long) Math.pow(i, n); } totalDiff = 0; } }

你可能感兴趣的:(算法)