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 1≤n≤105, 0≤k≤109, 0≤ai≤109
思路
用 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 了呢?