Codeforces Round #806 (Div. 4) D(DP,特判大数)

G. Good Key, Bad Key

题意
给定 n n n 个箱子,每个箱子里金钱 a i a_i ai
每个箱子可以选择用好钥匙开还是用坏钥匙开:

  • 用好钥匙开,钥匙花费 k k k,拿走箱子里的所有金钱;
  • 用坏钥匙开,钥匙花费 0 0 0,但当前箱子及后面所有箱子中的金钱数都会向下折半。

钥匙花费可以赊账,每个钥匙只能用开一个箱子。

问,这些箱子中的金钱最多能得到多少?

1 ≤ n ≤ 1 0 5 ,   0 ≤ k ≤ 1 0 9 ,   0 ≤ a i ≤ 1 0 9 1 \leq n \leq 10^5,\ 0 \leq k \leq 10^9,\ 0 \leq a_i \leq 10^9 1n105, 0k109, 0ai109

思路
用 DP 的思路:
定义状态 f[i, j] 表示,前 i 个箱子,用 j 个坏钥匙,能拿到的最大金钱数。

状态转移:

  • 如果当前位置用坏钥匙的话,那么前 i-1 个位置就用了 j-1 个坏钥匙,那么当前箱子的价值为 a[i]/2,除j次。
    f[i, j] = f[i-1, j-1] + (a[i]>>j);
  • 否则当前位置用好钥匙,那么前 i-1 个位置就用了 j 个坏钥匙,好钥匙花费k,那么当前箱子的价值为 a[i]/2,除j次,再减去k
    f[i, j] = f[i-1, j] + (a[i]>>j) - k;

但是注意到,n 为 1e5,两重循环就超时了。
但是,ai 最大为 1e9,当选择了 31 个坏钥匙时,最后一个箱子的金钱数就已经是 0 了,如果再多用坏钥匙时,对答案没有贡献,所以只需要循环到 31。
对于后面的钥匙,f[i, 31] 表示前 i 个箱子,用坏钥匙数大于等于 31 时,能拿到的最大金钱数。

#include<bits/stdc++.h>
using namespace std;

#define Ios ios::sync_with_stdio(false),cin.tie(0)

const int N = 200010, mod = 1e9+7;
int T, n, m;
int a[N], k;
ll f[N][32];

signed main(){
	Ios;
	
	cin >> T;
	while(T--)
	{
		cin >> n >> k;
		for(int i=1;i<=n;i++) cin >> a[i];
		
		for(int i=1;i<=n;i++)//定义的状态为恰好,需要初始化为负无穷 
			for(int j=0;j<=31;j++)
				f[i][j] = -1e9;
		
		f[0][0] = 0;
		
		ll ans = 0; 
		for(int i=1;i<=n;i++){
			for(int j=0;j<=i && j<=31;j++)
			{
				f[i][j] = f[i-1][j] + a[i]/(1<<j) - k;
				if(j) f[i][j] = max(f[i][j], f[i-1][j-1] + a[i]/(1<<j));
				if(j == 31) f[i][j] = max(f[i][j], f[i-1][j]);
			
				if(i==n) ans = max(ans, f[i][j]);
			}
		}
		cout << ans << endl;
	}
	
	return 0;
}
/*
1
40 20
12 23 32 43 45 23 65 786 43 23 6 34 23 56 76 43 34 56 7 453 43 65 45 3 423 45 54 45 3223 23 23 23 23 3243 43 65 65 786 78  65
*/

但是最后 if(j == 31) f[i][j] = max(f[i][j], f[i-1][j]) 不太理解,过段时间再看看。

最后一个状态,f[i, 31] 应该表示的是,前 i 个箱子,用了大于等于31个坏钥匙得到的最大钱数。
对于 j=31 时,不加特判的时候,如果第 i 个箱子用好钥匙,其应该从 f[i-1, 31] 转移过来,更新为 f[i-1, 31] - k;如果用坏钥匙,更新为 f[i-1, 30]
正如上面说的,f[i, 31] 表示的是用了大于等于31个坏钥匙的情况,假设用了前 i 个位置40个坏钥匙,f[i, 40] 应该是从 f[i-1, 39] 来转移,但在这个状态定义中,就是从 f[i-1, 31] 来转移,所以对于 j=31(实际上表示的是用了大于等于31个坏钥匙, j>=31)时,答案可能是 f[i-1, 31]。但是代码中并不能从这个状态来转移,所以要在 j==31 时额外加上。

当 j==31 时,此时第 i 个箱子的权值已经是 0 了,没必要花费 k 用好钥匙了,用坏钥匙即可,所以这时好钥匙更新可以去掉。
所以比较准确的写法应该是这样:

cin >> n >> k;
for(int i=1;i<=n;i++) cin >> a[i];
		
for(int i=1;i<=n;i++) //定义的状态为恰好,需要初始化为负无穷 
	for(int j=0;j<=31;j++)
		f[i][j] = -1e9;

f[0][0] = 0;
		
ll ans = 0; 
for(int i=1;i<=n;i++){
	for(int j=0;j<=i && j<=31;j++)
	{
		if(j != 31) f[i][j] = f[i-1][j] + (a[i]>>j) - k;
		if(j) f[i][j] = max(f[i][j], f[i-1][j-1] + (a[i]>>j));
		if(j == 31) f[i][j] = max(f[i][j], f[i-1][j]);
			
		if(i==n) ans = max(ans, f[i][j]);
	}
}
cout << ans << endl;

最后的状态没有枚举,但确实存在,所以其转移的时候也不能忽略。


这题还可以用贪心,好钥匙都用在前面,坏钥匙都用到后面,枚举用了多少好钥匙即可。

但是如果对于每个箱子好钥匙的花费不一样,是不是就只能用 dp 了呢?

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值