(ΦωΦ)<これのつづきですよー。目次はここ。
---
これ、考えてみよう。
HPが $H$ の化け猫を倒すことを考えます。
与ダメージ $A$ の通常攻撃は必ずヒットしますが、与ダメージ $D$ の必殺攻撃は $2/3$ の確率でしか成功せず、失敗したときはダメージを与えられません。
毎回どちらかの攻撃をしたとして、化け猫を倒すまでに攻撃する回数の期待値を最小とするように攻撃していったとき、
倒したときの攻撃回数の期待値はいくらですか。yukicoder No.23
$H, A, D$ はともに $10{,}000$ 以下。
全探索が書けるかい?
今までどれだけのダメージを与えたかという情報が分かれば、攻撃回数の最小の期待値は決まる。
与えたダメージが化け猫のHP以上になったときは、もう攻撃しなくていい。
$2/3$ でしか攻撃が当たらないというのは、この攻撃がヒットするまでの回数の期待値は逆数の $3/2$ になる。
これだけ知っていれば、書けるんじゃないかい?
(ΦωΦ)<コード 2-1
#include <iostream>
#include <algorithm>
using namespace std;
const int inf = 987654321;
int h, a, d;
// 今までの与ダメージがdamageのときの
// 攻撃回数の最小の期待値を返す
double rec(int damage) {
if(damage >= h) { return 0; }
return min(rec(damage + a) + 1,
rec(damage + d) + 3./2);
}
int main(void) {
scanf("%d%d%d", &h, &a, &d);
double res = rec(0);
printf("%lf\n", res);
return 0;
}
---
あとはこれをメモ化すればいい。
(ΦωΦ)<コード 2-2
#include <iostream>
#include <algorithm>
using namespace std;
const int inf = 987654321;
vector<double> dp;
int h, a, d;
// 今までの与ダメージがdamageのときの
// 攻撃回数の最小の期待値を返す
double rec(int damage) {
if(dp[damage] != inf) { return dp[damage]; }
if(damage >= h) { return dp[damage] = 0; }
return
dp[damage] = min(rec(damage + a) + 1,
rec(damage + d) + 3./2);
}
int main(void) {
scanf("%d%d%d", &h, &a, &d);
dp.assign(h+10, inf);
double res = rec(0);
printf("%lf\n", res);
return 0;
}
---
再帰関数では何をもらって何を返せばいいのか、これをちゃんと考えられたら活路は見出せるように思う。
動的計画法やメモ化再帰への布石となる全探索のインターフェース(メソッドのシグニチャ)は、
"知りたい答えを返して、それを一意に決めるための状態を引数にする"なんだろう。
01ナップサックだと、「いま何番目を見てるか」と「これまでの重さはいくらか」という2つが決まれば「価値の最大値」が分かるし、
今回の問題だと、「与ダメージ」(あるいは逆に「残りHP」でもいい)さえ分かれば「攻撃回数の最小の期待値」が分かる。
なので、「これこれさえ決まってくれればなあ」というのを引数にして、知りたい答えを返し、
さらに、引数を状態としてdpテーブルに答えを覚えておく、これがメモ化再帰なんだろうね、きっと>(ΦωΦ)
だから全探索は大事。
(ΦωΦ)<つづく