怪獸存活概率問題
作者:Grey
原文地址:
題目描述
給定3個參數,N,M,K 怪獸有 N 滴血,等着英雄來砍自己,英雄每一次打擊,都會讓怪獸流失,
怪獸每一次流失的血量在區間[0……M]上等概率的獲得一個值,求 K 次打擊之後,英雄把怪獸砍死的概率。
主要思路
由題目含義可知:怪獸在經歷 K 次打擊後所有可能的掉血情況有 (M+1) 的 K 次方種,,即:
long all = (long) Math.pow(M + 1, K)
如果怪獸在 K 次打擊後,被砍死的情況有 kill 種,那麼
(double) kill / (double) all;
即爲怪獸被砍死的概率。
暴力解法
定義遞歸函數
long process(int times, int M, int hp)
遞歸含義是:怪獸還剩 hp 點血,每次的傷害在[0……M]範圍上,還有 times 次可以砍,返回砍死的情況數。
那麼 base case 有如下兩種情況
// 情況一:已經沒有被砍的次數了,這個時候,血量如果正好是小於等於0的值, 說明怪獸已經被砍死一次
// 否則怪獸不可被砍死
if (times == 0) {
return hp <= 0 ? 1 : 0;
}
// 情況二:怪獸已經死了,但是還可以砍
// 此時,所有的砍法都滿足條件,所以情況就是(long) Math.pow(M + 1, times)
if (hp <= 0) {
return (long) Math.pow(M + 1, times);
}
接下來就是普遍情況,由於每次攻擊是 [0……M] 中等概率的一個值,則枚舉從 0 到 M 任意一個值跑遞歸函數即可。
long ways = 0;
for (int i = 0; i <= M; i++) {
ways += process(times - 1, M, hp - i);
}
完整代碼如下
public class Code_KillMonster {
public static double right(int N, int M, int K) {
if (N < 1 || M < 1 || K < 1) {
return 0;
}
// monster在經歷K次打擊後所有可能的掉血情況是
long all = (long) Math.pow(M + 1, K);
long kill = process(K, M, N);
return (double) kill / (double) all;
}
//怪獸還剩 hp 點血,每次的傷害在[0……M]範圍上,還有 times 次可以砍,返回砍死的情況數。
public static long process(int times, int M, int hp) {
// 情況一:已經沒有被砍的次數了,這個時候,血量如果正好是小於等於0的值, 說明怪獸已經被砍死一次
// 否則怪獸不可被砍死
if (times == 0) {
return hp <= 0 ? 1 : 0;
}
// 情況二:怪獸已經死了,但是還可以砍
// 此時,所有的砍法都滿足條件,所以情況就是(long) Math.pow(M + 1, times)
if (hp <= 0) {
return (long) Math.pow(M + 1, times);
}
long ways = 0;
for (int i = 0; i <= M; i++) {
ways += process(times - 1, M, hp - i);
}
return ways;
}
}
動態規劃(未做枚舉優化)
根據上述暴力遞歸函數可以得知,遞歸函數的可變參數有兩個,分別是 times 和 hp,且變化範圍是固定的,可以定義一個二維數組 dp,表示所有的遞歸過程解
long[][] dp = new long[K + 1][N + 1];
dp[times][hp]
就表示遞歸函數long process(int times, int M, int hp)
的含義,即:怪獸還剩 hp 點血,每次的傷害在[0……M]範圍上,還有 times 次可以砍,砍死的情況數有多少。
根據 base case, 可知
dp[0][0] = 1;
且
dp[times][0] = (long) Math.pow(M + 1, times)
接下來就是普遍位置,根據上述暴力遞歸函數可知:process(times, M, hp)
依賴process(times - 1, M, hp - i)
即dp[times][hp]
依賴dp[times-1][hp-i]
位置,如下圖所示
圖中綠色部分的格子依賴黃色部分的格子,
代碼如下,
for (int times = 1; times <= K; times++) {
dp[times][0] = (long) Math.pow(M + 1, times);
for (int hp = 1; hp <= N; hp++) {
long ways = 0;
for (int i = 0; i <= M; i++) {
if (hp - i >= 0) {
ways += dp[times - 1][hp - i];
} else {
ways += (long) Math.pow(M + 1, times - 1);
}
}
dp[times][hp] = ways;
}
}
完整代碼如下
public class Code_KillMonster {
public static double dp1(int N, int M, int K) {
if (N < 1 || M < 1 || K < 1) {
return 0;
}
long all = (long) Math.pow(M + 1, K);
long[][] dp = new long[K + 1][N + 1];
dp[0][0] = 1;
for (int times = 1; times <= K; times++) {
dp[times][0] = (long) Math.pow(M + 1, times);
for (int hp = 1; hp <= N; hp++) {
long ways = 0;
for (int i = 0; i <= M; i++) {
if (hp - i >= 0) {
ways += dp[times - 1][hp - i];
} else {
ways += (long) Math.pow(M + 1, times - 1);
}
}
dp[times][hp] = ways;
}
}
long kill = dp[K][N];
return (double) ((double) kill / (double) all);
}
}
動態規劃(枚舉優化)
上述動態規劃解法中的第三個循環可以優化,再一次看下依賴關係圖
當我們得到綠色格子,即dp[times][hp]
位置的值以後,如果要求dp[times+1][hp]
位置的時候,即如下 target 位置
可以考慮 G 和 H 兩個位置
因爲 G 位置求的時候,紫色部分格子已經求過了,補上一個 H 位置,就可以把 target 求出來,省略了枚舉行爲。
完整代碼如下
public class Code_KillMonster {
public static double dp2(int N, int M, int K) {
if (N < 1 || M < 1 || K < 1) {
return 0;
}
long all = (long) Math.pow(M + 1, K);
long[][] dp = new long[K + 1][N + 1];
dp[0][0] = 1;
for (int times = 1; times <= K; times++) {
dp[times][0] = (long) Math.pow(M + 1, times);
for (int hp = 1; hp <= N; hp++) {
dp[times][hp] = dp[times][hp - 1] + dp[times - 1][hp];
if (hp - 1 - M >= 0) {
dp[times][hp] -= dp[times - 1][hp - 1 - M];
} else {
dp[times][hp] -= Math.pow(M + 1, times - 1);
}
}
}
long kill = dp[K][N];
return (double) ((double) kill / (double) all);
}
}