打砖块 题解
\(50 pts\)
对于没有 \(Y\) 砖的情况,可以用分组背包解决,算出每一列打 \(j\) 块砖需要的子弹以及对分数的贡献,按照分组背包即可。
对于包含 \(Y\) 砖的情况,不能直接分组背包解决。这实际上是打的顺序问题,比如:
N Y
N Y
如果手上有两枚子弹,最优策略是先打掉第二列,再打掉第一列;但分组背包的思路是:在打第二列的时候,由于至少需要一个,所以第一列也只能用一枚子弹打,也就是:第一列打了一个,第二列打了两个,显然不优。
\(100pts\)
我们发现,打一个 \(Y\) 砖块的要求和影响分别是:手中必须握有至少一枚子弹,打完后子弹总数没有减少。
可以将列分为最后打的列(显然只有一列)和不是最后打的列,对于不是最后打的列,用 \(k-1\) 枚子弹去打,这时就可以直接跑分组背包,少的那枚子弹藏在手里,当遇到 \(Y\) 砖时需要至少一枚子弹时就拿出来,由于遇到 \(Y\) 砖子弹数不会减少,所以这一枚子弹始终存在。
对于最后打的列:枚举剩下的子弹(加上手中的一枚),按照 \(dp\) 状态和这一列产生的贡献直接计算即可。
那么在枚举最后打的列时,如何快速计算其他列做背包的值呢?可以用前后缀合并的思想做。
设 \(f_{i,j}\) 表示前 \(i\) 列用 \(j\) 子弹的最大分数,\(g_{i,j}\) 表示后 \(i\) 列用 \(j\) 子弹的最大分数。对于二者直接分组背包,时间复杂度 \(O(n^3)\)。
将 \(f_{i-1}\) 和 \(g_{i+1}\) 合并(排除掉第 \(i\) 列)只需枚举 \(f\) 用的子弹数和 \(g\) 用的子弹数,时间复杂度 \(O(n^2)\)。
总结:本解法在非 \(dp\) 的地方进行了转化,从而简化了 \(dp\) 难度。
具体看代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=210;
int n,m,k;
int s[N][N],pd[N][N];
struct node {
int st,v,w;
}; vector<node> a[N];
int f[N][N],g[N][N],dp[N];
int main(){
ios::sync_with_stdio(0);
cin>>n>>m>>k;
for(int i=1;i<=n;i++) {
for(int j=1;j<=m;j++) {
char c; cin>>s[i][j]>>c;
if(c=='Y') pd[i][j]=1;
else pd[i][j]=0;
}
}
for(int j=1;j<=m;j++) {
int st=0,v=0,w=0;
for(int i=n;i>=1;i--) {
v++; w+=s[i][j];
a[j].push_back({v,v-pd[i][j],w});
v-=pd[i][j];
}
}
k--;//在手中留一枚子弹
for(int i=1;i<=m;i++) {//预处理 f
for(auto t:a[i]) {
for(int j=0;j<=k;j++) {
f[i][j]=max(f[i][j],f[i-1][j]);
if(j>=t.v) f[i][j]=max(f[i][j],f[i-1][j-t.v]+t.w);
}
}
}
for(int i=m;i>=1;i--) {//预处理 g
for(auto t:a[i]) {
for(int j=0;j<=k;j++) {
g[i][j]=max(g[i][j],g[i+1][j]);
if(j>=t.v) g[i][j]=max(g[i][j],g[i+1][j-t.v]+t.w);
}
}
}
int maxn=0;
for(int i=1;i<=m;i++) {
memset(dp,0,sizeof(dp));
for(int j=0;j<=k;j++) {//合并f,g
for(int z=0;z<=k;z++) {
if(j+z<=k) dp[j+z]=max(dp[j+z],f[i-1][j]+g[i+1][z]);
}
}
int num=k+1;//将手中的子弹搞回来
for(int j=0;j<=k;j++) {
for(auto t:a[i]) {
if(num-j>=t.st) maxn=max(maxn,dp[j]+t.w);
}
}
}
cout<<maxn;
return 0;
}