斜率优化学习笔记
paulpao
·
·
算法·理论
前言
前置知识:单调队列,李超线段树。
对于DP的经典模型:dp_i=\min_{L(i)\le j \le R(i)}(dp_j+val(i,j)),当 val 中每一项都仅与 i,j 中的一个有关时,我们可以把带 i 的项全部提出来,对于带 j 的项,使用如单调队列,线段树等数据结构优化DP。
如果有与 i,j 的乘积相关的项,就可以使用斜率优化。
引入
题目描述
从零时刻开始,这些任务被分批加工,第 $i$ 个任务单独完成所需的时间为 $t_i$。在每批任务开始前,机器需要启动时间 $s$,而完成这批任务所需的时间是各个任务需要时间的总和(同一批任务将在同一时刻完成)。
每个任务的费用是它的完成时刻乘以一个费用系数 $f_i$。请确定一个分组方案,使得总费用最小。
#### 说明/提示
对于 $100\%$ 的数据,$1\le n \le 300000$,$0 \le s \le 50$,$1\le t_i,f_i \le 100$。
### Solution
先考虑最简单的 $O(n^3)$ 做法:定义 $dp_{i,j}$ 为把前 $i$ 个任务分成 $j$ 批完成的最小代价,
$dp_{i,j}=\min_k(dp_{k,j-1}+(\sum_{p=1}^i t_p+j\times s)\times \sum_{p=k+1}^i f_p)$,所有求和全部用前缀和维护一下。
我们发现瓶颈在于对 $j$ 的枚举,考虑从这里入手,优化DP,我们定义 $dp_i$ 为把前 $i$ 个任务分批完成的最小代价,这时候我们发现一个问题:后面的任务与前面分的批数是有关的。我们观察一下式子中与 $s$ 有关的项。
$$
j\times s\times \sum_{p=k+1}^if_p
$$
我们换个视角看这个式子,对于每次重启,所有后面的任务的转移都会多花费 $f_p\times s$,那么我们不妨直接把贡献提前计算,在分批完成的时候就把惩罚值给加上!
那么 $dp_{i}=\min_k(dp_{k}+\sum_{p=1}^i t_p\times \sum_{p=k+1}^i f_p+s\times \sum_{p=k+1}^n f_p)$。时间复杂度 $O(n^2)$,还是不够。
先对状态转移方程式做变形,把一些项提出来,令 $sumf_i=\sum_{i=1}^n f_i$,$sumT$ 同理。
$$
dp_{i}=\min_k(dp_{k}+\sum_{p=1}^i t_p\times \sum_{p=k+1}^i f_p+s\times \sum_{p=k+1}^n f_p)=\min_{k}(dp_k-(s+sumT_i)\times sumf_k)+sumT_i\times sumf_i+s\times sumf_N
$$
对于每个 $k$,有:
$$
dp_k = (S + \text{sumT}_i) \times \text{sumf}_k + dp_i - \text{sumT}_i \times \text{sumf}_i - S \times \text{sumf}_N
$$
我们发现这样整理完以后,类似于一次函数的
$$
y=kx+b
$$
引入斜率优化,对于每个决策点 $(x,y)$,或者说 $(sumf_k,dp_k)$,考虑这么一条直线,斜率是定值 $S+\text{sumT}_i$,并且他过了这个点,这个时候这条直线是唯一确定的,也就是从 $k$ 转移到 $i$ 的情况,那么我们可以自然而然地把 $b$ 解出来,进而得到 $dp_i$。我们目标最小化 $dp_i$,那么也就是要最小化截距。我们上一张图。

~~图是别的地方搬的,不会制图qaq~~
我们注意到这句话的意思就是我们向上平移一条斜率 $S+sumT_i$ ,截距为 $-\inf$的直线,那么第一个"碰到"的点就是最优决策点。观察 A,B,C 这三个点,他们满足一些神奇的几何性质,是上凸形的,这样的话要碰到 B 必定会先碰到 A,C,B 是无用的,只有下凹形的点才是有用的,比如图中的红点。形式化的说,要求 $k_{A,B}\le k_{B,C}$。即 $\frac{dp_B-dp_A}{sumf_B-sumf_A}\le \frac{dp_C-dp_B}{sumf_C-sumf_B}$。
对于每个新的决策点,由于 $sumf_B$ 是递增的,所以决策点必然出现在最右端,同时我们要找的答案本质上就是第一个斜率大于等于逼近斜率的点对中靠下方的点,而需要转移的斜率也递增。基于这些单调性,我们可以使用**单调队列**来维护这个下凸壳,使得“连接相邻两点的线段斜率”也是递增的。具体步骤如下:
检查队头:因为斜率 $K = S + \text{sumT}_i$ 单调递增,若队头两点构成的斜率 $\frac{dp_{q_{l+1}} - dp_{q_l}}{\text{sumf}_{q_{l+1}} - \text{sumf}_{q_l}} \le K$,说明当前队头已不是最优决策点,弹出队头 $q_l$。
取最优转移:当队头满足条件后,直接取队头 $j = q[l]$ 作为最优决策点,代入状态转移方程计算 $dp_i$。
维护队尾凸性:将新的决策点 $i$ 插入队尾前,检查队尾三点 $j_1 = q_{r-1}, j_2 = q_{r}, j_3 = i$ 是否满足下凸性条件。若满足 $\frac{dp_{j_2} - dp_{j_1}}{\text{sumf}_{j_2} - \text{sumf}_{j_1}} \ge \frac{dp_i - dp_{j_2}}{\text{sumf}_i - \text{sumf}_{j_2}}$,则说明 $j_2$ 是“无用点”,将其弹出($r \leftarrow r - 1$),继续检查直到满足下凸性,最后将 $i$ 插入队列。
由于每个点最多进出单调队列一次,时间复杂度 $O(n)$。
### Code(Deepseek添加注释):
```cpp
#include<iostream>
#include<deque>
#define int long long
using namespace std;
int n,s;
const int N=300005;
int t[N],f[N];
int sumt[N],sumf[N];
int dp[N];
int x[N], y[N]; // 横坐标 sumf,纵坐标 dp
signed main(){
cin>>n>>s;
for(int i=1;i<=n;i++){
cin>>t[i]>>f[i];
sumt[i]=sumt[i-1]+t[i];
sumf[i]=sumf[i-1]+f[i];
dp[i]=1e18;
}
// 初始化
dp[0] = 0; // 前0个任务,代价为0
x[0] = sumf[0]; // x为sumf
y[0] = dp[0];
deque<int> q;
q.push_back(0); // 将起点0加入队列
for(int i=1;i<=n;i++){
// 当前直线斜率为 sumt[i] + s
int k = sumt[i] + s;
// 维护队头:淘汰斜率小于等于k的点 (用乘法替代除法避免精度误差)
while(q.size() >= 2) {
int j1 = q[0], j2 = q[1];
// 比较 (y[j2]-y[j1]) / (x[j2]-x[j1]) <= k
if((__int128)(y[j2] - y[j1]) <= (__int128)k * (x[j2] - x[j1])) {
q.pop_front();
} else break;
}
// 取最优转移
int j = q.front();
// dp[i] = y[j] - k * x[j] + sumt[i]*sumf[i] + s*sumf[n]
dp[i] = y[j] - (__int128)k * x[j] + (__int128)sumt[i] * sumf[i] + (__int128)s * sumf[n];
// 新决策点坐标
x[i] = sumf[i];
y[i] = dp[i];
// 维护队尾:插入前检查凸性 (用乘法替代除法)
while(q.size() >= 2) {
int j1 = q[q.size()-2], j2 = q.back();
// 比较 (y[j2]-y[j1]) / (x[j2]-x[j1]) <= (y[i]-y[j2]) / (x[i]-x[j2])
if((__int128)(y[j2] - y[j1]) * (x[i] - x[j2]) >= (__int128)(y[i] - y[j2]) * (x[j2] - x[j1])) {
q.pop_back();
} else break;
}
q.push_back(i);
}
cout<<dp[n];
return 0;
}
```
等等,带负数版本怎么办?
使用李超线段树维护即可。
我们重新去看我们的式子。
$$dp_{i}=\min_{k}(dp_k-(s+sumT_i)\times sumf_k)+sumT_i\times sumf_i+s\times sumf_N$$
整理一下:
$$
dp_i-sumT_i\times sumf_i-s\times sumf_N=\min_k(-sumT_i\times sumf_k+dp_k-s\times sumf_k)
$$
对于每个决策点,它对应一条线段,那么转移就是要从 $x=sumT_i$ 的各直线中取最值去转移,这个就是李超线段树可以随便维护的东西!所以事实上,所有斜率优化都可以用李超线段树无脑做。
但其实还有个问题:值域上界极大,直接开肯定开不下,使用离散化或者动态开点线段树即可,这里使用离散化。
李超线段树会多一个 $\log$,但一般更容易想,所以推荐使用李超线段树。
### Code
```cpp
#include<iostream>
#include<cmath>
#include<algorithm>
#define int long long
using namespace std;
int n,lans=0,s;
const int N=3e5+5,V=3e5+5;
int ls[N],ys[N];
int t[N],c[N],sumt[N],sumc[N],dp[N];
struct line{
int k,b;
}l[N];
int cal(line ll,int x){
return ll.k*x+ll.b;
}
struct Node{
int l,r;
line bst;
int id;
}tr[8*V];
line e={0,(int)1e18};
bool cmp(int i,int id){
int mid=ys[(tr[i].l+tr[i].r)>>1];
if(cal(tr[i].bst,mid)>cal(l[id],mid) or (abs(cal(tr[i].bst,mid)-cal(l[id],mid))==0&&id<tr[i].id)){
return 1;
}
return 0;
}
void build(int i,int l,int r){
tr[i]={l,r};
tr[i].bst=e;
tr[i].id=0;
if(l==r){
return;
}
int mid=(l+r)>>1;
build(2*i,l,mid);
build(2*i+1,mid+1,r);
}
void modify(int i,int id){
line tl=l[id];
int tid=id;
if(tr[i].l==tr[i].r){
if(cmp(i,id)){
tr[i].bst=l[id];
tr[i].id=id;
}
return;
}
if(cmp(i,id)){
swap(tr[i].bst,tl);
swap(tr[i].id,tid);
}
if(cal(tl,ys[tr[i].l])<=cal(tr[i].bst,ys[tr[i].l])){
modify(2*i,tid);
}
else if(cal(tl,ys[tr[i].r])<=cal(tr[i].bst,ys[tr[i].r])){
modify(2*i+1,tid);
}
}
void update(int i,int l,int r,int id){
if(tr[i].l>r or tr[i].r<l) return;
if(l<=tr[i].l && tr[i].r<=r){
modify(i,id);
return;
}
update(2*i,l,r,id);
update(2*i+1,l,r,id);
}
int query(int i,int pos){
int pos2=ys[pos];
int ans=cal(tr[i].bst,pos2);
if(tr[i].l==tr[i].r){
return ans;
}
int mid=ys[(tr[i].l+tr[i].r)>>1];
if(pos2<=mid){
int la=query(2*i,pos);
return min(la,ans);
}
else if(pos2>mid){
int la=query(2*i+1,pos);
return min(la,ans);
}
}
int tot=0;
void add(int x0,int y0,int x1,int y1){
if(x0>x1){
swap(x0,x1);
swap(y0,y1);
}
tot++;
if(x0==x1){
l[tot].k=0;
l[tot].b=max(y0,y1);
update(1,x0,x1,tot);
return;
}
l[tot].k=(int)(y0-y1)/(int)(x0-x1);
l[tot].b=(int)y0-(int)l[tot].k*x0;
update(1,x0,x1,tot);
}
const int mod=1e9;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n>>s;
for(int i=1;i<=n;i++){
cin>>t[i]>>c[i];
}
for(int i=1;i<=n;i++){
sumt[i]=sumt[i-1]+t[i];
ls[i]=sumt[i];
}
sort(ls+1,ls+n+1);
int m=unique(ls+1,ls+n+1)-ls-1;
for(int i=1;i<=n;i++){
sumt[i]=lower_bound(ls+1,ls+m+1,sumt[i])-ls;
}
for(int i=1;i<=m;i++){
ys[i]=ls[i];
}
for(int i=1;i<=n;i++){
sumc[i]=sumc[i-1]+c[i];
}
build(1,1,m);
dp[0]=0;
l[0]={0,0};
modify(1,0);
dp[1]=t[1]*c[1]+s*sumc[n];
l[1]={-sumc[1],dp[1]-s*sumc[1]};
modify(1,1);
for(int i=2;i<=n;i++){
dp[i]=query(1,sumt[i])+ys[sumt[i]]*sumc[i]+s*sumc[n];
l[i]={-sumc[i],dp[i]-s*sumc[i]};
modify(1,i);
}
cout<<dp[n];
return 0;
}
```
再看几道例题。
## P4072 征途
### 题目描述
Pine 开始了从 $S$ 地到 $T$ 地的征途。
从 $S$ 地到 $T$ 地的路可以划分成 $n$ 段,第 $i$ 段路长为 $a_i$,相邻两段路的分界点设有休息站。
Pine 计划用 $m$ 天到达 $T$ 地。除第 $m$ 天外,每一天晚上 Pine 都必须在休息站过夜。所以,一段路必须在同一天中走完。
Pine 希望每一天走的路长度尽可能相近,所以他希望每一天走的路的长度的方差尽可能小。
帮助 Pine 求出最小方差是多少。
设方差是 $v$,可以证明,$v\times m^2$ 是一个整数。为了避免精度误差,输出结果时输出 $v\times m^2$。
$1 \le n \le 3000$。
保证从 $S$ 到 $T$ 的总路程不超过 $3\times 10^4$。
### Solution
方差的定义:$\frac{1}{m}\sum_{i=1}^m(x_i-\bar{x})^2$。
其中 $x_i$ 为第 $i$ 天走的路程,那么 $m^2v=\sum_{i=1}^m m(x_i^2-2x_i\bar{x}+\bar{x}^2)$。
记 $\sum_{i=1}^m x_i=s=\sum_{i=1}^n a_i$,那么 $$m^2v=\sum_{i=1}^m m(x_i^2-2x_i\frac{s}{m}+\frac{s^2}{m^2})=m\sum_{i=1}^m x_i^2-s^2$$。
那么我们就最小化 $\sum_{i=1}^m x_i^2$。
那么我们定义 $dp_{i,j}$ 为前 $i$ 段路分为 $j$ 段走,得到的平方和最小值。
$$
dp_{i,j}=\min_k(dp_{k,j-1}+(s_i-s_{k})^2)=\min_k(dp_{k,j-1}+s_i^2+s_{k}^2-2s_is_{k})
$$
整理得
$$
dp_{i,j}-s_{i}^2=\min_k(-2s_is_{k}+s_{k}^2+dp_{k,j-1})
$$
转化成了线段的形式。
我们的转移线段都在上一层,所以先枚举 $j$,每层清空李超线段树即可。
### 评注:
大部分斜率优化题都是类似的,流程是拆式子 $\to$ 分离变量 $\to$ 转化成线段 $\to$ 李超线段树
还有一道类似的题,建议自己推一下。
## P3628 特别行动队
### 题目描述
你有一支由 $n$ 名预备役士兵组成的部队,士兵从 $1$ 到 $n$ 编号,你要将他们拆分成若干特别行动队调入战场。出于默契的考虑,同一支特别行动队中队员的编号**应该连续**,即为形如 $(i, i + 1, \cdots,i + k)$ 的序列。所有的队员都应该属于且仅属于一支特别行动队。
编号为 $i$ 的士兵的初始战斗力为 $x_i$,一支特别行动队的初始战斗力 $X$ 为队内士兵初始战斗力之和,即 $X = x_i + x_{i+1} + \cdots + x_{i+k}$。
通过长期的观察,你总结出对于一支初始战斗力为 $X$ 的特别行动队,其修正战斗力 $X'= aX^2+bX+c$,其中 $a,b,c$ 是已知的系数($a < 0$)。 作为部队统帅,现在你要为这支部队进行编队,使得所有特别行动队的修正战斗力之和最大。试求出这个最大和。
对于 $100\%$ 的数据,$1 \leq n \leq 10^6$,$-5 \leq a \leq -1$,$-10^7 \leq b \leq 10^7$,$-10^7 \leq c \leq 10^7$,$1 \leq x_i \leq 100$。
### Solution
与上题类似。我们定义 $dp_i$ 为前 $i$ 个士兵分段获得的最大收益,那么
$$
dp_i=\max_k(dp_k+a(sum_i-sum_k)^2+b(sum_i-sum_k)+c)
$$
对于每个 $k
dp_i=dp_k+asum_i^2+asum_k^2-2asum_isum_k+bsum_i-bsum_k+c
即
dp_i-asum_i^2-bsum_i-c=dp_k+asum_k^2-bsum_k-2asum_isum_k
转化成线段了,直接上李超。
Code
#include<iostream>
#include<cmath>
#include<algorithm>
using namespace std;
int n,lans=0;
long long a,b,c;
const int N=1e6+5,V=1e6+5;
int ls[N],ys[N];
int x[N],sum[N];
long long dp[N];
struct line{
long long k,b;
}l[N];
inline long long cal(line ll,int x){
return ll.k*x+ll.b;
}
struct Node{
int l,r;
line bst;
int id;
}tr[8*V];
line e={0,-(long long)1e18};
inline bool cmp(int i,int id){
int mid=ys[(tr[i].l+tr[i].r)>>1];
if(cal(tr[i].bst,mid)<cal(l[id],mid) or (cal(tr[i].bst,mid)-cal(l[id],mid)==0&&id<tr[i].id)){
return 1;
}
return 0;
}
inline void build(int i,int l,int r){
tr[i]={l,r};
tr[i].bst=e;
tr[i].id=0;
if(l==r){
return;
}
int mid=(l+r)>>1;
build(2*i,l,mid);
build(2*i+1,mid+1,r);
}
inline void modify(int i,int id){
line tl=l[id];
int tid=id;
if(tr[i].l==tr[i].r){
if(cmp(i,id)){
tr[i].bst=l[id];
tr[i].id=id;
}
return;
}
if(cmp(i,id)){
swap(tr[i].bst,tl);
swap(tr[i].id,tid);
}
if(cal(tl,ys[tr[i].l])>cal(tr[i].bst,ys[tr[i].l])){
modify(2*i,tid);
}
else if(cal(tl,ys[tr[i].r])>cal(tr[i].bst,ys[tr[i].r])){
modify(2*i+1,tid);
}
}
inline void update(int i,int l,int r,int id){
if(tr[i].l>r or tr[i].r<l) return;
if(l<=tr[i].l && tr[i].r<=r){
modify(i,id);
return;
}
update(2*i,l,r,id);
update(2*i+1,l,r,id);
}
inline long long query(int i,int pos){
int pos2=ys[pos];
long long ans=cal(tr[i].bst,pos2);
if(tr[i].l==tr[i].r){
return ans;
}
int mid=ys[(tr[i].l+tr[i].r)>>1];
if(pos2<=mid){
long long la=query(2*i,pos);
return max(la,ans);
}
else if(pos2>mid){
long long la=query(2*i+1,pos);
return max(la,ans);
}
}
int tot=0;
inline void add(int x0,int y0,int x1,int y1){
if(x0>x1){
swap(x0,x1);
swap(y0,y1);
}
tot++;
if(x0==x1){
l[tot].k=0;
l[tot].b=max(y0,y1);
update(1,x0,x1,tot);
return;
}
l[tot].k=(int)(y0-y1)/(int)(x0-x1);
l[tot].b=(int)y0-(int)l[tot].k*x0;
update(1,x0,x1,tot);
}
const int mod=1e9;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
cin>>a>>b>>c;
for(int i=1;i<=n;i++){
cin>>x[i];
sum[i]=sum[i-1]+x[i];
ls[i]=sum[i];
}
sort(ls+1,ls+n+1);
int mm=unique(ls+1,ls+n+1)-ls-1;
for(int i=1;i<=n;i++){
sum[i]=lower_bound(ls+1,ls+mm+1,sum[i])-ls;
}
for(int i=1;i<=mm;i++){
ys[i]=ls[i];
}
build(1,1,mm);
dp[0]=0;
l[0]={0,0};
modify(1,0);
for(int i=1;i<=n;i++){
dp[i]=a*ys[sum[i]]*ys[sum[i]]+b*ys[sum[i]]+c+query(1,sum[i]);
l[i]={-2*a*ys[sum[i]],dp[i]+a*ys[sum[i]]*ys[sum[i]]-b*ys[sum[i]]};
modify(1,i);
// cout<<dp[i]<<' ';
}
cout<<dp[n];
return 0;
}
评注
如果读者初步理解了斜率优化,应当可以轻松解决。
再来一道需要一些推理的题
P4360 锯木厂选址
题目描述
从山顶上到山底下沿着一条直线种植了 n 棵老树。当地的人们决定把他们砍下来。为了不浪费任何一棵木材,树被砍倒后要运送到锯木厂。
木材只能朝山下运。山脚下有一个锯木厂。另外两个锯木厂将新修建在山路上。你必须决定在哪里修建这两个锯木厂,使得运输的费用总和最小。假定运输每公斤木材每米需要一分钱。
你的任务是编写一个程序,从输入文件中读入树的个数和他们的重量与位置,计算最小运输费用。
### Solution
记号:$v$ 是重量,$pos$ 是位置,$sumv$ 是 $v$ 前缀和。
定义 $$val(i,j)=\sum_{k=i}^j(pos_{k}-pos_i)v_k$$
即 :
$$
val(i,j)=\sum_{k=i}^jpos_kv_k-pos_i(sumv_j-sumv_{i-1})
$$
题目本质等价于计算:
$$
\min_{1\le i<j\le n} val(1,i-1)+val(i,j-1)+val(j,n)
$$
那我们只需要枚举 $j$,对于每个点 $j$ 计算:
$$
ans_j=\min_i val(1,i-1)+val(i,j-1)=\sum_{k=1}^{j-1}pos_kv_k-pos_1sumv_i-pos_isumv_{j-1}+pos_isumv_{i-1}
$$
整理一下。
$$
ans_j-\sum_{k=1}^{j-1}pos_kv_k=-pos_isumv_{j-1}-(pos_1sumv_i
-pos_isumv_{i-1})$$
至此,转化成了线段的形式,李超线段树无脑做即可。
### Code
```cpp
#include<iostream>
#include<cmath>
#include<algorithm>
#define int long long
using namespace std;
int n,lans=0;
long long a,b,c;
const int N=2e4+5,V=1e5+5;
int ls[N],ys[N];
int w[N],d[N],sum[N],sumpv[N];
int pos[N];
long long dp[N];
struct line{
long long k,b;
}l[N];
inline long long cal(line ll,int x){
return ll.k*x+ll.b;
}
struct Node{
int l,r;
line bst;
int id;
}tr[8*V];
line e={0,(long long)1e18};
inline bool cmp(int i,int id){
int mid=ys[(tr[i].l+tr[i].r)>>1];
if(cal(tr[i].bst,mid)>cal(l[id],mid) or (cal(tr[i].bst,mid)-cal(l[id],mid)==0&&id<tr[i].id)){
return 1;
}
return 0;
}
inline void build(int i,int l,int r){
tr[i]={l,r};
tr[i].bst=e;
tr[i].id=0;
if(l==r){
return;
}
int mid=(l+r)>>1;
build(2*i,l,mid);
build(2*i+1,mid+1,r);
}
inline void modify(int i,int id){
line tl=l[id];
int tid=id;
if(tr[i].l==tr[i].r){
if(cmp(i,id)){
tr[i].bst=l[id];
tr[i].id=id;
}
return;
}
if(cmp(i,id)){
swap(tr[i].bst,tl);
swap(tr[i].id,tid);
}
if(cal(tl,ys[tr[i].l])<cal(tr[i].bst,ys[tr[i].l])){
modify(2*i,tid);
}
else if(cal(tl,ys[tr[i].r])<cal(tr[i].bst,ys[tr[i].r])){
modify(2*i+1,tid);
}
}
inline void update(int i,int l,int r,int id){
if(tr[i].l>r or tr[i].r<l) return;
if(l<=tr[i].l && tr[i].r<=r){
modify(i,id);
return;
}
update(2*i,l,r,id);
update(2*i+1,l,r,id);
}
inline long long query(int i,int pos){
int pos2=ys[pos];
long long ans=cal(tr[i].bst,pos2);
if(tr[i].l==tr[i].r){
return ans;
}
int mid=ys[(tr[i].l+tr[i].r)>>1];
if(pos2<=mid){
long long la=query(2*i,pos);
return min(la,ans);
}
else if(pos2>mid){
long long la=query(2*i+1,pos);
return min(la,ans);
}
}
int tot=0;
inline void add(int x0,int y0,int x1,int y1){
if(x0>x1){
swap(x0,x1);
swap(y0,y1);
}
tot++;
if(x0==x1){
l[tot].k=0;
l[tot].b=max(y0,y1);
update(1,x0,x1,tot);
return;
}
l[tot].k=(int)(y0-y1)/(int)(x0-x1);
l[tot].b=(int)y0-(int)l[tot].k*x0;
update(1,x0,x1,tot);
}
const int mod=1e9;
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>w[n-i+1]>>d[n-i+1];
}
for(int i=1;i<=n;i++){
pos[i]=pos[i-1]+d[i];
sumpv[i]=sumpv[i-1]+w[i]*pos[i];
sum[i]=sum[i-1]+w[i];
ls[i]=sum[i];
}
sort(ls+1,ls+n+1);
int mm=unique(ls+1,ls+n+1)-ls-1;
for(int i=1;i<=n;i++){
sum[i]=lower_bound(ls+1,ls+mm+1,sum[i])-ls;
}
for(int i=1;i<=mm;i++){
ys[i]=ls[i];
}
build(1,1,mm);
dp[0]=0;
l[0]={0,0};
modify(1,0);
for(int i=1;i<=n;i++){
dp[i]=sumpv[i-1]+query(1,sum[i-1]);
l[i]={-pos[i],pos[i]*ys[sum[i-1]]};
modify(1,i);
// cout<<dp[i]<<' ';
}
long long ans=1e18;
for(int i=1;i<=n;i++){
ans=min(ans,dp[i]+sumpv[n]-sumpv[i-1]-pos[i]*(ys[sum[n]]-ys[sum[i-1]]));
}
cout<<ans;
return 0;
}
```