FUNCoder-第8回活動
第8回活動 2024/06/26 18:15~19:45 @593教室
/icons/hr.icon
はじめに
ICPC に参加しない部員へ
$ 20分ほどしたら本日の話題に入っていきます。本日は比較的濃い内容となっていますのであらかじめ読んでいても全く問題ありません。
ICPC に参加する部員へ
ICPC の練習をするメンバーはなるべく本番環境に近づけた方が良いと思うので、騒がしくない隣の部屋なので$ 3時間ほど集中して取り組むのが良いと思います。
初回は、本番同様の時間で取り組むのが良いと思います。というのも、実際の時間を経験することで「長くてやることがなかった」、「もう少し時間あればできた」、「〇〇なら本番までにできそう」など色々な発見があると思います。
なので時間を測って取り組むことを強くオススメします。
全体へ
新年度から活動を始めて3ヶ月が経とうとしていますが、メンバーのレベル・意欲・活動日数など様々な部分に差が出始めてきた頃かと思います。
中間テストやブロック崩しの発表会等がひと段落し、競プロの力をもっと付けていきたい、と思っている学生はどんどん自主的に活動していくことをオススメします。
何度も言っていますが、競プロは週1回、90分程度集まっただけでは中々できるようにはなりません。伸ばしていこと思ったらどうしても1人で勉強する時間というのが必要不可欠となります。なので隙間時間を見つけて効率的な学習を心がけていきましょう!
個人的には毎週新しいことをどんどん取り入れて活動していこうと思っておりますので今後ともよろしくお願いいたします。ご要望やご意見等ありましたら遠慮なくご連絡ください。
/icons/hr.icon
本日のタイトル
本日は フィボナッチ数列の高速化 に焦点を当てながら様々なアルゴリズムを学習していこうと思います。
/icons/hr.icon
1. フィボナッチ数列とは?
$ 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, \dots といったように前$ 2つの数字を足したものが次の項の値になる数列をフィボナッチ数列といいます。
一般化すると、以下の式で表されます。
$ a_{n} = a_{n - 1} + a_{n - 2} \quad (n \geq 3, \quad a_1 = 1, \quad a_2 = 1)
$ a_1と$ a_2が定まると任意の$ a_nを求めることができます。
一般項は以下の式で用いられます。
$ a_n = \frac{1}{\sqrt{5}} \left\{ \left( \frac{1 + \sqrt{5}}{2} \right) ^ n - \left( \frac{1 - \sqrt{5}}{2} \right) ^ n \right\}
黄金比が出てきたりして、かなり美しい式で表されます。
/icons/hr.icon
2. フィボナッチ数列の性質
任意のフィボナッチ数列の項 $ a_i は、$ a_i = a_1 * x + a_2 * y で表すことができます。
具体的には、
$ a_3 = a_2 + a_1
$ a_4 = a_3 + a_2 = 2a_2 + a_1
$ a_5 = a_4 + a_3 = 3a_2 + 2a_1
$ a_6 = a_5 + a_4 = 5a_2 + 3a_1
$ a_7 = a_6 + a_5 = 8a_2 + 5a_1
といった感じで、$ x, y が分かれば任意の項の値を求めることができます。
また、この$ 2つの値は前計算で求めることができます。
この考えが今日の最終目標である 行列累乗 につながってきたりもします。
/icons/hr.icon
3. 再帰関数でフィボナッチ数列を求める
まずは再帰関数(DFS)を用いてフィボナッチ数列を求めることを考えます。
再帰関数とは、関数を再帰する、つまり関数の中で関数を呼び出すイメージです。
再帰関数の具体例として、$ 1\sim 10までの和を求めることを考えます。
普通にやれば以下のように求めるのが一般的だと思います。
code:Python
ans = 0
for i in range(1, 11):
ans += i
print(ans)
code:Python
N = 10
ans = N * (N + 1) // 2
print(ans)
前者は for 文を使用して全探索を行うことで総和を求めているので計算量は$ 1から$ Nまでの総和を求めるとすると$ O(N)となります。
$ N \leq 10^5などであれば間に合いますが、$ N \leq 10^{18}などとなるとまず間に合いません。
後者は高校数学で学習する$ \sumを用いて高速に計算をしています。
この場合の計算量は for 文を用いず、式$ 1つで求めているので$ O(1)です。
なので$ N \leq 10^{18}でも余裕で間に合います。
しかし、この問題をあえて再帰で解くと以下のようになります。
code:Python
def f(x: int):
if x <= 0:
return 0
return x + f(x - 1)
N = 10
print(f(10))
これはまず最初に$ f(10)を呼び出します。
すると、$ x \leq 0ではないので return x + f(x - 1)が呼び出され、返り値は$ 10 + f(9)になります。
次に$ f(9)が呼び出されたので、同様に考えると返り値は$ 9 + f(8)になります。
これを$ 10以下全てに対して行うと以下のようになります。
$ \begin{aligned} f(10) &= 10 + f(9) \\ &= 10 + (9 + f(8)) \\ &= 10 + 9 + (8 + f(7)) \\ &= 10 + 9 + 8 + (7 + f(6)) \\ &= 10 + 9 + 8 + 7 + (6 + f(5)) \\ &= 10 + 9 + 8 + 7 + 6 + (5 + f(4)) \\ &= 10 + 9 + 8 + 7 + 6 + 5 + (4 + f(3)) \\ &= 10 + 9 + 8 + 7 + 6 + 5 + 4 + (3 + f(2)) \\ &= 10 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + (2 + f(1)) \\ &= 10 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + (1 + f(0)) \\ &= 10 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 + 0 \\ &= 55 \end{aligned}
最後の$ f(0)が呼び出された場合に限り$ x \leq 0を満たし、$ 0を return するので関数を呼び出さずにこの再帰は終了します。
これを一般に$ 1 \sim Nまでの和を再帰関数で呼び出す場合は以下のようになります。
code:Python
def f(x: int):
if x <= 0:
return 0
return x + f(x - 1)
N = int(input())
print(f(N))
ここまで来たら話を戻します。
今やりたいことは再帰関数を用いてフィボナッチ数列を求めることです。
これは以下のように実現できます。
code:Python
def f(x: int):
if x <= 2:
return 1
return f(x - 1) + f(x - 2)
N = int(input())
print(f(N))
フィボナッチ数列では前$ 2項を足し合わせた値が答えになるので$ f(x)を呼び出した場合は返り値を$ f(x - 1) + f(x - 2)とすることで再帰関数を実現することができます。
再帰を終了する条件(関数を呼び出さないタイミング)は、$ a_1 = 1, a_2 = 1なので$ x \leq 2であれば return 1をしてあげます。
このようにすることで答えを求めることができます。
一旦座学も飽きたと思うので以下の問題を解いてみましょう(10分)
再帰でフィボナッチ数列を解いた際の計算量を考えてみます
と、思いましたが、めんどかったので既存の解説を参考にしてください。
/icons/hr.icon
4. dp を用いてフィボナッチ数列を求める
次に、動的計画法を用いてフィボナッチ数列を求めることを考えます。
動的計画法(dinamic programming)とは、漸化式を立式し、解いていくような感じです。
$ {\mathrm dp}[i] = {\mathrm フィボナッチ数列の第i項} と定義します。
$ {\mathrm dp}[1] = 1, {\mathrm dp}[2] = 1 です。
code:Python
N = int(input())
if N <= 2:
print(1)
else:
for i in range(3, N + 1):
print(dpN) # フィボナッチ数列の第 N 項を出力する このような処理でフィボナッチ数列の処理を行うと、再帰では指数オーダーかかっていたものが、$ O(N)まで落ちるので$ N \leq 10^5くらいまでなら余裕で解けるようになります。
最後に$ N \leq 10^{18}でも解けるような解法を考えます。
/icons/hr.icon
5. 行列累乗の準備をする
ここからが今日の最終目標である行列累乗による高速化です。
名前の通り 行列 が出てきます。しかし、身構える必要は特になく線形代数を学び始めてる1年生であれば問題なく理解できるので頑張ってついて来てください。
まずは行列累乗というアルゴリズムを学ぶ前に 行列をプログラミングで処理する方法 について学習をします。
次のような$ N \times Mと$ M \times Kの行列を掛け算することを考えます。
計算した結果は$ N \times K行列になります。
$ \left( \begin{array} {ccccc} a_{11} & \cdots & a_{1i} & \cdots & a_{1M}\\ \vdots & \ddots & & & \vdots \\a_{i1} & & a_{ii} & & a_{iM} \\ \vdots & & & \ddots & \vdots \\ a_{N1} & \cdots & a_{Ni} & \cdots & a_{NM} \end{array} \right) \cdot \left( \begin{array} {ccccc} a_{11} & \cdots & a_{1i} & \cdots & a_{1K}\\ \vdots & \ddots & & & \vdots \\a_{i1} & & a_{ii} & & a_{iK} \\ \vdots & & & \ddots & \vdots \\ a_{M1} & \cdots & a_{Mi} & \cdots & a_{MK} \end{array} \right) = \left( \begin{array} {ccccc} a_{11} & \cdots & a_{1i} & \cdots & a_{1K} \\ \vdots & \ddots & & & \vdots \\a_{i1} & & a_{ii} & & a_{iK} \\ \vdots & & & \ddots & \vdots \\ a_{N1} & \cdots & a_{Ni} & \cdots & a_{NK} \end{array} \right)
この行列を便宜上$ A \times B = Cと表すことにします。
つまり計算結果の$ N \times K行列を$ Cとします。
この時、$ Cの$ i \: (1 \leq i \leq N)行$ j \: (1 \leq j \leq K)行成分の値を$ C_{i,j}とします。
この時、具体例をいくつか考えてみます。
$ C_{1, 1} = A_{1, 1} B_{1, 1} + A_{1, 2}B_{2, 1} + A_{1, 3}B_{3,1} + \dots + A_{1,M} B_{M, 1}
$ C_{1, 2} = A_{1, 1} B_{1, 2} + A_{1, 2}B_{2, 2} + A_{1, 3}B_{3,2} + \dots + A_{1,M} B_{M, 2}
$ \vdots
$ C_{2,1} = A_{2,1} B_{1,1} + A_{2, 2}B_{2, 1} + A_{2,3}B_{3,1} + \dots + A_{2,M} B_{M,1}
$ \vdots
$ C_{N,K} = A_{N,1} B_{1,K} + A_{N,2} B_{2,K} + A_{N,3} B_{3,K} + \dots + A_{N,M} B_{M, K}
これは既習済みだと思うので各自時間を取って確認してみてください。
これを整理すると、一般的に以下のことが成り立ちます。
$ C_{i,j} = A_{i,1} B_{1,j} + A_{i,2} B_{2,j} + A_{i,3} B_{3,j} + \dots + A_{i,M} B_{M,j}
ここで途中式について着目すると、これは$ M項の和から成り立っているため、$ C_{i,j}成分の値は$ O(M)で求めることができます。
したがって、全ての成分について求める$ Cは$ N \times Kなので全体の計算量は$ O(MNK)で求めることができます。
これをプログラムに起こすと下記のようになります。
code:Python
N, M, K = map(int, input().split())
# A は N*M 行列
# B は M*K 行列
# C は N*K 行列
C = [0*K for _ in range(N)] # C の (i, j) 成分を全探索する
for i in range(N):
for j in range(K):
# 各 (i, j) 成分に対しては O(M) で求められる
for k in range(M):
# N 行を 1 行ずつ出力する
for i in range(N):
では、これを実際に手元で解いてみましょう。
ヒント: 入力の$ lを上記で解説した$ Kだと思えば良いです。上のコードを貼っ付ければ AC できます。
また、この後この行列を何回も繰り返し計算する必要が出てくるので以下のように関数にしておきます。
code:Python
def matrix_multi(A: list, B: list) -> list:
N, M, K = len(A), len(B), len(B0) C = [0*len(B0) for _ in range(N)] # C は N*K 行列 for i in range(N):
for j in range(K):
for k in range(M):
return C
/icons/hr.icon
行列の解説が終わったので次は いよいよ行列累乗について解説 します。
まずはじめに、行列累乗は主に次の$ 2つの処理が必要です。
1. 行列の計算
2. (繰り返し二乗法)ダブリング
1. に関しては先ほどの解説の通りです。
問題は 2. についてです。
繰り返し二乗法は高度なアルゴリズムの1つですが、この際なんとなくでも良いので学んでみましょう。
具体例として$ 2^{101}を求めることを考えます。
この指数部分の数を$ 2の冪乗で表すことを考えます。
$ 101 = 2^6 + 2^5 + 2^2 + 2^0 = 64 + 32 + 4 + 1 = 101と表すことが出来ます。
これは$ 101を$ 2進数に置き換えればわかりやすい話で、$ (101)_{10} = (1100101)_2であり、この下から$ k番目が$ 1の時の$ 2^k和を求めると$ 101が求まります。
このように$ 1が立っている状態を「ビットが立っている」と表現することがあります。
つまり言い換えると、ビットが立っていない桁に関しては計算しなくても求めることができ、ビットが立っている場合に$ 2^kを足し合わせることで求めることが出来ます。
ではここでビットが立っているかどうかをどのように判定するのか考える必要が出てきます。
これは$ 1の位を見れば良いですね。
これを聞いて「?」となった方は FUNCoder の第4回活動の こちら をもう一度復習してください。 話を元に戻します。
求めたい数は$ 2^{101}であり、これを$ 2の冪乗を用いて表すと、
$ 2^{101} = 2^{64} \cdot 2^{32} \cdot 2^4 \cdot 2^1となります。
したがって、$ 2^{101}を以下の手順で求めることを考えます。
1. 初期値が$ 0の変数 ansを用意する
2. while 文を用いて$ 2^iから$ 2^{i+1}を求める
3. ビットが立っている場合に限り、ansに$ 2^iを掛ける
ここまでの問題をプログラムに落とし込むと下記のようになります。
code:Python
N = 101
ans = 1 # 2^N の答えを求める変数
cnt = 2 # 2の冪乗を計算する変数
while N > 0:
# ビットが立っている時に 2^k を ans に足す
if N % 2 == 1:
ans *= cnt
# cnt を 2乗する
cnt *= cnt
# N を右にシフト(2で割る)する
N //= 2
# 2^101 が求まっていることを確認する
print(ans, ans == 2**101)
この while 文の処理が繰り返し二乗法の本質になっています。
while 文の中身を1行ずつ確認していきましょう。
※$ (101)_{10} = (1100101)_2です。
while 文 1周目
1. $ N \: (=101)を$ 2で割った余りが$ 1なので if 文が処理され、$ \mathrm{ans} = \mathrm{ans} \times \mathrm{cnt} = 1 \times 2 = 2 = 2^1となる
2. $ \mathrm{cnt}を二乗するので$ \mathrm{cnt^2} = 2^2 = 4となる
3. $ Nを右にシフト($ 2で割る)するので$ N = (50)_{10} = (110010)_2となる
while 文 2周目
4. $ N \: (=50)を$ 2で割った余りが$ 1ではないので if 文は実行されない
5. $ \mathrm{cnt}を二乗するので$ \mathrm{cnt^2} = 4^2 = {2^2}^2 = 2^4 = 16となる
6. $ Nを右にシフト($ 2で割る)するので$ N = (25)_{10} = (11001)_2となる
while 文 3周目
7. $ N \: (=25)を$ 2で割った余りが$ 1なので if 文が処理され、$ \mathrm{ans} = \mathrm{ans} \times \mathrm{cnt} = 2 \times 16 = 32 = 2^1 \times 2^4 = 2^5となる
8. $ \mathrm{cnt}を二乗するので$ \mathrm{cnt^2} = 16^2 = {2^4}^2 = 2^8 = 256となる
9. $ Nを右にシフト($ 2で割る)するので$ N = (12)_{10} = (1100)_2となる
while 文 4周目
10. $ N \: (=12)を$ 2で割った余りが$ 1ではないので if 文は実行されない
11. $ \mathrm{cnt}を二乗するので$ \mathrm{cnt^2} = 256^2 = {2^{8}}^2 = 2^{16} = 65536となる
12. $ Nを右にシフト($ 2で割る)するので$ N = (6)_{10} = (110)_2となる
while 文 5周目
13. $ N \: (=6)を$ 2で割った余りが$ 1ではないので if 文は実行されない
14. $ \mathrm{cnt}を二乗するので$ \mathrm{cnt^2} = 65536^2 = {2^{16}}^2 = 2^{32} = 4294967296となる
15. $ Nを右にシフト($ 2で割る)するので$ N = (3)_{10} = (11)_2となる
while 文 6周目
16. $ N \: (=3)を$ 2で割った余りが$ 1なので if 文が処理され、$ \mathrm{ans} = \mathrm{ans} \times \mathrm{cnt} = 32 \times 4294967296 = 2^5 \times 2^{32} = 137438953472 = 2^{37}となる
17. $ \mathrm{cnt}を二乗するので$ \mathrm{cnt^2} = 65536^2 = {2^{32}}^2 = 2^{64} = 18446744073709551616となる
18. $ Nを右にシフト($ 2で割る)するので$ N = (1)_{10} = (1)_2となる
while 文 7周目
19. $ N \: (=1)を$ 2で割った余りが$ 1なので if 文が処理され、$ \mathrm{ans} = \mathrm{ans} \times \mathrm{cnt} = 137438953472 \times 18446744073709551616 = 2^{37} \times 2^{64} = 2535301200456458802993406410752 = 2^{101}となる
20. $ \mathrm{cnt}を二乗するので$ \mathrm{cnt^2} = 18446744073709551616^2 = {2^{64}}^2 = 2^{128} = 340282366920938463463374607431768211456となる
21. $ Nを右にシフト($ 2で割る)するので$ N = (0)_{10} = (0)_2となる
22. $ Nが$ 0となったので while 文を抜ける
このようにして$ 2^{101}を求めることが出来ました。
/icons/hr.icon
6. 繰り返し二乗法の何が良いの?
ここまで繰り返し二乗法について学習して来ましたが、これの何が良いのかについて考えます。
例えば、$ 2^{101}程度であれば良いですが$ 2^{10^{18}}を求めよの問題が出た際に$ 10^{18}回$ 2を掛け算していては間に合いませんね$ O(N)解法。
そこで$ (10^{18})_{10} = (110111100000101101101011001110100111011001000000000000000000)_2なので、繰り返し二乗法を用いるとこのビット列の長さ、$ 60回のみの while 文で実行することが出来ます。
これは計算量が$ O(\log N)となり、かなり高速な処理を行うことが出来ます。
したがって、$ Nが$ 10^5あたりを超えて$ N \leq 10^9や$ N \leq 10^{18}のような制約に対しては繰り返し二乗法が効果的なので制約を見る癖をつけるようにしましょう。
/icons/hr.icon
7. フィボナッチ数列を行列で表す
さて、色々遠回りをして来ましたが、本題に戻ります。
今日のゴールは「行列累乗を用いてフィボナッチ数列の第$ N項を高速に求める」ことです
つまり、フィボナッチ数列を行列に乗せる必要があります。
この方法を$ 7章では考えます。
フィボナッチ数列は以下で定義される数列でした。
$ a_{n} = a_{n - 1} + a_{n - 2} \quad (n \geq 3, \quad a_1 = 1, \quad a_2 = 1)
$ a = \{1,1,2,3,5,8,13,21,\ldots\}
これを行列に落とし込むと次の式を作ることが出来ます。
$ \left( \begin{array} {c} a_N \\ a_{N - 1} \end{array} \right) = \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right) \left( \begin{array} {c} a_{N - 1} \\ a_{N - 2} \end{array} \right)
ここで具体例を計算してみましょう。
$ \left( \begin{array} {c} 2 \\ 1 \end{array} \right) = \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right) \left( \begin{array} {c} 1 \\ 1 \end{array} \right)
$ \left( \begin{array} {c} 3 \\ 2 \end{array} \right) = \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right) \left( \begin{array} {c} 2 \\ 1 \end{array} \right)
$ \left( \begin{array} {c} 5 \\ 3 \end{array} \right) = \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right) \left( \begin{array} {c} 3 \\ 2 \end{array} \right)
上記の具体例を見るに、前$ 2項($ N項を求めるのに$ N - 1項と$ N - 2項)があれば次の項が求められていることが確認できます。
ではこの$ 3つ目の式を少し変形してみます。
$ \begin{aligned} \left( \begin{array} {c} 5 \\ 3 \end{array} \right) &= \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right) \left( \begin{array} {c} 3 \\ 2 \end{array} \right) \\ &= \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right) \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right) \left( \begin{array} {c} 2 \\ 1 \end{array} \right) \\ &= \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right) \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right) \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right) \left( \begin{array} {c} 1 \\ 1 \end{array} \right) \\ &= \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right)^3 \left( \begin{array} {c} 1 \\ 1 \end{array} \right) \end{aligned}
つまりフィボナッチ数列の$ 5は先頭から第$ 5項目の値ですが、これは$ \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right)を$ 3回かけて、$ \left( \begin{array} {c} 1 \\ 1 \end{array} \right)をかけることで求めることが出来ます。
したがって、例えば第$ 101項を求めたい場合は、
$ \begin{aligned} \left( \begin{array} {c} a_{101} \\ a_{100} \end{array} \right) &= \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right)^{99} \left( \begin{array} {c} 1 \\ 1 \end{array} \right) \\ &= \left( \begin{array} {c} 573147844013817084101 \\ 354224848179261915075 \end{array} \right) \end{aligned}
といった具合に求めることが出来ます。
ここまで理解できてる人は気づいたと思います。
つまり、第$ N項を求めるには以下のうに求めることが出来ます。
$ \left( \begin{array} {c} a_N \\ a_{N - 1} \end{array} \right) = \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right)^{N - 2} \left( \begin{array} {c} 1 \\ 1 \end{array} \right)
この$ \left( \begin{array} {c} 1 & 1 \\ 1 & 0 \end{array} \right)^{N - 2}を高速に求めるために行列累乗が必要ということです!!
/icons/hr.icon
8. 行列累乗を用いてフィボナッチ数列を求める
いよいよ本日のゴールです。
まずはこの行列をダブリングを用いて高速に計算するコードを以下に示します。
code:Python
def matrix_pow(a: list, n: int) -> list:
"""a は行列(今回で言う 1, 1], [1, 0), n は冪乗"""
len_a = len(a)
# 計算するための行列の初期値
cnt = [0 * len_a for _ in range(len_a)] # 単位行列は体格成分が 1 なのでそれの処理をする
for i in range(len_a):
while n > 0:
if n % 2 == 1:
# ビットが立っている場合は A^i を行列に掛け算する
cnt = matrix_multi(a, cnt)
# A^{2i} の行列積を求めるために A^i × A^i の行列を計算する
a = matrix_multi(a, a)
# n を 2 で割る
n //= 2
return cnt
本質はそちらが理解できていれば何も難しくはなく、答えが int から 行列、計算の処理が int の二乗から 行列 の二乗になっただけです。
あとは繰り返し二乗法とダブリングを両方記述し、$ N-2乗した行列に$ \left( \begin{array} {c} 1 \\ 1 \end{array} \right)をかければ答えが求まります。
コード全体は以下になります。
code:Python
def matrix_multi(A: list, B: list) -> list:
N, M, K = len(A), len(B), len(B0) C = [0*len(B0) for _ in range(N)] # C は N*K 行列 for i in range(N):
for j in range(K):
for k in range(M):
return C
def matrix_pow(A: list, N: int) -> list:
"""A は行列(今回で言う 1, 1], [1, 0), N は冪乗"""
len_a = len(A)
# 計算するための行列の初期値
matrix = [0 * len_a for _ in range(len_a)] # 単位行列は体格成分が 1 なのでそれの処理をする
for i in range(len_a):
while N > 0:
if N % 2 == 1:
# ビットが立っている場合は A^i を行列に掛け算する
matrix = matrix_multi(A, matrix)
# A^{2i} の行列積を求めるために A^i × A^i の行列を計算する
A = matrix_multi(A, A)
# n を 2 で割る
N //= 2
return matrix
N = int(input())
A = 1, 1], [1, 0
# 行列累乗を呼び出して N-2 乗回の計算を高速に行う
A = matrix_pow(A, N - 2)
print(ans)
/icons/hr.icon
最後に
かなり急ぎ早に説明したと思うので1回で理解するのはまず難しいと思います。
何度も見返して自分の糧にしていただければ幸いです。
P.S. 死にながら本日の議事録を作成していたので、誤字脱字や誤植等あるかもしれません。あったらこっそり教えてください😫