[CEOI2017]Building Bridges
题意简述
-
有 \(n\) 根柱子,第 \(i\) 根柱子高度为 \(h_i\);拆除一个柱子需要花费 \(w_i\);在柱子 \(i\)、\(j\) 间连接的花费为 \((h_i-h_j)^2\)。你需要保留一些柱子,其中第 \(1\) 和 \(n\) 号柱子必须保留,拆除其余柱子,并连接相邻的保留的柱子。
-
你需要确定一个保留方案,使得总花费最小。
-
\(n \le 10^5\),\(h_i, |w_i| \le 10^6\)。
算法
花费为平方式,这很斜率优化。
设 \(f_i\) 表示考虑前 \(i\) 个柱子并强制保留第 \(i\) 个柱子的最小花费,记 \(s_i=\sum\limits_{j=1}^iw_j\)。
\[f_i = \min\{f_j+s_{i-1}-s_j+(h_i-h_j)^2\}
\]
用斜优的套路拆拆式子,记 \(X_i=h_i\),\(Y_i=h_i^2+f_i-s_i\),且 \(j \le k\):
\[2h_i(X_k-X_j) \ge {Y_k-Y_j}
\]
你开心地写了个单调队列二分,然后开心的爆零了。
然后你发现这题不仅斜率不单调,连自变量取值也是不单调的。也就是说仅仅单调队列是没法维护的。
那怎么办呢?
不妨强行让自变量单调。考虑分治,此时右区间对左边是没有贡献的,所以先计算左区间的 DP 值,然后考虑其对右区间的影响,最后再递归右区间。回溯时再按照 \(X_i\) 为第一关键字,\(Y_i\) 为第二关键字归并排序。
具体来说,我们在递归前先在该区间内按照 \(X_i\) 排序,这样就保证了自变量和斜率单调不降,故左区间对右区间的转移直接单调队列即可。
当递归到单个结点 \(l\) 时,它的 DP 值已经计算完了,可以算出 \(Y_l\)。
你发现这就是 CDQ 分治,容易发现像这样在分治过程中转移是不会遗漏的。
时间复杂度 \(O(n\log n)\)。
代码实现
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAXN = 100010;
int n;
ll h[MAXN], s[MAXN], f[MAXN];
inline ll pw2(ll x) {return x*x;}
inline ll X(int i) {return h[i];}
inline ll Y(int i) {return pw2(h[i])+f[i]-s[i];}
struct node{
ll x, y;
int ind;
node(ll X=0,ll Y=0,int I=0):x(X),y(Y),ind(I){}
bool operator<(const node&o)const{return x ^ o.x ? x < o.x : y < o.y;}
}a[MAXN], tmp[MAXN];
int q[MAXN], he, ta;
void cdq(int l, int r)
{
if(l == r)
{
a[l].y = Y(l);
return ;
}
int mid = (l+r)>>1;
int x = l, y = mid+1;
for(int i=l;i<=r;i++)
if(a[i].ind <= mid) tmp[x++] = a[i];
else tmp[y++] = a[i];
for(int i=l;i<=r;i++)
a[i] = tmp[i];
cdq(l, mid);
he = 1; ta = 0;
for(int i=l;i<=mid;i++)
{
if(i > l && a[i].x == a[i-1].x) continue;
while(he < ta && (a[i].y-a[q[ta]].y)*(a[q[ta]].x-a[q[ta-1]].x) <= (a[q[ta]].y-a[q[ta-1]].y)*(a[i].x-a[q[ta]].x))
--ta;
q[++ta] = i;
}
for(int i=mid+1;i<=r;i++)
{
while(he < ta && a[q[he+1]].y-a[q[he]].y <= 2*a[i].x*(a[q[he+1]].x-a[q[he]].x)) ++he;
int u = a[i].ind, v = a[q[he]].ind;
f[u] = min(f[u], f[v] + s[u-1] - s[v] + pw2(h[u] - h[v]));
}
cdq(mid+1, r);
x = l; y = mid+1;
int now = l;
while(x <= mid && y <= r)
{
if(a[x].x < a[y].x || (a[x].x == a[y].x && a[x].y < a[y].y)) tmp[now++] = a[x++];
else tmp[now++] = a[y++];
}
while(x <= mid) tmp[now++] = a[x++];
while(y <= r) tmp[now++] = a[y++];
for(int i=l;i<=r;i++) a[i] = tmp[i];
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%lld",h+i), a[a[i].ind = i].x = h[i];
for(int i=1;i<=n;i++)
scanf("%lld",s+i), s[i] += s[i-1];
for(int i=2;i<=n;i++)
f[i] = 0x3f3f3f3f3f3f3f3f;
sort(a+1, a+n+1);
cdq(1, n);
printf("%lld\n",f[n]);
return 0;
}