【牛客网】货物分组
dp+决策单调性
part1.30pts
显然,前30分是很好写的。
\(dp[i][j]\)表示前i个箱子分成了j个组的最小代价。
复杂度:\(O(n^3)\)
代码:
namespace p30{
ll dp[510][510],ans=2e18+7;
void work(){
memset(dp,63,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
int mx=A[i],mi=A[i];
for(int k=i-1;k>=0;k--){
if(sum[i]-sum[k]>W)break;
dp[i][j]=min(dp[i][j],0LL+dp[k][j-1]+1LL*(sum[i]-sum[k])*j+mx-mi);
mx=max(mx,A[k]);
mi=min(mi,A[k]);
}
}
}
for(int i=1;i<=n;i++)ans=min(ans,dp[n][i]);
cout<<ans;
}
}
part2.60pts
对于这部分费用:
对于第i个箱子,如果里面装的货物总重量为w,那么费用为i*w 。
我们转化一下,假设有5个物品,编号为1,2,3,4,5。其中装的箱子为{1,2},{3},{4,5}。
那么上述费用就是:\(A[1]\cdot 1+A[2]\cdot 1+A[3]\cdot2+A[4]\cdot3+A[5]\cdot3\),也可以写成\((A[1]+A[2]+A[3]+A[4]+A[5])+(A[3]+A[4]+A[5])+(A[4]+A[5])\)。
这样,当我们考虑到第i个物品时,不管第i+1个到第n个怎么取,最终答案至少要多花费\(\sum_{j=i+1}^{j\le n}A[j]\)。
因此,我们设\(dp[i]\)表示考虑前i个物品时,最终花费的最小总代价。
转移方程:
\[dp[0]=\sum_{k=1}^{k \le n}A[k]
\]
\[dp[i]=min\{dp[j]+max(j+1,i)-min(j+1,i)\}+\sum_{k=i+1}^{k\le n}A[k] j\in[0,i-1],\sum_{k=j+1}^{k\le i}A[k]\le W
\]
其中\(max(j+1,i)\)表示j+1到i中A的最大值,\(min(j+1,i)\)则表示最小值。
复杂度:\(O(n^2)\)
代码:
namespace p60{
ll dp[MAXN],ans=2e18+7;
void work(){
memset(dp,63,sizeof(dp));
dp[0]=sum[n];
for(int i=1;i<=n;i++){
int mx=A[i],mi=A[i];
for(int j=i-1;j>=0;j--){
if(sum[i]-sum[j]>W)break;
dp[i]=min(dp[i],0LL+dp[j]+mx-mi+sum[n]-sum[i]);
mx=max(mx,A[j]);
mi=min(mi,A[j]);
}
}
cout<<dp[n];
}
}
part3.100pts
对于\(max(j+1,i)\)和\(min(j+1,i)\)可以用单调栈维护。
由于\(max(j+1,i)\)和\(min(j+1,i)\)是不断变化的,所以我们需要开一棵线段树维护。
复杂度:\(O(n\cdot logn)\)
具体细节请看代码吧。
代码:
namespace p100 {
int stkMx[MAXN],stkMi[MAXN],topMx,topMi;
struct sgt {
int L,R;
ll sum,lazy;
} tree[MAXN<<2];
ll dp[MAXN];
void Down(int p) {
if(tree[p].lazy==0)return;
tree[p<<1].sum+=tree[p].lazy,tree[p<<1|1].sum+=tree[p].lazy;
tree[p<<1].lazy+=tree[p].lazy,tree[p<<1|1].lazy+=tree[p].lazy;
tree[p].lazy=0;
return;
}
void Up(int p) {
tree[p].sum=min(tree[p<<1].sum,tree[p<<1|1].sum);
}
void build(int L,int R,int p) {
tree[p].L=L,tree[p].R=R;
if(L==R) {
tree[p].sum=2e18+7;
return;
}
int mid=(L+R)>>1;
build(L,mid,p<<1);
build(mid+1,R,p<<1|1);
Up(p);
}
void update1(int pos,int p,ll x) {
if(tree[p].L==tree[p].R) {
tree[p].sum=x;
return;
}
Down(p);
int mid=(tree[p].L+tree[p].R)>>1;
if(pos<=mid)update1(pos,p<<1,x);
else update1(pos,p<<1|1,x);
Up(p);
}
void update2(int L,int R,int p,int x) {
if(tree[p].L==L&&tree[p].R==R) {
tree[p].sum+=x;
tree[p].lazy+=x;
return;
}
Down(p);
int mid=(tree[p].L+tree[p].R)>>1;
if(R<=mid)update2(L,R,p<<1,x);
else if(L>=mid+1)update2(L,R,p<<1|1,x);
else update2(L,mid,p<<1,x),update2(mid+1,R,p<<1|1,x);
Up(p);
}
ll Query(int L,int R,int p) {
if(tree[p].L==L&&tree[p].R==R)return tree[p].sum;
Down(p);
int mid=(tree[p].L+tree[p].R)>>1;
if(R<=mid)return Query(L,R,p<<1);
else if(L>=mid+1)return Query(L,R,p<<1|1);
else return min(Query(L,mid,p<<1),Query(mid+1,R,p<<1|1));
}
void work() {
build(1,n+1,1);
dp[0]=sum[n];
update1(1,1,dp[0]);//我这里是有便宜的,线段树上[i,i]对应的是dp[i-1]+mx-mi
for(int i=1; i<=n; i++) {
while(topMi&&A[i]<A[stkMi[topMi]]) {
int now=stkMi[topMi],la=stkMi[topMi-1];
update2(la+1,now,1,A[stkMi[topMi]]);//把原先减掉的mi给加回来(回溯)
topMi--;
}
update2(stkMi[topMi]+1,i,1,-A[i]);//把现在的mi给减过去(更新)
stkMi[++topMi]=i;
while(topMx&&A[i]>A[stkMx[topMx]]) {
int now=stkMx[topMx],la=stkMx[topMx-1];
update2(la+1,now,1,-A[stkMx[topMx]]);//把原先加上的mx给减掉(回溯)
topMx--;
}
update2(stkMx[topMx]+1,i,1,A[i]);//把现在的mx给加过去(更新)
stkMx[++topMx]=i;
int l=0,r=i-1,res;
while(l<=r){//二分找能转移到的最左端,即满足res到i这一段和刚好小于等于W
int mid=(l+r)>>1;
if(sum[i]-sum[mid]<=W)res=mid,r=mid-1;
else l=mid+1;
}
dp[i]=0LL+Query(res+1,i,1)+sum[n]-sum[i];
update1(i+1,1,dp[i]);
}
cout<<dp[n];
}
}