引言
今日心血来潮,想起了一道之前没有解决的算法题,但我现已近一年没碰过算法了。
根底问题
相信咱们都玩过奇特宝物和相似游戏,本文咱们一起考虑奇特宝物和相似游戏对战的一系列简化模型,并研讨其中的概率论问题。假如你之前没有学过概率dp和希望dp,那么本文能够教你一点概率dp和希望dp的套路,并提升一点点写小模仿的才干。
- 我方和BOSS轮番开释技术,技术能够形成损伤、给自身和队友(假如允许多打多)回血、添加特点等。
- 特点包含进犯、防护、特攻、特防、血量、速度等。开释技术先后顺序由速度决议;技术损伤由威力、我方进犯、对手防护、技术系别、对手系别(水、火、草……)等要素决议,并且会乘一个随机系数添加可玩性。
- 技术有运用次数约束,名为PP值,还能够吃药补PP值。
- ……
一道算法题,要考虑上述一切要素就太复杂了,咱们先提炼一个精简的模型,来编写一道简略的概率论题目。根底问题如下:BOSS有x
滴血,我一局打BOSS掉y
滴血,BOSS有p
的概率回z
滴血,有1-p
的概率进犯我。我均匀需求几局能打败BOSS?假定:(1)我血量无限。(2)血量不能超过血量上限。(3)我先手,且打赢BOSS那局应该算1局而非0.5局。(3)y <= x
。
全希望公式、全概率公式和希望dp、概率dp
全概率公式:P(B) = sum(P(A[i]) * P(B | A[i]))
全希望公式:E(B) = sum(P(A[i]) * E(B | A[i]))
这两个公式思想是一致的,便是将问题划分为若干个互斥事情,并分别求条件概率和条件希望。在算法竞赛中,条件概率和条件希望往往能够用dp
来描绘,dp
的意义一般为“从初始状况到终态”的希望步数or概率。而dp
的各个维度既能将问题划分为若干互斥事情,也是对当时局势的一种描绘。
有时咱们写出的状况搬运方程存在循环依靠,此刻能够尝试消除循环依靠,比方:设中间变量。但假如做不到,咱们往往需求将其建模为方程组才干求解,不过大多数状况下希望dp和概率dp的方程组都是线性方程组。
经过其他算法题,了解了上述套路后,本文你就不需求再看了,由于本文涉及的技巧都太过明显了,只剩下了完成上的难度。假如没有了解,能够以本文为例帮助了解上述套路。
作者:hans774882968以及hans774882968以及hans774882968
本文52pojie:www.52pojie.cn/thread-1766…
本文juejin:/post/721596…
本文CSDN:blog.csdn.net/hans7748829…
根底问题:希望dp做法
设dp[i]
为BOSS从i
滴血的初始局势到被打败均匀需求几局。由全希望公式:
dp[0] = 0
dp[1~y] = 1
dp[y+1] = 1 + p*dp[min(z+1,x)] + (1-p)*dp[1]
dp[y+2] = 1 + p*dp[min(z+2,x)] + (1-p)*dp[2]
# 即
dp[i] = 1 + p*dp[min(i-y+z,x)] + (1-p)*dp[i-y]
这个dp有依靠关系,线性时刻复杂度的做法似乎不好想,那就用线性方程组解吧!我选择用numpy
来解线性方程组,假如import
失利,需求pip install numpy
进行装置。具体见代码。
根底问题:brute force,或许说是蒙特卡洛模仿
蒙特卡洛模仿的做法主要是用于对拍。由于我手头没有数据,所以我以为对拍程序和算法输出结果差异较小,即为两者完成均正确。在brute force做法中,为了完成方便,我用了一个小技巧:把退出条件设为cur_blood > y
,退出后只需求1局或0局即可打败BOSS。
也能够将我放技术和BOSS放技术的动作拆解开来。这个写法在“扩展2”及以后一切扩展问题中都是必需的。
根底问题:代码
brute force函数名base_bf
。
import random
import numpy as np
def base_dp(x: int, y: int, z: int, p: float):
b = np.array([[1 if i else 0] for i in range(x + 1)])
a = [[0] * (x + 1) for _ in range(x + 1)]
for i in range(x + 1):
a[i][i] = 1
for i in range(y + 1, x + 1):
a[i][min(i - y + z, x)] -= p
a[i][i - y] += p - 1
a = np.array(a)
ans = np.linalg.solve(a, b)
return ans[x][0]
def base_bf(x: int, y: int, z: int, p: float, trys=1000000):
def bf(x: int, y: int, z: int, p: float):
ret = 0
cur_blood = x
while cur_blood > y:
ret += 1
rv = random.random()
if rv < p:
cur_blood = min(cur_blood - y + z, x)
else:
cur_blood -= y
# 剩1~y滴血时,1局可打败
return ret + (1 if cur_blood > 0 else 0)
tot = 0
for _ in range(trys):
tot += bf(x, y, z, p)
return tot / trys
print(base_dp(4, 1, 2, 0.25)) # 6.037037037037038
print(base_bf(4, 1, 2, 0.25))
print(base_dp(80, 14, 22, 0.2)) # 8.216744626007252
print(base_bf(80, 14, 22, 0.2))
扩展1:考虑我方血量
BOSS有x1
滴血,我有x2
滴血,BOSS一局打我掉y1
滴血,我一局打BOSS掉y2
滴血,BOSS有p
的概率回z
滴血,有1-p
的概率进犯我。赢界说为:BOSS血量小于等于0且我方血量大于0。我赢的概率是多少?假定:(1)血量不能超过血量上限。(2)我先手。(3)y2 <= x1, y1 <= x2
。
PS:本来还想问:我在成功的状况下的希望局数。但想来原理是差不多的,就先不写了。
扩展1:2维的概率dp
能够用(BOSS血量, 我的血量)
来描绘一个局势。设dp[i][j]
为当时局势BOSS有i
滴血,我有j
滴血,到“赢”的各个状况,即局势(0, j > 0)
的概率之和。由全概率公式:
dp[0~x1][0] = 0
dp[0][1~x2] = 1
# 值得留意的是,这儿抽出了我方再进犯一次就成功(必胜)的局势,这是针对“扩展1”的特殊性的做法,能够简化代码完成,但后续不能再运用这个技巧。
dp[1~y2][1~x2] = 1
dp[i][j] = p*dp[min(i-y2+z,x1)][j] + (1-p)*dp[i-y2][j-y1], i = y2+1~x1, j = 1~x2
这儿需求越界判别,假如j <= y1
,则dp[i-y2][j-y1]
应改为取0。
依旧是用矩阵完成,这儿的完成难度会比上面大一些。我运用的技巧是,将上面给出的dp公式进行编号,dp[0~x1][0] = 0
为第0~x1
号公式,依此类推,于是能够保护一个expression_count
变量。有expression_count
后,咱们正常枚举BOSS血量和自己的血量,并初始化矩阵即可。别的,能够引进一个pos
函数,来指出某个局势对应的矩阵中的方位,以减小编码难度:
# boss, me
def pos(i, j):
if i < 0 or i > x1 or j < 0 or j > x2:
raise Exception("越界!")
return i * (x2 + 1) + j
扩展1:brute force
仍然是一道小模仿,但难度更大了。学习上一题的主意,我把退出条件仅设定为BOSS必败,并在循环中识别我失利的场景。
也能够将我放技术和BOSS放技术的动作拆解开来。这个写法在后续一切的扩展问题中都是有必要的。
扩展1:代码
brute force函数名i_may_lose_bf
。在测试数据中发现了一些规则。
import random
import numpy as np
# boss血量,我血量,boss进犯,我进犯,boss回血技术回血量,boss发起回血技术概率
def i_may_lose_dp(x1: int, x2: int, y1: int, y2: int, z: int, p: float):
mat_size = (x1 + 1) * (x2 + 1)
# boss, me
def pos(i, j):
if i < 0 or i > x1 or j < 0 or j > x2:
raise Exception("越界!")
return i * (x2 + 1) + j
b = [[0] for _ in range(mat_size)]
for i in range(x1 + 1, x1 + 1 + x2 + y2 * x2):
b[i][0] = 1
b = np.array(b)
a = [[0] * mat_size for _ in range(mat_size)]
expression_count = 0
for i in range(x1 + 1):
a[expression_count][pos(i, 0)] = 1
expression_count += 1
for i in range(1, x2 + 1):
a[expression_count][pos(0, i)] = 1
expression_count += 1
for i in range(1, y2 + 1):
for j in range(1, x2 + 1):
a[expression_count][pos(i, j)] = 1
expression_count += 1
assert expression_count == x1 + 1 + x2 + y2 * x2
for i in range(y2 + 1, x1 + 1):
for j in range(1, x2 + 1):
a[expression_count][pos(i, j)] = 1
a[expression_count][pos(min(i - y2 + z, x1), j)] -= p
if j > y1:
a[expression_count][pos(i - y2, j - y1)] += p - 1
expression_count += 1
assert expression_count == mat_size
a = np.array(a)
ans = np.linalg.solve(a, b)
return ans[mat_size - 1][0]
def i_may_lose_bf(
x1: int,
x2: int,
y1: int,
y2: int,
z: int,
p: float,
trys=1000000
):
def bf(x1: int, x2: int, y1: int, y2: int, z: int, p: float):
ret = 0
boss_blood, my_blood = x1, x2
while boss_blood > y2:
ret += 1
rv = random.random()
if rv < p:
boss_blood = min(boss_blood - y2 + z, x1)
else:
boss_blood -= y2
my_blood -= y1
if my_blood <= 0:
return -1, False
# boss剩1~y2滴血时,1局后我必胜
return ret + (1 if boss_blood > 0 else 0), True
tot, win_count = 0, 0
for _ in range(trys):
step, is_win = bf(x1, x2, y1, y2, z, p)
if is_win:
tot += step
win_count += 1
return tot / win_count if win_count else -114514, win_count / trys
cases = [
# (4, 5, 1, 1, 2, 0.25), # 0.80859375
# (4, 6, 1, 1, 2, 0.3), # 0.8673489999999998
# (17, 18, 3, 4, 5, 0.2), # 0.9998172160000002
# (17, 17, 3, 3, 5, 0.25), # 0.31640625
# (26, 27, 5, 5, 9, 0.3), # 0.5282199999999998
(26, 17, 2, 3, 2, 0.3), # 1
(26, 17, 2, 3, 3, 0.3), # 1
(26, 17, 2, 3, 4, 0.3), # 0.2552983299999999
(26, 17, 2, 3, 5, 0.3), # 0.08235429999999996
(26, 17, 2, 3, 6, 0.3), # 0.08235429999999996
# (24, 24, 5, 5, 5, 0.3), # 1
# (24, 24, 5, 5, 6, 0.3), # 0.6516999999999998
# (24, 24, 5, 5, 7, 0.3), # 0.3429999999999999
# (24, 24, 5, 5, 9, 0.3), # 0.3429999999999999
# (24, 24, 5, 5, 10, 0.3), # 0.3429999999999999
# (24, 24, 5, 5, 25, 0.3), # 0.3429999999999999
# (23, 22, 4, 4, 4, 0.3), # 1
# (23, 22, 4, 4, 5, 0.3), # 0.5282199999999998
# (23, 22, 4, 4, 6, 0.3), # 0.24009999999999992
# (23, 22, 4, 4, 7, 0.3), # 0.24009999999999992
# (23, 22, 4, 4, 20, 0.3), # 0.24009999999999992
# (23, 20, 4, 4, 0, 0.3), # 0.8319300000000001
# (23, 20, 4, 4, 1, 0.3), # 0.8319300000000001
# (23, 20, 4, 4, 2, 0.3), # 0.579825
# (23, 20, 4, 4, 3, 0.3), # 0.3529305
# (23, 20, 4, 4, 4, 0.3), # 0
# (23, 15, 4, 4, 0, 0.25), # 0.3671875
# (23, 15, 4, 4, 1, 0.25), # 0.16943359375
# (23, 15, 4, 4, 2, 0.25), # 0.070556640625
# (23, 15, 4, 4, 3, 0.25), # 0.003505706787109375
# (23, 15, 4, 4, 4, 0.25), # 0
]
for c in cases:
ans1 = i_may_lose_dp(*c)
ans2 = i_may_lose_bf(*c)
print(ans1 - ans2[1], ans1, ans2)
扩展2:两边都能进犯或回血
这个问题有对称性了。做法和扩展1差不多,也比扩展3简略,但时刻关系我先搁置了,由咱们补全代码~
扩展3:我方能运用加进犯技术且有次数约束,对方能无限次运用回血技术
BOSS和我分别有boss_blood, my_blood
滴血,BOSS和我的进犯、防护分别为boss_attack, my_attack, boss_defense, my_defense
,BOSS回血量为boss_add_blood
,回血技术运用概率为boss_add_blood_probability
,我运用加进犯技术的概率为my_add_attack_probability
,我运用加进犯技术的次数上限为my_add_attack_limit
。我打BOSS的血量为my_power = max((k + 1) * my_attack - boss_defense, 0)
,k
表明我当时的进犯等级,在这个问题中简化为运用加进犯技术的次数,取值0~my_add_attack_limit
。BOSS打我的血量为boss_power = max(boss_attack - my_defense, 0)
。赢界说为:BOSS血量小于等于0且我方血量大于0。我赢的概率是多少?假定:(1)两边血量不能超过血量上限。(2)我先手,且打赢BOSS那局应该算1局而非0.5局。(3)boss_power > 0
且my_biggest_power = (my_add_attack_limit + 1) * my_attack - boss_defense
大于0,这条约束保证战役能完毕。
扩展3:3维dp
做法和“扩展1”相似,咱们需求引进一个新的维度,表明初始状况下我现已运用的加进犯技术的次数。更具体地说,dp界说扩展为:dp[i][j][k]
表明boss初始血量为i
,我初始血量为j
,我初始状况现已运用的加进犯次数为k
的局势下我赢的概率,这儿“赢”依旧是指抵达一系列局势的概率之和。答案为dp[boss_blood][my_blood][0]
。
边界条件:
dp[0~boss_blood][0][0~my_add_attack_limit] = 0
dp[0][1~my_blood][0~my_add_attack_limit] = 1
值得留意的是,扩展1将一种必胜局势抽出,作为边界条件,以简化代码完成。但咱们在此只给出了真正的边界条件,不再专门取出一切的1局后必胜条件,由于假如问题继续扩展,那么人工枚举一切的必胜条件几乎是不可能的。
状况搬运方程:dp[i][j][k] = (1-my_add_attack_probability*boss_add_blood_probability*dp[min(i-my_power+boss_add_blood,boss_blood)][j][k] + (1-my_add_attack_probability)*(1-boss_add_blood_probability)*dp[i-my_power][j-boss_power][k] + my_add_attack_probability*boss_add_blood_probability*dp[i][min(j+boss_add_blood,boss_blood)][k+1] + my_add_attack_probability*(1-boss_add_blood_probability)*dp[i][j-boss_power][k+1]
,其主意便是简略枚举一切的技术运用状况。这个状况搬运方程仅仅一种直观描绘,实际上,我打BOSS和BOSS打我的状况都要留意断定血量,血量需求取到非正数时,dp值应改为取常量1或0。dp值取常量1表明b[expression_count]
要进行相应添加,dp值取常量0表明对a
的操作越过。别的,还需求判别我运用加进犯技术的次数是否现已到达上限,假如到达了运用加进犯技术次数上限,那么进犯的概率应该是1。在代码完成中,为了简略,我把k < my_add_attack_limit
和k == my_add_attack_limit
的状况拆为2个循环。放在一个循环中,并界说一个变量my_actual_add_attack_probability = my_add_attack_probability if k < my_add_attack_limit else 0
的完成方式应该也是可行的。
TODO:找一个和dp
状况数相同数量级的做法,假如找不到,也能够退而求其次,找一个稀疏矩阵的算法。
相关代码如下:
def i_can_add_attack_dp(
boss_blood: int,
my_blood: int,
boss_attack: int,
my_attack: int,
boss_defense: int,
my_defense: int,
boss_add_blood: int,
boss_add_blood_probability: float,
my_add_attack_probability: float,
my_add_attack_limit: int
):
mat_size = (boss_blood + 1) * (my_blood + 1) * (my_add_attack_limit + 1)
# boss_blood, my_blood, my_add_attack_num
def pos(i: int, j: int, k: int):
if i < 0 or i > boss_blood or j < 0 or j > my_blood or k < 0 or k > my_add_attack_limit:
raise Exception("越界!(%s, %s, %s)" % (i, j, k))
return i * (my_blood + 1) * (my_add_attack_limit + 1) + \
j * (my_add_attack_limit + 1) + k
a = [[0] * mat_size for _ in range(mat_size)]
b = [[0] for _ in range(mat_size)]
expression_count = 0
for i in range(boss_blood + 1):
for k in range(my_add_attack_limit + 1):
a[expression_count][pos(i, 0, k)] = 1
expression_count += 1
for j in range(1, my_blood + 1):
for k in range(my_add_attack_limit + 1):
a[expression_count][pos(0, j, k)] = 1
b[expression_count][0] = 1
expression_count += 1
for i in range(1, boss_blood + 1):
for j in range(1, my_blood + 1):
for k in range(my_add_attack_limit):
my_power = max((k + 1) * my_attack - boss_defense, 0)
boss_power = max(boss_attack - my_defense, 0)
a[expression_count][pos(i, j, k)] = 1
# 我进犯,BOSS加血
if my_power >= i:
b[expression_count][0] += (
1 - my_add_attack_probability) * boss_add_blood_probability
else:
a[expression_count][pos(min(i - my_power + boss_add_blood, boss_blood), j, k)] -= (
1 - my_add_attack_probability) * boss_add_blood_probability
# 我进犯,BOSS进犯
if my_power >= i:
b[expression_count][0] += (1 - my_add_attack_probability) * \
(1 - boss_add_blood_probability)
elif boss_power < j:
a[expression_count][pos(i - my_power, j - boss_power, k)] -= (
1 - my_add_attack_probability) * (1 - boss_add_blood_probability)
# 我加进犯,BOSS加血
a[expression_count][pos(min(i + boss_add_blood, boss_blood), j, k + 1)
] -= my_add_attack_probability * boss_add_blood_probability
# 我加进犯,BOSS进犯
if boss_power < j:
a[expression_count][pos(
i, j - boss_power, k + 1)] -= my_add_attack_probability * (1 - boss_add_blood_probability)
expression_count += 1
for i in range(1, boss_blood + 1):
for j in range(1, my_blood + 1):
my_power = max((my_add_attack_limit + 1) *
my_attack - boss_defense, 0)
boss_power = max(boss_attack - my_defense, 0)
a[expression_count][pos(i, j, my_add_attack_limit)] = 1
# 我进犯,BOSS加血
if my_power >= i:
b[expression_count][0] += boss_add_blood_probability
else:
a[expression_count][pos(min(i - my_power + boss_add_blood, boss_blood),
j, my_add_attack_limit)] -= boss_add_blood_probability
# 我进犯,BOSS进犯
if my_power >= i:
b[expression_count][0] += 1 - boss_add_blood_probability
elif boss_power < j:
a[expression_count][pos(
i - my_power, j - boss_power, my_add_attack_limit)] -= 1 - boss_add_blood_probability
expression_count += 1
assert expression_count == mat_size
a = np.array(a)
b = np.array(b)
ans = np.linalg.solve(a, b)
return ans[pos(boss_blood, my_blood, 0)][0]
扩展3:brute force
需求保护的状况多了一个:我当时的进犯等级my_cur_attack_level
。值得留意的是,咱们有必要拆解我方和BOSS开释技术的动作。
咱们引进了my_actual_add_attack_probability
变量my_actual_add_attack_probability = my_add_attack_probability if my_cur_attack_level < my_add_attack_limit else 0
,这样后续代码就只需求人工枚举两边开释技术的一切可能状况,简化了代码完成。
相关代码如下(PS:这段代码应该能够用面向对象来规范一下,但我懒得做了……):
def i_can_add_attack_bf(
boss_blood: int,
my_blood: int,
boss_attack: int,
my_attack: int,
boss_defense: int,
my_defense: int,
boss_add_blood: int,
boss_add_blood_probability: float,
my_add_attack_probability: float,
my_add_attack_limit: int,
trys=1000000
):
assert boss_attack > my_defense # 暂不支撑BOSS打不动我的状况
assert 0 <= boss_add_blood_probability and boss_add_blood_probability <= 1
assert 0 <= my_add_attack_probability and my_add_attack_probability < 1
def bf(
boss_blood: int,
my_blood: int,
boss_attack: int,
my_attack: int,
boss_defense: int,
my_defense: int,
boss_add_blood: int,
boss_add_blood_probability: float,
my_add_attack_probability: float,
my_add_attack_limit: int
):
ret = 0
boss_cur_blood, my_cur_blood, my_cur_attack_level = boss_blood, my_blood, 0
while True:
ret += 1
pivot_boss_skill = random.random()
pivot_my_skill = random.random()
# 引进 my_actual_add_attack_probability
# ,这样后续代码就能够简化为两边开释状况的人工枚举,即若干个if块
my_actual_add_attack_probability = my_add_attack_probability if my_cur_attack_level < my_add_attack_limit else 0
boss_skill_is_attack = pivot_boss_skill >= boss_add_blood_probability
boss_skill_is_add_blood = pivot_boss_skill < boss_add_blood_probability
my_skill_is_attack = pivot_my_skill >= my_actual_add_attack_probability
my_skill_is_add_attack = pivot_my_skill < my_actual_add_attack_probability
if my_skill_is_attack and boss_skill_is_add_blood:
my_power = max((my_cur_attack_level + 1) *
my_attack - boss_defense, 0)
if my_power >= boss_cur_blood:
return ret, True
boss_cur_blood = min(
boss_cur_blood - my_power + boss_add_blood,
boss_blood
)
if my_skill_is_attack and boss_skill_is_attack:
my_power = max((my_cur_attack_level + 1) *
my_attack - boss_defense, 0)
if my_power >= boss_cur_blood:
return ret, True
boss_cur_blood -= my_power
boss_power = max(boss_attack - my_defense, 0)
if boss_power >= my_cur_blood:
return -1, False
my_cur_blood -= boss_power
if my_skill_is_add_attack and boss_skill_is_add_blood:
my_cur_attack_level += 1
boss_cur_blood = min(
boss_cur_blood + boss_add_blood,
boss_blood
)
if my_skill_is_add_attack and boss_skill_is_attack:
my_cur_attack_level += 1
boss_power = max(boss_attack - my_defense, 0)
if boss_power >= my_cur_blood:
return -1, False
my_cur_blood -= boss_power
tot, win_count = 0, 0
for _ in range(trys):
step, is_win = bf(boss_blood,
my_blood,
boss_attack,
my_attack,
boss_defense,
my_defense,
boss_add_blood,
boss_add_blood_probability,
my_add_attack_probability,
my_add_attack_limit)
if is_win:
tot += step
win_count += 1
return tot / win_count if win_count else -114514, win_count / trys
扩展3:主动生成测试用例
人工测试用例,由“扩展1”的测试用例(由于“扩展1”是“扩展3”的子问题)和一些随意写的测试用例组成。为了方便测试代码正确性,咱们写一段引进主动生成测试用例的代码。这段代码的主体是一个无限循环,取随机数直到满意我的条件:(1)战役能够较快完毕。(2)矩阵的大小不能太大。
别的,为什么失利条件设得比较宽松delta >= 0.001
呢?由于我发现蒙特卡洛模仿,只做1e6
次的状况下得到的成功次数差异能够到达1000
,且限于计算资源,模仿次数不能设太大。再说一个小tips,假如发现差值总是正数或总是负数,并且概率差值有时能到达0.01
,这说明你的代码有一些纤细的错误。
def gen_cases(case_count=3):
cases = []
for _ in range(case_count):
while True:
boss_blood = random.randint(1, 20)
my_blood = random.randint(1, 20)
boss_attack = random.randint(1, 10)
my_attack = random.randint(1, 10)
boss_defense = random.randint(1, 10)
my_defense = random.randint(1, 10)
boss_add_blood = random.randint(1, 15)
boss_add_blood_probability = random.random() / 1.2
my_add_attack_probability = random.random() / 1.2
my_add_attack_limit = random.randint(0, 3)
my_biggest_power = (my_add_attack_limit + 1) * \
my_attack - boss_defense
boss_power = max(boss_attack - my_defense, 0)
mat_size = (boss_blood + 1) * (my_blood + 1) * \
(my_add_attack_limit + 1)
if my_biggest_power > 0 and boss_blood / \
my_biggest_power < 5 and boss_power > 0 and my_blood / boss_power < 5 and mat_size <= 1000:
cases.append((
boss_blood,
my_blood,
boss_attack,
my_attack,
boss_defense,
my_defense,
boss_add_blood,
boss_add_blood_probability,
my_add_attack_probability,
my_add_attack_limit
))
break
return cases
def run_auto_cases(case_count=3):
cases = gen_cases(case_count)
for c in cases:
ans1 = i_can_add_attack_dp(*c)
ans2 = i_can_add_attack_bf(*c)
delta = abs(ans1 - ans2[1])
if delta >= 0.001:
print('[run_auto_cases] 代码在这个用例下可能有问题qwq', c)
else:
print('[run_auto_cases]', c)
print(ans1 - ans2[1], ans1, ans2[1])
run_auto_cases()
扩展3:完整代码
import random
import numpy as np
# boss_blood my_blood boss_attack my_attack boss_defense my_defense boss_add_blood
# boss_add_blood_probability my_add_attack_probability my_add_attack_limit
# boss血量,我血量,boss进犯力,我进犯力,boss防护力,我防护力,boss回血技术回血量,boss发起回血技术概率,我运用加进犯技术概率,我加进犯次数约束
# dp[i][j][k] boss初始血量 我初始血量 我初始状况现已运用的加进犯次数,答案 = dp[boss_blood][my_blood][0]
# dp[0~boss_blood][0][0~my_add_attack_limit] = 0
# dp[0][1~my_blood][0~my_add_attack_limit] = 1
# dp[i][j][k] = (1-my_add_attack_probability)*boss_add_blood_probability*dp[min(i-my_power+boss_add_blood,boss_blood)][j][k] +
# (1-my_add_attack_probability)*(1-boss_add_blood_probability)*dp[i-my_power][j-boss_power][k] +
# my_add_attack_probability*boss_add_blood_probability*dp[i][min(j+boss_add_blood,boss_blood)][k+1] +
# my_add_attack_probability*(1-boss_add_blood_probability)*dp[i][j-boss_power][k+1]
# my_power = max((k + 1) * my_attack - boss_defense, 0)
# boss_power = max(boss_attack - my_defense, 0)
# 我打BOSS和BOSS打我的状况都要留意断定血量,血量需求取到非正数时,dp值应改为取常量1或0,dp值取常量1表明b[expression_count]要进行相应添加,dp值取常量0表明对a的操作越过
def i_can_add_attack_dp(
boss_blood: int,
my_blood: int,
boss_attack: int,
my_attack: int,
boss_defense: int,
my_defense: int,
boss_add_blood: int,
boss_add_blood_probability: float,
my_add_attack_probability: float,
my_add_attack_limit: int
):
mat_size = (boss_blood + 1) * (my_blood + 1) * (my_add_attack_limit + 1)
# boss_blood, my_blood, my_add_attack_num
def pos(i: int, j: int, k: int):
if i < 0 or i > boss_blood or j < 0 or j > my_blood or k < 0 or k > my_add_attack_limit:
raise Exception("越界!(%s, %s, %s)" % (i, j, k))
return i * (my_blood + 1) * (my_add_attack_limit + 1) + \
j * (my_add_attack_limit + 1) + k
a = [[0] * mat_size for _ in range(mat_size)]
b = [[0] for _ in range(mat_size)]
expression_count = 0
for i in range(boss_blood + 1):
for k in range(my_add_attack_limit + 1):
a[expression_count][pos(i, 0, k)] = 1
expression_count += 1
for j in range(1, my_blood + 1):
for k in range(my_add_attack_limit + 1):
a[expression_count][pos(0, j, k)] = 1
b[expression_count][0] = 1
expression_count += 1
for i in range(1, boss_blood + 1):
for j in range(1, my_blood + 1):
for k in range(my_add_attack_limit):
my_power = max((k + 1) * my_attack - boss_defense, 0)
boss_power = max(boss_attack - my_defense, 0)
a[expression_count][pos(i, j, k)] = 1
# 我进犯,BOSS加血
if my_power >= i:
b[expression_count][0] += (
1 - my_add_attack_probability) * boss_add_blood_probability
else:
a[expression_count][pos(min(i - my_power + boss_add_blood, boss_blood), j, k)] -= (
1 - my_add_attack_probability) * boss_add_blood_probability
# 我进犯,BOSS进犯
if my_power >= i:
b[expression_count][0] += (1 - my_add_attack_probability) * \
(1 - boss_add_blood_probability)
elif boss_power < j:
a[expression_count][pos(i - my_power, j - boss_power, k)] -= (
1 - my_add_attack_probability) * (1 - boss_add_blood_probability)
# 我加进犯,BOSS加血
a[expression_count][pos(min(i + boss_add_blood, boss_blood), j, k + 1)
] -= my_add_attack_probability * boss_add_blood_probability
# 我加进犯,BOSS进犯
if boss_power < j:
a[expression_count][pos(
i, j - boss_power, k + 1)] -= my_add_attack_probability * (1 - boss_add_blood_probability)
expression_count += 1
for i in range(1, boss_blood + 1):
for j in range(1, my_blood + 1):
my_power = max((my_add_attack_limit + 1) *
my_attack - boss_defense, 0)
boss_power = max(boss_attack - my_defense, 0)
a[expression_count][pos(i, j, my_add_attack_limit)] = 1
# 我进犯,BOSS加血
if my_power >= i:
b[expression_count][0] += boss_add_blood_probability
else:
a[expression_count][pos(min(i - my_power + boss_add_blood, boss_blood),
j, my_add_attack_limit)] -= boss_add_blood_probability
# 我进犯,BOSS进犯
if my_power >= i:
b[expression_count][0] += 1 - boss_add_blood_probability
elif boss_power < j:
a[expression_count][pos(
i - my_power, j - boss_power, my_add_attack_limit)] -= 1 - boss_add_blood_probability
expression_count += 1
assert expression_count == mat_size
a = np.array(a)
b = np.array(b)
ans = np.linalg.solve(a, b)
return ans[pos(boss_blood, my_blood, 0)][0]
def i_can_add_attack_bf(
boss_blood: int,
my_blood: int,
boss_attack: int,
my_attack: int,
boss_defense: int,
my_defense: int,
boss_add_blood: int,
boss_add_blood_probability: float,
my_add_attack_probability: float,
my_add_attack_limit: int,
trys=1000000
):
assert boss_attack > my_defense # 暂不支撑BOSS打不动我的状况
assert 0 <= boss_add_blood_probability and boss_add_blood_probability <= 1
assert 0 <= my_add_attack_probability and my_add_attack_probability < 1
def bf(
boss_blood: int,
my_blood: int,
boss_attack: int,
my_attack: int,
boss_defense: int,
my_defense: int,
boss_add_blood: int,
boss_add_blood_probability: float,
my_add_attack_probability: float,
my_add_attack_limit: int
):
ret = 0
boss_cur_blood, my_cur_blood, my_cur_attack_level = boss_blood, my_blood, 0
while True:
ret += 1
pivot_boss_skill = random.random()
pivot_my_skill = random.random()
# 引进 my_actual_add_attack_probability
# ,这样后续代码就能够简化为两边开释状况的人工枚举,即若干个if块
my_actual_add_attack_probability = my_add_attack_probability if my_cur_attack_level < my_add_attack_limit else 0
boss_skill_is_attack = pivot_boss_skill >= boss_add_blood_probability
boss_skill_is_add_blood = pivot_boss_skill < boss_add_blood_probability
my_skill_is_attack = pivot_my_skill >= my_actual_add_attack_probability
my_skill_is_add_attack = pivot_my_skill < my_actual_add_attack_probability
if my_skill_is_attack and boss_skill_is_add_blood:
my_power = max((my_cur_attack_level + 1) *
my_attack - boss_defense, 0)
if my_power >= boss_cur_blood:
return ret, True
boss_cur_blood = min(
boss_cur_blood - my_power + boss_add_blood,
boss_blood
)
if my_skill_is_attack and boss_skill_is_attack:
my_power = max((my_cur_attack_level + 1) *
my_attack - boss_defense, 0)
if my_power >= boss_cur_blood:
return ret, True
boss_cur_blood -= my_power
boss_power = max(boss_attack - my_defense, 0)
if boss_power >= my_cur_blood:
return -1, False
my_cur_blood -= boss_power
if my_skill_is_add_attack and boss_skill_is_add_blood:
my_cur_attack_level += 1
boss_cur_blood = min(
boss_cur_blood + boss_add_blood,
boss_blood
)
if my_skill_is_add_attack and boss_skill_is_attack:
my_cur_attack_level += 1
boss_power = max(boss_attack - my_defense, 0)
if boss_power >= my_cur_blood:
return -1, False
my_cur_blood -= boss_power
tot, win_count = 0, 0
for _ in range(trys):
step, is_win = bf(boss_blood,
my_blood,
boss_attack,
my_attack,
boss_defense,
my_defense,
boss_add_blood,
boss_add_blood_probability,
my_add_attack_probability,
my_add_attack_limit)
if is_win:
tot += step
win_count += 1
return tot / win_count if win_count else -114514, win_count / trys
def gen_cases(case_count=3):
cases = []
for _ in range(case_count):
while True:
boss_blood = random.randint(1, 20)
my_blood = random.randint(1, 20)
boss_attack = random.randint(1, 10)
my_attack = random.randint(1, 10)
boss_defense = random.randint(1, 10)
my_defense = random.randint(1, 10)
boss_add_blood = random.randint(1, 15)
boss_add_blood_probability = random.random() / 1.2
my_add_attack_probability = random.random() / 1.2
my_add_attack_limit = random.randint(0, 3)
my_biggest_power = (my_add_attack_limit + 1) * \
my_attack - boss_defense
boss_power = max(boss_attack - my_defense, 0)
mat_size = (boss_blood + 1) * (my_blood + 1) * \
(my_add_attack_limit + 1)
if my_biggest_power > 0 and boss_blood / \
my_biggest_power < 5 and boss_power > 0 and my_blood / boss_power < 5 and mat_size <= 1000:
cases.append((
boss_blood,
my_blood,
boss_attack,
my_attack,
boss_defense,
my_defense,
boss_add_blood,
boss_add_blood_probability,
my_add_attack_probability,
my_add_attack_limit
))
break
return cases
def run_auto_cases(case_count=3):
cases = gen_cases(case_count)
for c in cases:
ans1 = i_can_add_attack_dp(*c)
ans2 = i_can_add_attack_bf(*c)
delta = abs(ans1 - ans2[1])
if delta >= 0.001:
print('[run_auto_cases] 代码在这个用例下可能有问题qwq', c)
else:
print('[run_auto_cases]', c)
print(ans1 - ans2[1], ans1, ans2[1])
# 扩展1的case
no_add_attack_cases = [
# (4, 5, 1, 1, 0, 0, 2, 0.25, 0, 0), # 0.80859375
# (4, 6, 1, 1, 0, 0, 2, 0.3, 0, 0), # 0.8673489999999998
# (17, 18, 3, 4, 0, 0, 5, 0.2, 0, 0), # 0.9998172160000002
# (17, 17, 3, 3, 0, 0, 5, 0.25, 0, 0), # 0.31640625
# (26, 27, 5, 5, 0, 0, 9, 0.3, 0, 0), # 0.5282199999999998
# (26, 17, 2, 3, 0, 0, 2, 0.3, 0, 0), # 1
# (26, 17, 2, 3, 0, 0, 3, 0.3, 0, 0), # 1
# (26, 17, 2, 3, 0, 0, 4, 0.3, 0, 0), # 0.2552983299999999
# (26, 17, 2, 3, 0, 0, 5, 0.3, 0, 0), # 0.08235429999999996
# (26, 17, 2, 3, 0, 0, 6, 0.3, 0, 0), # 0.08235429999999996
# (24, 24, 5, 5, 0, 0, 5, 0.3, 0, 0), # 1
# (24, 24, 5, 5, 0, 0, 6, 0.3, 0, 0), # 0.6516999999999998
# (24, 24, 5, 5, 0, 0, 7, 0.3, 0, 0), # 0.3429999999999999
# (24, 24, 5, 5, 0, 0, 9, 0.3, 0, 0), # 0.3429999999999999
# (24, 24, 5, 5, 0, 0, 10, 0.3, 0, 0), # 0.3429999999999999
# (24, 24, 5, 5, 0, 0, 25, 0.3, 0, 0), # 0.3429999999999999
# (23, 22, 4, 4, 0, 0, 4, 0.3, 0, 0), # 1
# (23, 22, 4, 4, 0, 0, 5, 0.3, 0, 0), # 0.5282199999999998
# (23, 22, 4, 4, 0, 0, 6, 0.3, 0, 0), # 0.24009999999999992
# (23, 22, 4, 4, 0, 0, 7, 0.3, 0, 0), # 0.24009999999999992
# (23, 22, 4, 4, 0, 0, 20, 0.3, 0, 0), # 0.24009999999999992
# (23, 20, 4, 4, 0, 0, 0, 0.3, 0, 0), # 0.8319300000000001
# (23, 20, 4, 4, 0, 0, 1, 0.3, 0, 0), # 0.8319300000000001
# (23, 20, 4, 4, 0, 0, 2, 0.3, 0, 0), # 0.579825
# (23, 20, 4, 4, 0, 0, 3, 0.3, 0, 0), # 0.3529305
# (23, 20, 4, 4, 0, 0, 4, 0.3, 0, 0), # 0
# (23, 15, 4, 4, 0, 0, 0, 0.25, 0, 0), # 0.3671875
# (23, 15, 4, 4, 0, 0, 1, 0.25, 0, 0), # 0.16943359375
# (23, 15, 4, 4, 0, 0, 2, 0.25, 0, 0), # 0.070556640625
# (23, 15, 4, 4, 0, 0, 3, 0.25, 0, 0), # 0.003505706787109375
# (23, 15, 4, 4, 0, 0, 4, 0.25, 0, 0), # 0
]
cases = [
*no_add_attack_cases,
# (4, 5, 1, 1, 0, 0, 2, 0.25, 0.25, 3), # 0.9160377010889875
# (4, 6, 1, 1, 0, 0, 2, 0.3, 0.25, 3), # 0.9741987084147316
# (17, 18, 3, 4, 0, 0, 5, 0.2, 0.25, 3), # 0.9820515464729096
# (17, 17, 3, 3, 0, 0, 5, 0.25, 0.25, 3), # 0.8115670869690697
# (26, 27, 5, 5, 0, 0, 9, 0.3, 0.25, 3), # 0.873849609228737
# (26, 17, 2, 3, 0, 0, 2, 0.3, 0.2, 3), # 0.9905333909547174
# (26, 17, 2, 3, 0, 0, 3, 0.3, 0.2, 3), # 0.9777544970640041
# (26, 17, 2, 3, 0, 0, 4, 0.3, 0.2, 3), # 0.9147414027044304
# (26, 17, 2, 3, 0, 0, 5, 0.3, 0.2, 3), # 0.8731776837795042
# (26, 17, 2, 3, 0, 0, 6, 0.3, 0.2, 3), # 0.8492809049470756
# (24, 24, 5, 5, 0, 0, 5, 0.3, 0.25, 3), # 0.9183074507949731
# (24, 24, 5, 5, 0, 0, 6, 0.3, 0.25, 3), # 0.8585987923823502
# (24, 24, 5, 5, 0, 0, 7, 0.3, 0.25, 3), # 0.7740757521611915
# (24, 24, 5, 5, 0, 0, 9, 0.3, 0.25, 3), # 0.7433988933602607
# (24, 24, 5, 5, 0, 0, 10, 0.3, 0.25, 3), # 0.7409449791377299
# (24, 24, 5, 5, 0, 0, 25, 0.3, 0.25, 3), # 0.6575242627019245
# (23, 22, 4, 4, 0, 0, 4, 0.3, 0.2, 3), # 0.9443943496217228
# (23, 22, 4, 4, 0, 0, 5, 0.3, 0.2, 3), # 0.8574007848491916
# (23, 22, 4, 4, 0, 0, 6, 0.3, 0.2, 3), # 0.7658891690144051
# (23, 22, 4, 4, 0, 0, 7, 0.3, 0.2, 3), # 0.7475453781164894
# (23, 22, 4, 4, 0, 0, 20, 0.3, 0.2, 3), # 0.6679427026235988
# (23, 20, 4, 4, 0, 0, 0, 0.3, 0.25, 3), # 0.8686731676567078
# (23, 20, 4, 4, 0, 0, 1, 0.3, 0.25, 3), # 0.8562376017170137
# (23, 20, 4, 4, 0, 0, 2, 0.3, 0.25, 3), # 0.771857783579355
# (23, 20, 4, 4, 0, 0, 3, 0.3, 0.25, 3), # 0.7163414966132311
# (23, 20, 4, 4, 0, 0, 4, 0.3, 0.25, 3), # 0.6691316566228978
# (23, 15, 4, 4, 0, 0, 0, 0.25, 0.2, 3), # 0.5077913500000001
# (23, 15, 4, 4, 0, 0, 1, 0.25, 0.2, 3), # 0.4285612324000001
# (23, 15, 4, 4, 0, 0, 2, 0.25, 0.2, 3), # 0.35735150696320006
# (23, 15, 4, 4, 0, 0, 3, 0.25, 0.2, 3), # 0.3222425538737369
# (23, 15, 4, 4, 0, 0, 4, 0.25, 0.2, 3), # 0.30972575195312513
# (12, 9, 6, 4, 10, 4, 11, 0.642597, 0.077409, 3), # 0.05168324414398858
# (18, 11, 10, 7, 5, 5, 3, 0.319059, 0.506039, 3), # 0.45683775982637675
# (2, 7, 8, 3, 7, 6, 8, 0.152426, 0.575946, 2), # 0.7198123697734008
# (6, 4, 6, 10, 7, 3, 7, 0.029775, 0.589464, 2), # 0.4342016554147815
# (3, 16, 10, 5, 10, 2, 7, 0.570169, 0.287427, 2), # 0.2696812367978718
# (1, 16, 9, 2, 5, 4, 5, 0.533026, 0.123434, 3), # 0.23279906085426308
# (5, 13, 9, 8, 9, 1, 11, 0.341245, 0.322049, 3), # 0.411064579885941
# (12, 7, 6, 9, 1, 2, 10, 0.587649, 0.30710, 1), # 0.9389672281289208
]
for c in cases:
ans1 = i_can_add_attack_dp(*c)
ans2 = i_can_add_attack_bf(*c)
print(ans1 - ans2[1], ans1, ans2)
delta = abs(ans1 - ans2[1])
if delta >= 0.001:
print('[manual_cases] 代码在这个用例下可能有问题qwq', c)
run_auto_cases()