题目重点:A先攻击,只有存活的B反击,日志应能重放出最终结果。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 两队有序,每个怪兽初始HP和攻击力为正。A先打B;B仍活才反击。死亡者立即记日志,替补在下一轮行动,不在本轮追加攻击。
- 属性克制用正有理数倍率,训练代码不取整;如需整数伤害必须固定floor/ceil及最低伤害。技能按实际伤害最大选,并列保留输入顺序。
- 本合同禁止临死反击,所以正HP双队不会同归于尽。仅双方初始空队返回draw;如要求同轮互杀,应明确同时结算或临死反击并改引擎,不能同时宣称只存活者反击。
解题路线
- 把怪兽数据、当前队员选择、伤害函数和日志事件分开。示例用dataclass与独立fight/best_attack函数,生产接口可包装Team/BattleResult,重点是规则统一。
- 每击记攻击方、目标、damage、before、after;HP≤0记death并前移队列位置,若有下一员记replacement。引擎根据这一击是否存活决定反击,替补提示不等于替补立即行动。
- 复制输入怪兽避免一次fight改变下一次测试起点;Fraction消除0.1倍率累积的舍入歧义。固定随机种子仅适用于另有随机命中率的合同,本题无需引入随机。
- 属性扩展先集中到damage函数,技能策略只选动作,不自己改HP或推进队列。日志最好结构化而非只有字符串,展示层可格式化,验证器可逐事件核对HP和事件顺序。
手动走一遍
A HP5/atk4,B HP3/atk9:第一击B HP3→−1,B死亡,A获胜;不应出现B反击。B还有第二只时先记replacement,下一轮仍由A攻击新B。不要让被击败一方的新怪兽抢到本轮反击权。对water,fire技能4伤害×1/2=2,water技能2伤害×1=2,并列选择先列出的fire技能。
Python 参考实现
下面的代码按上文假设实现,适合本地运行并追加自己的测试用例。
代码区可横向滚动;使用键盘时,先聚焦代码区再按方向键查看。
from dataclasses import dataclass
from fractions import Fraction
@dataclass
class Monster:
name:str
health:Fraction
attack:Fraction
kind:str='normal'
def fight(team_a,team_b,multipliers=None):
# A attacks first; only a surviving B counterattacks. Inputs are not mutated.
teams=[[Monster(m.name,Fraction(m.health),Fraction(m.attack),m.kind) for m in team] for team in (team_a,team_b)]
if any(m.health<=0 or m.attack<=0 for team in teams for m in team):raise ValueError("positive health/attack")
multipliers={} if multipliers is None else dict(multipliers)
if any(Fraction(x)<=0 for x in multipliers.values()):raise ValueError("positive damage multiplier")
index=[0,0];logs=[]
def attack(source,target):
a=teams[source][index[source]];b=teams[target][index[target]]
damage=a.attack*Fraction(multipliers.get((a.kind,b.kind),1));before=b.health;b.health-=damage
logs.append(('attack',source,a.name,target,b.name,damage,before,b.health))
if b.health<=0:
logs.append(('death',target,b.name));index[target]+=1
if index[target]<len(teams[target]):logs.append(('replacement',target,teams[target][index[target]].name))
return False
return True
while all(index[i]<len(teams[i]) for i in (0,1)):
survived=attack(0,1)
if survived:attack(1,0)
alive=[index[i]<len(teams[i]) for i in (0,1)]
winner='A' if alive[0] else 'B' if alive[1] else 'draw';logs.append(('result',winner));return winner,logs
def best_attack(attacks,defender,multipliers=None):
multipliers=multipliers or {}
if not attacks:raise ValueError("at least one skill")
# Skills: (name,positive_base_damage,kind); ties preserve input order.
return max(attacks,key=lambda skill:Fraction(skill[1])*Fraction(multipliers.get((skill[2],defender),1)))
复杂度
K次实际攻击、M总怪兽,时间O(K+M)、状态O(M)、完整日志O(K+M)。K依HP/最低伤害而变,不能只说O(M);Fraction精确运算还与分子分母位长有关。
易错点与追问
容易失分的边界
- 允许死亡B反击却仍按survived决定反击,会让规则与平局示例冲突。
- 只返回胜者无法检查补位及倍率阶段错误。
- 零伤害或无限回血扩展会破坏必终止前提,需要轮数上限/循环判定。
继续深挖
- 允许同时攻击时如何保存回合开头两人的伤害并一起结算?
- 技能有冷却、能量和目标选择时,策略接口与状态推进怎样拆开?
