再论斜率优化

再论斜率优化

前言

早在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

相关推荐
万法若空1 小时前
【数学-简单数论】同余中的逆
线性代数·算法
水利行业RTU手艺人1 小时前
RTU固件中的“数据保险箱”——离线数据补发系统设计
c++·stm32·单片机·物联网
心中有国也有家1 小时前
鸿蒙Flutter开发环境从零搭建教程(Windows/macOS双平台·避坑版)
学习·flutter·华为·harmonyos
凯瑟琳.奥古斯特2 小时前
力扣1009补码解法C++实现
开发语言·c++·算法·leetcode·职场和发展
KaMeidebaby2 小时前
卡梅德生物技术快报|蛋白的叠氮基修饰:实操解析:核酸模板耦合蛋白的叠氮基修饰实现靶蛋白定点共价标记
前端·人工智能·物联网·算法·百度
十月的皮皮3 小时前
C语言学习笔记20260706-栈的压入、弹出序列验证
c语言·笔记·学习
通信仿真爱好者3 小时前
第【60期】--大规模MIMO系统信号检测算法误码率比较 --matlab完整代码+参考文章
算法·matlab·mimo·信号检测
AI科技星3 小时前
全域谱分析:无穷维超复数信息场分形统一场论——自然、量子、金融多重分形第一性原理完整体系(中英双语终稿)
人工智能·python·算法·金融·乖乖数学·全域数学
心中有国也有家4 小时前
Flutter 鸿蒙适配第一步:从 hive 迁移到 hive\_ce
hive·学习·flutter·华为·harmonyos