Rustで動的計画法:Dequeue
はじめに
動的計画法を実装してみて、Rustの勉強をやってみる。問題としてはEducational DP Contestという動的計画法の練習を目的としたコンテストのものを使用。
AからZまで問題が設定されているが、今回はLのDeque。交互に数値を取っていって、点数を競い合うゲーム。
利用したライブラリ
標準入力からデータを読み取る部分はproconio
を利用。
久々にchmax
を使うかな?と思ったが、max
しか要らなかったので、std::cmp::max
を使用。
完成したコード
実装というより、アルゴリズムの方が難しくなってきている。
use proconio::input;
fn main(){
input!(
n: usize,
queue: [i64; n]
);
let mut dp: Vec<Vec<i64>> = Vec::with_capacity(n);
for i in 0..n {
let mut row = vec![0i64; i + 1];
row[i] = queue[i];
dp.push(row);
}
for i in 1..n {
for j in (0..i).rev() {
dp[i][j] = max(queue[i] - dp[i - 1][j], queue[j] - dp[i][j + 1]);
}
}
println!("{}", dp[n-1][0]);
}
アルゴリズム
まず、「太郎君は
dp[i][j]
をdp[i][j]
にはdp[i][j]
にはdp[i][i]
はqueue[i]
となる(初期値)。
dp[i][j]
の状態からの遷移はdp[i][j+1]
とdp[i-1][j]
となる。相手はスコアを最大にする手を選択するので、dp[i][j]
の値は取った数値から、遷移先の相手の点数の最大値を引いたものである。言い方を変えると、転移先の点数に-1
をかけたもの(符号を変えたもの)が自分のスコアになるので、それに今取った数値を足すと、この状態での自分のスコアになる。これの最大化をはかるので、最大値を選べばいい。
dp[N-1][0]
が、開始状態(
下三角行列の表現方法
dp[i][j]
の添え字はdp[0]
には長さ1のベクタが入ってるが、dp[n-1]
には、長さ
これで問題ないとは思うが、他の表現方法と比較をしてみたい。
富豪的表現手法
ケチケチせずに
let mut dp = vec![vec![0i64; n]; n];
for i in 0..n {
dp[i][i] = queue[i];
}
1次元での表現手法
昔こんな風に実装していたなぁという記憶をたどりつつ実装した。1次元配列を用意して2次元空間をそこにマッピングする。各行の要素の数が差分1の等差数列なのでマッピングはすぐできる。
let pos = |i:usize, j:usize| { i * (i + 1) / 2 + j };
let mut dp = vec![0i64; pos(n, 0)];
for i in 0..n {
dp[pos(i,i)] = queue[i];
}
パフォーマンス
処理時間の測定
メモリ量の測定
Heapメモリの量を見るためにdhatを使った。ドキュメントにあるようにすれば、profilerのライフタイム終了時にメモリの利用量がでてくる。
それぞれの方式での、
古典的な1次元にマッピングするのが、速度/メモリ的に一番良さそうではあるが、読みやすさや汎用性から#1のアプローチも悪くないとは思う。
memory | allocation | |
---|---|---|
#1 | 4,057,366 bytes | 1,011 blocks |
#2 | 8,053,415 bytes | 1,011 blocks |
#3 | 4,033,408 bytes | 11 blocks |
関連記事
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