有沒有自然數 n,使得 n! 的前四個數字是 2020?
其實只要把滿足
的所有
找到就行了,所以不需要大整數運算即可在很快的時間內得出
之內滿足要求的數恰好有
個。
另外,我們可以把
的十進位制表示看成一個各位均為隨機的數(但是這一點未經證明),所以根據著名的本福特定律,滿足條件的數在全體自然數中所佔比例並不是
,而是
,可以看到逼近於此比例的速度還是比較慢的。
UPD:
其實壓根就不需要用
去算,這樣不僅引入精度誤差,而且慢。只需要在算階乘的過程之中,如果大於10000了馬上除個10,就可以保證一直在精度範圍之內了。如果說您非要使用
去算,需要注意一些數值穩定性的小trick,然後精度開double就夠了。
在我的電腦上,使用
計算的程式碼需要8。6s左右才能跑出
以內的答案,只需要浮點數乘除法的程式碼只需要0。33秒。(Ubuntu 20。04。2, g++ 10。2。0, 編譯開關-Ofast -march=native)
並且有人提到浮點誤差的問題,我用GMP庫的高精度驗算了一下,保留200位小數的意義下也還是這個答案,可以基本排除浮點誤差的可能。
大家都喜歡貼程式碼,那麼我也貼一個C++程式碼吧:
#include
long
double
x
=
1
,
b
=
1
;
int
ans
=
0
;
int
main
(){
for
(
int
i
=
1
,
a
=
10
;
i
<=
1e8
;
i
++
){
if
(
i
==
a
)
a
=
i
*
10
,
b
/=
10
;
x
*=
i
*
b
;
if
(
x
>
1e4
)
x
/=
10
;
if
(
x
>=
2020
&&
x
<
2021
)
ans
++
;
}
printf
(
“%d
\n
”
,
ans
);
return
0
;
}
顯然
384! = 20205002129876021521366889509458521857103131384424164703……
8297! = 20207983458785287090983863080884101922314442223694268578……
8795! = 20206439534140183506320813566892771922507666440925556515……
16624! = 20204447515212337619006649257632217013568552466164768543……
17775! = 20201706710997231611432820358540894429946023160331657707……
故至少存在自然數n,使得n!的前四個數字是2020
證明過程如下
#! /usr/bin/python3
sum
=
1
math
=
1
while
True
:
sum
*=
math
sum_str
=
str
(
sum
)
if
(
sum_str
[
0
:
4
]
==
“2020”
):
(
“{}! = {}”
。
format
(
math
,
sum
))
math
+=
1
#EOF
先給個不那麼嚴謹的證明
題目等價於存在一對正整數
滿足:
簡單變形一下:
令
,
,上式就可以寫成:
故只要存在正整數
滿足
就可以了(這裡
表示
的小數部分)
取一個足夠大的正整數
,使得區間
內的正整數個數大於
,對於其中任一正整數
,容易知道:
假設該區間內最小的正整數為
,最大的正整數為
,那麼
,故:
所以在
中必然存在正整數
滿足
,故
。
直觀上理解就是
“增加”的過程中“超過”了
,而每次的“步長”又小於
,所以一定存在一個臨界的
滿足
。
其實程式設計簡單地列舉一下就知道
是滿足條件的一個正整數,但是精確地計算階乘對於計算機來說開銷不少。 @SarvaTathagata 的回答裡說可以利用
這個式子來避免大整數運算,可是在實際操作中也會遇到精度相關的問題
我們用C++來簡單測試一下,找到所有
以內滿足條件的正整數
// test。cpp
#include
#include
template
<
typename
F
>
void
count_factorial
(
FILE
*
output
)
{
int
count
=
0
;
F
log_sum
=
0
;
for
(
int
i
=
2
;
i
<
100000000
;
++
i
)
{
log_sum
+=
std
::
log10
((
F
)
i
);
F
tmp
=
std
::
pow
((
F
)
10
,
(
F
)
3
+
log_sum
-
std
::
floor
(
log_sum
));
if
(
tmp
>=
2020
&&
tmp
<
2021
)
{
++
count
;
fprintf
(
output
,
“%d
\n
”
,
i
);
}
}
printf
(
“%d
\n
”
,
count
);
}
int
main
()
{
count_factorial
<
float
>
(
fopen
(
“float。txt”
,
“w”
));
count_factorial
<
double
>
(
fopen
(
“double。txt”
,
“w”
));
count_factorial
<
long
double
>
(
fopen
(
“ldouble。txt”
,
“w”
));
return
0
;
}
編譯執行,檢視輸出,結果令人驚訝:使用float型別(4byte)時一個也沒找著,使用double(8byte)時有21456個,而使用long double型別(16byte)時則有21488個。
這表明在實際運算的過程中,各種近似誤差的疊加最終超過閾值,影響到了最終結果。
利用GMP庫,我們來做個測試。計算
的前16位數字
// gmp_check。c
#include
int
main
()
{
mpz_t
fact
,
base
;
mpz_init_set_ui
(
base
,
10
);
mpz_fac_ui
(
fact
,
99999999
);
size_t
digits
=
mpz_sizeinbase
(
fact
,
10
);
mpz_pow_ui
(
base
,
base
,
digits
-
20
);
mpz_t
result
;
mpz_div
(
result
,
fact
,
base
);
gmp_printf
(
“%Zd
\n
”
,
result
);
return
0
;
}
//
輸出
16172037949214623863
使用C++標準庫,用三種浮點型別分別計算這個結果
// test2。cpp
#include
#include
#include
template
<
typename
F
>
F
test
()
{
F
log_sum
=
0
;
for
(
int
i
=
2
;
i
<
100000000
;
++
i
)
{
log_sum
+=
std
::
log10
((
F
)
i
);
}
return
std
::
pow
((
F
)
10
,
(
F
)
3
+
log_sum
-
std
::
floor
(
log_sum
));
}
int
main
()
{
using
namespace
std
;
auto
s1
=
test
<
float
>
();
auto
s2
=
test
<
double
>
();
auto
s3
=
test
<
long
double
>
();
cout
<<
setprecision
(
16
);
cout
<<
s1
<<
endl
<<
s2
<<
endl
<<
s3
<<
endl
;
return
0
;
}
得到
1
1617。089863098135
1617。203354540494
(float, float你在幹什麼啊float)
過載float型別的庫函式可能實現略有不同,兩次測試都顯現出巨大的差異。(事實上,這裡float型別計算出的對數之和僅在1。3e8左右,與實際值7。6e8差距巨大)
對比正確數值1617。2037949214623863,可以發現double型別在第5位出現不同,而long double型別在第8位出現差異。
BTW,雖然在第一個測試中使用long double型別僅比double型別多找到32個數,但是對比輸出檔案可知並情況沒有看上去這麼簡單
comm -3 <
(
sort double。txt
)
<
(
sort ldouble。txt
)
> diff。txt
這個diff。txt
僅
有2048行,非常的巧
在dalao @SarvaTathagata 的提點下做了一些改進,取得了顯著的效果。
// test_opt。cpp
#include
#include
const
long
double
SUP
=
std
::
log10
(
2。021
L
);
const
long
double
INF
=
std
::
log10
(
2。020
L
);
template
<
typename
F
>
void
count_factorial
(
FILE
*
output
)
{
int
count
=
0
;
F
log_sum
=
0
;
for
(
int
i
=
2
;
i
<
100000000
;
++
i
)
{
log_sum
+=
std
::
log10
((
F
)
i
);
log_sum
-=
std
::
floor
(
log_sum
);
if
(
log_sum
>=
INF
&&
log_sum
<
SUP
)
{
++
count
;
fprintf
(
output
,
“%d
\n
”
,
i
);
}
}
printf
(
“%d
\n
”
,
count
);
}
int
main
()
{
count_factorial
<
float
>
(
fopen
(
“float2。txt”
,
“w”
));
count_factorial
<
double
>
(
fopen
(
“double2。txt”
,
“w”
));
count_factorial
<
long
double
>
(
fopen
(
“ldouble2。txt”
,
“w”
));
return
0
;
}
// 個數分別為21658, 21486 和 21486double與long double所得數字完全相同
(我確實太菜了
環境:x86_64,gcc10。2。0 on archlinux
2021年了,該換2021開頭的了,第一個以2021開頭的n!為16979!
由於Sterling公式的存在,尋找某個特點模式開頭的階乘還是比較容易的,比如以圓周率的前若干位開始的最小階乘數n!有:
d = 3, n = 9
d = 31, n = 62
d = 314, n = 62
d = 3141, n = 10044
d =31415, n = 50583
d = 314159, n = 1490717
d = 3141592, n = 5573998 開始數字是3141592381
d = 31415926, n = 65630447 開始數字是3141592602
d = 314159265, n = 688395641 開始數字3141592651957527
d = 3141592653, n = 5777940569 起始數字314159265301
d = 31415926535, n = 77773146302, 開始 數字314159265353488
d = 314159265358, n = 1154318938997開始數字3141592653584138
d = 3141592653589, n = 1544607046599開始數字31415926535899498
另外還有
927877657573029! = 3。141 592 653 589 793 763154362168869644108970257452834353669890 * 10^13485028080082190
當然如果不要求求最小結果,構造性的尋找結果,會很簡單。
比如我們想找20210417開頭的階乘數,我們先找一個充分大的10的冪,由於目標8位數,我們選擇從
開始,計算得到
而目標
為此我們需要讓小數部分增長0。825401945276067893551001564,這個數字乘以
轉化為1。9005582149209609973521088。
現在假設目標為
,k相對
不大,那麼小數部分增長取自然對數後為
可以求得
經檢驗,這個資料略小,可以調整為k=1949645206145
得出
在補充一個結果
問題:
前四位為
若
,
現兩邊取自然對數得:
根據尤拉-麥克勞林公式
可得:
觀察影象(圖1):
圖1
可見近似效果不太好,但夠數值計算。
現在我們定義符號
表示:
若
則稱
所以我們要找到一個較好的
使得
現置
計算
接下來計算
事實上,
由:
知:
好像有點麻煩,翻下書
所以
選擇比較多,我選
所以
先放結論:
再換底就行,坑我慢慢填
仍未完待續
(釋出於8。15下午,算是挖這個老問題,預計接下來算常數
留坑