斜率优化dp

u p d upd upd:本文相关参考资料 link

引入

首先,我们经常会看到形如下面的DP式子:( m i n min min 同理)
f i = max ⁡ 0 ≤ j < i − 1 f j + b j + a i f_i = \max_{0 \le j \lt i-1} f_j +b_j+a_i fi=0j<i1maxfj+bj+ai
形如这样的式子,如果采用暴力的话,时间复杂度为 O ( n 2 ) O(n^2) O(n2) ,很容易超时。
那么就会想到 单调队列优化DP,将 max ⁡ 0 ≤ j < i − 1 f j + b j \max_{0 \le j \lt i-1} f_j +b_j max0j<i1fj+bj 用单调队列,就可以做到 O ( n ) O(n) O(n) 级别。

但如果DP式子形如下呢:( m i n min min 同理)
f i = max ⁡ 0 ≤ j < i − 1 f j + b j ∗ a i f_i = \max_{0 \le j \lt i-1} f_j + b_j*a_i fi=0j<i1maxfj+bjai
此时,式子中有一项同时包含 i i i j j j,用单调队列的话不足以维护此式。
那么此时一般会有以下三种解决方案:

  1. 斜率优化
  2. 李超线段树
  3. 决策单调性

但本文只讲解 “1.斜率优化” 这一方法。

例题1 lgP3195 [HNOI2008] 玩具装箱

link

题意
n n n 个玩具,每个玩具的长度为 C i C_i Ci ,要求将这 n n n 个玩具装进若干个容器中,且每个容器中的玩具的编号是连续的。
假设某个容器中装入玩具 i i i ~ j j j ( i < j i \lt j i<j) ,那么这个容器的长度为 x = j − i + ∑ k = i j C k x = j - i +\sum_{k=i}^{j} C_k x=ji+k=ijCk ,费用为 ( x − L ) 2 (x-L)^2 (xL)2
希望所有容器的总费用最小,求这个最小值。

  • n n n, C i C_i Ci , L L L 题目给出
  • 1 ≤ n ≤ 5 × 1 0 4 1 \le n \le 5 \times 10^4 1n5×104 1 ≤ L ≤ 1 0 7 1 \le L \le 10^7 1L107 1 ≤ C i ≤ 1 0 7 1 \le C_i \le 10^7 1Ci107

思路
显然本人认为直接将原理不方便理解,于是我们便从例题入手。
看题,一眼DP。
于是有状态 f i f_i fi :把前 个玩具装箱所需最小费用。同时有 s u m i sum_i sumi C i C_i Ci的前缀和。
很容易得出转移方程:
f i = min ⁡ 0 ≤ j < i f j + ( i − j + 1 + s u m i − s u m j − L ) 2 f_i = \min_{0 \le j \lt i} f_j+(i-j+1+sum_i-sum_j-L)^2 fi=0j<iminfj+(ij+1+sumisumjL)2
缩减一下式子,我们令:

g i = i + s u m i g_i = i+sum_i gi=i+sumi
b i = i + s u m i + L + 1 b_i = i+sum_i + L + 1 bi=i+sumi+L+1

得到缩减后的式子:
f i = min ⁡ 0 ≤ j < i f j + ( g i − b j ) 2 f_i = \min_{0 \le j \lt i} f_j+(g_i-b_j)^2 fi=0j<iminfj+(gibj)2

为了方便观察,我们把 i i i j j j 的项分离开来。
得到:
f i = g i 2 + min ⁡ 0 ≤ j < i f j + b j 2 − 2 g i b j f_i = g_i^2 + \min_{0 \le j \lt i} f_j + b_j^2 - 2g_ib_j fi=gi2+0j<iminfj+bj22gibj
显然看出, − 2 g i b j - 2g_ib_j 2gibj 不可以 O ( 1 ) O(1) O(1) 做到,即需要 O ( n 2 ) O(n^2) O(n2)

下面研究 j > k j\gt k j>k f i j < f i k f_{i_{j}} \lt f_{i_{k}} fij<fik(意思是: f i f_i fi 通过 j j j 转移得到的结果 优于 f i f_i fi 通过 k k k 转移得到的结果) 的前提条件:
g i 2 + f j + b j 2 − 2 g i b j < g i 2 + f k + b k 2 − 2 g i b k g_i^2 + f_{j} + b_{j}^2 - 2g_ib_{j} \lt g_i^2 + f_k + b_k^2 - 2g_ib_k gi2+fj+bj22gibj<gi2+fk+bk22gibk
y i = f i − b i 2 y_i = f_i -b_i^2 yi=fibi2 x i = b i x_i = b_i xi=bi
因为 x i x_i xi 单调递增,通过移项整理得:
y j − y k x j − x k < 2 g i \frac{y_j - y_k} {x_j - x_k} \lt 2g_i xjxkyjyk<2gi
发现这条式子很像求 k k k j j j两点的斜率,所以我们可以用 斜率优化 解决。

注意这里 2 g i 2g_i 2gi 满足单调性。

又令 T ( i , j ) = y j − y i x j − x i T(i,j) = \frac{y_j - y_i} {x_j - x_i} T(i,j)=xjxiyjyi

j > k j\gt k j>k 时,若 j j j 决策比 k k k 决策优,则满足 T ( i , k ) < 2 g i T(i,k) \lt 2g_i T(i,k)<2gi

下面给出两个结论:
在这里插入图片描述
在这里插入图片描述
所以代码即类似 单调队列优化DP 的写法:
在这里插入图片描述
这样的时间复杂度就为 O ( n ) O(n) O(n) 了。

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=5e4+5;

int n,q[maxn];
ll L,a[maxn],sum[maxn],g[maxn],b[maxn],f[maxn];

double X(int i){ return b[i]; }

double Y(int i){ return f[i]+b[i]*b[i]; }

double Slope(int i,int j){
	return (Y(j)-Y(i))/(X(j)-X(i));
}

int main(){
	cin>>n>>L;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		sum[i]=sum[i-1]+a[i];
		g[i]=i+sum[i];
		b[i]=sum[i]+i+L+1;
	}
	b[0]=L+1;//!!!
	int hd=1,tal=1;
	for(int i=1;i<=n;i++){
		while(hd<tal&&Slope(q[hd],q[hd+1])<2*g[i]) hd++;//1.删头 
		int j=q[hd]; f[i]=f[j]+(g[i]-b[j])*(g[i]-b[j]);//2.取头 
		while(hd<tal&&Slope(q[tal-1],q[tal])>Slope(q[tal],i)) tal--;//3.删尾 
		q[++tal]=i;//4.插入 
	}
	cout<<f[n];
	return 0;
}

练习题1 lgP3628 [APIO2010] 特别行动队

link

题意
n n n 个士兵,将这些士兵编成若干支特别行动队,且每支队伍的士兵编号连续。
每个士兵有战斗值 x i x_i xi ,设 X = X = X= 队内士兵的战斗力之和,那么一支队伍的战斗力 X ′ = a X 2 + b X + c X\rq = aX^2 + bX + c X=aX2+bX+c
求出所有队伍的战斗力之和的最大值。

  • n n n, x i x_i xi, a a a, b b b, c c c 题目给出
  • 1 ≤ n ≤ 1 0 6 1 \le n \le 10^6 1n106 − 5 ≤ a ≤ − 1 -5 \le a \le -1 5a1 − 1 0 7 ≤ b ≤ 1 0 7 -10^7 \le b \le 10^7 107b107 − 1 0 7 ≤ c ≤ 1 0 7 -10^7 \le c \le 10^7 107c107 1 ≤ x i ≤ 100 1 \le x_i \le 100 1xi100

思路
大体同 例题 1 例题1 例题1 了。

首先有状态: f i f_i fi :将前 i i i 个士兵分组后的最大战斗力值 ,前缀和 s u m i sum_i sumi 1 1 1 ~ i i i 士兵的战斗力之和
有转移:
f i = m a x 0 ≤ j < i f j + s u m i 2 a + s u m j 2 a − 2 s u m i s u m j a + s u m i b − s u m j b + c f_i = max_{0\le j\lt i} f_j + sum_i^2a + sum_j^2a - 2sum_isum_ja + sum_ib-sum_jb + c fi=max0j<ifj+sumi2a+sumj2a2sumisumja+sumibsumjb+c
然后令:
g i = s u m i 2 a + s u m i b + c g_i = sum_i^2a+sum_ib+c gi=sumi2a+sumib+c
k i = f i + s u m i 2 a − s u m i b k_i= f_i+sum_i^2a-sum_ib ki=fi+sumi2asumib

整理原式得:
f i = g i + k j − 2 a s u m i s u m j f_i = g_i + k_j - 2asum_isum_j fi=gi+kj2asumisumj

下面研究 j > j 2 j\gt j_2 j>j2 f i j > f i j 2 f_{i_{j}} \gt f_{i_{j_2}} fij>fij2(意思是: f i f_i fi 通过 j j j 转移得到的结果 优于 f i f_i fi 通过 j 2 j_2 j2 转移得到的结果) 的前提条件:
g i + k j − 2 a s u m i s u m j > g i + k j 2 − 2 a s u m i s u m j 2 g_i + k_j - 2asum_isum_j \gt g_i + k_{j_2} - 2asum_isum_{j_2} gi+kj2asumisumj>gi+kj22asumisumj2
y i = k i y_i = k_i yi=ki x i = s u m i x_i = sum_i xi=sumi
因为 x i x_i xi 单调递增,通过移项整理得:
y j − y j 2 x j − x j 2 > 2 a s u m i \frac{y_j - y_{j_2}} {x_j - x_{j_2}} \gt 2asum_i xjxj2yjyj2>2asumi

因为 2 a s u m i 2asum_i 2asumi 满足单调性,所以可以斜率优化。

i < j i\lt j i<j T ( i , j ) = y j − y i x j − x i T(i,j) = \frac{y_j-y_i}{x_j-x_i} T(i,j)=xjxiyjyi
所以,本题有:(类似 例题 1 例题1 例题1 推理得到)

  • 最优决策取队首
  • 队列中相邻决策应满足: 2 a s u m i > T ( j 1 , j 2 ) > T ( j 2 , j 3 ) > . . . > T ( j t a i l − 1 , j t a i l ) 2asum_i \gt T(j_1,j_2) \gt T(j_2,j_3) \gt ...\gt T(j_{tail-1},j_{tail}) 2asumi>T(j1,j2)>T(j2,j3)>...>T(jtail1,jtail),其中 j 1 < j 2 < j 3 < . . . < j t a i l − 1 < j t a i l j_1 \lt j_2 \lt j_3 \lt ... \lt j_{tail-1} \lt j_{tail} j1<j2<j3<...<jtail1<jtail

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e6+5;

ll sum[maxn],k[maxn];
double X(int i){ return sum[i]; }
double Y(int i){ return k[i]; }
double Slope(int i,int j){ return (Y(j)-Y(i))/(X(j)-X(i)); }

int n,q[maxn];
ll a,b,c,g[maxn],f[maxn];
int main(){
	cin>>n>>a>>b>>c;
	for(int i=1;i<=n;i++){
		ll x; cin>>x;
		sum[i]=sum[i-1]+x;
		g[i]=sum[i]*sum[i]*a+sum[i]*b+c;
	}
	int hd=1,tal=1;
	for(int i=1;i<=n;i++){
		while(hd<tal&&Slope(q[hd],q[hd+1])>2*a*sum[i]) hd++;
		int j=q[hd]; f[i]=g[i]+k[j]-2*a*sum[i]*sum[j];
		k[i]=f[i]+sum[i]*sum[i]*a-sum[i]*b;
		while(hd<tal&&Slope(q[tal-1],q[tal])<Slope(q[tal],i)) tal--;
		q[++tal]=i;
	}
	cout<<f[n];
	return 0;
}

练习题2 lgP2900 [USACO08MAR] Land Acquisition G

link

题意
n n n 块土地,每块土地给出 长 w i w_i wi 和 宽 l i l_i li
如果单买一块土地,则价格为这块土地的面积;如果同时买若干块土地(不需要编号连续),则价格为这些土地中最大的长乘以最大的宽。
求购买所有土地所需的最小费用。

  • n n n, w i w_i wi, l i l_i li 题目给出
  • 1 ≤ N ≤ 5 × 1 0 4 1 \leq N \leq 5 \times 10^4 1N5×104 w i , l i ≤ 1 0 6 w_i , l_i \le 10^6 wi,li106

思路
思路同 例题 1 例题1 例题1 差不多,但需要点思维。

如果一块土地的宽和高都比另一块要小,那么肯定是直接合并购买,这样这一块对答案没有任何贡献。所以,可以先把这些“无用”土地给去掉。
于是,剩下的就是一个 高度递减、宽度递增 的矩形序列。
如果要合并购买,就直接挑序列中的一段区间,这样贡献答案的就只有最左边矩形的高 × \times × 最右边矩形的宽,中间的土地就没有贡献了。

所以可以设状态: f i f_i fi :前 i i i 块土地的最小花费
转移为:
f i = min ⁡ 0 ≤ j < i f j + w j + 1 l i f_i = \min_{0 \le j \lt i} f_j + w_{j+1}l_i fi=0j<iminfj+wj+1li
推推式子即可用斜率优化解决。

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=5e4+5;

ll f[maxn];
struct NODE{ ll x,y; }a_[maxn],a[maxn];
double X(int i){ return a[i+1].x; }
double Y(int i){ return f[i]; }
double Slope(int i,int j){ return (Y(i)-Y(j))/(X(j)-X(i)); }

int q[maxn];
int main(){
	int nn,n=0; cin>>nn;
	for(int i=1;i<=nn;i++)
		cin>>a_[i].x>>a_[i].y;
	sort(a_+1,a_+nn+1,[](NODE a,NODE b){ return a.x!=b.x?a.x>b.x:a.y>b.y; });
	for(int i=1;i<=nn;i++)
		if(!n||a_[i].y>a[n].y) a[++n]=a_[i];
	int hd=1,tal=1;
	for(int i=1;i<=n;i++){
		while(hd<tal&&Slope(q[hd],q[hd+1])<a[i].y) hd++;
		int j=q[hd]; f[i]=f[j]+a[j+1].x*a[i].y;
		while(hd<tal&&Slope(q[tal-1],q[tal])>Slope(q[tal],i)) tal--;
		q[++tal]=i;
	}
	cout<<f[n];
	return 0;
}

例题2 游戏 [jzoj 4249]【五校联五7day1】游戏

link

题意
有一条数轴,从 0 0 0 ~ n n n ,每个点上都有一个分数 a i a_i ai 。一个人从原点(即 0 0 0)开始向正方向走,若某次他从 i i i 走到 j j j i < j i \lt j i<j),那么得到分数为 a j × ( j − i ) a_j \times (j-i) aj×(ji)
最后必须走到 n n n ,求最终得分的最大值。

  • n n n, a i a_i ai 题目给出
  • n ≤ 1 0 5 n \le 10^5 n105 0 ≤ a i ≤ 50 0 \le a_i \le 50 0ai50

思路
d p dp dp 状态为:
f i = m a x 0 ≤ j < i f j + a i ⋅ ( i − j ) f_i = max_{0 \le j \lt i} f_j+a_i \cdot (i-j) fi=max0j<ifj+ai(ij)
展开式子:
f i = a i ⋅ i + m a x 0 ≤ j < i f j − a i ⋅ j f_i = a_i \cdot i + max_{0 \le j \lt i} f_j - a_i \cdot j fi=aii+max0j<ifjaij

下面研究 j > j 2 j\gt j_2 j>j2 f i j > f i j 2 f_{i_{j}} \gt f_{i_{j_2}} fij>fij2(意思是: f i f_i fi 通过 j j j 转移得到的结果 优于 f i f_i fi 通过 j 2 j_2 j2 转移得到的结果) 的前提条件:
f j − a i ⋅ j > f j 2 − a i ⋅ j 2 f_j - a_i \cdot j \gt f_{j_2} - a_i \cdot {j_2} fjaij>fj2aij2
y i = f i y_i = f_i yi=fi x i = i x_i = i xi=i
因为 x i x_i xi 单调递增,通过移项整理得:
y j − y j 2 x j − x j 2 > a i \frac{y_j - y_{j_2}} {x_j - x_{j_2}} \gt a_i xjxj2yjyj2>ai

乍一看,这条式子很可以斜率优化,但是注意到 a i a_i ai不单调,不能进行斜率优化!
但是单调队列中相邻两项的单调性还是有的,所以可以使用单调队列维护,只不过不能进行删头操作。
然后可以通过 二分 找到对应的答案,时间复杂度会多一个 O ( l o g n ) O(logn) O(logn)

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e6+5;

ll f[maxn];
double X(int i){ return i; }
double Y(int i){ return f[i]; }
double Slope(int i,int j){ return (Y(j)-Y(i))/(X(j)-X(i)); }

int n,q[maxn];
ll a[maxn];
int main(){
	cin>>n;
	for(int i=1;i<=n;i++)
		cin>>a[i];
	int hd=1,tal=1;
	for(int i=1;i<=n;i++){
		int l=0,r=tal+1,mid;
		while(l+1<r){//二分找出答案
			mid=(l+r)>>1;
			if(Slope(q[mid-1],q[mid])>a[i]) l=mid;
			else r=mid;
		}
		int j=q[l]; f[i]=f[j]+a[i]*(i-j);//取答案
		while(hd<tal&&Slope(q[tal-1],q[tal])<Slope(q[tal],i)) tal--;//删尾
		q[++tal]=i;//插入
	}
	cout<<f[n];
	return 0;
}

例题3 lgP4655 [CEOI2017] Building Bridges

link

题意
n n n 个柱子,每个柱子有两个值 h i h_i hi w i w_i wi 。可以将第 i i i 根和第 j j j 根连在一起( i < j i \lt j i<j),代价为 ( h i − h j ) 2 + ∑ k = i + 1 j − 1 w k (h_i-h_j)^2 + \sum_{k=i+1}^{j-1}w_k (hihj)2+k=i+1j1wk
若可以连若干次,求要将 1 1 1 ~ n n n 的柱子连在一起的最小代价。

  • n n n, h i h_i hi, w i w_i wi 题目给出
  • 2 ≤ n ≤ 1 0 5 2\le n\le 10^5 2n105 0 ≤ h i , ∣ w i ∣ ≤ 1 0 6 0\le h_i,\vert w_i\vert\le 10^6 0hi,wi106

思路
首先,设 :
f i f_i fi:从 1 1 1 连到第 i i i 根柱子所需要的最小花费。
s u m i sum_i sumi ∑ j = 1 i w j \sum_{j=1}^{i} w_j j=1iwj

然后可以列出转移式子:
f i = m i n 0 ≤ j < i f j + ( h i − h j ) 2 + s u m i − 1 − s u m j f_i= min_{0 \le j \lt i}f_j+(h_i-h_j)^2 + sum_{i-1}- sum_j fi=min0j<ifj+(hihj)2+sumi1sumj
整理式子:
f i = m i n 0 ≤ j < i ( s u m i − 1 + h i 2 ) + ( f j + h j 2 − s u m j ) − 2 h i h j f_i = min_{0 \le j \lt i}(sum_{i-1} + h_i^2 ) + (f_j + h_j^2 - sum_j ) - 2h_ih_j fi=min0j<i(sumi1+hi2)+(fj+hj2sumj)2hihj

g i = s u m i − 1 + h i 2 g_i = sum_{i-1} + h_i^2 gi=sumi1+hi2 b i = f i + h i 2 − s u m i b_i = f_i + h_i^2-sum_i bi=fi+hi2sumi,化简原式得:
f i = m i n 0 ≤ j < i g i + b j − 2 h i h j f_i = min_{0 \le j \lt i}g_i + b_j - 2h_ih_j fi=min0j<igi+bj2hihj

下面研究 j > j 2 j\gt j_2 j>j2 f i j < f i j 2 f_{i_{j}} \lt f_{i_{j_2}} fij<fij2(意思是: f i f_i fi 通过 j j j 转移得到的结果 优于 f i f_i fi 通过 j 2 j_2 j2 转移得到的结果) 的前提条件:
g i + b j − 2 h i h j < g i + b j 2 − 2 h i h j 2 g_i + b_j - 2h_ih_j \lt g_i + b_{j_2} - 2h_ih_{j_2} gi+bj2hihj<gi+bj22hihj2
化简得:
b j − b j 2 < 2 h i ( h j − h j 2 ) b_j - b_{j_2} \lt 2h_i (h_j - h_{j_2}) bjbj2<2hi(hjhj2)
y i = b i y_i = b_i yi=bi x i = h i x_i = h_i xi=hi ,则原式变为:
y j − y j 2 < 2 h i ( x j − x j 2 ) y_j-y_{j_2} \lt 2h_i (x_j-x_{j2}) yjyj2<2hi(xjxj2)

这时候出现了问题:

  1. 按照正常情况,现在应该将 ( x j − x j 2 ) (x_j-x_{j2}) (xjxj2) 除过去,但是我们不确定不等号是否需要改变,即我们不知道 x j x_j xj x j 2 x_{j_2} xj2 的大小关系。我们想对 x x x 数组排序,这样就能确定大小关系了,但是这样下标 i i i 就会变化。于是,我们可以采用 cdq分治 。即,对于一个序列,可以先处理完序列的左半部分,然后计算序列左半部分对右半部分的贡献,这时序列的左半部分就可以排序保证 x i x_i xi 递增或递减了。
  2. 发现 h i h_i hi 也没有单调性,我们当然可以使用 二分 (如 例题 2 2 2 ),但是这没有任何必要,仔细分析一下,在使用 cdq分治 中算贡献的时候,是序列的左半部分对右半部分的每个数分别计算贡献,与右半部分数的次序无关。于是可以将序列的右半部分按 h i h_i hi 排序后再计算,这样就可以保证 h i h_i hi 的单调性了。

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e5+5;

ll h[maxn],sum[maxn],f[maxn];
double X(int i){ return h[i]; }
double Y(int i){ return f[i]+h[i]*h[i]-sum[i]; }
double Slope(int i,int j){
	if(X(j)-X(i)==0) return 1e18*1.0;//!!!!!!!!!!!!!!!不特判的话65pts 
	return (Y(j)-Y(i))/(X(j)-X(i));
}

bool cmp(int a,int b){ return X(a)!=X(b)?X(a)<X(b):Y(a)<Y(b); }

int q[maxn],id[maxn];
void Cdq(int l,int r){
	if(l==r) return;
	int mid=(l+r)>>1;
	Cdq(l,mid);
	for(int i=l;i<=r;i++)
		id[i]=i;
	sort(id+l,id+mid+1,cmp);
	sort(id+mid+1,id+r+1,cmp);
	int hd=1,tal=0;
	for(int i=l;i<=mid;i++){
		while(hd<tal&&Slope(q[tal-1],q[tal])>Slope(q[tal],id[i])) tal--;
		q[++tal]=id[i];
	}
	for(int i=mid+1;i<=r;i++)
		if(hd<=tal){
			int i2=id[i];
			while(hd<tal&&Slope(q[hd],q[hd+1])<2*h[i2]) hd++;
			int j=q[hd]; f[i2]=min(f[i2],f[j]+(h[i2]-h[j])*(h[i2]-h[j])+sum[i2-1]-sum[j]);
		}
	Cdq(mid+1,r);
}

int main(){
	int n; cin>>n;
	for(int i=1;i<=n;i++)
		cin>>h[i];
	for(int i=1;i<=n;i++){
		ll w; cin>>w;
		sum[i]=sum[i-1]+w;
	}
	for(int i=2;i<=n;i++)
		f[i]=1e18;
	Cdq(1,n);
	cout<<f[n];
	return 0;
}

例题4 lgP4027 [NOI2007] 货币兑换

link

题意
在第一天你有 S S S 元钱,每一天中 A A A 券和 B B B 券的价值分别为 A i A_i Ai B i B_i Bi(元/单位金券)。
每一天你都可以选择以下两种操作执行任意次:

  1. 卖出金券:你提供一个 [ 0 , 100 ] [0,100] [0,100] 内的实数 O P OP OP ,将 O P OP OP% 的 A A A 券和 O P OP OP% 的 B B B 券以当天的价值兑换为纸币;
  2. 买入金券:你支付 I P IP IP 元钱,将会得到价值为 I P IP IP 的金券,且满足 A A A 券和 B B B 券的比例在第 i i i 天恰好为 R a t e i Rate_i Ratei

问: N N N 天后最多能够获得多少元钱。

  • N N N, S S S, A i A_i Ai, B i B_i Bi, R a t e i Rate_i Ratei 题目给出
  • N ≤ 1 0 5 N \le 10^5 N105 0 < A K ≤ 10 0 < A_K \leq 10 0<AK10 0 < B K ≤ 10 0 < B_K\le 10 0<BK10 0 < R a t e K ≤ 100 0 < \mathrm{Rate}_K \le 100 0<RateK100 M a x P r o f i t ≤ 1 0 9 \mathrm{MaxProfit} \leq 10^9 MaxProfit109

Tips (这个是本题的关键入手点,但是我就不告诉你!!!)
必然存在一种最优的买卖方案满足:每次买进操作使用完所有的人民币,每次卖出操作卖出所有的金券。

放一个关于 tips 的证明:
在这里插入图片描述

思路
d p dp dp 状态: f i f_i fi:前 i i i 天的最大获利。
考虑如何转移:
若 状态 i i i 由 状态 j j j 转移得到,设 R a t e j = a b Rate_j= \frac{a}{b} Ratej=ba,表示 A A A 卷数量 : : : B B B 卷数量 = a : b = a:b =a:b ,一共有 k 张卷。
则:
A 卷数量 = k ⋅ a a + b = k ⋅ 1 a + b a = k ⋅ 1 1 + b a = k ⋅ 1 1 + 1 R a t e j = k ⋅ 1 R a t e j + 1 R a t e j = k ⋅ R a t e j R a t e j + 1 \begin{aligned} A 卷数量 &=k \cdot \frac{a}{a+b} \\ &=k \cdot \frac{1}{\frac{a+b}{a}}\\ &=k \cdot \frac{1}{1+\frac{b}{a}}\\ &=k \cdot \frac{1}{1+\frac{1}{Rate_j}}\\ &=k \cdot \frac{1}{\frac{Rate_j+1}{Rate_j}}\\ &=k \cdot \frac{Rate_j}{Rate_j+1} \end{aligned} A卷数量=ka+ba=kaa+b1=k1+ab1=k1+Ratej11=kRatejRatej+11=kRatej+1Ratej
B 卷数量 = k ⋅ b a + b = k ⋅ 1 R a t e j + 1 (同理可得) \begin{aligned} B 卷数量 &=k \cdot \frac{b}{a+b} \\ &=k \cdot \frac{1}{Rate_j+1} (同理可得) \end{aligned} B卷数量=ka+bb=kRatej+11(同理可得)
又有:
f j = k ⋅ R a t e j R a t e j + 1 ⋅ A j + k ⋅ 1 R a t e j + 1 ⋅ B j = k ⋅ R a t e j A j + B j R a t e j + 1 \begin{aligned} f_j &=k \cdot \frac{Rate_j}{Rate_j+1} \cdot A_j + k \cdot \frac{1}{Rate_j+1} \cdot B_j \\ &=k \cdot \frac{Rate_jA_j+B_j}{Rate_j+1} \end{aligned} fj=kRatej+1RatejAj+kRatej+11Bj=kRatej+1RatejAj+Bj
由此得到:
k = f j ⋅ R a t e j + 1 R a t e j A j + B j k=f_j \cdot \frac{Rate_j+1}{Rate_jA_j+B_j} k=fjRatejAj+BjRatej+1
所以:
A 卷数量 = f j ⋅ R a t e j + 1 R a t e j A j + B j ⋅ R a t e j R a t e j + 1 = f j ⋅ R a t e j R a t e j A j + B j A 卷数量 = f_j \cdot \frac{Rate_j+1}{Rate_jA_j+B_j} \cdot \frac{Rate_j}{Rate_j+1} = f_j \cdot \frac{Rate_j}{Rate_jA_j+B_j} A卷数量=fjRatejAj+BjRatej+1Ratej+1Ratej=fjRatejAj+BjRatej
B 卷数量 = f j ⋅ R a t e j + 1 R a t e j A j + B j ⋅ 1 R a t e j + 1 = f j ⋅ 1 R a t e j A j + B j B 卷数量 = f_j \cdot \frac{Rate_j+1}{Rate_jA_j+B_j} \cdot \frac{1}{Rate_j+1} = f_j \cdot \frac{1}{Rate_jA_j+B_j} B卷数量=fjRatejAj+BjRatej+1Ratej+11=fjRatejAj+Bj1

得到转移方程:
f i = { S i = 1 max ⁡ { f i − 1 , f j ⋅ R a t e j A i + B i R a t e j A j + B j } 1 ≤ j < i f_i = \begin{cases} S & i = 1 \\ \max\{f_{i-1},f_j \cdot \frac{Rate_jA_i+B_i}{Rate_jA_j+B_j} \} & 1 \le j \lt i \end{cases} fi={Smax{fi1,fjRatejAj+BjRatejAi+Bi}i=11j<i

设: g i = f i R a t e i A i + B i g_i=\frac{f_i}{Rate_iA_i+B_i} gi=RateiAi+Bifi
f i f_i fi 重点部分可得到:
f i = g j ⋅ ( R a t e j A i + B i ) = g j ⋅ R a t e j A i + g j ⋅ B i f_i=g_j \cdot (Rate_jA_i+B_i) = g_j \cdot Rate_jA_i+g_j \cdot B_i fi=gj(RatejAi+Bi)=gjRatejAi+gjBi

下面研究 j > j 2 j\gt j_2 j>j2 f i j > f i j 2 f_{i_{j}} \gt f_{i_{j_2}} fij>fij2(意思是: f i f_i fi 通过 j j j 转移得到的结果 优于 f i f_i fi 通过 j 2 j_2 j2 转移得到的结果) 的前提条件:
g j R a t e j A i + g j B i > g j 2 R a t e j 2 A i + g j 2 B i g_jRate_jA_i+g_jB_i \gt g_{j_2}Rate_{j_2}A_i+g_{j_2}B_i gjRatejAi+gjBi>gj2Ratej2Ai+gj2Bi
化简得:
g j R a t e j − g j 2 R a t e j 2 > B i A i ⋅ ( − g j − ( − g j 2 ) ) g_jRate_j - g_{j_2}Rate_{j_2} \gt \frac{B_i}{A_i} \cdot (-g_j - (-g_{j_2})) gjRatejgj2Ratej2>AiBi(gj(gj2))
y i = g i R a t e i y_i = g_iRate_i yi=giRatei x i = − g i x_i = -g_i xi=gi ,则原式变为:
y j − y j 2 > B i A i ⋅ ( x j − x j 2 ) y_j-y_{j_2} \gt \frac{B_i}{A_i} \cdot (x_j - x_{j_2}) yjyj2>AiBi(xjxj2)

P r o b l e m 1 \rm Problem1 Problem1 :因为我们无法确定 x x x 数组的单调性,所以可以通过 cdq分治 x x x 数组有单调性。
即,可将式子看成:
y j − y j 2 x j − x j 2 > B i A i \frac{y_j-y_{j_2}}{x_j - x_{j_2}} \gt \frac{B_i}{A_i} xjxj2yjyj2>AiBi

P r o b l e m 2 \rm Problem2 Problem2 :又因为发现 B i A i \frac{B_i}{A_i} AiBi 没有单调性,所以不能使用单调队列维护,但是可以通过 单调栈 + 二分 单调栈+二分 单调栈+二分 实现。

综上所述,本题可以看成是 例题2 + 例题3 的综合题。

g j gj gj说:做了这题,你就大概把这个专题各类型的题都见过了。

代码

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5;

double a[maxn],b[maxn],rate[maxn],f[maxn];
double X(int i){ return -f[i]/(rate[i]*a[i]+b[i]); }
double Y(int i){ return f[i]/(rate[i]*a[i]+b[i])*rate[i]; }
double Slope(int i,int j){
	if(X(j)==X(i)) return 1e9*1.0;
	return (Y(j)-Y(i))/(X(j)-X(i));
}

int q[maxn];
int Bry_Sch(int l,int r,double k){//这里的二分用了递归,直接while也可以,但是细节有点多,我还没调出来
	if(l==r) return l;
	int mid=(l+r)>>1;
	if(Slope(q[mid],q[mid+1])>=k) return Bry_Sch(mid+1,r,k);
	return Bry_Sch(l,mid,k);
}

bool cmp(int a,int b){ return X(a)!=X(b)?X(a)<X(b):Y(a)<Y(b); }

int id[maxn];
void Cdq(int l,int r){
	if(l==r){
		f[l]=max(f[l],f[l-1]);
		return;
	}
	int mid=(l+r)>>1;
	Cdq(l,mid);
	for(int i=l;i<=mid;i++)
		id[i]=i;
	sort(id+l,id+mid+1,cmp);
	int hd=1,tal=0;
	for(int i=l;i<=mid;i++){
		while(hd<tal&&Slope(q[tal-1],q[tal])<Slope(q[tal],id[i])) tal--;
		q[++tal]=id[i];
	}
	for(int i=mid+1;i<=r;i++){
		int j=q[Bry_Sch(hd,tal,b[i]/a[i])]; 
		f[i]=max(f[i],f[j]*(rate[j]*a[i]+b[i])/(rate[j]*a[j]+b[j]));
	}
	Cdq(mid+1,r);
}

int main(){
	int n; cin>>n>>f[1];
	for(int i=1;i<=n;i++)
		cin>>a[i]>>b[i]>>rate[i];
	Cdq(1,n);
	printf("%.3lf",f[n]);
	return 0;
}

总结

遇到这类题目,首先得把朴素的 d p dp dp 模型建出来,然后再在这个暴力上进行斜率优化(一堆推式子 / 乱搞)。

以后可能还会继续补充……的吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值