再论斜率优化

再论斜率优化

前言

早在2024年9月3日,本蒟蒻就发布了一篇斜率优化的讲解,然而我那个时候过于菜了,理解不是很深刻,于是今天重写一篇。

问题

见洛谷P10979,至于那个削弱版的绿题,就是费用提前计算,我之前讲得挺明白了,这里不再讲了。

再那个题中,我们使用了前缀和和费用提前计算优化,DP转移式如下。

d p i = m i n ( d p j + s u m t i × ( s u m c i − s u m c j ) + s × ( s u m c n − s u m c j ) ) dp_i = min(dp_j + sumt_i \times (sumc_i - sumc_j) + s \times (sumc_n - sumc_j)) dpi=min(dpj+sumti×(sumci−sumcj)+s×(sumcn−sumcj))

其中 s u m c sumc sumc和 s u m t sumt sumt就是 c c c和 t t t的前缀和,其余变量如题中所述, d p i dp_i dpi就是完成前 i i i个任务的最小花费, j j j是枚举的。

我们要枚举 j j j,就得 n 2 n^2 n2,过不了。

考虑怎么不枚举 j j j。

在线段树优化DP的时候,我们都是把和 j j j有关的挪到一边,这是有道理的,因为这么做就可以把 i i i和 j j j拆开,方便上手法维护。

化一化式子,得到:

d p j − ( s u m t i + s ) × s u m c j = d p i − s u m c i × s u m t i − s u m c n × s dp_j - (sumt_i + s) \times sumc_j = dp_i - sumc_i \times sumt_i - sumc_n \times s dpj−(sumti+s)×sumcj=dpi−sumci×sumti−sumcn×s

上线段树的想法彻底破灭了,左边还有 i i i,你发现含 i i i的项和含 j j j的项相乘了,但是只有一项,这个就要使用斜率优化了。

斜率优化

然后你继续瞪眼,发现这个像直线的方程,你要右面尽可能小,就是将 d p j dp_j dpj和 s u m c j sumc_j sumcj带入这条斜率为 s u m t i + s sumt_i + s sumti+s的直线,让截距最小 (防止你不会:截距指直线和y轴交点到原点的距离)

为了方便,我们统一钦定 s u m c j sumc_j sumcj为x轴 d p j dp_j dpj为y轴。

然后怎么让截距最小呢?可以让这条直线从负无穷的位置移上来,这里复用一下两年前的老图。

发现碰到的第一个点就是最小转移点。

这个有性质,如图。

只有黄色的点能碰到,也就是一个下凸包,这个凸包可以单调队列维护。

维护出来了之后,我们只需要找到最后一条斜率小于 s u m t i + s sumt_i + s sumti+s的线段的右端点就行了。

但是,最坏情况下,所有点都在凸包中,我们扫一遍还是O(n)的。

怎么办,考虑斜率是 s u m t i + s sumt_i + s sumti+s,随着 i i i增加肯定是递增的。

所以可以放进单调队列里,斜率小就弹掉,队头就是最佳转移点,时间复杂度O(n)。

cpp 复制代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 300010;
int n,s;
int c[N],t[N],sumc[N],sumt[N]; 
int dp[N];
int q[N],head = 1,tail = 1;
signed main(){
	cin>>n>>s;
	for(int i = 1;i<=n;i++){
		cin>>t[i]>>c[i];
		sumt[i] = sumt[i-1]+t[i];
		sumc[i] = sumc[i-1]+c[i];
	}
	dp[1] = 0;
	q[1] = 0;
	for(int i = 1;i<=n;i++){
		int k = s + sumt[i];//斜率
		//计算前两个点的斜率,为(dp[q[head+1]] - dp[q[head]]) / (sumc[q[head+1]] - sumc[q[head]]),斜率<=k弹出 
		while(head<tail && (dp[q[head+1]] - dp[q[head]]) <= (sumc[q[head+1]] - sumc[q[head]]) * k){
			head++;
		}
		//更新dp值,我们已经维护出了最优转移点,式子套上来就行 
		dp[i] = dp[q[head]] - (k * sumc[q[head]]) + (sumc[i] * sumt[i]) + (sumc[n] * s);
		//i和tail的斜率 (dp[i] - dp[q[tail]]) / (sumc[i] - sumc[q[tail]])
		//tail和tail-1的斜率 (dp[q[tail]] - dp[q[tail-1]]) / (sumc[q[tail]] - sumc[q[tail-1]]) 
		while(head<tail && (dp[i] - dp[q[tail]]) * (sumc[q[tail]] - sumc[q[tail-1]]) <= (dp[q[tail]] - dp[q[tail-1]]) * (sumc[i] - sumc[q[tail]])){
			tail--;
		}
		//插入i 
		q[++tail] = i;
	}
	cout<<dp[n]<<'\n';
	return 0;
}

另一种问题

t i t_i ti可以为负,话说为啥用时还能是负的?

我们的斜率不仅失去了单调递增的性质,而且还可以是负的。

照常维护下凸包,维护出的是这样的:

绿色的是之前没有的。

发现斜率还是单调的,我们判定最佳转移点的方式不变,只是我们不能直接弹掉队头,之后可能还有用。

直接二分 就好了,多一个log时间复杂度可以接受。

代码如下

cpp 复制代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 300010;
int n,s;
int c[N],t[N],sumc[N],sumt[N]; 
int dp[N];
int q[N],tail = 1;
signed main(){
	cin>>n>>s;
	for(int i = 1;i<=n;i++){
		cin>>t[i]>>c[i];
		sumt[i] = sumt[i-1]+t[i];
		sumc[i] = sumc[i-1]+c[i];
	}
	dp[1] = 0;
	q[1] = 0;
	for(int i = 1;i<=n;i++){
		int k = s + sumt[i];//斜率
		//我们规定一下,二分的是斜率<=k的线段的左端点,这样+1就是答案了 
		int l = 1,r = tail,mid,res = -1;
		while(l<=r&&tail!=1){
			mid = (l+r)>>1;
			int h1 = q[mid],h2 = q[mid+1];//左右端点
			//(dp[h2] - dp[h1]) / (sumc[h2] - sumc[h1]) <= k
			if((dp[h2] - dp[h1]) <= k * (sumc[h2] - sumc[h1])){
				res = mid;
				l = mid+1; 
			}else{
				r = mid-1;
			}
		}
		if(res==-1)res = 1;//就一个点特判一下 
		else res++;
		//res为q的下标,我们将其还原为原序列的下标 
		res = q[res];
		//更新dp值,我们已经维护出了最优转移点,式子套上来就行 
		dp[i] = dp[res] - (k * sumc[res]) + (sumc[i] * sumt[i]) + (sumc[n] * s);
		//i和tail的斜率 (dp[i] - dp[q[tail]]) / (sumc[i] - sumc[q[tail]])
		//tail和tail-1的斜率 (dp[q[tail]] - dp[q[tail-1]]) / (sumc[q[tail]] - sumc[q[tail-1]]) 
		while(tail > 1 && (dp[i] - dp[q[tail]]) * (sumc[q[tail]] - sumc[q[tail-1]]) <= (dp[q[tail]] - dp[q[tail-1]]) * (sumc[i] - sumc[q[tail]])){
			tail--;
		}
		//插入i 
		q[++tail] = i;
	}
	cout<<dp[n]<<'\n';
	return 0;
}

后记

可以发现,dp问题中想暴力很重要,一眼看不出用什么,那就化式子,化完之后就能看出用线段树之类的数据结构还是斜率优化了,用slope trick之类的也说不定?

本文作者是蒟蒻,如有错误请各位神犇指点
森林古猿出品,必属精品,请认准CSDN森林古猿1

相关推荐
国科安芯15 分钟前
FreeRTOS RISC-V 浮点上下文切换移植:在 IAR 工程中完整保存 FPU 寄存器
java·开发语言·单片机·嵌入式硬件·算法·系统架构·risc-v
小陈phd38 分钟前
集成检索介绍
算法
小poop40 分钟前
轮转数组:从暴力到最优,一题掌握算法复杂度分析
数据结构·算法·leetcode
笨鸟先飞的橘猫1 小时前
只会脚本语言的游戏后端自我提升之路——c++学习(开篇)
学习·游戏
樱桃读报僵尸1 小时前
说说 LLMRouter,Agent 执行过程中怎么动态的选择 LLM
算法
汉克老师1 小时前
GESP2026年3月认证C++八级( 第三部分编程题(2、子图最短路)精讲
c++·最短路·floyd·gesp8级
绿浪19842 小时前
async/await 深入学习
学习
gis开发之家2 小时前
《Vue3 从入门到大神40篇》Vue3 源码详解(十):diff 算法全解析 —— 为什么 Vue3 比 Vue2 更快?
javascript·算法·typescript·前端框架·vue3·vue3源码
进击的丸子2 小时前
虹软人脸服务器SDK-C++语言Demo实操指南
后端·算法
abcd_zjq2 小时前
海康线扫相机使用
c++·海康