记录一次有趣的算法题。
土生土长的北京妞儿,在胡同里长大,房不多,就一个四合院和近郊的别墅。不算美如天仙但还算标致,在清华读的经管,现在在做基金经理(不想被人肉,就不暴露单位啦) ,个人擅长基本面分析,价值投资。现在只想找个聪明靠谱的IT男。硬性要求是年龄,不要超过88年,还有不要特别矮或胖。我对智商的要求比较高,下面就出个题测试下。
我的微信ID是大写字母NY后面跟着两个质数,大的在前,小的在后,乘积是707829217,可直接搜索添加,另外还有个附加题目,在刚刚微信ID的数字中,从1开始到这个数字的奇数序列里,一共出现了多少个3,如果私信我正确的答案,我将直接邀你见面!期待缘分降临~
问题1 求解微信号
// 两个质数的乘积是707829217,求质数
int num = 707829217;
int i = 1;
while (i <= num) {
i += 2;
if (num % i == 0) {
System.out.println("发现: " + num + " / " + i + " = " + (num / i));
}
}
打印结果:
发现: 707829217 / 8171 = 86627
发现: 707829217 / 86627 = 8171
发现: 707829217 / 707829217 = 1
Process finished with exit code 0
所以我们得到第一问的答案:NY866278171
问题2 求解奇数序列中,3出现的次数
我们看到这个数值866278171,为8亿多,去掉偶数,只看奇数,也有4亿多。
问这4亿个数中3出现了多少次,这个问题有点费解。
方式一 暴力破解
所谓暴力破解,就是遍历每一个数值,统计3出现的次数。下面的各个版本仅供参考:
该方案耗时:2m 52s 139ms
// 奇数序列中,一共出现了多少次3
int number = 866278171;
int sum = 0;
for (int i = 1; i <= number; i = i + 2) {
sum += String.valueOf(i).replace("3", "_#_")
.split("#")
.length - 1;
}
// 总数: 441684627
System.out.println("总数: " + sum);
该方案耗时:1m 41s 259ms
// 奇数序列中,一共出现了多少次3
int number = 866278171;
int sum = 0;
for (int i = 1; i <= number; i = i + 2) {
String str = String.valueOf(i);
if (str.contains("3")) {
sum += str.length() - str.replace("3", "").length();
}
}
// 总数: 441684627
System.out.println("总数: " + sum);
该方案耗时:22s 942ms
// 奇数序列中,一共出现了多少次3
int number = 866278171;
int sum = 0;
for (int i = 1; i <= number; i = i + 2) {
String str = String.valueOf(i);
for (int j = 0; j < str.length(); j++) {
if (str.charAt(j) == '3') {
sum++;
}
}
}
// 总数: 441684627
System.out.println("总数: " + sum);
该方案耗时:6s 669ms
// 奇数序列中,一共出现了多少次3
int number = 866278171;
int sum = 0;
for (int i = 1; i <= number; i = i + 2) {
int k = i;
while (k > 1) {
if (k % 10 == 3) {
sum++;
}
k /= 10;
}
}
// 总数: 441684627
System.out.println("总数: " + sum);
我们看到,走了好多的弯路,String类中的replace
和contains
都是重量级方法。当我们使用有限次数时,并不会感觉到慢。但是当我们需要重复执行上亿次时,就很慢了。
方式二 公式法
规律总结
- 对于1位数
3只出现1次。
- 对于2位数:
3出现在个位数,十位数可以任意0-9,有10种。33暂时算一次
3出现在十位数,个位数可以任意0-9,有10种,33暂时算一次,加上上一次的补齐
所以,对于任意两位数,3出现了20次。
- 对于3位数:
3出现在个位数,十位数、百位数 可以任意00-99,有100种。 x33、3x3、333暂时算一次
3出现在十位数,个位数、百位数 可以任意00-99,有100种。 x33、33x、333暂时算一次
3出现在百位数,个位数、十位数 可以任意00-99,有100种。 3x3、33x、333暂时算一次
少算的,都补齐了,所以,对于任意3位数,3出现了300次。
- 对于4位数:
3出现在个位数,十位数、百位数、千位数 可以任意000-999,有1000种。
xx33、x3x3、3xx3、x333、33x3、3x33、3333暂时算一次
3出现在十位数,个位数、百位数、千位数 可以任意000-999,有1000种。
xx33、x33x、3x3x、x333、3x33、333x、3333暂时算一次
3出现在百位数,个位数、十位数、千位数 可以任意000-999,有1000种。
x3x3、x33x、33xx、x333、33x3、333x、3333暂时算一次
3出现在千位数,个位数、十位数、百位数 可以任意000-999,有1000种。
3xx3、3x3x、33xx、3x33、33x3、333x、3333暂时算一次
少算的,都补齐了,所以,对于任意4位数,3出现了4000次。
好像有点规律了。。
对于任意N位数,3出现的次数为
n * 10^(n-1)
翻译成代码:
/**
* 任意N位数,3出现的次数
*/
public double anyN(int n) {
if (n < 1)
return 0;
return n * Math.pow(10, n - 1);
}
问题来了,对于一个有限度的N位数,3出现了多少次?
比如: 0 ~ 2918,3出现了多少次?
拆分下:
0 ~ 2000区间段,
可以理解为2个1000,也就是2个任意3位数,所以 :2 * 300 + 0 (对于任意3位数3出现的次数为300,不包含3000~3999 整个以3开头的千位数)
2000 ~ 2900区间段,
可以理解为9个100,也就是9个任意2位数,所以:9 * 20 + 100( 任意2位数3出现的次数为20,包含300-399 整个以3开头的百位数)
2900 ~ 2910区间段,
可以理解为1个10,也就是1个任意1位数,所以:1 * 1 + 0( 任意1位数3出现的次数为1,不包含30-39 整个以3开头的十位数)
2910 ~ 2918区间段,
只看个位数,0 ~ 8,包含一个3,所以: 1
综合起来就是:
(2 * 300 + 0)+(9 * 20 + 100) + (1 * 1 + 0) + (1)= 600+280+1+1 = 882
我们拆开翻译,
- 步骤1
对于0 ~ n * 10^w 的数,3出现了多少次。
比如0~100,0~4000,0~800000000
/**
* 计算一个 [ 0 , n*10^w ) 的数中,3出现的次数
* <p>
* e.g: 4000 n = 4 , w = 3
*
* @param n 数值
* @param w 0的个数
* @return
*/
public int count3(int n, int w) {
// n * 10^(w-1) + 10^w
double sum = n * anyN(w);
if (n > 3) {
sum += Math.pow(10, w);
}
return (int) sum;
}
- 步骤2
对于任意0 ~ N, 3出现了多少次
/**
* 计算0~N的数中,3出现的次数
*/
public int anyNumCount3(int anyN) {
double sum = 0;
int number = anyN;
int count0 = 0;
while (number > 1) {
int n = number % 10;
sum += count3(n, count0);
if (n == 3) {
// 如果该位为3,需要将低位数再加一遍。
// 比如 389,[300,389]共89+1=90个
sum += (anyN % Math.pow(10, count0)) + 1;
}
number /= 10;
count0++;
}
return (int) sum;
}
该方案耗时:1ms ?
至此,我们通过找规律,发现了对于[0~N]中3出现的次数的公式。
只看奇数序列
针对本题的只看奇数序列,我们总结下规律:
奇数,也就是限定了个位数只能是1、3、5、7、9共5种选择,而更高位可以是0-9共十种选择。
对于任意N位奇数
- 对于1位数
3只出现1次。
- 对于2位数:
3出现在个位数,十位数可以任意0-9,有10种。33暂时算一次
3出现在十位数,个位数可以是13579,有5种,33暂时算一次,加上上一次的补齐
所以,对于任意两位数,3出现了15次。
- 对于3位数:
3出现在个位数,十位数0-9、百位数0-9,有10*10=100种。 x33、3x3、333暂时算一次
3出现在十位数,百位数0-9,个位13579,有10*5=50种。 x33、33x、333暂时算一次
3出现在百位数,十位数0-9,个位13579,有10*5=50种。 3x3、33x、333暂时算一次
少算的,都补齐了,所以,对于任意3位数,3出现了200次。
- 对于4位数:
3出现在个位数,十、百、千位数 可以任意0-9,有1000种。
xx33、x3x3、3xx3、x333、33x3、3x33、3333暂时算一次
3出现在十位数,个位13579、百、千位数 可以任意0-9,有500种。
xx33、x33x、3x3x、x333、3x33、333x、3333暂时算一次
3出现在百位数,个位13579、十、千位数 可以任意0-9,有500种。
x3x3、x33x、33xx、x333、33x3、333x、3333暂时算一次
3出现在千位数,个位13579、十、百位数 可以任意0-9,有500种。
3xx3、3x3x、33xx、3x33、33x3、333x、3333暂时算一次
少算的,都补齐了,所以,对于任意4位数,3出现了2500次。
规律:对于任意N位数,只看奇数,3出现的次数为
1*10^(n-1) + (n-1)*5*10^(n-2)
翻译成代码:
/**
* 任意N位奇数,3出现的次数
* e.g: 9999 n = 4
*/
public int anySingleN(int n) {
// 1*10^3 + 3*5*10^2
if (n < 1) return 0;
double sum = Math.pow(10, n - 1);
if (n >= 2) {
sum += (n - 1) * 5 * Math.pow(10, n - 2);
}
return (int) sum;
}
对于有限制的N位奇数,3出现的次数
- 比如:4000以内的奇数
4个任意三位奇数 + 3开头的,任意4位奇数。即4*anySingleN(3) + 10*10*5
翻译成代码:
/**
* 计算一个 [ 0 , n*10^w ) 的奇数中,3出现的次数
* <p>
* e.g: 4000 n = 4 , w = 3
*
* @param n 数值
* @param w 0的个数
* @return
*/
public int countSingle3(int n, int w) {
// 4 * anySingleN(3) + 5*10^2
if (w < 1)// 个位数
return (n >= 3) ? 1 : 0;
double sum = n * anySingleN(w);
if (n > 3) {
sum += 5 * Math.pow(10, w - 1);
}
return (int) sum;
}
- 对于0~N的任意奇数中,3出现的次数
/**
* 计算0~N的奇数数中,3出现的次数
*/
public int anySingleNumCount3(int anyN) {
int sum = 0;
int number = anyN;
int count0 = 0;
while (number > 0) {
int n = number % 10;
sum += countSingle3(n, count0);
if (n == 3) {
// 如果该位为3,需要将低位奇数再加一遍
// 比如 389,[300-389]共(89+1)/2 = 50个
sum += (anyN % Math.pow(10, count0) + 1) / 2;
}
number /= 10;
count0++;
}
return (int) sum;
}
该方案耗时:1ms ?
至此,我们通过找规律,发现了对于[0,N]奇数序列中3出现的次数的公式。
总结
我们通过 方案一 暴力破解 和 方案二 公式法 来解决了这个问题。
速度对比那就更不用说了