Rustで動的計画法の実装:Grid
はじめに
動的計画法を実装してみて、Rustの勉強をやってみる。問題としてはEducational DP Contestという動的計画法の練習を目的としたコンテストのものを使用。
AからZまで問題が設定されているが、今回はHのGrid1とYのGrid2、壁のあるグリッドでの最短経路の数を求める問題。同じ問題であるがGrid1はグリッドのサイズに制限があり、壁の数に制限がない。Grid2は壁の数に制限がある代わりにグリッドのサイズの制限がGrid1より緩い。このため、解き方が異なる。
利用したライブラリ
標準入力からデータを読み取る部分はproconio
を利用。入力のノード番号が1オリジン(1-indexed)なので、0オリジン(0-indexed)に変更するためにmarker::Usize1
も使う。
Grid1: 完成したコード
動的計画法っぽく、順番を意識しながら(0,0)
から数え上げれば解ける。オーバーフローを避けるためにMOD
で割った余りを出力するという指定があるので、計算の度に引いている。
use proconio::input;
use proconio::marker::Chars;
const MOD:i64 = 1000000007;
fn main(){
input!(
h: usize, w: usize, n:usize,
grid: [Chars; n]
);
let mut dp: Vec<Vec<i64>> = vec![vec![0; w]; h];
dp[0][0] = 1;
for i in 0..h {
for j in 0..w {
if i < h - 1 && grid[i+1][j] == '.' { dp[i+1][j] += dp[i][j]; if dp[i+1][j] >= MOD { dp[i+1][j] -= MOD; } }
if j < w - 1 && grid[i][j+1] == '.' { dp[i][j+1] += dp[i][j]; if dp[i][j+1] >= MOD { dp[i][j+1] -= MOD; } }
}
}
println!("{}", dp[h-1][w-1]);
}
//
Rustの&&はLazy処理
Rustの&&はLazy Operatiorである。つまり、&&の前の項がfalse
であれば後ろの項は処理されない。このため、以下のように配列のindexのチェックと配列の参照を&&で繋げるとエラーは発生しない。
if i < h - 1 && grid[i+1][j] == '.' { dp[i+1][j] += dp[i][j]; if dp[i+1][j] >= MOD { dp[i+1][j] -= MOD; } }
つまり、以下のような処理と同じになることが保証されている。
if i < h - 1 {
if grid[i+1][j] == '.' {
Perlでは&&, ||と別にLazyな処理が保証された演算子であるand
とor
が導入されていた。以下のような書き方はPerlの定番である。
fopen(INFILE, "foo.txt") or die "Cannot open file: $!";
昔は実装依存とかそんな話があったような気がするが、Wikipediaによると、いまどきはだいたいLazy Operatorsであるようだ。
Grid2: 解いてみる
最初Gird2をみた時、入力フォーマットの違いだけかと思った。つまり、以下のようにフォーマットを変換してあげれば解けるのではないかと。実際は入力例の4、
fn main(){
input!(
h: usize, w: usize, n:usize,
wall: [(usize, usize); n]
);
let mut grid = vec![vec!['.'; w]; h];
for (x, y) in wall {
grid[x - 1][y - 1] = '#';
}
包除原理で解く
数え上げる方法での計算オーダーは
剰余演算も必要なので実装も面倒ではあるが、AtCoder Liburary ac_library
に便利なライブラリが用意されているので、これを使って実装した。
Closure
Closureは無名関数だが、scope内の変数を参照できるので、インラインマクロのようにも使える。
階乗をfactorial
とfactorial_inv
で予め計算して、それを使ってconbinationを計算する関数combination(a,b)
を以下のように定義している。
let combination = |a, b| factorial[a] * factorial_inv[b] * factorial_inv[a - b];
two-phase borrow
以下の部分。+=
を使って書きたくなるがエラーがでてしまう。dp[i][j]
がModInt<1000000007>
型であるのが問題のようで、これまでi64
やusize
のような組込の型では問題にならなかった。two-phase borrowが適用されるかどうかという話らしい。以下のように展開してしまえば問題ない。
dp[j][m] = dp[j][m] + dp[i][1 - m] * combination(dx + dy, dx);
詳しくは以下の文章が分かりやすい。
計算量の測定
問題を作成するスクリプトを作って計算量の測定もやってみた。入力値のチェックをしていないので、グリッドサイズに対して大量の壁を要求すると、処理が終わらないとは思うが、テスト用なので問題はない。
#!/user/bin/env python
import random
import sys
(h, w, n) = (int(sys.argv[1]), int(sys.argv[2]), int(sys.argv[3]))
s = set()
while len(s) < n:
pos = (random.randint(1,h), random.randint(1,w))
if pos != (1,1) and pos != (h, w) and pos not in s:
s.add(pos)
print('{} {} {}'.format(h, w, n))
for (y, x) in s:
print('{} {}'.format(y,x))
数え上げ(counting)と包除原理(PIE; Principle of Inclusion-Exclusion) での計算時間の比較。PIEの方は階乗計算を予め計算しており、その時間も含む。
上は壁の数を10個に固定して
下のグラフはグリットサイズを
PIEでは今回、階乗だけでなく階乗の逆元も予め求めており、Conbinationを求める時は掛け算だけで処理できるようにしている。当然、元の式である以下のようにしても計算可能である。
let combination = |a, b| factorial[a] / (factorial[b] * factorial[a - b]);
逆元を予め求める場合(PIE)と予め求めない場合(PIE not inv)の比較を以下に示す。予め計算するための時間にも差があるはずであるが、それよりも計算量の増加が大きい。メモリ量や計算規模の問題もあるが、逆元は予め計算しておいた方が速い。
関連記事
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