分割数列を串刺しする合同式

このページはマス旅の一部です。 「整数の分割数」列といえば、オイラーの母関数の発見と説明が有名ですね。 それで終わりではありませんでした。 ラマヌジャンはその分割数列を串刺しにする合同式をみつけたり、 分割数列そのものをパラメータを使った関数にしたりと、さらに深堀しました。 今回は「分割数列」です。

1.オイラーの見つけたことを確認しよう。

ラマヌジャンに入る前に かるくオイラーの「分割数列」とその説明を確認しておこう。 「5の和分解」の問題を解くのに、 5=5, 4+1, 3+2, 3+1+1, 2+2+1, 2+1+1+1, 1+1+1+1+1 と相互の脈絡なく、数の大小関係だけで7通りを求めるでしょう。 オイラーさんなら、 1の倍数、2の倍数、3の倍数、4の倍数、5の倍数の指数の母関数を作り、 その積を求めるでしょう。 {1+x^1+x^(1+1)+x^(1+1+1)+x^(1+1+1+1)+x^(1+1+1+1+1)+...}× {1+x^2+x^(2+2)+...}× {1+x^3+x^(3+3)+...}× {1+x^4+....}× {1+x^5+...}×...... =1+ax^1+bx^2+cx^3+dx^4+ex^5+....... ここで、xの絶対値が1より小さいと仮定して、eが7になればよいですよね。 x進法だと思って係数を6桁分かける。 1の位はすべて1になるように反転してかけます。 111111×010101×001001×010001×100001×1.....=...753211だから、 1の位から順に1,a,b,c,d,e=1,1,2,3,5,7となる。 だから、e=7となるね。つまり、5次の係数が5の和分解7になるのです。 だから、1,a.b,c,d,eは0,1,2,3,4,5の和分解を表すことになるのですね。 1+y+y^2+y^3+.....=1/(1-y)とかけること、 φ(x)=(1-x)(1-x^2)(1-x^3)(1-x^4)......... nの和分解をp(n)とかくことにすると、 無限級数の積の左辺は 1/(1-x)*1/(1-x^2)*1/(1-x^3)*.....=1/φ(x)となります。 右辺はp(0)+p(1)x^1+p(2)x^2+p(3)x^3+p(4)x^5+p(5)x^5+.........です。 1/φ(x)は和分解数列の母だったのですね。 これで終わりではありません。 φ(x)を展開しましょう。 φ(x)=1-x^1-x^2+x^5+x^7-x^12-x^15+x^22+x^26+.....。 この項の偶数番目の指数をならべてみましょう。1,5,12,22です。 これはなんだかわかりすか、三角数、四角数ならぱっと出る人が多いでしょうけど、 これは5角数です。 5角数の公式は知らなくても、少し書き出せば、f(n)=n*(3n-1)/2が作れるでしょう。 n=1,2,3,4を入れると1,5,12,22となるのです。分割数の影に5角数あり。 話はこれで終わりません。 奇数番目の指数は0,2,7,15,26。これはゴミではないのです。 f(0)=0,f(-1)=2,f(-2)=7,f(-3)=15,f(-4)=26。 これがオイラーの見つけた「分割数の構造化と5角数との表裏一体性」でした。 では、これ以上に、ラマヌジャンは何を見つけたというのでしょうか?

2、ラマヌジャンが見つけたこと

ラマヌジャンはオイラーの見つけた分割数列p(n)を串刺しにしました。 数列p(n)をnとペアでならべてみましょう。 (n,p(n))のタプルです。 (1,1),(2,2),(3,3),(4,5), (5,7),(6,11),(7,15), (8,22),(9,30), (10,42),(11,56),(12,77), (13,101),(14,135),(15,176), (16,231),(17,297),................. p進的な距離のときもそうですが、大切な視点なのは、素数、差、剰余 そして、ラマヌジャンはブロードキャスト・串刺しでまとめてみることです。 p(n)が5の倍数になるnを抜き出すと、4,7,9,14です。 n≡4(mod 5)ならp(n)≡0(mod5) というきれいな決まりが見つかりますね。 p(n)が7の倍数になるnを抜き出すと、5,10,11,12です。 n≡5(mod 7)ならp(n)≡0(mod7) というきれいな決まりが見つかります。 p(n)が11の倍数になるnを抜き出すと、6,8,12,15,16,17です。 n≡6(mod 11)ならp(n)≡0(mod11) というきれいな決まりが見つかります。 ただ、1対1の法則というミクロなバカ真面目な視点ではなく、 もっと俯瞰した態度必要条件だけでいい、 素数が5,7,11と素数の番号が1ずつ増えると、 剰余が4,5,6と1ずつ増えるという 串刺し的、ブロードキャストな発想が面白いですよね。

3、分割数列を作ろう。

分割数列を作ろう> nの分割数をp[n]としよう。 階段を上るときに1段のぼりと2段のぼりの2通りがあるとしたら、 階段の上り方がフィボナッチ数列になることを思い出そう。 ある段の上り方は、1つ前の段と2つ前の段の上り方数の和になる。 この漸化式の考え方が動的計画法につながる。 2段上りまでできるのが、フィボナッチ、 3段上りまでできるのが、トリボナッチだった。 n段目までは1段前からn段前からすべて上ることができることを利用しよう。 ただし、この発想だけだと、いつもn以下の上り方ができるため、和分解にはならない。 たとえば、4=1+1+2、1+2+1、2+1+1のように4段上りには順番がつけられる。 そこで、 最初は各段に1段上りだけでいく、 次は各段に2段上りをつけたす。 3段上りをつけたす。 4段上りをつけたす。。。。。 このように、「上りのサイズ」は最後が大きくなるようにすればよい ですね。 そうすると、1,1,2は1+1+2に決まる。 では、具体的なコードにつながるように例を作ろう。 最初にすべてのp[n]は0にしておこう。 0段上りは1通りで、p[0]=1がフィックス。 たとえば、p[5]を求めたいとしよう。 1段のぼりのドミノ倒しでp[1]から[5]までがすべて0+1=1通りになる。p[1]=1がフィックス。 2段のぼりのドミノ倒しで、+2をする前の分割数を更新するのが2系統できる。 p[2]+=p[0]=>1+1=2, p[3]+=p[1]=>1+1=2,p[4]+=p[2]=>1+2=3,p[5]+=p[3]=>1+2=3。 p[2]=2がフィックス。 3段上りで3系統のドミノ倒しで、更新 P[3]+=p[0]=>2+1=3, p[4]+=p[1]=>3+1=4, p[5]+=p[2]=>3+2=5。p[3]=3でフィックス。 4段上りのドミノ倒しで,p[4]+=p[0]=3+1=4, p[5]+=p[1]=5+1=6。p[4]=4でフィックス。 5段上りのドミノ倒しで,p[5]+=p[0]=6+1=7でフィックス。 これをコードにすればよいね。 課題:分割数列p(n)を動的計画法でもとめ、ラマヌジャンの法5,7,11の串刺しテストをしよう。 def generate_partitions(max_n): """オイラーの動的計画法アプローチで高速に分割数列p(n)を生成""" dp = [0] * (max_n + 1) dp[0] = 1 for num in range(1, max_n + 1): for i in range(num, max_n + 1): dp[i] += dp[i - num] return dp # ========================================== # 遊びのパラメータ:どこまでの項を観察するか max_check = 100 # ========================================== p_list = generate_partitions(max_check) print(" 【5の串】 n ≡ 4 (mod 5) のとき p(n) を 5 で割った余り") for n in range(max_check + 1): if n % 5 == 4: print(f"p({n:2d}) = {p_list[n]:<15} -> 余り: {p_list[n] % 5}") print("\n 【7の串】 n ≡ 5 (mod 7) のとき p(n) を 7 で割った余り") for n in range(max_check + 1): if n % 7 == 5: print(f"p({n:2d}) = {p_list[n]:<15} -> 余り: {p_list[n] % 7}") print("\n 【11の串】 n ≡ 6 (mod 11) のとき p(n) を 11 で割った余り") for n in range(max_check + 1): if n % 11 == 6: print(f"p({n:2d}) = {p_list[n]:<15} -> 余り: {p_list[n] % 11}") [OUT] 【5の串】 n ≡ 4 (mod 5) のとき p(n) を 5 で割った余り p( 4) = 5 -> 余り: 0 p( 9) = 30 -> 余り: 0 p(14) = 135 -> 余り: 0 p(19) = 490 -> 余り: 0 p(24) = 1575 -> 余り: 0 p(29) = 4565 -> 余り: 0 p(34) = 12310 -> 余り: 0 p(39) = 31185 -> 余り: 0 p(44) = 75175 -> 余り: 0 p(49) = 173525 -> 余り: 0 p(54) = 386155 -> 余り: 0 p(59) = 831820 -> 余り: 0 p(64) = 1741630 -> 余り: 0 p(69) = 3554345 -> 余り: 0 p(74) = 7089500 -> 余り: 0 p(79) = 13848650 -> 余り: 0 p(84) = 26543660 -> 余り: 0 p(89) = 49995925 -> 余り: 0 p(94) = 92669720 -> 余り: 0 p(99) = 169229875 -> 余り: 0 【7の串】 n ≡ 5 (mod 7) のとき p(n) を 7 で割った余り p( 5) = 7 -> 余り: 0 p(12) = 77 -> 余り: 0 p(19) = 490 -> 余り: 0 p(26) = 2436 -> 余り: 0 p(33) = 10143 -> 余り: 0 p(40) = 37338 -> 余り: 0 p(47) = 124754 -> 余り: 0 p(54) = 386155 -> 余り: 0 p(61) = 1121505 -> 余り: 0 p(68) = 3087735 -> 余り: 0 p(75) = 8118264 -> 余り: 0 p(82) = 20506255 -> 余り: 0 p(89) = 49995925 -> 余り: 0 p(96) = 118114304 -> 余り: 0 【11の串】 n ≡ 6 (mod 11) のとき p(n) を 11 で割った余り p( 6) = 11 -> 余り: 0 p(17) = 297 -> 余り: 0 p(28) = 3718 -> 余り: 0 p(39) = 31185 -> 余り: 0 p(50) = 204226 -> 余り: 0 p(61) = 1121505 -> 余り: 0 p(72) = 5392783 -> 余り: 0 p(83) = 23338469 -> 余り: 0 p(94) = 92669720 -> 余り: 0
<振り返り> ラマヌジャンはこのようなPCを使った力業を使うはずもない。 それなのにすごい近似公式をハーディらと作ってしまったらしい。 p(n)~1/(4n√3) e^(π√(2n/3))とか、 p(n)=1/(2π√2) d/dn(e^λn/λn) +O(e^Hn*1/2) , λn=√(n-1/24)などです。 ラマヌジャン・ハーディの公式ですね。 p(200),p(243),p(721),p(14031)の計算などを巡って、 マクマホン、レーマー、ラドマッハーらも相互に刺激しあって 近似式を計算したり、開発しあったようです。 課題:動的計画法のベタな方法のp(n)と近似式との差を比べてみよう。 import math import matplotlib.pyplot as plt def generate_partitions(max_n): """オイラーの動的計画法で厳密な分割数p(n)を生成(真値)""" dp = [0] * (max_n + 1) dp[0] = 1 for num in range(1, max_n + 1): for i in range(num, max_n + 1): dp[i] += dp[i - num] return dp def ramanujan_approx(n): """ラマヌジャン・ハーディの簡易近似式""" if n <= 0: return 0 # 分母: 4 * n * sqrt(3) denominator = 4 * n * math.sqrt(3) # 指数部: pi * sqrt(2n / 3) exponent = math.pi * math.sqrt((2 * n) / 3.0) # 一発計算! return math.exp(exponent) / denominator # ========================================== # 遊びのパラメータ:n=10 から n=500 まで比較してみる max_n = 500 # ========================================== p_true = generate_partitions(max_n) print(f"{'n':>5} | {'真値 p(n)':>25} | {'ラマヌジャン近似値':>18} | {'誤差の割合 (%)':>10}") print("-" * 75) # 代表的なnの値をピックアップして比較 test_points = [10, 50, 100, 200, 300, 500] for n in test_points: true_val = p_true[n] approx_val = ramanujan_approx(n) # 誤差の割合 = |真値 - 近似値| / 真値 * 100 error_percent = (abs(true_val - approx_val) / true_val) * 100 print(f"{n:5d} | {true_val:27d} | {approx_val:25.2f} | {error_percent:9.2f}%") [OUT] n | 真値 p(n) | ラマヌジャン近似値 | 誤差の割合 (%) --------------------------------------------------------------------------- 10 | 42 | 48.10 | 14.53% 50 | 204226 | 217590.50 | 6.54% 100 | 190569292 | 199280893.35 | 4.57% 200 | 3972999029388 | 4100251432187.85 | 3.20% 300 | 9253082936723602 | 9494094811674990.00 | 2.60% 500 | 2300165032574323995027 | 2346386625611059167232.00 | 2.01% 課題:PDとラマヌジャン・ハーディの近似式を視覚化してくらべよう。 import math import matplotlib.pyplot as plt def generate_partitions(max_n): """オイラーの動的計画法で厳密な分割数p(n)を生成(真値)""" dp = [0] * (max_n + 1) dp[0] = 1 for num in range(1, max_n + 1): for i in range(num, max_n + 1): dp[i] += dp[i - num] return dp def ramanujan_approx(n): """ラマヌジャン・ハーディの簡易近似式""" if n <= 0: return 0 denominator = 4 * n * math.sqrt(3) exponent = math.pi * math.sqrt((2 * n) / 3.0) return math.exp(exponent) / denominator # ========================================== # 遊びのパラメータ:n=10 から n=500 までをプロット max_n = 500 # ========================================== # データの準備 p_true_all = generate_partitions(max_n) n_list = list(range(10, max_n + 1)) true_vals = [p_true_all[n] for n in n_list] approx_vals = [ramanujan_approx(n) for n in n_list] error_percents = [(abs(true_vals[i] - approx_vals[i]) / true_vals[i]) * 100 for i in range(len(n_list))] # グラフ描画領域の設定 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5)) # ---------------------------------------------------- # 左グラフ:真値 vs 近似値(対数スケール) # ---------------------------------------------------- ax1.plot(n_list, true_vals, label="True p(n) (Euler DP)", color="#1f77b4", linewidth=2.5) ax1.plot(n_list, approx_vals, label="Ramanujan Approx", color="#ff7f0e", linestyle="--", linewidth=2) ax1.set_yscale("log") # 縦軸を対数スケールに ax1.set_title("p(n) Growth: True vs Ramanujan (Log Scale)", fontsize=12, fontweight="bold") ax1.set_xlabel("n", fontsize=10) ax1.set_ylabel("p(n) (Log Value)", fontsize=10) ax1.grid(True, which="both", linestyle=":", alpha=0.6) ax1.legend(fontsize=10) # ---------------------------------------------------- # 右グラフ:誤差の割合(%)の収束 # ---------------------------------------------------- ax2.plot(n_list, error_percents, color="#d62728", linewidth=2) ax2.set_title("Relative Error Rate (%)", fontsize=12, fontweight="bold") ax2.set_xlabel("n", fontsize=10) ax2.set_ylabel("Error (%)", fontsize=10) ax2.set_ylim(0, 15) # 誤差の範囲を0〜15%で見やすく固定 ax2.grid(True, linestyle=":", alpha=0.6) # レイアウトを整えて表示 plt.tight_layout() plt.show()
Image
神がかってますね。 こんな不規則にみえて、しかも猛烈に増大する分割数列をきれいに、 √3、π、指数関数を使って、高い精度で予測できるなんて、 絶句です。 オイラーは天才でしたが、ラマヌジャンは神の子ですね。