有,其中最小的自然數為 n=384。

384!
=
20205002129876021521366889509458521857103131384424164703865633675390
18051160495716743506601574409795917922928563661299169269617068846773
01229576788637571823309033713973447373497759772077557498674190878950
85555858041382225358897077307262143988184652395676694547987597481082
42045386512721839333046204349301762565136352275077524224647524747714
86396742989664230239433214649824117145479984513359446662469851311138
75121097651996768775622775133017838802100903333902268129172961937277
25084255908023383476149641735328778812131475694211759651482790988260
72666361443374874234852854408882112301367626134918709620006585891596
43594934505150789217346436153635132813435806722718188852357091873258
00311071868796569251630544786139037035591559504566681600000000000000
00000000000000000000000000000000000000000000000000000000000000000000
000000000000


其實只要把滿足 [公式] 的所有 [公式] 找到就行了,所以不需要大整數運算即可在很快的時間內得出 [公式] 之內滿足要求的數恰好有 [公式] 個。

另外,我們可以把 [公式] 的十進位表示看成一個各位均為隨機的數(但是這一點未經證明),所以根據著名的本福特定律,滿足條件的數在全體自然數中所佔比例並不是 [公式] ,而是 [公式] ,可以看到逼近於此比例的速度還是比較慢的。


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 ",ans); return 0; }


先給個不那麼嚴謹的證明

題目等價於存在一對正整數 [公式] 滿足:

[公式]

簡單變形一下:

[公式]

[公式][公式] ,上式就可以寫成:

[公式]

故只要存在正整數 [公式] 滿足 [公式] 就可以了(這裡 [公式] 表示 [公式] 的小數部分)

取一個足夠大的正整數 [公式] ,使得區間 [公式] 內的正整數個數大於 [公式] ,對於其中任一正整數 [公式] ,容易知道:

[公式]

假設該區間內最小的正整數為 [公式] ,最大的正整數為 [公式] ,那麼 [公式] ,故:

[公式]

所以在 [公式] 中必然存在正整數 [公式] 滿足 [公式] ,故 [公式]

直觀上理解就是 [公式] 「增加」的過程中「超過」了 [公式] ,而每次的「步長」又小於 [公式] ,所以一定存在一個臨界的 [公式] 滿足 [公式]


其實編程簡單地枚舉一下就知道 [公式] 是滿足條件的一個正整數,但是精確地計算階乘對於計算機來說開銷不少。 @SarvaTathagata 的回答裏說可以利用 [公式] 這個式子來避免大整數運算,可是在實際操作中也會遇到精度相關的問題

我們用C++來簡單測試一下,找到所有 [公式] 以內滿足條件的正整數

// test.cpp
#include &
#include &

template&
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 ", i); } } printf("%d ", count); } int main() { count_factorial&(fopen("float.txt", "w"));
count_factorial&(fopen("double.txt", "w"));
count_factorial&(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
", result);

return 0;
}
// 輸出16172037949214623863

使用C++標準庫,用三種浮點類型分別計算這個結果

// test2.cpp
#include &
#include &
#include &

template&
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&();
auto s2 = test&();
auto s3 = test&();

cout &

得到

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 & diff.txt

這個diff.txt有2048行,非常的巧


在dalao @SarvaTathagata 的提點下做了一些改進,取得了顯著的效果。

// test_opt.cpp
#include &
#include &

const long double SUP = std::log10(2.021L);
const long double INF = std::log10(2.020L);

template&
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 ", i); } } printf("%d ", count); } int main() { count_factorial&(fopen("float2.txt", "w"));
count_factorial&(fopen("double2.txt", "w"));
count_factorial&(fopen("ldouble2.txt", "w"));

return 0;
}
// 個數分別為21658, 21486 和 21486double與long double所得數字完全相同

(我確實太菜了

環境:x86_64,gcc10.2.0 on archlinux


顯然

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"):
print("{}! = {}".format(math, sum))
math += 1
#EOF


384! = 202050021298760215213668895094585218571031313844241647038656336753901805116049571674350660157440979591792292856366129916926961706884677301229576788637571823309033713973447373497759772077557498674190878950855558580413822253588970773072621439881846523956766945479875974810824204538651272183933304620434930176256513635227507752422464752474771486396742989664230239433214649824117145479984513359446662469851311138751210976519967687756227751330178388021009033339022681291729619372772508425590802338347614964173532877881213147569421175965148279098826072666361443374874234852854408882112301367626134918709620006585891596435949345051507892173464361536351328134358067227181888523570918732580031107186879656925163054478613903703559155950456668160000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

之前看到過字元串乘法的題目,今天趁著無聊寫著玩~不過好久不寫python了,有點生疏~

def multiply(a, b):
"""字元串乘法,不限制長度
by [email protected]"""
if len(a)&= len(b)
result = "0"
for i in range(len(b)):
result = multiply_a(result, multiply_m(a, b[-i-1], i))
return result

def multiply_m(a, bi, n=0): # """單個數與a相乘,n為bi後面的0
if len(bi)&>1:
return
add = 0
result = ""
bi = int(bi)
for i in a[::-1]:
result = str((bi*int(i)+add) % 10) + result
add = (bi*int(i)+add) // 10
if add:
return str(add)+result + "0"*n
return result + "0"*n

def multiply_a(a, b): #字元串相加
if len(a)&= len(b)
b = "0"*(len(a)-len(b)) + b
add = 0
result = ""
for i in range(-1, -1*len(a)-1, -1):
result = str((int(a[i])+int(b[i])+add) % 10) + result
add = (int(a[i])+int(b[i]) + add) // 10
return str(add)+result if add&>0 else result

if __name__ == __main__:
s = "1" #階乘結果
i = "1" #i的階乘
while 1:
i = multiply_a(i, "1") #i+1
s = multiply(s, i) #計算新的階乘
if s.startswith("2020"):
print(i+"! = ", s, "
")
break

2021年2月8日 15:27:02

沒想到還有人看。我看了一下自己前兩天寫的代碼,什麼玩意,莫名其妙的命名,連個注釋都沒加~

經評論區提醒,python3的整形沒有長度限制,所以不需要用字元串去做,直接算就好了。

result = 1 #階乘結果
i = 1 #i的階乘
n = 2 #結果數量
while n:
i += 1
result *= i #計算階乘
if str(result).startswith("2020"):
print("%d! = %d
"%(i,result))
n -= 1


推薦閱讀:
相關文章