Boss Rush
题意
给定 n 个技能,每个技能只能用一次。
每个技能有冷却时间
t
i
t_i
ti,如果一个技能在时间 x 释放,那么直到
x
+
t
i
−
1
x+t_i-1
x+ti−1 时间内就不能再释放其他技能。
每个技能有伤害持续时间
l
e
n
i
len_i
leni,如果技能在时间 x 释放,那么将会在第
x
+
j
(
0
≤
j
<
l
e
n
i
)
x+j\ (0\leq j < len_i)
x+j (0≤j<leni) 时刻产生
d
i
,
j
d_{i,j}
di,j 伤害。
问,将生命值为 H 的 boss 击败最少需要用多少时间?
1
≤
n
≤
18
,
1
≤
H
≤
1
0
18
,
1 \leq n \leq 18, 1\leq H\leq 10^{18},
1≤n≤18,1≤H≤1018,
1
≤
t
i
,
l
e
n
i
≤
100
000
,
1
≤
d
i
,
j
≤
1
0
9
1 \leq t_i,len_i\leq 100\,000,\ 1\leq d_{i,j}\leq 10^9
1≤ti,leni≤100000, 1≤di,j≤109
思路
n 很小,能考虑到两个算法:二进制枚举
和 状压DP
。
首先来想 二进制枚举:
枚举状态,计算用这些技能击败 boss 所花费的最短时间。
要找到一种排列,使得打出的伤害能够击败 boss,即伤害尽可能大,又要使得花费的时间尽可能小。两个可变的量,不好搞。
考虑把其中的一个变量固定(常用二分答案),从而使得另一个变量最大或者最小。
伤害值固定了,那就是要找到排列让伤害值最少为 mid,然后使得花费时间尽可能小。不太好搞。
那就把花费的时间固定,要找到一个排列使得花费的时间最多为 mid,然后使得伤害值尽可能大。这时可以考虑贪心,把伤害值大的技能在前面释放,伤害值小的后面释放。
假设每个技能在每个时刻的伤害值都固定的话,那么每个技能的持续时间就是其贡献,那么就把持续时间长的技能在前面释放,持续短的时间在后面释放,然后看在 mid 的时间内打出的最大伤害是否大于 boss 血量。如果大于等于,那么满足,mid 往左来。可行。
但是当前每个技能在每个时刻的伤害值都不一样,所以也无法贪心找到这样一个使得伤害值最大的排列,这种做法也不可行。
再来想 状压DP:
状压DP,可以求打出的最大伤害。
但是时间并不固定,无法使得时间花费最少,所以考虑二分答案将时间固定,然后在这个时间范围内状压DP,看打出的最大伤害是否大于 H。
DP 便不必额外考虑顺序了,因为动态规划已经所有最优的排列都考虑到了。
从小到大枚举所有集合,对于当前集合,遍历所有不在该集合的事件,用该集合的状态 更新 加上该事件后集合的状态:
为了使得伤害最大,每个技能都在最小能释放的时间释放。
当前技能释放的最小时间为,集合中所有技能的冷却时间之和。伤害从释放的时间开始,到释放时间 + len[i] - 1 结束,结束时间要和二分的时间 T 取 min。
f[i | 1 << j] = max(f[i | 1<<j], f[i] + dmg[j][min(end-sum, len[j]-1)]);
时间复杂度: O ( n ∗ 2 n ∗ l o g a n s ) O(n*2^n*log\ ans) O(n∗2n∗log ans)
最后时间卡的很紧,要剪剪枝。
1.枚举所有集合的时候,如果当前集合都没有被更新过,那就不用再去更新其他集合了,直接跳过。所以初始化将所有状态初始为-1,0状态初始为0。如果集合状态为 -1 就跳过。因为其要更新的集合已经被 0 状态更新过了。
2.当前伤害满足后立刻退出,没必要更新完所有集合了。
3.也可以把每个集合的冷却时间总和预处理出,这样就不用每次循环求了。
Code
#include<bits/stdc++.h>
using namespace std;
#define Ios ios::sync_with_stdio(false),cin.tie(0)
#define int long long
const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N];
int t[20], len[20];
int dmg[20][N];
int f[1<<18];
bool check(int mid)
{
int end = mid;
mem(f, -1);
f[0] = 0;
for(int i=0;i<1<<n;i++)
{
if(f[i] == -1) continue;
int sum = 0;
for(int j=0;j<n;j++) if(i >> j & 1) sum += t[j]; //可以将此块预处理
if(sum > end) continue;
for(int j=0;j<n;j++)
{
if(i >> j & 1) continue;
f[i | 1 << j] = max(f[i | 1<<j], f[i] + dmg[j][min(end-sum, len[j]-1)]);
if(f[i | 1<<j] >= m) return 1;
}
}
return 0;
}
signed main(){
Ios;
cin >> T;
while(T--)
{
cin >> n >> m;
int l = 0, r = 0;
for(int i=0;i<n;i++) //用到状压时数组位置尽量从0开始
{
cin >> t[i] >> len[i];
for(int j=0;j<len[i];j++)
{
cin >> dmg[i][j];
if(j) dmg[i][j] += dmg[i][j-1];
}
r += t[i] + len[i] - 1; //最大时间
}
while(l < r)
{
int mid = l + r >> 1;
if(check(mid)) r = mid;
else l = mid + 1;
}
if(check(l)) cout << l << endl;
else cout << -1 << endl;
}
return 0;
}
经验
一看范围很小,那么就应该想到两种算法:二进制枚举 和 状压DP。
二进制枚举可能受排列限制,如果可以的话需要贪心使得贡献值最优。
状压DP便是直接用最优解转移,不需要额外考虑顺序,但更新的方式需要推推,要靠经验积累。
如果同时存在两个变量的话,既要让这个最优,又要让那个最优,那么不妨二分答案一下,在这个变量是mid的情况下另一个变量的最优值是多少,如果满足要求的话,就可以让二分的那个变量继续最优,以此将两个变量都最优。
很好的一道结合题!