题意,n(<=70)本书,每本书有一个高度(<=150),宽度(<=30),把他们分到书架里的三层,每层的高度就是这层里所有书的最大高度,宽度就是所有书的宽度和。ans=三层高度和*三层宽度最大值,求ans最小值
又是一道ACM毒瘤好dp,最近做dp感觉出来了点规律,很多时候可以制定一些规则,使无论怎样都有最优答案都能满足这些规则,这样减少了最优答案的数量dp自然就简化了。所以在这个题中,如果求出来最优方案后,调整每一层的顺序不会改变答案,所以就定第一层高度最高,第二层其次。因为第一本书肯定要放进书架里,所以先把它放进第一层。
设状态的时候,设dp(i,j,k)为放了i本书,第二层宽度为j,第三层宽度为j,二三层高度和的最小值。这样根据j,k可以推出来第一层宽度。所以每次对于第i本书有放进三层三种决策,转移方程推一下就可以了。
这样会发现,如果把所有书放到一层,那么这一层宽度最大30*70=2100,空间用滚动数组优化,时间不优化是70*2100*2100,大数据会被卡(事实上vjudge上数据不强,最多20组测试数据跑了1180ms)
书上介绍了两个神仙优化,看完了以后我第一次知道dp还可以剪枝
1.这个还比较好想,放第i本书时j+k应小于第2本书到第i本书宽度和,这样可以在循环k时剪去一部分无用状态
2.所有书宽度和为sum,设想如果相邻两层宽度差>30,那么把一本书从这层拿到邻层,高度和不加,宽度也不加,所以我们只保留状态w1+30>=w2,w2+30>=w3,化式子可得w2<=(sum+30)/2,w3<=(sum+60)/3,这样w2最大1065,w3最大720常数小了六倍左右
这两个优化一上,常数小了六倍多,跑了180ms
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define INF (2100010)
#define LL long long
using namespace std;
int dp[2][1100][800],n,A[200];
struct Book{
int w,h;
void Read(){
scanf("%d %d",&h,&w);
}
bool operator < (const Book &a) const{
return h>a.h;
}
}T[200];
LL Min(LL a,LL b){
if (a<b) return a;
return b;
}
void Work(){
int i,j,k,sum=0,max2,max3;
scanf("%d",&n);
memset(A,0,sizeof(A));
for (i=1;i<=n;i++) T[i].Read();
sort(T+1,T+n+1);
for (i=2;i<=n;i++) A[i]=T[i].w+A[i-1];
sum=A[n]+T[1].w;
memset(dp,127,sizeof(dp));
max2=(sum+30)/2+10; max3=(sum+60)/3+10;
dp[1][0][0]=0;
for (i=2;i<=n;i++)
for (j=0;j<=max2;j++)
for (k=0;k+j<=A[i],k<=max3;k++){
int &ans=dp[i&1][j][k];
ans=INF;
ans=min(ans,dp[(i-1)&1][j][k]);//把书放在第一层
if (j-T[i].w>=0)//把书放在第二层
ans=min(ans,dp[(i-1)&1][j-T[i].w][k]+T[i].h*(j==T[i].w));
if (k-T[i].w>=0)//把书放在第三层
ans=min(ans,dp[(i-1)&1][j][k-T[i].w]+T[i].h*(k==T[i].w));
}
LL ans=210000000000000000;
for (i=1;i<=max2;i++)
for (j=1;j<=max3;j++)
ans=Min(ans,(LL)max(max(i,j),sum-i-j)*(LL)(dp[n&1][i][j]+T[1].h));
cout<<ans<<endl;
}
int main(){
int Case_Num;
scanf("%d",&Case_Num);
while (Case_Num--) Work();
return 0;
}