时限20s,T(T<=100)组样例,共E(E<=100)次锻炼,每次锻炼可能需要W(W<=100)种杠铃,
第i天第j种所需要的杠铃片数为Xij(0<=Xij<=100),
杠铃片放到一个形如「栈」的杠铃架上,
比如,第一天是AABA,第二天是AACCDD,
则需要第一天放入AABA,弹掉A,弹掉B,
再放入C,放入C,放入D,放入D,才能得到第二天的杠铃片
最后求杠铃架从空的初始状态,锻炼完E次后,再保持空状态,
所操作的杠铃片的最小数量是多少
官方题解
jiangly代码
注意到,如果第[1,n]次锻炼内有一些杠铃片是公共的,
则这些杠铃片可以直接垫在底下,最开始放入,最后取出,然后忽略这些杠铃片
然后考虑分治,第[l,r]次锻炼的答案可以由[l,x]和[x+1,n]求得,
设第[l,r]次锻炼公共的杠铃片为mn[l][r],
则第[l,r]次锻炼,可以拆解成,
一开始在栈内垫入mn[l][x]个杠铃片,进行第[l,x]次锻炼,完成后弹栈把杠铃片删成mn[l][r]个,
再垫杠铃片使栈内杠铃片数为mn[x+1][r]个,进行第[x+1,r]次锻炼,完成后把杠铃片删完
一开始初始化长度为1的区间,其贡献为2*mn[i][i],表示杠铃片的变化为0->mn[i][i]->0
则区间合并的时候,合并前杠铃片的变化是0->mn[l][x]->0->mn[x+1][r]->0,
合并后,杠铃片的变化为0->mn[l][x]->mn[l][r]->mn[x+1][r]->0,即发生合并时,答案减去2*mn[l][r]
想到这个公共杠铃片的trick了,但还是最终没能想到区间dp
看了看自己做过的题,发现和hdu4283 You Are the One(区间DP)这个题很像
题解是按jiangly代码写的,官方题解与之大同小异。
官方题解
dp[l][r]表示把第[l,r]次锻炼公有的杠铃片(其个数记为C[l,r])事先加载进栈,锻炼完第[l,r]次后,弹栈使得栈内最终只保留这C[l,r]个杠铃片的最小操作次数。
转移考虑枚举一个中间次数x,dp[l][r]=min(dp[l][r],dp[l][x]+dp[x+1][r]+2*((C[l,x]-C[l,r])+(C[x+1,r]-C[l,r]))),表示一个拆解为子问题的过程。
要锻炼[l,r]这些次,栈中现在有公有杠铃片C[l,r]个,先入栈使栈内杠铃片变成C(l,x)个,再进行第[l,x]次锻炼,再弹栈让杠铃片恢复为C[l,r]个,再入栈使杠铃片变为C[x+1,r]个,再进行第[x+1,r]次锻炼,再弹栈让杠铃片恢复为C[l,r]个。
最后答案为dp[1][n]+2*C[1][n]。
或者引入空元素0、n+1,最后答案为dp[0][n+1]。
#include
using namespace std;
const int N=105,INF=0x3f3f3f3f;
int t,n,m,dp[N][N],a[N][N],mn[N][N],now[N];
int main(){
scanf("%d",&t);
for(int ca=1;ca<=t;++ca){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){
for(int j=1;j<=m;++j){
scanf("%d",&a[i][j]);
}
}
memset(mn,0,sizeof mn);
for(int l=1;l<=n;++l){
memset(now,INF,sizeof now);
for(int r=l;r<=n;++r){
for(int j=1;j<=m;++j){
now[j]=min(now[j],a[r][j]);
mn[l][r]+=now[j];
}
}
}
for(int i=1;i<=n;++i){
dp[i][i]=2*mn[i][i];
}
for(int len=2;len<=n;++len){
for(int l=1;l+len-1<=n;++l){
int r=l+len-1;
dp[l][r]=INF;
for(int k=l;k+1<=r;++k){
dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]-2*mn[l][r]);
}
}
}
printf("Case #%d: %d\n",ca,dp[1][n]);
}
return 0;
}