传送门 HDU 6789 Fight
Problem Description
Mr Left,Mr Mid,Mr Right 正在玩游戏。他们初始都有 1000 血,Mr Left,Mr Mid,Mr Right 的攻击力分别为 x,y,z。对于每一轮,假设仍然剩下至少两个人的血量大于 0,那么选出两个血量大于 0 的人对打,他们的血量分别扣除和他们对打的另一个人的攻击力。
当有至少两个人的血量小于等于 0 时,游戏结束。
请问在最优情况下,这个游戏最少多少轮结束?
Input
第一行一个正整数 test (1≤test≤100) 表示数据组数。接下来 test 行,每行三个正整数 x,y,z (1≤x,y,z≤1000) 表示 Mr Left, Mr Mid, Mr
Right的攻击力。Output
对于每组数据,一行一个整数表示答案。
文章目录
1. 官方题解以及引言
枚举 Left、Mid,Left、Right 之间打了多少轮,那么 Mid、Right 还要打几轮是可以直接算出来的,求最值即可。
官方题解说枚举前两种对打组合,计算最后一种对打组合。
为什么不是枚举一种计算两种或者直接计算三种或者枚举三种?
这便是本文章重点要讨论的东西。
2. 特殊含义名词与符号声明
- 首先我们用 0,1 ,2来指代题目中的那三个人。
两两对打的组合只有 C 3 2 = 3 \mathrm{C}_3^2=3 C32=3种
分别为:(0,1),(0,2),(1,2) - ”双亡“:两个人在一次互打中同时阵亡
- ”单杀“:一方使另一方阵亡
- “碰” : 互相攻击一次但不致死
3. 为什么不直接计算三种?
直接算其实是博主本人最开始想到的办法,理由也很朴素:反正最后都是减去2000多点血,那肯定我每次输出最多,那我结束的就越快啊。所以得尽量让攻击力最大的多打几轮。
现在我们对他们按递增次序排个序,也就是 0 ≤ 1 ≤ 2 0\le1\le 2 0≤1≤2
例子:
0,1,2的攻击力分别为:200 200 900
如果按照我们一开始想的尽量让攻击力最大的2多出场
那可以 让 0,2 打两次,1,2打两次 总回合数是:4
这是不是最少的呢?
如果我们让0,1 先打一次 再让0,2打一次 1,2打一次,总回合数是:3
yi?
问题出在哪呢?
想每次尽量多输出其实没有错,但是关键是攻击力大的输出就一定多吗?
0,2打完一次后 0就剩100点血了你900攻击力也扣不了人900点血呀
那咋办啊?
感觉这点背包问题的意思啊,贪心不了呀。
贪不了就枚举遍历呗
在枚举之前我们得确定我们怎么枚举
是枚举每回合的选择吗?
如果是的话,那就类似于dfs了,可能的枚举的量: 3 1000 3^{1000} 31000
这种时间复杂度是我们无法接受的。
我看别的题解中,剪枝一下用dp也能做,我没仔细看,也不太懂。
实际上我们可以忽略时序关系,只关心每种对打组合的回合的数量。
4. 为什么不枚举三种?
一是枚举三种的话一个case需要计算1000*1000*1000是不是有可能TLE啊
二是我们只需要讨论游戏能结束的情况,在这些情况中找最优,当我们知道前两种打了几次,死伤情况也知道了,如果已经有两人阵亡那游戏就已经结束了,如果有一个阵亡那多少回合后再有一个阵亡也是可以直接算的,如果没人阵亡那最后一种就必须得双亡才能结束游戏了。
一不小心把官方题解的枚举两种算一种的给说了哈哈
那上代码吧
注意:我们求两个整数相除然后取ceil时,记得将先将结果转化为浮点数再取ceil,不然整数相除结果会先向下取整,那你再向上取整就没意义了。
代码中对三者排了序只是为了方便计算,不排序也是完全可以的。
完整代码:https://github.com/dq116/astar_preliminary_contest3/blob/master/fight1.cpp
5. 为什么不枚举一种计算两种?
回到开头的那个例子,我们发现并不是我们选择一种对打组合后,就一定使要用这种组合直到一方阵亡。
0,1,2的攻击力分别为:200 200 900
在最优的情况中,我们先让(0,1)碰一次,虽然0,1都没阵亡,但却让2可以更快地单杀0和1.
也就是说有的对打方式是为了分出胜负而有的就是碰几下消耗一下血量。
上述方式一:(0,1)碰,2单杀0,2单杀1
如果调整一下顺序,其实等价于:
方式二:(0,2)碰,1单杀0,2单杀1
方式三:(1,2)碰,0单杀1,2单杀0
所以其实任意一种“一碰两单杀”都可以通过调整顺序转化成另外一种“一碰两单杀”
那两碰一双亡的情况呢?
我们再调整一下顺序发现以上任意一种方式都等价于:
方式四:(0,2)碰,(1,2)碰,(0,1) 双亡
实际上,任意一种“两碰一双亡”,可以通过把把双亡提前,转化为一碰两单杀。(可比较一下方式四与方式一)
三碰不会有两人阵亡也不就会结束游戏,所以不予讨论。
综上,所以可能的情况都可以转化为:一碰两单杀,而且选择哪种对打组合来碰都是可以的。
于是我们便可以随意选择一种对打组合来枚举其碰的次数,然后计算一下剩余两种组合单杀分出胜负所需的次数。也对应了本小节标题所说的枚举一种计算两种。
注意:剩余两种单杀的顺序我们是不知的,所以得分别算一下。
6. 需不需考虑顺序问题?
关不关心顺序跟我们的处理问题的角度有关。
- 在“枚举两种算一种”的方法中,我们是从结果出发的,就是游戏结束了,把每种组合的回合数统计在一起,就没有顺序了。反正我0,1之间总共就打了两次,至于这两次是发生在第几回合不重要。
- 在“枚举一种算两种”的方法中,因为我们要计算两种,不是枚举两种,所以这两种是有顺序的。
我们再来审视一下开头说的直接计算三种,其实理论上应该也是可以的。但是你枚举的越小,需要自己思考的就越多。博主认为优化到只枚举一种已经可以了。
枚举一种计算两种 代码:
#include<iostream>
#include<algorithm>
#include<math.h>
using namespace std;
int test=1;
int a[3]; //攻击力
int hp[3]; //血量
int mini; //最小回合数
int func()
{
mini=100000;
// int num0_1_max=1000; 遍历数量设置成最大的1000也是可以的
int num0_1_max=ceil(1000*1.0/a[1]);//(0,1)对打最大的可能的回合数
for(int i=0;i<=num0_1_max;i++) //枚举(0,1)碰的次数
{
hp[0]=1000-i*a[1];
hp[1]=1000-i*a[0];
if(hp[0]<=0 && hp[1]<= 0) //0,1 已双亡
{
mini=min(mini,i);
continue;
}
if(hp[0]>0)
{
//先0和2打,再1和2打
int num0_2=ceil(hp[0]*1.0/a[2]);
hp[2]=1000-num0_2*a[0];
int num1_2=min(ceil(hp[2]*1.0/a[1]),ceil(hp[1]*1.0/a[2]));
if(hp[2]<=0) //如果 0,2双亡 1,2就不用打了
num1_2=0;
mini=min(mini,i+num0_2+num1_2);
//先1和2打,再0和2打
num1_2=ceil(hp[1]*1.0/a[2]);
hp[2]=1000-num1_2*a[1];
num0_2=min(ceil(hp[2]*1.0/a[0]),ceil(hp[0]*1.0/a[2]));
if (hp[2]<=0) //如果 1,2双亡 0,2就不用打了
num0_2=0;
mini=min(mini,i+num1_2+num0_2);
}
else //0已阵亡,2血多且攻击力大,只能2单杀1
{
int num1_2=ceil(hp[1]*1.0/a[2]);
hp[2]=1000-num1_2*a[0];
mini=min(mini,i+num1_2);
}
}
return mini;
}
int main()
{
cin>>test;
for(int k=0;k<test; k++)
{
scanf("%d%d%d",&a[0],&a[1],&a[2]);
sort(a,a+3);
cout<<func()<<endl;
}
}
7. 做题以及写题解的记录
开头也说了,我本来想不枚举直接算出来的,就想着让攻击力最大的多上场,然后WA了。后竟转向了dfs,主要好久没写dfs了都快忘了,恰逢初赛二有个dfs的题没做出来,就照这那个题的葫芦画这个题的瓢,一跑就卡住,半天没反应过来是递归层数大多了。呐,摊牌了,我这就这么菜。。。后妥协到把所有可能的对打顺序都算出来取最小,还是WA。比完赛后群里有人提醒了我:打不一定就要一直打到死啊。也就是前文所说的有的只是碰一下。后来听说得枚举两种算一种,想了半天才搞明白为啥要这么做,当然这还不是我认为最关键的,关键是为什么不是枚举一种算两种?这个我着实想了很久,反复论证,后得出结论这么做是可以的,编码一跑果然可以,后有人问我T5我就去整T5了。写完T5题解回来后写这题题解时,我似乎忘了我之前咋想的了,写着写着就发现不对啊,感觉我似乎并没有搞清楚这题的本质,前前后后写了4个版本,最终版和我之前那个忘掉的方法好像有些相似。但经过几番,对这题的本质理解得也更加透彻了。