WUSTOJ 1255: 打怪升级
Description
对于多数RPG游戏来说,除了剧情就是打怪升级。本题的任务是用最短的时间取得所有战斗的胜利。这些战斗必须按照特定的顺序进行,每打赢一场,都可能会获得一些补药,用来提升力量。本题只有两种补药:“加1药”和“乘2药”,分别让你的力量值加1和乘以2。
战斗时间取决于你的力量。每场战斗可以用6个参数描述:p1, p2, t1, t2, w1, w2。如果你的力量小于p1,你将输掉战斗;如果你的力量大于p2,需要t2秒赢得战斗;如果力量位于p1和p2(包括p1和p2),战斗时间从t1线性递减到t2。比如p1=50,p2=75,t1=40,t2=15,你的力量为55,则战斗获胜需要35秒。注意,战斗时间可能不是整数。最后两个参数w1和w2分别表示战斗胜利后获得的“加1药”和“乘2药”的数量。注意,你不一定要立刻使用这些补药,可以在需要的时候再用,但不能在战斗中使用补药。
按顺序给出每场战斗的参数,输出赢得所有战斗所需的最短总时间。战斗必须按顺序进行,且不能跳过任何一场战斗。
Input
输入最多包含25组测试数据。每组数据第一行为两个整数n和p(1<=n<=1000, 1<=p<=100),即战斗的场数和你的初始力量值。以下n行每行6个整数p1, p2, t1, t2, w1, w2(1<=p1<p2<=100, 1<=t2<t1<=100, 0<=w1,w2<=10),按顺序给出各场战斗的参数。输入结束标志为n=p=0。
Output
对于每组数据,输出最短总时间(单位:秒),保留两位小数。如果无解,输出“Impossible”(不含引号)。
Sample Input
1 55
50 75 40 15 10 0
2 55
50 75 40 15 10 0
50 75 40 15 10 0
3 1
1 2 2 1 0 5
1 2 2 1 1 0
1 100 100 1 0 0
1 7
4 15 35 23 0 0
1 1
2 3 2 1 0 0
0 0
Sample Output
35.00
60.00
41.00
31.73
Impossible
题目分析
读完这个题目,我们首先应该想到的是,这两瓶药水应该怎么用。
我们都知道:当力量越大的时候,“乘2药”的作用就越大,比如:力量为1,用了“乘2药”后,力量增加到了2;力量为2,用了“乘2药”,力量就增加到了4。因此,我们需要用搜索遍历
的方法来计算最终用时,按照“乘2药”使用的瓶数来遍历。因此我们要讨论的问题是:战斗结束,你是否使用“乘2药”,用的话,你会用几瓶。
大致需要注意哪些:首先我们要知道搜索什么时候结束;力量值不能战斗胜利怎么办,力量值已经超过了此次战斗最大的力量值我们怎么处理。这3个问题弄明白之后,基本上就能解决了。
“加1药”只要有就用掉,它越早用越好,没必要留到后面用。
以上就是题目的粗略分析,不是很清楚的话,可以看代码,自认为注释非常详细了。
代码
// 1262ms
import java.util.Scanner;
public class Main {
// 关卡类,记录每场战斗的信息
private class Combat {
// 前面6个是输入的数据,即战斗的信息
// gapP = p2 - p1 后面计算用的比较频繁,直接算出来比较好
// gapT = t1- t2 同上
int p1, p2, t1, t2, w1, w2, gapP, gapT;
}
private Combat[] combat; // 记录所有战斗的信息,下标从0开始
private Scanner sc;
private int n, p; // 战斗场数,初始战力值
private double totalTime; // 所有战斗胜利用时,即最终结果
// battle()方法中要用到
private int newPower; // 新力量值
private int newBottles; // 新药水数量
private double newUsedTime; // 新用时
// 当前力量值与最低力量值的差,同最大力量差的比值,用于计算当前战斗用时
private double scale;
public Main() {
sc = new Scanner(System.in);
int i;
// 申请堆空间
combat = new Combat[1001];
for(i = 0; i < 1001; i++) {
combat[i] = new Combat();
}
while(input()) {
totalTime = Double.MAX_VALUE; // 赋最大值
battle(0, p, 0, 0); // 初始胜利0场,力量值p,药水0瓶,用时0秒
if(totalTime == Double.MAX_VALUE) {
System.out.println("Impossible");
} else {
System.out.printf("%.2f", totalTime);
System.out.println();
}
}
sc.close();
}
/**
* @return 是否结束输入
*/
private boolean input() {
n = sc.nextInt();
p = sc.nextInt();
if(0 == n) {
return false;
}
int i;
for(i = 0; i < n; i++) {
combat[i].p1 = sc.nextInt();
combat[i].p2 = sc.nextInt();
combat[i].t1 = sc.nextInt();
combat[i].t2 = sc.nextInt();
combat[i].w1 = sc.nextInt();
combat[i].w2 = sc.nextInt();
// 计算差值
combat[i].gapP = combat[i].p2 - combat[i].p1;
combat[i].gapT = combat[i].t1 - combat[i].t2;
}
return true;
}
/**
* @param wons 已经胜利战斗数量
* @param power 上一次战斗结束时候的力量值
* @param bottles 目前“乘2药”的瓶数
* @param usedTime 胜利wons场战斗用的时间
*/
private void battle(int wons, int power, int bottles, double usedTime) {
// 如果已经用的时间超过了目前的最短时间
// 后面的战斗也就没必要了,直接退出
if(usedTime > totalTime) {
return;
}
// 所有战斗都胜利了
if(wons == n) {
// 如果用时比目前最短时间还少的话
if(usedTime < totalTime) {
totalTime = usedTime; // 更新最短时间记录
}
return; // 退出
}
int i;
// 这是最后一场战斗,战斗前将药水用完,不用就浪费了
if(wons == n - 1) {
for(i = 0; i < bottles; i++) {
power *= 2;
}
bottles = 0;
}
// 循环考虑怎么处理剩余的药水
for(i = 0; i <= bottles; i++) {
if(i > 0) {
// i等于0的时候,表示不用药水,不能乘2;其他情况,每次用1瓶
power *= 2;
}
// 力量值不低于当前战斗要求的最低力量值,才能继续战斗;否则循环,继续用药水
if(power >= combat[wons].p1) {
// 力量值不低于要求最高力量值
if(power >= combat[wons].p2) {
// 力量值已经超过了最大值100
if(power >= 100) {
// 后面的所有战斗,将会以最快速度结束
for(i = wons; i < n; i++){
usedTime += combat[i].t2;
}
battle(n, power, 0, usedTime);
break;
}
// 当前战斗胜利,将“加1药”全部用完
newPower = power + combat[wons].w1;
// 原来有bottles瓶,用了i瓶,当前战斗胜利,获得了w2瓶
newBottles = bottles - i + combat[wons].w2;
newUsedTime = usedTime + combat[wons].t2;
battle(wons + 1, newPower, newBottles, newUsedTime);
break; // 这个break很关键,没有的话会超时,亲测...
} else { // 力量值低于最高力量值
newPower = power + combat[wons].w1;
newBottles = bottles - i + combat[wons].w2;
// 下面3行,计算当前战斗用时
scale = (power - combat[wons].p1) * 1.0 / combat[wons].gapP;
newUsedTime = usedTime;
newUsedTime += combat[wons].t1 - scale * combat[wons].gapT;
battle(wons + 1, newPower, newBottles, newUsedTime);
}
}
}
}
public static void main(String[] args) {
new Main();
}
}
代码补充
可能大家在Java里面数组内存
比较喜欢——用多少,就申请多少这种做法(反正我是这样的),但是这个题目,由于搜索开销较大,我们应该从开销方面来考虑如何写代码。题目指明可能有1000场战斗,也就是数组要1000大小,这么大的申请堆空间的时间开销还是很大的。我提交的相差了大约500ms,很可怕。所以此题应该直接一次性申请堆空间1001大小。
代码中有几个地方用到了break
,这里主要是为了除去不必要的遍历,以节约时间。这种做法也就是我们通常说的剪枝
,自认为剪的还不够好。如果大家通过阅读代码之后想到了更好的方法(也就是提交时间更短),或者还有疑问的话,欢迎评论以及留言(邮箱在友情链接
),我会及时回复。
小结
这个题目并没有我们想象的难,主要是注意一些细节问题。这道题目还检查出了我的关于内部类知识的漏洞,申请类对象数组需要两次new
,否则知识申请了一个数组,并没有实例。有这个疑问的可参考我自己写的与此对应的博客给内部类对象数组属性赋值时报错:Exception in thread “main” java.lang.NullPointerException。