什么是水仙花数?
请参见: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; } }