Boss Rush (二分答案 + 状压DP)

Boss Rush

题意
给定 n 个技能,每个技能只能用一次。
每个技能有冷却时间 t i t_i ti,如果一个技能在时间 x 释放,那么直到 x + t i − 1 x+t_i-1 x+ti1 时间内就不能再释放其他技能。
每个技能有伤害持续时间 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 (0j<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}, 1n18,1H1018,
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 1ti,leni100000, 1di,j109

思路
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(n2nlog 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的情况下另一个变量的最优值是多少,如果满足要求的话,就可以让二分的那个变量继续最优,以此将两个变量都最优。

很好的一道结合题!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值