Rustで動的計画法の実装:Coins
はじめに
動的計画法を実装してみて、Rustの勉強をやってみる。問題としてはEducational DP Contestという動的計画法の練習を目的としたコンテストのものを使用。
AからZまで問題が設定されているが、今回はIのCoins。
利用したライブラリ
標準入力からデータを読み取る部分はproconio
を利用。
完成したコード
完成してから感じたが、動的計画法っぽくするのなら、dp[i][j]
として実装するのが正しい。ただ今回の問題の場合、確率なので復元も必要ないし、dp[j]
1本で実装してしまった。
use proconio::input;
fn main(){
input!(
n: usize,
mut coins: [f64; n]
);
let target = (n / 2) + 1;
let mut dp = vec![0.0; target + 1];
dp[0] = 1.0;
for i in 0..n {
dp[target] = dp[target - 1] * coins[i] + dp[target];
for j in (1 .. target).rev() {
dp[j] = dp[j - 1] * coins[i] + dp[j] * (1.0 - coins[i]);
}
dp[0] = dp[0] * (1.0 - coins[i]);
}
println!("{}", dp[target]);
}
アルゴリズム
アルゴリズムという程のものはないが、
このため、
dp[j] = dp[j - 1] * coins[i] + dp[j] * (1.0 - coins[i]);
また、target
個以上)の表がdp[target]
をtarget
枚以上の表がでている場合を表す事として、以下の式にする。
dp[target] = dp[target - 1] * coins[i] + dp[target];
降順でのループ
dp[j]
を求めるために、dp[j]
とdp[j-1]
を利用している。今回の実装はdp[j-1]
はdp[j]
の後で計算しないといけない。つまり、j
は降順でループさせないといけない。
for j in (1 .. target).rev() {
Rustではrev
を使って弱順にする。 1..5は 1, 2, 3, 4
になるが、5..1は空になってしまう。このため。一度正順で作成したものをrev()
で逆転させる必要がある。つまり(1..5).rev()
で4, 3, 2, 1
になる。5..1で期待するであろう5, 4, 3, 2
を得るには(2..6).rev()
になる。
2本のベクトルを使う
そもそも、1本のベクトルでやろうとしてたので順番を気にする必要があったのではないか。ならば、今(dp[]
と、次(next_dp[]
の2本を使った方が分かりやすい。
ループの最後にdp
にnext_dp
を入れているが、ここはベクトルのコピーではなく、ownerの変更なので重い処理では無い。
dp[0] = 1.0;
for i in 0..n {
let mut next_dp = vec![0.0; n];
next_dp[0] = dp[0] * (1.0 - coins[i]);
for j in 1 .. target {
next_dp[j] = dp[j - 1] * coins[i] + dp[j] * (1.0 - coins[i]);
}
next_dp[target] = dp[target - 1] * coins[i] + dp[target];
dp = next_dp;
}
計算時間の測定
計算量としては変わらないはずであるが、2本のベクトルを使った方が毎回メモリ確保をしないといけないので遅いはず...と予測して計算量を測定した。問題を作るスクリプトは以下のような感じ。
import random
import sys
n = int(sys.argv[1])
p = [random.random() for i in range(n)]
print('{}'.format(n))
print(' '.join(map(lambda x: f'{x:.02f}', p)))
コインの数
差がでてくる可能性としては以下の部分。ベクトルが1本の場合は、ループを順番に処理しないといけないが、2本のベクトルを使う場合は計算の順番に依存しないので並列化が可能。コンパイラがうまいこと並列化しなのではないか?と推測する。
for j in (1 .. target).rev() {
dp[j] = dp[j - 1] * coins[i] + dp[j] * (1.0 - coins[i]);
}
for j in 1 .. target {
next_dp[j] = dp[j - 1] * coins[i] + dp[j] * (1.0 - coins[i]);
}
上で説明文を書いていて感じたが、2本のベクトルを使ったコードの方が分かりやすい。つまり、結論としてはケチるとうまくいかないこともあるという話であった。
関連記事
Rustで動的計画法の実装:
🐸Frog | 🌴Vacation | 🎒Knapsack | 🐍LCS | 🚶♂️Longest Path | 🕸️Grid | 💰Coins | 🍣Sushi | 🪨Stones | 📐dequeue | 🍬Candies | 🫥Slimes | 💑Matching | 🌲Indipendent Set | 🌻Flowers | 👣Walk | 🖥️Digit Sum | 🎰Permutation | 🐰Grouping | 🌿Subtree | ⏱️Intervals | 🗼Tower | 🐸Frog3
Discussion