1. 程式人生 > 其它 >洛谷 P2120 [ZJOI2007] 倉庫建設

洛谷 P2120 [ZJOI2007] 倉庫建設

連結:

P2120


題意:

\(n\) 個點依次編號為 \(1\sim n\)。給出這 \(n\) 個點的資訊,包括位置 \(x_i\),所擁有的的物品數量 \(p_i\),在此建設一個倉庫的費用 \(c_i\)

每個物品可以向編號更大的點移動,一個物品移動一個單位距離的費用為1。

求將所有物品都放進倉庫所需的最小費用。


分析:

我們可以比較容易地想出一個 \(dp[i]\) 表示在第 \(i\) 個點建一個倉庫時的最優費用,那麼列舉上一個倉庫的位置,有

\[dp[i]=dp[j]+\sum_{k=j+1}^i(x_i-x_k)\cdot p_k+c_i \]

\(\sum\) 拆開,有

\[dp[i]=dp[j]+x_i\sum_{k=j+1}^ip_k-\sum_{k=j+1}^i(x_k\cdot p_k)+c_i \]

於是設 \(sum1[i]=\sum\limits_{k=1}^ip_k\)\(sum2[i]=\sum\limits_{k=1}^i(x_k\cdot p_k)\),可以預處理出來。

\[dp[i]=dp[j]+x_isum1[i]-x_isum1[j]-sum2[i]+sum2[j]+c_i \]

發現有 \(x_isum1[j]\) 這種既與當前位置有關,又與決策有關的項,於是按照斜率優化的套路,我們把式子化成直線表示式的形式:

\[(sum2[j]+dp[j])=x_isum1[j]+(dp[i]-x_isum1[i]+sum2[i]-c_i) \]

於是我們只需要最小化斜率為 \(x_i\)

,過點 \((sum1[j],sum2[j]+dp[j])\) 的直線的截距。

發現和玩具裝箱那題很像,也是維護一個下凸包,同樣有斜率 \(x_i\) 遞增,同樣用單調佇列優化,於是就可以 \(O(n)\) 了。


演算法:

單調佇列維護下凸包,同時維護最優決策點,然後每次根據最優決策的資訊得到 \(dp[i]\),繼續維護凸包即可。時間複雜度 \(O(n)\)

這裡有我做題時遇到的幾個細節:

細節1:

本題與玩具裝箱不一樣,決策點之間的橫座標是可能相等的,這也意味著我們需要特殊處理斜率,需要在不同情況下返回 inf

c++ 中會有這樣的特性,一個 double\(x\) 除以整數0,如果 \(x>0\)

會返回inf,如果 \(x<0\) 會返回 -inf,其中 inf 甚至大於 LONG_LONG_MAX,看似滿足我們的需求,但當 \(x=0\) 時會返回 nan,於是你就 RE 了(實際因為弱弱的資料並沒有RE)

這裡可以使用類似 #define slope(x,y) (dx(x,y)?(double)dy(x,y)/dx(x,y):dy(x,y)>=0?inf:-inf) 的寫法。

其中當 dy(x,y)==0 時需要返回 inf 還是 -inf 需要根據單調佇列維護答案時的比較 while(qh<qt&&slope(q[qh+1],q[qh])<x[i])qh++; 來調整。

細節2:

這個細節是僅關於本題的,也是洛谷上的 hack 資料。

我們對 dp 的定義是在第 \(i\) 個位置建一個倉庫時的最優費用,於是如果 dp 完直接輸出 dp[n] 是有問題的。當最後連續多個點根本沒有物品時,最後一個倉庫可能會建在最後一個有物品的點和最後一個點及其之間的所有點上,所以我們最後還要取一個 min


程式碼:
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define in read()
inline int read(){
	int p=0,f=1;
	char c=getchar();
	while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
	while(isdigit(c)){p=p*10+c-'0';c=getchar();}
	return p*f;
}
#define X(x) (sum1[x])
#define Y(x) (sum2[x]+dp[x])
#define dx(x,y) (X(x)-X(y))
#define dy(x,y) (Y(x)-Y(y))
#define slope(x,y) (dx(x,y)?(double)dy(x,y)/dx(x,y):dy(x,y)>=0?inf:-inf)
const int N=1e6+5;
const int inf=0x7fffffffffffffff;
int n,x[N],p[N],c[N],dp[N],sum1[N],sum2[N],q[N],qh,qt;
signed main(){
	n=in;
	for(int i=1;i<=n;i++)
		x[i]=in,p[i]=in,c[i]=in,
		sum1[i]=sum1[i-1]+p[i],
		sum2[i]=sum2[i-1]+x[i]*p[i];
	qh=qt=1;
	for(int i=1;i<=n;i++){
		while(qh<qt&&slope(q[qh+1],q[qh])<x[i])qh++;
		dp[i]=Y(q[qh])-x[i]*X(q[qh])+x[i]*sum1[i]-sum2[i]+c[i];
		while(qh<qt&&slope(q[qt],q[qt-1])>slope(i,q[qt-1]))qt--;
		q[++qt]=i;
	}
	int ans=dp[n];
	while(!p[n])ans=min(ans,dp[--n]);
	cout<<ans;
	return 0;
}

題外話:

有點細節需要注意,在做其他斜優dp的時候也應該注意