斜率优化DP

斜率优化 DP,又叫凸壳优化 (Convex Hull Trick) 。它是用来求解
d p [ i ] = max ⁡ j ∈ S ( i ) f ( i ) + k ( j ) x ( i ) + b ( j ) d p[i]=\max _{j \in S(i)} f(i)+k(j) x(i)+b(j) dp[i]=maxjS(i)f(i)+k(j)x(i)+b(j) 并且满足一定条件的问题。
比如,满足右边和 d p [ j ] d p[j] dp[j] 无关,并且 S ( i ) S(i) S(i) 为全集。或者 S ( i ) = { j ∣ j < i } S(i)=\{j \mid j<i\} S(i)={jj<i} or S ( i ) = { j ∣ j > i } S(i)=\{j \mid j>i\} S(i)={jj>i} 等等。 其中max也可以改为 min ⁡ \min min

先考虑一个简化的问题:
d p [ i ] = max ⁡ j ∈ S ( i ) f ( i ) + k ( j ) x ( i ) + b ( j ) d p[i]=\max _{j \in S(i)} f(i)+k(j) x(i)+b(j) dp[i]=maxjS(i)f(i)+k(j)x(i)+b(j)
d p \mathrm{dp} dp 式子右边和 d p [ j ] \mathrm{dp}[\mathrm{j}] dp[j] 无关,且 S ( i ) S(i) S(i) 为全集。
对于固定的 i i i 来说 f ( i ) , x ( i ) f(i), x(i) f(i),x(i) 都是常数,将 f ( i ) f(i) f(i) 忽略, x ( i ) x(i) x(i) 记为 x , d p [ i ] x , d p[i] xdp[i] 记为 y y y ,我们可以将转移方程写成
y = max ⁡ j k ( j ) x + b ( j ) y=\max _{j} k(j) x+b(j) y=maxjk(j)x+b(j)
对于每个 j j j 来说,将 x x x 看成变量, l j ( x ) = k ( j ) x + b ( j ) l_{j}(x)=k(j) x+b(j) lj(x)=k(j)x+b(j) 是一条直线。而上式就是在这些直线中 x = x ( i ) x=x(i) x=x(i) 时, 对应 y y y 值最大的。即
y ( i ) = max ⁡ j l j ( x ( i ) ) y(i)=\max _{j} l_{j}(x(i)) y(i)=maxjlj(x(i))
如果该式在 j = p j=p j=p 时取最大,说明 l p ( x ( i ) ) ≥ l q ( x ( i ) ) , ∀ q l_{p}(x(i)) \geq l_{q}(x(i)), \forall q lp(x(i))lq(x(i)),q

用图来表示,对于任意的 i i i要求 y ( i ) y(i) y(i)的最大值那么一定是落在蓝色的直线上面的,而维护的蓝色的这些直线即为凸包。
在这里插入图片描述这里提一下如何在加边的过程中维护凸包?
给出一种确定方法,先将直线按照斜率从小到大排序或者从大到小排序(越来越斜,注意正负,对于 k k k为负数的直线, k k k越小越斜,对于 k k k为正数的直线, k k k越大)再依次加入在这里插入图片描述

(对于图中的直线先加入绿色再加入黑色再加入紫色)
每次加入新的直线时算出它与倒数第二条直线的交点的横坐标,比如这里黑色与紫色的交点为1,然后再拿这个坐标与最后一个直线和倒数第二个直线的交点2的横坐标比较,如果1的横坐标小于等于2的横坐标,那么就把最后一条直线删除,即把黑线删除,可以看出加入紫色后构成下凸包的直线应该为绿和紫。

然后对于每一个 x ( i ) x(i) x(i) ,二分得到所在线段,即可得到答案,复杂度为 O ( n log ⁡ n ) O(n \log n) O(nlogn)。或者 x ( i ) x(i) x(i) 按从小到大顺序,按序判断,移动指针,如果 x ( i ) x(i) x(i) 的次序可以 O ( n ) O(n) O(n) 得到,则复杂度为 O ( n ) O(n) O(n)

Codeforces Round #816 (Div. 2) E

#include<bits/stdc++.h>
#define int long long
#define io ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
const int maxn=1e6+5;
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
struct node{
	int dis,pos;
	bool operator<(const node&x)const{
		return x.dis<dis;
	}
};
struct Line {  //存直线
	int k, b;   //y=kx+b
	Line() {}
	Line(int k, int b) : k(k), b(b) {}
	
	//求两直线交点横坐标
	double intersect(Line l) {  
		//交点
		double db = l.b - b;
		double dk = k - l.k;
		return db / dk;
	}
	
	int operator () (int x) {
		return k * x + b;
	}
};
struct ConvexHullTrick{  //凸壳优化
	vector<double>points;  //存交点 
	vector<Line>lines;     //存直线
	//每个直线范围是上一个直线的points[i-1]到自己的points[i]的范围
	int size(){
		return points.size();
	}
	void reset(){
		points.clear();
		lines.clear();
	}
	void init(Line l){
		points.push_back(-inf);
	}
	void addLine(Line l){
		if(points.size()==0){
			points.push_back(-inf);
			lines.push_back(l);
			return;
		}
		/*
		加入的直线和倒数第二个直线的交点,小于最后一个直线和倒数第二个直线的交点
		那么最后一条直线就可以直接丢掉了
		*/
		while(lines.size()>=2&&
			l.intersect(lines[lines.size() - 2]) <= points.back()){
			points.pop_back();
			lines.pop_back();
		}
		points.push_back(l.intersect(lines.back()));
		lines.push_back(l);
	}
	int query(int x,int id){
		return lines[id](x);
	}
	int query(int x){
		int id=upper_bound(points.begin(),points.end(),x)-points.begin()-1;
		return lines[id](x);
	}
};
vector<pair<int,int>>vec[maxn];
int n,m,k;
int dist[maxn];
int vis[maxn];
void dijkstra(){
	memset(vis,0,sizeof(vis));
	priority_queue<node>q;
	for(int i=1;i<=n;i++){
		q.push({dist[i],i});
	}
	while(!q.empty()){
		auto[distance,u]=q.top();
		q.pop();
		if(vis[u]) continue;
		vis[u]=1;
		for(auto[v,w]:vec[u]){
			if(dist[v]>distance+w){
				dist[v]=distance+w;
				if(!vis[v]){
					q.push({dist[v],v});
				}
			}
		}
	}
}
void solve(){
	cin>>n>>m>>k;
	for(int i=1;i<=m;i++){
		int u,v,w;
		cin>>u>>v>>w;
		vec[u].push_back({v,w});
		vec[v].push_back({u,w});
	}
	memset(dist,inf,sizeof(dist));
	dist[1]=0;
	dijkstra();
	ConvexHullTrick cht;
	while(k--){
		cht.reset();
		for(int u=1;u<=n;u++){
			cht.addLine({-2*u,dist[u]+u*u});
			/*
			k=-2u b=dist[u]+u*u
			y=kx+b 
			加入新的边,要求的是越来越斜,因为这里的u是单调递增的所以
			斜率-2u就是越来越斜的
			*/
		}
		for(int v=1;v<=n;v++){
			dist[v]=cht.query(v)+v*v;
		}
		dijkstra();
	}
	for(int i=1;i<=n;i++){
		cout<<dist[i]<<" ";
	}
}
signed main(){
	int t=1;
	//cin>>t;
	while(t--){
		solve();
	}
}

在斜率k(j)单调递增,并且 x ( i ) x(i) x(i) 单调递增的时候我们可以优化掉一个 l o g log log,因为不用对直线排序所以排序的 l o g log log省了,此外 x ( i ) x(i) x(i)也是单调递增的所以我们找的线段一定是越来越往后的,这个可以移动指针来判断,符合这种情况的时候又叫做单调队列优化斜率DP,可以用单调队列的写法完成,但是为了统一处理更加方便,这里我们还是用凸包的方法解决,ConvexHullTrick里面新增一个queuequery函数,通过移动指针sta来判断是否到了 x ( i ) x(i) x(i)所在的线段上,注意边界即可
任务安排2

#include<bits/stdc++.h>
#define int long long
#define io ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
using namespace std;
const int maxn=1e6+5;
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
struct Line {  //存直线
	int k, b;   //y=kx+b
	Line() {}
	Line(int k, int b) : k(k), b(b) {}
	
	//求两直线交点横坐标
	double intersect(Line l) {  
		//交点
		double db = l.b - b;
		double dk = k - l.k;
		return db / dk;
	}
	
	int operator () (int x) {
		return k * x + b;
	}
};
struct ConvexHullTrick{  //凸壳优化
	int sta=0;
	vector<double>points;  //存交点 
	vector<Line>lines;     //存直线
	//每个直线范围是上一个直线的points[i-1]到自己的points[i]的范围
	int size(){
		return points.size();
	}
	void reset(){
		sta=0;
		points.clear();
		lines.clear();
	}
	void init(Line l){
		points.push_back(-inf);
	}
	void addLine(Line l){
		if(points.size()==0){
			points.push_back(-inf);
			lines.push_back(l);
			return;
		}
		/*
		加入的直线和倒数第二个直线的交点,小于最后一个直线和倒数第二个直线的交点
		那么最后一条直线就可以直接丢掉了
		 */
		while(lines.size()>=2&&
			l.intersect(lines[lines.size() - 2]) <= points.back()){
			points.pop_back();
			lines.pop_back();
		}
		points.push_back(l.intersect(lines.back()));
		lines.push_back(l);
	}
	int query(int x,int id){
		return lines[id](x);
	}
	int query(int x){
		int id=upper_bound(points.begin(),points.end(),x)-points.begin()-1;
		return lines[id](x);
	}
	int queuequery(int x){
		while(sta<points.size()&&points[sta]<=x){
			sta++;
		}		
		sta--;
		return query(x,sta);
	}
};
int n,s;
int t[maxn],c[maxn];
int st[maxn],sc[maxn];
int dp[maxn];
void solve(){
	cin>>n>>s;
	for(int i=1;i<=n;i++){
		cin>>st[i]>>sc[i];
		st[i]+=st[i-1];
		sc[i]+=sc[i-1];
	}
	ConvexHullTrick cht;
	for(int i=1;i<=n;i++){
		if(i==1){
			dp[i]=st[i]*sc[i]+s*sc[n];
		}
		else{
			dp[i]=st[i]*sc[i]+s*sc[n];
			dp[i]=min(st[i]*sc[i]+s*sc[n]+cht.queuequery(st[i]),dp[i]);
		}
		cht.addLine({-sc[i],dp[i]-s*sc[i]});
	}
	cout<<dp[n]<<"\n";
}
signed main(){
	int t=1;
	//cin>>t;
	while(t--){
		solve();
	}
}

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: hdu 2829 Lawrence 斜率优化dp 这道题是一道经典的斜率优化dp题目,需要用到单调队列的思想。 题目大意是给定一个序列a,求出一个序列b,使得b[i]表示a[1]~a[i]中的最小值,且满足b[i] = min{b[j] + (i-j)*k},其中k为给定的常数。 我们可以将上式拆开,得到b[i] = min{b[j] - j*k} + i*k,即b[i] = i*k + min{b[j] - j*k},这个式子就是斜率优化dp的形式。 我们可以用单调队列来维护min{b[j] - j*k},具体思路如下: 1. 首先将第一个元素加入队列中。 2. 从第二个元素开始,我们需要将当前元素加入队列中,并且需要维护队列的单调性。 3. 维护单调性的方法是,我们从队列的末尾开始,将队列中所有大于当前元素的元素弹出,直到队列为空或者队列中最后一个元素小于当前元素为止。 4. 弹出元素的同时,我们需要计算它们对应的斜率,即(b[j]-j*k)/(j-i),并将这些斜率与当前元素的斜率比较,如果当前元素的斜率更小,则将当前元素加入队列中。 5. 最后队列中的第一个元素就是min{b[j] - j*k},我们将它加上i*k就得到了b[i]的值。 6. 重复以上步骤直到处理完所有元素。 具体实现可以参考下面的代码: ### 回答2: HDU 2829 Lawrence 斜率优化 DP 是一道经典的斜率优化 DP 题目,其思想是通过维护一个下凸包来优化 DP 算法。下面我们来具体分析一下这道题目。 首先,让我们看一下该题目的描述。题目给定一些木棒,要求我们将这些木棒割成一些给定长度,且要求每种长度的木棒的数量都是一样的,求最小的割枝次数。这是一个典型的背包问题,而且在此基础上还要求每种长度的木棒的数量相同,这就需要我们在状态设计上走一些弯路。 我们来看一下状态的定义。定义 $dp[i][j]$ 表示前 $i$ 个木棒中正好能割出 $j$ 根长度为 $c_i$ 的木棒的最小割枝次数。对于每个 $dp[i][j]$,我们可以分类讨论: 1. 不选当前的木棒,即 $dp[i][j]=dp[i-1][j]$; 2. 选当前的木棒,即 $dp[i][j-k]=dp[i-1][j-k]+k$,其中 $k$ 是 $j/c_i$ 的整数部分。 现在问题再次转化为我们需要在满足等量限制的情况下,求最小的割枝次数。可以看出,这是一个依赖于 $c_i$ 的限制。于是,我们可以通过斜率优化 DP 来解决这个问题。 我们来具体分析一下斜率优化 DP 算法的思路。我们首先来看一下动态规划的状态转移方程 $dp[i][j]=\min\{dp[i-1][k]+x_k(i,j)\}$。可以发现,$dp[i][j]$ 的最小值只与 $dp[i-1][k]$ 和 $x_k(i,j)$ 有关。其中,$x_k(i,j)$ 表示斜率,其值为 $dp[i-1][k]-k\times c_i+j\times c_i$。 接下来,我们需要维护一个下凸包,并通过斜率进行优化。我们具体分析一下该过程。假设我们当前要计算 $dp[i][j]$。首先,我们需要找到当前点 $(i,j)$ 在凸包上的位置,即斜率最小值的位置。然后,我们根据该位置的斜率计算 $dp[i][j]$ 的值。接下来,我们需要将当前点 $(i,j)$ 加入到下凸包上。 我们在加入点的时候需要注意几点。首先,我们需要将凸包中所有斜率比当前点小的点移除,直到该点能够加入到凸包中为止。其次,我们需要判断该点是否能够加入到凸包中。如果不能加入到凸包中,则直接舍弃。最后,我们需要保证凸包中斜率是单调递增的,这就需要在加入新的点之后进行上一步操作。 以上就是该题目的解题思路。需要注意的是,斜率优化 DP 算法并不是万能的,其使用情况需要根据具体的问题情况来确定。同时,该算法中需要维护一个下凸包,可能会增加一些算法的复杂度,建议和常规 DP 算法进行对比,选择最优的算法进行解题。 ### 回答3: 斜率优化DP是一种动态规划优化算法,其主要思路是通过对状态转移方程进行变形,提高算法的时间复杂度。HDU2829 Lawrence问题可以用斜率优化DP解决。 首先,我们需要了解原问题的含义。问题描述如下:有$n$个人在数轴上,第$i$个人的位置为$A_i$,每个人可以携带一定大小的行李,第$i$个人的行李重量为$B_i$,但是每个人只能帮助没有他们重量大的人搬行李。若第$i$个人搬运了第$j$个人的行李,那么第$i$个人会累加$C_{i,j}=\left|A_i-A_j\right|\cdot B_j$的体力消耗。求$m$个人帮助每个人搬运行李的最小体力消耗。 我们可以通过斜率优化DP解决这个问题。记$f_i$为到前$i$个人的最小体力消耗,那么状态转移方程为: $$f_i=\min_{j<i}\{f_j+abs(A_i-A_j)\cdot B_i\}$$ 如果直接使用该方程,时间复杂度为$O(n^2)$,如果$n=10^4$,则需要计算$10^8$次,运算时间极长。斜率优化DP通过一些数学推导将方程变形,将时间复杂度降低到$O(n)$,大大缩短了计算时间。 通过斜率优化DP的推导式子,我们可以得到转移方程为: $$f_i=\min_{j<i}\{f_j+slope(j,i)\}$$ 其中,$slope(j,i)$表示直线$j-i$的斜率。我们可以通过如下方式来求解$slope(j,i)$: $$slope(j,i)=\frac{f_i-f_j}{A_i-A_j}-B_i-B_j$$ 如果$slope(j,i)\leq slope(j,k)$,那么$j$一定不是最优,可以直接舍去,降低计算时间。该算法的时间复杂度为$O(n)$。 综上所述,斜率优化DP是一种动态规划优化算法,可以大大缩短计算时间。在处理类似HDU2829 Lawrence问题的时候,斜率优化DP可以很好地解决问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值