Algorithms (2023 Summer)
#レポート: ⾏列積計算アルゴリズム
⽴花 卓遠
Twitter: @takuo_20020622
2023/07/08
このスライドは、東京⼤学⼯学部電⼦情報⼯学科・電気電⼦⼯学科
の授業であるアルゴリズム(https://iis-lab.org/algorithms2023)
のレポート課題として制作されたものを⼀般に公開したものになります.本
スライドの内容に関して,お気づきの点やさらに良くするためのコメントが
ございましたら、ぜひ本スライドの制作者にご共有ください.
2次元⾏列計算のアルゴリズムが本レポートのテーマです.
数値計算はNumPyに頼ることも多く、特に、⾏列計算について意識することは少ないと思います.
⾏列計算を⾼速化する⼿法について⼀緒に考えてみましょう︕
テーマ
⾏列計算の需要は近年急激に増えています😅
特に、AI時代では、⼤量のデータと複雑なモデルを扱う必要があります。
⾏列計算は、データ処理、パターン認識、特徴抽出、モデルトレーニングなど、AIの基盤となる数学
的な操作です︕
なぜ⾏列計算︖
⽬次
1 基本的な⾏列積
2 シュトラッセン法による⾼速化
3 ⾏列連続積の⾼速化
4
現在地
1 基本的な⾏列積
2 シュトラッセン法による⾼速化
3 ⾏列連続積の⾼速化
⾏列積の基本的な計算
簡単な⾏列積の復習から始めましょう︕
𝐴 =
𝑎!! 𝑎!" 𝑎!#
𝑎"! 𝑎"" 𝑎"#
, 𝐵 =
𝑏!! 𝑏!"
𝑏"! 𝑏""
𝑏#! 𝑏#"
としたとき、 𝑝×𝑞 ⾏列 A と 𝑞×𝑟 ⾏列 B の⾏列積 C = AB は、
𝐶 =
𝑎!! 𝑎!" 𝑎!#
𝑎"! 𝑎"" 𝑎"#
𝑎!!𝑏!! + 𝑎!"𝑏"! + 𝑎!#𝑏#! 𝑎!!𝑏"! + 𝑎!"𝑏"" + 𝑎!#𝑏#"
𝑎"!𝑏!! + 𝑎""𝑏"! + 𝑎"#𝑏#! 𝑎"!𝑏!" + 𝑎""𝑏"" + 𝑎"#𝑏#!
と計算されます. (今は𝑝 = 2, 𝑞 = 3 , r = 2 の場合を⽰しています︕)
⾏列積の基本的な計算
このとき、C =
𝑎!!𝑏!! + 𝑎!"𝑏"! + 𝑎!#𝑏#! 𝑎!!𝑏"! + 𝑎!"𝑏"" + 𝑎!#𝑏#"
𝑎"!𝑏!! + 𝑎""𝑏"! + 𝑎"#𝑏#! 𝑎"!𝑏!" + 𝑎""𝑏"" + 𝑎"#𝑏#!
の各要素は
𝑐$% = 1
&'!
(
𝑎$&𝑏&%
と表せるので、この式に基づいて実装できるはずです !
この定義に基づいて実装したとき、ループは何回必要でしょうか︖😊
⾏列積の基本的な実装
def matrix_mul (A, B):
# p*q 型⾏列 A と q*r 型⾏列 B の掛け算
[⾏列A, Bの型を取得]
[p*r 型⾏列 C を初期化]
# ⾏列積の計算
for i in range(p):
for j in range(r):
for k in range(q):
C[i][j] += A[i][k] * B[k][j]
return C
⾏列積の基本的な実装
def matrix_mul (A, B):
# p*q 型⾏列 A と q*r 型⾏列 B の掛け算
[⾏列A, Bの型を取得]
[p*r 型⾏列 C を初期化]
# ⾏列積の計算
for i in range(p): ← pのループ
for j in range(r): ← qのループ
for k in range(q): ← rのループ
C[i][j] += A[i][k] * B[k][j]
return C
基本的な⾏列積の計算量
𝑐$% = ∑&'!
(
𝑎$&𝑏&% の定義に基づいたナイーブな実装だと3回のループを持ちます.
そのため計算量は、O(𝑝𝑞𝑟) .
特に、⼊⼒の⾏列が両⽅とも 𝑛×𝑛 型の場合は、 O(𝑛#) となります。
次に改善された⾏列積のアルゴリズムを⾒ていきましょう︕
現在地
1
2 シュトラッセン法による⾼速化
3 連続⾏列積の⾼速化
基本的な⾏列積
シュトラッセン法
1968年にドイツの数学者、Volker Strassenによって、
⾏列積の計算をO(𝑛#)よりも⾼速にできる、初めての
アルゴリズムが発表されました。
→シュトラッセン法
現在も、中〜⼤規模な密⾏列(↔疎⾏列)の
実⽤的な乗算アルゴリズムとされてます。
2×2⾏列におけるシュトラッセン法
まず、2×2⾏列の⾏列積で考えてみましょう︕
𝐴 =
𝑎!! 𝑎!"
𝑎"! 𝑎""
, 𝐵 =
𝑏!! 𝑏!"
𝑏"! 𝑏""
としたとき、⾏列積 C = AB は、
𝐶 = 𝐴𝐵 =
𝑎!!𝑏!! + 𝑎!"𝑏"! 𝑎!!𝑏!" + 𝑎!"𝑏""
𝑎"!𝑏!! + 𝑎""𝑏"! 𝑎"!𝑏!" + 𝑎""𝑏""
と計算されます.
シュトラッセン法の⽅針
このときの掛け算(×)と⾜し算(+)の実⾏回数を数えてみると、
𝐶 = 𝐴𝐵 =
𝑎!!𝑏!! + 𝑎!"𝑏"! 𝑎!!𝑏!" + 𝑎!"𝑏""
𝑎"!𝑏!! + 𝑎""𝑏"! 𝑎"!𝑏!" + 𝑎""𝑏""
掛け算→ 8回
⾜し算→ 4回
となっていることが分かります。
シュトラッセン法の⽅針
このときの掛け算(×)と⾜し算(+)の実⾏回数を数えてみると、
𝐶 = 𝐴𝐵 =
𝑎!!𝑏!! + 𝑎!"𝑏"! 𝑎!!𝑏!" + 𝑎!"𝑏""
𝑎"!𝑏!! + 𝑎""𝑏"! 𝑎"!𝑏!" + 𝑎""𝑏""
掛け算→ 8回 ←この回数を減らしていくのがシュトラッセン法の⽅針︕
⾜し算→ 4回
となっていることが分かる。
シュトラッセン法の⽅針
少し唐突ですが、以下のように𝑥!~ 𝑥)を定義してみます。
𝑥! = (𝑎!! + 𝑎"")×(𝑏!! + 𝑏"")
𝑥" = (𝑎"! + 𝑎"")×𝑏!!
𝑥# = 𝑎!!×(𝑏!" − 𝑏"")
𝑥* = 𝑎""×(𝑏"! − 𝑏!!)
𝑥+ = (𝑎!! + 𝑎"")×𝑏""
𝑥, = (𝑎"! − 𝑎!!)×(𝑏!! + 𝑏"")
𝑥) = (𝑎!" − 𝑎"")×(𝑏"! + 𝑏"")
これらを⽤いてCを表すと。
𝐶 =
𝑎!!𝑏!! + 𝑎!"𝑏"! 𝑎!!𝑏!" + 𝑎!"𝑏""
𝑎"!𝑏!! + 𝑎""𝑏"! 𝑎"!𝑏!" + 𝑎""𝑏""
シュトラッセン法の⽅針
𝐶 =
𝑎!!𝑏!! + 𝑎!"𝑏"! 𝑎!!𝑏!" + 𝑎!"𝑏""
𝑎"!𝑏!! + 𝑎""𝑏"! 𝑎"!𝑏!" + 𝑎""𝑏""
=
𝑥! + 𝑥* − 𝑥+ + 𝑥) 𝑥# + 𝑥+
𝑥" + 𝑥* 𝑥! + 𝑥# − 𝑥" + 𝑥,
とCを𝑥!~ 𝑥) から求めることができます。
これは計算量が本当に減っているのでしょうか︖
掛け算(×)と⾜し算(+)の実⾏回数を数えてみましょう。😊
シュトラッセン法の⽅針
𝑥! = (𝑎!! + 𝑎"")×(𝑏!! + 𝑏"")
𝑥" = (𝑎"! + 𝑎"")×𝑏!!
𝑥# = 𝑎!!×(𝑏!" − 𝑏"")
𝑥* = 𝑎""×(𝑏"! − 𝑏!!)
𝑥+ = (𝑎!! + 𝑎"")×𝑏""
𝑥, = (𝑎"! − 𝑎!!)×(𝑏!! + 𝑏"")
𝑥) = (𝑎!" − 𝑎"")×(𝑏"! + 𝑏"")
𝐶 =
𝑥! + 𝑥* − 𝑥+ + 𝑥) 𝑥# + 𝑥+
𝑥" + 𝑥* 𝑥! + 𝑥# − 𝑥" + 𝑥,
掛け算→ 7回
⾜し算→ 18回 (引き算も⾜し算としてカウント)
シュトラッセン法の⽅針
⾜し算は増えたけど、掛け算は確かに減りました︕︕︕
ナイーブな⽅法
掛け算 8 回
⾜し算 4 回
合計 12 回
シュトラッセン法
掛け算 7 回
⾜し算 18 回
合計 25 回
シュトラッセン法の⽅針
シュトラッセン法を使うと⾜し算は増えるますが、掛け算を減らすことができました︕😊
2×2⾏列の場合は短縮できていると⾔い難いですが、⾏列のサイズを⼤きくして⾏ったときに
“掛け算を減らす”ことがありがたくなってきます︕
⼀般的な𝑛×𝑛⾏列の⾏列積についても考えてみましょう︕
⼀般的なシュトラッセン法
⾏列積を求めたい𝑛×𝑛⾏列A,Bについてそれぞれ、4つの
-
"
×
-
"
⾏列に分解します.
𝐴 =
𝐴!! 𝐴!"
𝐴"! 𝐴""
𝐵 =
𝐵!! 𝐵!"
𝐵"! 𝐵""
ここでは簡単のために、nが2のべき乗の場合のみを考えています。
↑ 𝐴!!~ 𝐵""は
-
"
×
-
"
の⾏列!
⼀般的なシュトラッセン法
⾏列式 𝐶 = 𝐴𝐵 は2×2の場合と同様に、
𝐶 =
𝐴!!𝐵!! + 𝐴!"𝐵"! 𝐴!!𝐵!" + 𝐴!"𝐵""
𝐴"!𝐵!! + 𝐴""𝐵"! 𝐴"!𝐵!" + 𝐴""𝐵""
と表せます.
⼀般的なシュトラッセン法
また、以下のように、𝑋!~ 𝑋)を定義します.
𝑋! = (𝐴!! + 𝐴"")×(𝐵!! + 𝐵"")
𝑋" = (𝐴"! + 𝐴"")×𝐵!!
𝑋# = 𝐴!!×(𝐵!" − 𝐵"")
𝑋* = 𝐴""×(𝐵"! − 𝐵!!)
𝑋+ = (𝐴!! + 𝐴"")×𝐵""
𝑋, = (𝐴"! − 𝐴!!)×(𝐵!! + 𝐵"")
𝑋)= (𝐴!" − 𝐴"")×(𝐵"! + 𝐵"")
これらを⽤いてCが以下のように表せます.
𝐶 =
𝑋! + 𝑋* − 𝑋+ + 𝑋) 𝑋# + 𝑋+
𝑋" + 𝑋* 𝑋! + 𝑋# − 𝑋" + 𝑋,
⼀般的なシュトラッセン法
𝑋!~ 𝑋) については、シュトラッセン法を再帰的に⽤いることで計算できます。
𝑋! = (𝐴!! + 𝐴"")×(𝐵!! + 𝐵"")
例えば、 𝑋!について(𝐴!!+𝐴""), (𝐵!! + 𝐵"") はそれぞれ
-
"
×
-
"
⾏列なので、
-
"
×
-
"
⾏列の⾏列積として、シュトラッセン法で計算できるはずです︕
それでは、実装してみましょう︕😊
シュトラッセン法の実装
シュトラッセン法を実装していきます︕
今回は分かりやすさのために、⼊⼒の⾏列はndarrayとし、実装では⼀部numpyを使⽤し
ています。ただ、計算量を議論する際にはnumpyを使っていませんので気をつけて下さい︕
import numpy as np
def strassen(A, B, n):
[𝑛 × 𝑛 ⾏列 A, B の⾏列積をシュトラッセン法で求める]
シュトラッセン法の実装
def strassen(A, B, n):
# ⾏列積の次元が1になった場合は、単純な積を計算
if n == 1:
return A * B
# 分割後の次元
new_size = n // 2
[A11~ B22の⾏列を4つのブロックに分割]
[X1~ X7の7つの補助⾏列を計算]
[X1~ X7を元に⾏列積を組み⽴てる]
この3つを順次実装していきます!
シュトラッセン法の実装
def strassen(A, B, n):
# ⾏列積の次元が1になった場合は、単純な積を計算
if n == 1:
return A * B
# 分割後の次元
new_size = n // 2
[A11~ B22の⾏列を4つのブロックに分割]
[X1~ X7の7つの補助⾏列を計算]
[X1~ X7を元に⾏列積を組み⽴てる]
# ⾏列を4つのブロックに分割
A11 = A[:new_size, :new_size]
A12 = A[:new_size, new_size:]
A21 = A[new_size:, :new_size]
A22 = A[new_size:, new_size:]
B11 = B[:new_size, :new_size]
B12 = B[:new_size, new_size:]
B21 = B[new_size:, :new_size]
B22 = B[new_size:, new_size:]
シュトラッセン法の実装
def strassen(A, B, n):
# ⾏列積の次元が1になった場合は、単純な積を計算
if n == 1:
return A * B
# 分割後の次元
new_size = n // 2
[A11~ B22の⾏列を4つのブロックに分割]
[X1~ X7の7つの補助⾏列を計算]
[X1~ X7を元に⾏列積を組み⽴てる]
# 7つの補助⾏列を計算
#再帰的にStrassenのアルゴリズムを適⽤︕
X1 = strassen (A11 + A22, B11 + B22)
X2 = strassen (A21 + A22, B11)
X3 = strassen (A11, B12 - B22)
X4 = strassen (A22, B21 - B11)
X5 = strassen (A11 + A12, B22)
X6 = strassen (A21 - A11, B11 + B12)
X7 = strassen (A12 - A22, B21 + B22)
シュトラッセン法の実装
def strassen(A, B, n):
# ⾏列積の次元が1になった場合は、単純な積を計算
if n == 1:
return A * B
# 分割後の次元
new_size = n // 2
[A11~ B22の⾏列を4つのブロックに分割]
[X1~ X7の7つの補助⾏列を計算]
[X1~ X7を元に⾏列積を組み⽴てる]
# 新しい⾏列の要素を計算
C11 = X1 + X4 - X5 + X7
C12 = X3 + X5
C21 = X2 + X4
C22 = X1 - X2 + X3 + X6
# 新しい⾏列を結合して返す
C = np.zeros((n, n))
C[:new_size, :new_size] = C11
C[:new_size, new_size:] = C12
C[new_size:, :new_size] = C21
C[new_size:, new_size:] = C22
シュトラッセン法の計算量
以下の3つの演算部分に⼤きく分けて実装してきました。
それぞれの計算量について考えてみましょう︕😊
1. [A11~ B22の⾏列を4つのブロックに分割]
2. [X1~ X7の7つの補助⾏列を計算]
3. [X1~ X7を元に⾏列積を組み⽴てる]
シュトラッセン法の計算量
1. [A11~ B22の⾏列を4つのブロックに分割]
2. [X1~ X7の7つの補助⾏列を計算]
3. [X1~ X7を元に⾏列積を組み⽴てる]
𝑛 × 𝑛 ⾏列を
-
"
×
-
"
⾏列に分解する操作。例えばA11をNumPyを使わずに実装すると、
となるので、計算量のオーダは𝑂 𝑛" .
A11 = []
for i in range(n/2): ← n/2のループ
row = []
for j in range(n/2): ← n/2のループ
row.append(A[i][j])
A11.append(row)
シュトラッセン法の計算量
1. [A11~ B22の⾏列を4つのブロックに分割]
2. [X1~ X7の7つの補助⾏列を計算]
3. [X1~ X7を元に⾏列積を組み⽴てる]
再帰的にシュトラッセン法を⽤いて補助⾏列を求める操作。
のように実装されるので、シュトラッセン法の計算量 𝑇 𝑛 としたとき、計算量は
7×𝑇
-
"
となります.
X1 = strassen (A11 + A22, B11 + B22)
↑引数はいずれも
-
"
×
-
"
⾏列
1. [A11~ B22の⾏列を4つのブロックに分割]
2. [X1~ X7の7つの補助⾏列を計算]
3. [X1~ X7を元に⾏列積を組み⽴てる]
再帰的にシュトラッセン法を⽤いて補助⾏列を求める操作。C11 を C に代⼊する部分を
考えると、
となることより、この部分の計算量のオーダは𝑂 𝑛" .
シュトラッセン法の計算量
C11 = X1 + X4 - X5 + X7 ← 注: ここも𝑂 𝑛!
.
for i in range(n/2): ← n/2のループ
for j in range(n/2): ← n/2のループ
C[i][j] = C11[i][j]
シュトラッセン法の計算量
1. [A11~ B22の⾏列を4つのブロックに分割] → 𝑂 𝑛"
2. [X1~ X7の7つの補助⾏列を計算] → 7×𝑇
-
"
3. [X1~ X7を元に⾏列積を組み⽴てる] → 𝑂 𝑛"
となるので、シュトラッセン法の計算量 𝑇 𝑛 は、
𝑇 𝑛 = 7×𝑇
𝑛
2
+ 𝑂 𝑛"
= 𝑂 𝑛./0! )
≈ 𝑂 𝑛".2!
と求まります。 (→導出はappendixを参照)
シュトラッセン法まとめ
シュトラッセン法の計算量は 𝑂 𝑛".2! となり、確かにナイーブな⽅法の𝑂 𝑛#
より⼩さくはなっています︕
ただし、この計算量は乗算の回数だけを考慮していて、⼤量に必要となる加減算のコストは
考慮しきれていません.
そのため、⼩さな n においては、ナイーブな⽅法より効率が悪くなってしまいます😢
シュトラッセン法まとめ
⾏列積の計算には𝑂 𝑛# が絶対必要だと考えられていたので、1969年にこれが発表された
ときは⼤変衝撃的でした.
以降、⾏列積の⾼速化アルゴリズムは研究が進められているものの、”最良“の⽅法はまだ
発⾒されていなく、計算機科学の最も有名な未解決問題の1つとされています.
現在では最も⼩さい⾏列積アルゴリズムで計算量が𝑂 𝑛".#)"3 のものが知られています😊
1 基本的な⾏列積
3 ⾏列連続積の⾼速化
2 シュトラッセン法による⾼速化
現在地
⾏列の連続積とは
⾏列の連続積とは3つ以上の⾏列を掛け合わせる時の⾏列の計算です.
以下に簡単な例を⽰します.
1 2 3
3 2 1
1 3
2 2
3 1
1 2
3 4
1 2 3 4
5 6 7 8
1 4 1
2 3 2
3 2 3
4 1 4
=
14220 11560 14220
33927 27578 33927
𝐴! 𝐴" 𝐴# 𝐴* 𝐴+
⾏列の連続積とは
連続積においては、「計算順序によらず結果は変わらない(結合則)」の性質が存在します.
例えば、先ほどの例では以下のような結合則が成⽴します.
1 2 3
3 2 1
1 3
2 2
3 1
1 2
3 4
1 2 3 4
5 6 7 8
1 4 1
2 3 2
3 2 3
4 1 4
=
1 2 3
3 2 1
((
1 3
2 2
3 1
1 2
3 4
)
1 2 3 4
5 6 7 8
)
1 4 1
2 3 2
3 2 3
4 1 4
⾏列の連続積とは
基本的な⾏列式で説明したように、ナイーブな⽅法で、⾏列積の計算量は O(𝑝𝑞𝑟)
となるのでした.
簡単に、計算量を 𝑝𝑞𝑟 として先ほどの例での計算量を⽐較してみましょう︕
⾏列の連続積とは
1 2 3
3 2 1
1 3
2 2
3 1
1 2
3 4
1 2 3 4
5 6 7 8
1 4 1
2 3 2
3 2 3
4 1 4
となり、𝐴!𝐴"𝐴#𝐴*𝐴+の順で計算したとき、計算量は 12 + 8 + 16 + 24 = 60 です.
2*3*2 = 12
2*2*2 = 8
2*2*4 = 16
2*2*4 = 24
𝐴! 𝐴" 𝐴# 𝐴* 𝐴+
⾏列の連続積とは
1 2 3
3 2 1
((
1 3
2 2
3 1
1 2
3 4
)
1 2 3 4
5 6 7 8
)
1 4 1
2 3 2
3 2 3
4 1 4
となり、𝐴!( 𝐴"𝐴# 𝐴*)𝐴+の順で計算したとき、計算量は 12 + 24 + 24 + 24 = 84 です.
3*2*2 = 12
3*2*4 = 24
2*3*4 = 24
2*4*3 = 24
𝐴! 𝐴" 𝐴# 𝐴* 𝐴+
⾏列の連続積とは
まとめると、
𝐴!𝐴"𝐴#𝐴*𝐴+の順 → 60
𝐴!( 𝐴"𝐴# 𝐴*)𝐴+の順 → 84
となり、演算の順番によって計算量が変わってしまうことが分かります😢
連続積の最適化
それでは、演算の順番を最適化する⽅法を考えましょう︕
最適な順番で演算すれば、計算量も最⼩になりそうですね😊
⾏列 𝐴$ から⾏列 𝐴% までの⾏列 𝐴$, 𝐴$5!, … 𝐴%に対して、連続積を計算するもっとも短い
計算時間を 𝑚(𝑖, 𝑗) とします.
このとき、⼀般的に 𝐴-までの最⼩の連続積計算量を求めることは、
𝑚(1, 𝑛)
を求めることと等しいです︕
連続積の最適化
それでは、演算の順番を最適化する⽅法を考えましょう︕
最適な順番で演算すれば、計算量も最⼩になりそうですね😊
⾏列 𝐴$ から⾏列 𝐴% までの⾏列 𝐴$, 𝐴$5!, … 𝐴%に対して、連続積を計算するもっとも短い
計算時間を 𝑚(𝑖, 𝑗) とします.
このとき、⼀般的に 𝐴-までの最⼩の連続積計算量を求めることは、
𝑚(1, 𝑛)
を求めることと等しいです︕
連続積の最適化
ところで、どんな順番で A6 から⾏列 A7 までの⾏列を計算するとしても、
(𝐴$𝐴$5! … 𝐴&)×(𝐴&5!𝐴&5" … 𝐴%)
という、2つの連続積を掛け合わせることになります.
また、この演算にかかる最⼩の時間は、
𝑚 𝑖, 𝑗, 𝑘 = 𝑚 𝑖, 𝑘 + 𝑚 𝑘 + 1, 𝑗 + 𝑝$𝑞&𝑟
%
と求められそうです︕
𝑀! 𝑀"
𝑀!×𝑀"の演算で発⽣するO(𝑝𝑞𝑟)
DP法で順番を最適化
𝑚 𝑖, 𝑗, 𝑘 = 𝑚 𝑖, 𝑘 + 𝑚 𝑘 + 1, 𝑗 + 𝑝$𝑞&𝑟
%
これで、計算量が漸化式で表せました…︕︖
そうです、漸化式で表せたということは動的計画法に落とせ込めそうですね︕😊
ここで、「⽮⾕式DPの考え⽅」に沿ってDPテーブルを作成していきましょう.
なお、「⽮⾕式DPの考え⽅」は、東京⼤学⼯学部電⼦情報⼯学科准教授の⽮⾕浩司
先⽣が授業中に解説されていたDPの設計⼿順です.(次スライド)
⽮⾕式DPの考え⽅
⽮⾕式DPの考え⽅(講義スライドより抜粋)
1. DPテーブルを設計する.
2. DPテーブルを初期化する.
3. DPテーブル上のあるセルに対して、1ステップの操作で他のどのセルから遷移できるかを
調べる.
4. 3でわかったことをコードに落とし込む.
これに従ってDPテーブルを設計していきます︕
今回は以下のようにDPテーブルを設計しました.
DPテーブルのセル:
⾏列 𝐴$ から⾏列 𝐴% までの⾏列 𝐴$, 𝐴$5!, … 𝐴%に対する
連続積の最⼩計算量 min(𝑚 𝑖, 𝑗, 𝑘 )
DPテーブルの⾏と列:
⾏ → 𝑚(𝑖, 𝑗, 𝑘) の 𝑖
列 → 𝑚(𝑖, 𝑗, 𝑘) の 𝑗
DPテーブルの設計
今回は単純に、 𝑖 と j の間隔が 0 である場合について、
𝑚 𝑖, 𝑗 = 0 (𝑖 = 𝑗 のとき)
と初期化すれば良さそうです︕
DPテーブルを初期化する
設計した表について確認していきましょう︕
この表のセルは以下のように連続積の最⼩計算量 𝑚(𝑖, 𝑗) を表すのでした.
DPテーブルの確認
1 2 3 4 5
1 𝑚(1,2)
2
3
4
特に、𝑚 𝑖, 𝑗 = 0 𝑖 = 𝑗 のとき と初期化するのでした.
また、今回DPテーブルの左下部分は使⽤しません.
DPテーブルの確認
_ 1 2 3 4 5
1 0
2 - 0
3 - - 0
4 - - - 0
漸化式は以下のように表されるのでした. (𝑚の引数に𝑘を加えているので注意︕)
𝑚 𝑖, 𝑗, 𝑘 = 𝑚 𝑖, 𝑘 + 𝑚 𝑘 + 1, 𝑗 + 𝑝$𝑞&𝑟
%
この時、𝑘 = 𝑖, 𝑖 + 1, … , 𝑗 − 1 を動きます.
𝑘を動かしたときの最⼩値 min(𝑚 𝑖, 𝑗, 𝑘 ) をDPテーブルに記録していけば良さそうです.
注:
以後、𝑚 𝑖, 𝑗, 𝑘 を 𝑚 𝑖, 𝑗, 𝑘 = 𝑚 𝑖, 𝑘 + 𝑚 𝑘 + 1, 𝑗 + 𝑝$𝑞&𝑟
% とし、𝑚 𝑖, 𝑗 をDPテーブ
ル中の値とします。
また、 𝑚 𝑖, 𝑗 = min(𝑚 𝑖, 𝑗, 𝑘 ) から計算されます。
操作のマッピング
ちなみに𝑘を動かすという操作は、
(𝐴$𝐴$5! … 𝐴&) × (𝐴&5!𝐴&5" … 𝐴%)
において、 最後に実⾏される掛け算の位置をずらしていることに相当します.その中で、どの
計算量が⼀番⼩さい調べる操作をこれから実装します.
以下の式でイメージを掴んどいて下さい.
𝐴$× (𝐴$5!𝐴&5" … 𝐴%) (k = 𝑖 のとき)
(𝐴$𝐴$5!) × (𝐴$5"𝐴&5# … 𝐴%) (k = 𝑖 + 1のとき)
⋮
(𝐴$𝐴$5!𝐴$5") × (𝐴$5#𝐴&5* … 𝐴%) (k = 𝑖 + 1のとき)
操作のマッピング
例えば、𝑖 = 1, 𝑗 = 3 のとき、
𝑚 1,3 = min (𝑚 1,3,1 , 𝑚 1,3,2 )
= min(𝑚 1,1 + 𝑚 2,3 + 𝑝!𝑞!𝑟#, 𝑚 1,2 + 𝑚 3,3 + 𝑝!𝑞"𝑟#)
と求めることができます︕
操作のマッピング
_ 1 2 3 4 5
1 0 𝑚 1,3
2 - 0
3 - - 0
4 - - - 0
𝑖
𝑗
𝑚 1,3,1
𝑚 1,3,2
#最初に⽰した例
A1 = [[1,2,3],[3,2,1]]
A2 = [[1,3],[2,2],[3,1]]
A3 = [[1,2],[3,4]]
A4 = [[1,2,3,4],[5,6,7,8]]
A5 = [[1,4,1],[2,3,2],[3,2,3],[4,1,4]]
[A1~A5を配列にappendしていったarray_listを定義]
def continuous_mul(array_list, n):
#引数は⾏列リストarray_listと⾏列数n
[DPテーブル(dp)を作る]
return dp[0][n+1]
コード化
def continuous_mul(array_list, n):
#DPテーブルを作成・初期化
dp = [[10**9 for _ in range(n)] for _ in range(n)]
for i in range(n):
dp[i][i] = 0
#関数mを定義(漸化式)
def m(i,j,k):
return dp[i-1][k-1] + dp[k][j-1]
+ len(array_list[i-1])*len(array_list[k-1][0])*len(array_list[j-1][0])
[DPテーブルを更新]
return dp[0][n+1]
コード化
def continuous_mul(array_list, n):
[DPテーブルを作成・初期化]
[関数mを定義(漸化式)]
# DPテーブルを更新
for i in range(1, n+1):
for j in range(i+1, n+1):
for k in range(i, j):
tmp_m = m(i,j,k)
if( dp[i-1][j-1] > tmp_m):
dp[i-1][j-1] = tmp_m
return dp[0][n+1]
コード化
1 2 3
3 2 1
1 3
2 2
3 1
1 2
3 4
1 2 3 4
5 6 7 8
1 4 1
2 3 2
3 2 3
4 1 4
での実⾏結果です.
DPの実⾏結果
_ 1 2 3 4 5
1 0 12 20 36 60
2 - 0 12 36 72
3 - - 0 16 40
4 - - - 0 24
5 - - - - 0
DPの実⾏結果
1 2 3
3 2 1
1 3
2 2
3 1
1 2
3 4
1 2 3 4
5 6 7 8
1 4 1
2 3 2
3 2 3
4 1 4
前スライドの結果より、今回の例では順番を変えずに𝐴!𝐴"𝐴#𝐴*𝐴+の順で
実⾏したときに計算量が最⼩で60となることが分かります︕
次に計算量について⾒ていきましょう︕😊
コード中の以下の部分が律速段階です.
for i in range(1, n+1): ← nオーダのループ
for j in range(i+1, n+1): ← n/2オーダのループ
for k in range(i, j): ← n/4オーダのループ
tmp_m = m(i,j,k)
if( dp[i-1][j-1] > tmp_m):
dp[i-1][j-1] = tmp_m
そのため、計算量は 𝑂 𝑛# となります︕
連続積の計算量
他の連続積アルゴリズム︖
これまでの説明を聞いていて、次のような疑問が湧いた⼈がいるかも知れません.
難しいこと考えずに、全ての並べ替えに対して計算量を求めてあげればいいんじゃね︖😎
確かに、それでも最適な順番が求まります.
⾏列の形状だけ考慮すれば、実際の掛け算を実⾏する必要もありません.
他の連続積アルゴリズム︖
しかしながら実際には全ての並べ替えか考えると計算量が増加してしまいます.
⼀般的に、nの⾏列連続積に関する計算順序は
𝟒𝒏
𝝅𝒏
𝟑
𝟐
より多いです.
そのため、全ての並べ替えを考える場合は、
指数時間に⽐例する計算量を要することになります😱
Appendix(1/2)
シュトラッセンのアルゴリズムの計算量の導出
Appendix(2/2)
参考資料

行列計算アルゴリズム