[ZJOI2007] 仓库建设 详细题解

温馨提示:如果你是第一次做斜率优化 DP 的题,那么可以先按照顺序做一下 任务安排 1、任务安排 2,并且食用我的博客:斜率优化 DP 例题:任务安排 三道题 详细讲解,以便初步掌握斜率优化 DP,再来看这份题解就比较容易了。


题目跳转:P2120 [ZJOI2007] 仓库建设

题目大意

山上有 \(n\) 个工厂,从山顶到山脚编号 \(1\sim n\)。第 \(i\) 个工厂有产品 \(p_i\) 件,距离工厂 \(1\) 的距离为 \(x_i\),在工厂 \(i\) 建仓库的费用为 \(c_i\)。产品只能运往山下(编号更大)的仓库,每件产品每运输一个单位距离花费 \(1\)。求最小总费用(建仓库费用 \(+\) 运输费用)。

题解

一步一步来,从最基础的 DP 开始。

\(O(n^3)\) 的做法(\(44pts\))

思路分析

设 \(dp_i\) 表示前 \(i\) 个工厂已经处理好,且在第 \(i\) 个工厂建立了一个仓库的总费用。

那么状态转移方程就能轻易写出来了:

\[dp_i=\min_{0\leq j\lt i}\{dp_j+c_i+\sum_{k=j+1}^{i}(x_i-x_k)\cdot p_k\} \]

注意点(记住这里,下面的解法中,这个注意点是一样的!)

因为题目里 \(p_i\) 是可以等于 \(0\) 的,如果最后若干个工厂的 \(p_i=0\),那么这些位置没有产品需要储存,不一定要在 \(n\) 号工厂建仓库,即最终答案不一定在第 \(n\) 个工厂。
设 \(pos\) 为最后一个 \(p_i>0\) 的工厂编号。

  • 如果所有 \(p_i=0\),答案直接为 \(0\)。
  • 否则,最后一个仓库可以建在 \(pos,pos+1,\dots,n\) 中的任意一个位置,答案为:

\[ans=\min_{pos\le i\le n}dp_i \]

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
uint8_t buf[1<<20],*p1,*p2;
#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<20,stdin),p1==p2)?EOF:*p1++)
ll rd(){
	ll x=0,f=1,ch=gc();
	while(ch<'0'||ch>'9'){
		if(ch=='-'){
            f=-1;
        }
        ch=gc();
	}
	while(ch>='0'&&ch<='9'){
		x=(x<<1)+(x<<3)+(ch-'0');
		ch=gc();
	}
	return x*f;
}
void write(ll x){
	if(x<0){
		x=-x;
		putchar('-');
	}
	if(x>9){
		write(x/10);
	}
	putchar(x%10+'0');
}
const ll maxn=1e6+5;
ll n,x[maxn],p[maxn],c[maxn],dp[maxn],pos,ans;
int main(){
	n=rd();
	for(ll i=1;i<=n;i++){
		x[i]=rd();
		p[i]=rd();
		c[i]=rd();
		dp[i]=1e18;
	}
	for(ll i=1;i<=n;i++){
		for(ll j=0;j<i;j++){
            ll sum=0;
			for(ll k=j+1;k<=i;k++){
                sum+=(x[i]-x[k])*p[k];
            }
            dp[i]=min(dp[i],dp[j]+c[i]+sum);
		}
	}
    for(ll i=n;i>=1;i--){
        if(p[i]>0){
            pos=i;
			ans=dp[pos];
            break;
        }
    }
    if(pos==0){
        putchar('0');
    }else{
       for(ll i=pos+1;i<=n;i++){
            if(dp[i]<ans){
                ans=dp[i];
            }
        }
        write(ans);
    }
	return 0;
}

\(O(n^2)\) 的做法(\(66pts\))

思路分析

考虑用前缀和优化掉一层循环。
先把 \(O(n^3)\) 时的状态转移方程搬过来:

\[dp_i=\min_{0\leq j\lt i}\{dp_j+c_i+\sum_{k=j+1}^{i}(x_i-x_k)\cdot p_k\} \]

因为 \(x_i\) 在这里是固定的,所以考虑把 \(x_i\) 拆出来,那么:

\[dp_i=\min_{0\leq j\lt i}\{dp_j+c_i+x_i\times\sum_{k=j+1}^{i}p_k-\sum_{k=j+1}^{i}x_k\cdot p_k\} \]

我们令

\[sump_i=\sum_{k=1}^{i}p_k\\ sumxp_i=\sum_{k=1}^{i}x_k\cdot p_k \]

显然这两个数组是可以预处理出来的,那么此时的状态转移方程就变成了:

\[dp_i=\min_{0\leq j\lt i}\{dp_j+c_i+x_i\cdot (sump_i-sump_j)-(sumxp_i-sumxp_j)\} \]

注意点和上面一样,忘了的点击跳转。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
uint8_t buf[1<<20],*p1,*p2;
#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<20,stdin),p1==p2)?EOF:*p1++)
ll rd(){
	ll x=0,f=1,ch=gc();
	while(ch<'0'||ch>'9'){
		if(ch=='-'){
            f=-1;
        }
        ch=gc();
	}
	while(ch>='0'&&ch<='9'){
		x=(x<<1)+(x<<3)+(ch-'0');
		ch=gc();
	}
	return x*f;
}
void write(ll x){
	if(x<0){
		x=-x;
		putchar('-');
	}
	if(x>9){
		write(x/10);
	}
	putchar(x%10+'0');
}
const ll maxn=1e6+5;
ll n,x[maxn],p[maxn],c[maxn],sump[maxn],sumxp[maxn],dp[maxn],pos,ans;
int main(){
	n=rd();
	for(ll i=1;i<=n;i++){
		x[i]=rd();
		p[i]=rd();
		c[i]=rd();
		sump[i]=sump[i-1]+p[i];
		sumxp[i]=sumxp[i-1]+x[i]*p[i];
		dp[i]=1e18;
	}
	for(ll i=1;i<=n;i++){
		for(ll j=0;j<i;j++){
			dp[i]=min(dp[i],dp[j]+c[i]+x[i]*(sump[i]-sump[j])-(sumxp[i]-sumxp[j]));
		}
	}
    for(ll i=n;i>=1;i--){
        if(p[i]>0){
            pos=i;
			ans=dp[pos];
            break;
        }
    }
    if(pos==0){
        putchar('0');
    }else{
       for(ll i=pos+1;i<=n;i++){
            if(dp[i]<ans){
                ans=dp[i];
            }
        }
        write(ans);
    }
	return 0;
}

正解 \(O(n)\) 的做法(\(100pts\))

思路分析

还是把状态转移方程搬过来:

\[dp_i=\min_{0\leq j\lt i}\{dp_j+c_i+x_i\cdot (sump_i-sump_j)-(sumxp_i-sumxp_j)\} \]

去掉 \(min\),再展开整理一下:

\[dp_i=dp_j+c_i+x_i\cdot sump_i-x_i\cdot sump_j-sumxp_i+sumxp_j \]

把与 \(j\) 有关和与 \(j\) 无关的分开:

\[dp_i=dp_j-x_i\cdot sump_j+sumxp_j+c_i+x_i\cdot sump_i-sumxp_i \]

移项:

\[dp_j+sumxp_j=-x_i\cdot sump_j+(dp_i+sumxp_i-c_i-x_i\cdot sump_i) \]

可以发现括号里面的部分都和 \(i\) 有关、和 \(j\) 无关,而括号外面的部分则和 \(j\) 有关。
我们设 \(Y=dp_j+sumxp_j\),\(X=sump_j\),\(K=-x_i\),\(B=(dp_i+sumxp_i-c_i-x_i\cdot sump_i)\),那么上面的式子就变成了:

\[Y=KX+B \]

显然,我们要求 \(dp_i\) 最小,就是求 \(B\) 最小,此时:

\[B=Y-KX \]

(下面就是斜率优化 DP 这类型题非常套路的地方):

对于两个决策点 \(a<b\),如果 \(b\) 比 \(a\) 更优,则:

\[Y_b-x_iX_b \leq Y_a-x_iX_a \]

整理得:

\[Y_b-Y_a \leq x_i(X_b-X_a) \]

即:

\[\frac{Y_b-Y_a}{X_b-X_a}\leq x_i(条件是 X_b>X_a) \]

由于 \(x_i\) 随 \(i\) 单调递增,可以用单调队列维护一个下凸壳。

维护凸壳时,对于三个点 \(a,b,c\),若 \(b\) 不在凸壳上,则满足:

\[(Y_b-Y_a)(X_c-X_a)\geq (Y_c-Y_a)(X_b-X_a) \]

此时弹出 \(b\)。

查询最优决策时,若队首的下一个点比队首更优,则弹出队首。

细节方面的处理

1. \(p_i\) 可能为 \(0\)

当 \(p_i=0\) 时,\(sump[i]=sump[i-1]\),即 \(X\) 坐标可能相同。此时凸壳中会出现竖直方向上的多个点。

对于相同的 \(X\),我们只需要保留 \(Y\) 最小的那个点(即费用更小的点)。

在插入新点 \(i-1\) 之前,如果发现它与队尾点的 \(X\) 相同,就比较 \(Y\):

  • 若新点 \(Y\) 更小,则删除队尾旧点;
  • 若新点 \(Y\) 更大,则直接不插入新点。

2. 最终答案不一定在 \(n\)

这里就和上面 \(O(n^3)\)、\(O(n^2)\) 的解法的注意点一样了,忘了的可以点击跳转。

3. 乘法溢出

凸壳判断和斜率比较中的乘法要用 __int128。

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
uint8_t buf[1<<20],*p1,*p2;
#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<20,stdin),p1==p2)?EOF:*p1++)
ll rd(){
	ll x=0,f=1,ch=gc();
	while(ch<'0'||ch>'9'){
		if(ch=='-'){
            f=-1;
        }
        ch=gc();
	}
	while(ch>='0'&&ch<='9'){
		x=(x<<1)+(x<<3)+(ch-'0');
		ch=gc();
	}
	return x*f;
}
void write(ll x){
	if(x<0){
		x=-x;
		putchar('-');
	}
	if(x>9){
		write(x/10);
	}
	putchar(x%10+'0');
}
const ll maxn=1e6+5;
ll n,x[maxn],p[maxn],C[maxn],sump[maxn],sumxp[maxn],l=1,r,q[maxn],dp[maxn],pos,ans;
bool flag;
ll gy(ll a){
	return dp[a]+sumxp[a];
}
ll gx(ll a){
	return sump[a];
}
bool check1(ll a,ll b,ll c){
	return (__int128)(gy(b)-gy(a))*(gx(c)-gx(a))>=(__int128)(gy(c)-gy(a))*(gx(b)-gx(a));
}
bool check2(ll a,ll b,ll c){
	return (__int128)gy(b)-gy(a)<=(__int128)x[c]*(gx(b)-gx(a));
}
int main(){
	n=rd();
	for(ll i=1;i<=n;i++){
		x[i]=rd();
		p[i]=rd();
		C[i]=rd();
		sump[i]=sump[i-1]+p[i];
		sumxp[i]=sumxp[i-1]+x[i]*p[i];
	}
	for(ll i=1;i<=n;i++){
        flag=true;
        if(l<=r&&gx(q[r])==gx(i-1)){
            if(gy(q[r])<=gy(i-1)){
                flag=false;
            }else{
                r--;
            }
        }
        if(flag){
            while(l<r&&check1(q[r-1],q[r],i-1)){
    			r--;
    		}
            q[++r]=i-1;
        }
		while(l<r&&check2(q[l],q[l+1],i)){
			l++;
		}
		dp[i]=dp[q[l]]+x[i]*(sump[i]-sump[q[l]])-(sumxp[i]-sumxp[q[l]])+C[i];
	}
    for(ll i=n;i>=1;i--){
        if(p[i]>0){
            pos=i;
			ans=dp[pos];
            break;
        }
    }
    if(pos==0){
        putchar('0');
    }else{
       for(ll i=pos+1;i<=n;i++){
            if(dp[i]<ans){
                ans=dp[i];
            }
        }
        write(ans);
    }
	return 0;
}

如果您觉得有不正确或不妥的地方,欢迎在评论区指正,感谢!

如果觉得对你有帮助,就帮忙点个赞吧!(❁´◡`❁)

posted @ 2026-11-12 10:54  beiciqi  阅读(5)  评论(0)    收藏  举报