[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\) 个工厂建立了一个仓库的总费用。
那么状态转移方程就能轻易写出来了:
注意点(记住这里,下面的解法中,这个注意点是一样的!)
因为题目里 \(p_i\) 是可以等于 \(0\) 的,如果最后若干个工厂的 \(p_i=0\),那么这些位置没有产品需要储存,不一定要在 \(n\) 号工厂建仓库,即最终答案不一定在第 \(n\) 个工厂。
设 \(pos\) 为最后一个 \(p_i>0\) 的工厂编号。
- 如果所有 \(p_i=0\),答案直接为 \(0\)。
- 否则,最后一个仓库可以建在 \(pos,pos+1,\dots,n\) 中的任意一个位置,答案为:
代码
#include
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=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]
\(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
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=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]
正解 \(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\) 比 \(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
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=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]
如果您觉得有不正确或不妥的地方,欢迎在评论区指正,感谢!
如果觉得对你有帮助,就帮忙点个赞吧!(❁´◡`❁)
"原文地址: https://www.cveoy.top/t/topic/qHWC 著作权归作者所有。请勿转载和采集!