スターリング数と多重分割を計算する
スターリング数の漸化式
前章では重複順列から段階を踏んで多重分割を生成し、包除原理を使いスターリング数を計算しました。ここでは、スターリング数の興味深い漸化式を使い、個数を計算し多重分割を生成します。
スターリング数を漸化式を使って使って計算する
SymPyライブラリのstirling関数による一覧表の作成
漸化式を考える上でのヒントにするため、SymPyライブラリの#5のstirling関数を使ってn=10までのスターリング数の一覧表を作成します。
表を作成する関数
- def print_table(array, w):
- print(f' ' * (w+2), end='')
- for k in range(len(array[0])):
- print(f'{k:^{w+2}}', end=' ')
- print('sigma')
- print(f'-' * (w+1), end='+')
- for k in range(len(array)+1):
- print('-' * (w+2), end='+')
- print()
- for n in range(len(array)):
- print(f'{n:^{w+1}}', end='|')
- sigma = 0
- for k in range(len(array[0])):
- print(f"{array[n][k]:>{w+2}}", end=' ')
- sigma += array[n][k]
- print(f"{sigma:>{w+2}}")
- n=10;k=10
- array = [[0]*(k+1) for _ in range(n+1)]
- for i in range(1,n+1):
- for j in range(1,k+1):
- array[i][j]=int(stirling(i,j))
- print_table(array, 3)
3. あらかじめ10×11の配列を初期化しておきます。2次元の配列を初期化するときはリスト内包表記を使わないと、集計でエラーになってしまいます。
SymPyライブラリのstirling関数を使ってスターリング数を計算し、arrayを書き換えます。
スターリング数の一覧表から漸化式を導く
表を眺めていると、スターリング数には次3つの法則があることがわかります。
① k=1の場合
学生がn人いてタクシー1台に乗る場合は、分乗する余地もなく1通りしかありません。タクシーの定員を考えなければ、nがどんなに大きくてもスターリング数は常に1になります。
② n=kの場合
n人でn台のタクシーに分乗する場合は、1人1台になります。タクシーにラベルはつけない前提なので、組分けを考える余地はなくスターリング数は1になります。
③ ①②以外の多くの場合
前例のようにS(6, 4)つまり、6人が4台のタクシーに分かれて市内観光をする予定にしていました。ところが急遽学生が1人増えることになり、これにともないタクシーを1台増やすと考え、スターリング数はS(7, 5)となります。
S(6, 4)とS(7, 5)の差は、新しく来た学生の扱いにより、次の2つのケースが考えられます。
1 学生6人がタクシー4台への分割はそのままにして、新しく来た学生は1人でタクシーに乗る。この場合、新しい分割の数はS(6, 4)と同じです。
2 タクシーが増える前提で予め6人を5台に分けておき、新しく来た学生は5台のうちいずれかに乗る。この場合、新しい分割の数はS(6, 5)×5になります。
1,2は背反で同時に起こることはありえないので、S(7,5)=S(6,4)+S(6,5) ×5=65+15×5=140となるはずです。
漸化式が正しいか、個別に検証する
上記の数式を、SymPyライブラリのstirling関数を使って検証します。
漸化式でスターリング数を計算
- print(f'S(6, 4)= {stirling(6, 4)} S(6, 5)={stirling(6, 5)}')
- print(f'S(7, 5)={stirling(7, 5)}')
- print(f'S(6, 4)+5×S(6, 5)={stirling(6, 4)+5*stirling(6, 5)}')
S(6, 4)= 65 S(6, 5)=15 S(7, 5)=140 S(6, 4)+5×S(6, 5)=140
3. 1.で計算したS(6, 4)とS(6, 5)と2.で計算したS(7, 5)を使い、漸化式が正しいことを確認します。
(1)の考え方が正しいこと確認できました。漸化式を一般化すると次の式になります。
スターリング数の漸化式
① k==1のとき1
② n==kのとき1
③ $S(n, k)=S(n-1, k-1)+k\; S(n-1, k)$
漸化式を使てスターリング数を計算し、多重分割を生成する
漸化式を使ってスターリング数を計算する関数
この漸化式をもとに再帰関数を使いスターリング数を求めるstirling_r関数を作成し、S(7, 5)を計算します。
漸化式を使ったスターリング数の計算
- def stirling_r(n, k):
- if k==1 or n==k:
- return 1
- return stirling_r(n-1, k-1) + k*stirling_r(n-1, k)
- stirling_r(7, 5)
140
2. 漸化式の①、②に対応します。
4. 漸化式の③に対応します。stirling関数からstirling関数を引数をnやkを1つずつ減らしながら呼び出します。引数n,kの値はどんどん減っていくので最終的には2.の条件を満たし、1を返します。
このように簡単なプログラムで、複雑な計算ができるのは驚きです。s(7, 5)の結果は#9と同じく140となります。
さらに、漸化式を使いスターリング数の一覧を作成し、SymPyライブラリのstirling関数と比較し正しく計算できていることを確認します。漸化式を使った計算では、#10で作成したstirling_r関数を使うことも考えられますが、100通りの計算に対して1つ1つ関数を適用するのは相当な処理量になるので、漸化式の定義を使う方法でコンピュータへの負荷を減らすようにします。
漸化式の考え方を使ったスターリング数の一覧を関数と比較
- n=10;k=10
- recursive_array = [[0]*(k+1) for _ in range(n+1)]
- for i in range(1,n+1):
- recursive_array[i][1]=1
- recursive_array[i][i]=1
- for i in range(1,n+1):
- for j in range(2,i):
- recursive_array[i][j]=recursive_array[i-1][j-1]+j*recursive_array[i-1][j]
- recursive_array == stirling_sympy
- print_table(recursive_array, 4)
True
2. 漸化式による一覧を作成するため、あらかじめ10×10の配列を用意し、全ての要素に0で初期化します。
3. 2からi=1、n=kのときスターリング数は1になるので、該当する箇所を1に変更します。
6. 1からnまでをiでカウントアップし、各々のiについて2からi-1まで(iの場合は.3.で変更済)まで、漸化式の③を使いスターリング数を計算します。
漸化式を使い、多重分割を生成する
同じ漸化式を使い、多重分割をリストとし一覧を作成し、以前作成したリストと比較します。ここではS(6, 4)を生成するとして、
図のとおり、S(n-1, k-1)においては、新しく増えた5は単独の新しい多重分割になるので、リストとして追加します。一方S(n-1, k-1)に対しては、新しく増えた5はすでに4つある多重分割のいずれかに追加されるので、4つの重複分割に順次追加していきます。この考え方により、多重分割を生成する関数multiset_funcを作成します。
漸化式を使った重複分割の作成
- def multiset_func(n, k):
- if k == 1:
- return [[list(range(n))]]
- if n == k:
- return[[[i] for i in range(n) ]]
- array=[]
- for elm in multiset_func(n-1, k-1):
- array.append(elm+[[n-1]])
- for elm in multiset_func(n-1, k):
- for i in range(len(elm)):
- array.append(elm[:i] + [elm[i] + [n-1]] + elm[1+i:])
- return array
- sorted(stirling_surjective_list)==sorted(multiset_func(6, 4))
True
2~3 k=1の場合0からn-1までの要素が1つの分割になります。例えば、n=5の場合[[0, 1, 2, 3, 4]]となります。
4~5 k=nの場合、0からn-1までの要素が1つずつの分割になります。例えば、n=5の場合[[0], [1], [2], [3], [4]]となります。
7~8 漸化式③の左辺の処理をします。multiset_func(n-1, k-1)で返された多重分割について、新しいnは単独の分割になるので、これまでの分割に[[n-1]]を結合して返します。
9~11 漸化式③の左辺の処理をします。multiset_func(n-1, k)で返された多重分割は、分割のリストがk個あります。新しいn-1はこのk個の分割のいずれかに追加して返します。