等比数列数码 4 计数
题目:
考虑等比数列 1, 2, 4, … 。这数列的第一百万项是个 301030 位数,最左边的数码是 4 。问题是:这个数列的前一百万项中,有多少项的最左边的数码是 4 ?
题目来源:
酒井算协 微信公众号:“沉思的图灵”活动
题解:
法一:暴力高精度 O ( n 2 ) O(n^2) O(n2)
用高精度暴力解题无疑是最自然也最容易想到的方法,虽然效率不高,但在想不到更好的方法时这也不失为一种好方法。
思路:
用高精度乘法(高精乘低精)计算出等比数列的每一项,同时判断最高位是否为 4,如果是 4 就累加。
时间复杂度:
一共n项,就需要做n次高精度乘法,每次需要做的乘法次数为 2n-1 的位数,即 ⌊ n l g 2 ⌋ + 1 \lfloor nlg2 \rfloor+1 ⌊nlg2⌋+1。故总共需要做 ∑ i = 1 n ⌊ i l g 2 ⌋ + 1 \sum\limits_{i=1}^n \lfloor ilg2\rfloor+1 i=1∑n⌊ilg2⌋+1次运算。此可近似看做等差数列求和,故时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
代码:
#include <iostream>
using namespace std;
const int M = 301030 + 10;
const int N = 1e6;
int s[M];
int main() {
int len, p, x, res = 0;
s[1] = len = 1;
for (int i = 2; i <= N; i++) {
p = 1, x = 0; //指针p,进位x
while (p <= len) { //高精度计算
s[p] = (s[p] << 1) + x;
x = s[p] / 10;
s[p] %= 10;
p++;
}
if (x == 0) p--;
else {
s[p] = x;
len++;
}
if (s[p] == 4) res++; //判断最高位是否为4
}
cout << res << endl;
return 0;
}
计算结果:96911
用时:760.8s(12分40秒)
法二:简单的数学运算 O ( n ) O(n) O(n)
注:数学上, ⌊ x ⌋ \lfloor x \rfloor ⌊x⌋表示x向下取整; ⌈ x ⌉ \lceil x \rceil ⌈x⌉表示x向上取整; { x } \{x\} {x}表示取 x 的小数部分,即 { x } = x - ⌊ x ⌋ \{x\}=x-\lfloor x \rfloor {x}=x-⌊x⌋.
思路:
1.首先我们可以先计算出
2
n
2^n
2n 的位数。 设
2
n
−
1
2^{n-1}
2n−1 的十进制表示有 m 位,则满足:
1
0
m
−
1
≤
2
n
<
1
0
m
10^{m-1}\le2^n<10^m
10m−1≤2n<10m
同取常用对数,得:
m
−
1
≤
n
l
g
2
<
m
m-1\le nlg2<m
m−1≤nlg2<m
m为正整数,故
m
=
⌊
n
l
g
2
⌋
+
1
m=\lfloor nlg2\rfloor+1
m=⌊nlg2⌋+1.
2.判断最高位是否为 4
2
n
2^n
2n的最高位为4的充要条件是:
4
×
1
0
m
−
1
≤
2
n
<
5
×
1
0
m
−
1
4×10^{m-1}\le 2^n<5×10^{m-1}
4×10m−1≤2n<5×10m−1
同取常用对数,得:
m
−
1
+
2
l
g
2
≤
n
l
g
2
<
m
−
1
+
l
g
5
m-1+2lg2\le nlg2<m-1+lg5
m−1+2lg2≤nlg2<m−1+lg5
即:
⌊
n
l
g
2
⌋
+
2
l
g
2
≤
n
l
g
2
<
⌊
n
l
g
2
⌋
+
l
g
5
\lfloor nlg2\rfloor+2lg2\le nlg2<\lfloor nlg2\rfloor+lg5
⌊nlg2⌋+2lg2≤nlg2<⌊nlg2⌋+lg5
2 l g 2 ≤ { n l g 2 } < l g 5 2lg2\le \lbrace nlg2 \rbrace <lg5 2lg2≤{nlg2}<lg5
经过简单的计算,我们可以得出结论:要判断 2 n 2^n 2n最高位是否为 4,只需判断不等式 2 l g 2 ≤ n l g 2 < l g 5 2lg2\le {n lg2}<lg5 2lg2≤nlg2<lg5是否成立。
于是通过依次判断 n 从0到 1 0 6 - 1 10^6-1 106-1变化时不等式是否成立,累加成立的次数,即可得出答案。
时间复杂度:
对于每个 n 我们都要进行一次判断,故时间复杂度为 O ( n ) O(n) O(n).
代码:
#include <iostream>
#include <cmath>
using namespace std;
const int N = 1e6;
const double minn = 0.60205999133; //2lg2
const double maxx = 0.69897000434; //lg5
const double k = 0.30102999566; //lg2
double f(double x) { //返回浮点数的小数部分
return x - floor(x);
}
int main() {
int i, res = 1; //初始直接算上4
for (i = 3; i < N; i++) {
double cmp = f(i * k);
if (cmp >= minn && cmp < maxx) res++;
}
cout << res << endl;
return 0;
}
法三:对法二再优化 O ( n ) O(n) O(n)
思路:
法二我们通过枚举 n 来逐个判断,现在我们换个角度,也可以尝试枚举十进制数的位数来解题。换句话说,统计每个区间 [ 1 0 i , 1 0 i + 1 ) [10^i,10^{i+1}) [10i,10i+1) 中2的整次幂的个数并累加,其中 0 ≤ i < 301030 0\le i<301030 0≤i<301030,且 i 为整数。
依然需要一点点数学运算:
由法二同理可得:
i
+
2
l
g
2
≤
k
l
g
2
<
i
+
l
g
5
i+2lg2\le klg2<i+lg5
i+2lg2≤klg2<i+lg5
同除以
l
g
2
lg2
lg2,得:
2
+
i
l
o
g
2
10
≤
k
<
l
o
g
2
5
+
i
l
o
g
2
10
2+ilog_210\le k<log_25+ilog_210
2+ilog210≤k<log25+ilog210
满足上不等式成立的整数k的个数,即区间
[
2
+
i
l
o
g
2
10
,
l
o
g
2
5
+
i
l
o
g
2
10
)
[2+ilog_210,log_25+ilog_210)
[2+ilog210,log25+ilog210) 中的整数个数,即:
⌊
l
o
g
2
5
+
i
l
o
g
2
10
⌋
-
⌈
2
+
i
l
o
g
2
10
⌉
+
1
\lfloor log_25+ilog_210\rfloor-\lceil 2+ilog_210\rceil + 1
⌊log25+ilog210⌋-⌈2+ilog210⌉+1
值得注意的是:以上说法有一点不严谨。因为我们计算的实际是所有位数小于等于301030的 2 的整次幂中最高位为 4 的个数,而我们不应考虑大于
1
0
6
10^6
106且位数恰好为301030的2的整次幂。不过经计算发现,恰好不存在这样的数字,因此我们也不需再特殊处理了。
时间复杂度:
显然相比法二,我们从枚举一百万个 n 优化到了只需枚举三十多万个 i ,确实效率高了。但是其实我们需要做的运算次数是 ⌊ n l g 2 ⌋ + 1 \lfloor nlg2\rfloor+1 ⌊nlg2⌋+1,时间复杂度依然是线性的不变,为 O ( n ) O(n) O(n).
代码:
#include <iostream>
#include <cmath>
using namespace std;
const int M = 301030;
const double a = 2.32192809489; //log2 5
const double b = 3.32192809489; //log2 10
int main() {
int i, res = 0;
for (i = 0; i < M; i++)
res += floor(a + i * b) - ceil(2 + i * b) + 1;
cout << res << endl;
return 0;
}
计算结果:96911
用时:0.03080s