アルゴ式「動的計画法 2-3」を Swift で解く
動的計画法 2-3 | アルゴ式(beta) の以下の回答について説明します。
高階関数を使ったメモ化と再帰
次の関数によって、再帰呼び出しの結果をメモ化することができます。
func memo<T: Hashable, U>(body: @escaping(((T) -> U), T) -> U) -> (T) -> U {
var memo = [T: U]()
var function: ((T) -> U)!
function = { x in
memo[x] ?? {
let r = body(function, x)
memo[x] = r
return r
}()
}
return function
}
パラメータ body
は (((T) -> U), T) -> U
、戻り値は (T) -> U
です。
body
は漸化式を表していて、これを memo
に渡すことでメモ化された関数に変換しています。
こちらの Qiita に掲載されている記事で丁寧に説明されています。
WWDC 2014 の Advanced Swift が元ネタみたいです。35:30 頃に説明されています。
例:フィボナッチ数
メモ化と再帰の例として、フィボナッチ数を計算してみます。
フィボナッチ数は次の漸化式で定義されます。
これをメモ化関数によって、次のように実装することができます。
/// フィボナッチ数列
///
/// - `F(0) = 0`
/// - `F(1) = 1`
/// - `F(i) = F(i-1) + F(i-2)`
let fibonacci: (Int) -> Int = memo { F, i in
switch i {
case 0: // 第0項
return 0
case 1: // 第1項
return 1
case _: // 一般項
return F(i-1) + F(i-2)
}
}
関数 memo
によって上述の漸化式は、計算済みの結果をキャッシュするため二度目の計算は高速です。たとえば、F(20)
を計算する場合を考えますと、F(20) = F(19) + F(18)
ですが、第二項 F(18)
は 第一項 F(19) = F(18) + F(17)
によって既に計算済みで、キャッシュを返すだけで実際には計算はされません。
60 番目のフィボナッチ数は次のようにして出力できます。
// 1548008755920
print(fibonacci(60))
20 番目までのフィボナッチ数は次のようにして出力できます。
// 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765
print(
(0...20)
.lazy
.map(fibonacci)
.map(String.init)
.joined(separator: " ")
)
インデックスが二次元の場合
メモ化関数はインデックスが Hashable
であることを要求します。
これは計算済みの結果を、インデックスをキーとして辞書にキャッシュするためです。
今回は次のように定義しました。
struct Index: Hashable {
let row: Int
let column: Int
}
二次元の場合も一次元の漸化式と同様、インデックスの初期値まで再帰すれば問題を解くことが可能です。
動的計画法 2-3 の問題を漸化式で表現する
ただし、0日目の場合は過去に遡ることはできないので合計ではなく0日目の報酬そのものです。
上述の内容を関数 memo
によって実装すると次のようになります。
let dp: (Index) -> Int = memo { F, index in
let (i,j) = (index.row, index.column)
switch i {
case 0: // 初項
return aa[i][j]
case _: // 一般項
return aa[i][j] + max(
F(Index(row: i-1, column: (j+1)%3)),
F(Index(row: i-1, column: (j+2)%3))
)
}
}
ここで、 (j+1)%3
, (j+2)%3
は j
と相異なる2数です。
aa
は報酬を表す二次元配列です。
let N = Int(readLine()!)!
var aa = [[Int]]()
for _ in 0...N-1 {
aa.append(readLine()!.split(separator: " ").map { Int(String($0))! })
}
報酬の最大値を求める
最終日に仕事
max(
dp(Index(row: N-1, column: 0)),
dp(Index(row: N-1, column: 1)),
dp(Index(row: N-1, column: 2))
)
これが解答です。しかし、 dp
の呼び出しを1度だけにすることもできます。
最終的な解答は以下になります。
func memo<T: Hashable, U>(body: @escaping(((T) -> U), T) -> U) -> (T) -> U {
var memo = [T: U]()
var function: ((T) -> U)!
function = { x in
memo[x] ?? {
let r = body(function, x)
memo[x] = r
return r
}()
}
return function
}
struct Index: Hashable {
let row: Int
let column: Int
}
let N = Int(readLine()!)!
var aa = [[Int]]()
for _ in 0...N-1 {
aa.append(readLine()!.split(separator: " ").map { Int(String($0))! })
}
aa.append([0, 0, 0])
aa.append([0])
let dp: (Index) -> Int = memo { F, index in
let (i,j) = (index.row, index.column)
switch i {
case 0: // 初項
return aa[i][j]
case _: // 一般項
return aa[i][j] + max(
F(Index(row: i-1, column: (j+1)%3)),
F(Index(row: i-1, column: (j+2)%3))
)
}
}
print(dp(Index(row: N+1, column: 0)))