Slope trick 学习笔记

Slope trick 学习笔记

概述

Slope trick 是一种维护凸函数优化 dp 的方式。通过记录函数的转折点和最右段的一次函数,就可以表示出一个凸函数。

一个转折点 x 表示在 x 处斜率变化量为 1(由维护的是上凸壳或下凸壳决定),若在 x 处斜率差 a >1,就放置 ax。通常在题目中,斜率和转折点都是整数,因此可以用一个堆 / 平衡树来存储所有转折点。

例如:f(x)=|x| 的转折点是 0,记录的序列是 {0,0}

性质:

两个凸函数 f(x),g(x),设其所有转折点分别为 S1,S2,直线为 f1,f2,则 h(x)=f(x)+g(x) 也是凸函数,且满足 S3=S1+S2,f3=f1+f2

例 1:【CF713C】Sonya and Problem Wihtout a Legend

aiaii,转化为单调不降。设 dpi,j 表示 ai=j 时最小花费,则 dpi,j=mink=1jdpi1,k+|aij|

gi,jdpI,j 前缀最小值,则 dpI,j=gi1,j+|aij|。 可归纳证明 dpi,gi 为下凸函数,且所有斜率都是整数。

由于 gi,jdpi,j 前缀最小值,设最小值位置为 h。则 gi 的图像和 fi 几乎完全相同,且 xh 后,gi 为一条平行于 x 轴的直线。因此可以忽略 xh 的所有转折点。

figi1 加入 |xai| 得到,根据凸函数性质,等价于加入 {ai,ai}

hai 时,所有斜率都应该 1ai 变成新的 h,因此直接加一个 ai

h>ai 时,原来的 h 应该被删去,新图像往上平移了 hai,因此将答案加上 hai,弹出 h,加入两个 ai

核心代码:

int main() {
    n = read(); 
    for(int i = 1; i <= n; ++i) {
        int x = read() - i; pq.push(x); 
        if(x < pq.top()) ans += pq.top() - x, pq.pop(), pq.push(x); 
    }
    cout << ans; 
}

例 2:【11.04-NOIP模拟】最大值

dpx,i 表示 x 子树中选择了 i 个点的最大距离和,则先由儿子背包得来,再 dpx,idpx,i+2w×min(i,ki)

经过归纳可知,dpx,i 为下凸函数,证明如下:

两个凸函数的 min, + 卷积为凸包的闵可夫斯基和,仍然是凸包,f(x)=2w×min(x,kx) 也是凸函数,因此 dpx 是关于 i 的凸函数。

同时,两个凸函数的 (min,+) 卷积,等于其差分做归并排序,由于 dpx,0=0,因此用 ai 表示 dpx,idpx,i1 即可。初始 a1=0,a2=,可以不加入。

f(x) 加上函数 g=min(x,kx) 后,等价于所有 ik2aiai+2wi>k2aiai2w,但当 k 为奇数时,k2k2+1 相同,因此需要用三个堆分别维护 k2,当 k 为奇数时的 k+12>k2ai,那么修改就可以用两个懒标记维护了。

核心代码:

	struct Heap {
		ll tg1, tg3; 
		std :: priority_queue < ll, vector < ll >, std :: greater < ll > > q1, q2, q3; 
		inline int gsize() {
	        return q1.size() + q2.size() + q3.size(); 
		}
		inline void ins(ll x) {
			q1.push(x - tg1); 
			if(q1.size() > k / 2) {
				ll v = q1.top() + tg1; q1.pop(); 
				q2.push(v); if(q2.size() > (k & 1)) {
					ll v2 = q2.top(); q2.pop(); q3.push(v2 - tg3); 
					if(q3.size() > k / 2) q3.pop(); 
 				}
			}
		}
		inline vector < ll > get() {
			vector < ll > sav; 
			while(q1.size()) sav.push_back(q1.top() + tg1), q1.pop(); 
			while(q2.size()) sav.push_back(q2.top()), q2.pop(); 
			while(q3.size()) sav.push_back(q3.top() + tg3), q3.pop(); 
			return sav; 
		}
	} h[M]; 
	inline void merge(Heap &x, Heap &y) {
		if(x.gsize() < y.gsize()) std :: swap(x, y); 
		vector < ll > cur = y.get(); 
		for(ll &z : cur) x.ins(z);
	}
	inline void dfs(int x, int fa, int w) {
		h[x].ins(0); 
		for(pii &it : adj[x]) {
			int y = it.fi, z = it.se; if(y == fa) continue ; dfs(y, x, z); 
			merge(h[x], h[y]); 
		}
		h[x].tg1 += (ll)2 * w, h[x].tg3 -= (ll)2 * w; 
	}
	inline void mian() {
		n = read(), k = read(); 
		for(int i = 1; i < n; ++i) {
			int u = read(), v = read(), w = read(); 
			adj[u].push_back(pii(v, w)), adj[v].push_back(pii(u, w)); 
		}
		dfs(1, 0, 0); 
		ll ans = 0; vector < ll > cur = h[1].get(); 
		for(ll &z : cur) ans += z; 
		cout << ans; 
	}

例 3:[ABC217H] Snuketoon

将所有事件按照 x 轴排序,设 dpi,j 表示当前 t=ix=j 的最小花费,转移 dpi,jmin(dpi1,j,dpi1,j,dpi1,j+1),若存在 (T,D,X)=(i,0,x),则 dpi,jdpi,j+max(0,Xj),否则 dpi,jdpi,j+max(0,jX)

注意到 f1(x)=max(0,Xx) 为斜率 -1 的一次函数,f2(x)=max(0,xX) 为斜率 1 的一次函数。一次操作 fi,jmin(fi1,j1,fi1,j,fi1,j+1) 等价于将原凸包按照最小值点分成两半,斜率 <0 的向左平移 1 位,>0 向右平移一位,最小值个数 +2,因此维护一下移动的偏移量即可。设两边最小值位置在 l,r

D=0Xr ,相当于在 <0 的点中加入 Xx>r 相当于在 >0 的点加入 X,将右侧 =0 的斜率变成 1,因此将其从 >0 的点加入 <0 的点,并累加答案。

D=1 同理。

核心代码:

	inline void mian() {
		n = read(); int lst = 0; ll d1 = 0, d2 = 0, ans = 0; 
		for(int i = 1; i <= n; ++i) pq1.push(0), pq2.push(0); 
		for(int i = 1; i <= n; ++i) {
			int t = read(), d = read(), x = read(), r = t - lst; lst = t; 
			d1 -= r, d2 += r; 
			if(!d) {
			    if(x <= pq2.top() + d2) pq1.push(x - d1); 
			    else ans += x - pq2.top() - d2, pq1.push(pq2.top() + d2 - d1), pq2.pop(), pq2.push(x - d2); 
			}
			else {
				if(x >= pq1.top() + d1) pq2.push(x - d2); 
				else ans += pq1.top() + d1 - x, pq2.push(pq1.top() + d1 - d2), pq1.pop(), pq1.push(x - d1); 
			}
		}
		cout << ans; 

例 4:【CF1534G】A New Beginning

切比雪夫距离 (x,y) 转曼哈顿距离 (x+y2,xy2),为了避免浮点数变成 (x+y,xy),最后答案再除以 2 即可。

问题变成初始在 (0,0) 的点,每次 x+1yy+1yy1。容易发现收集一个 (x0,y0) 最优的方式是当 x=x0 时,代价为 |yy0|,当向上或者向下是一定都不如直接到 x0 优。因此 fi,jmin(fi1,j1,fi1,j+1)fi,jfi,j+|jy|,(x,y),s.t. i=x

虽然 fi,j 不能由 fi1,j 转移,但容易发现能走到的 (x,y) 必然同奇偶,因此若走到 (x,y)x,y 不同奇偶,则该点上一定不存在 potato,不会对答案造成影响,因此可以加上 fi1,j 的转移,于是就和 例 4 相同了。

例 5: [APIO2016] 烟火表演

dpx,i 表示 x 子树中,所有叶子节点距离 xi 的最小修改量。转移显然:dpx,i=yminjidpy,j+|wy+ji|

容易发现,dpx,i 是下凸函数。对每个 dpx,i 分别考虑最小值的来源,设 gx,i=minjidpy,j+|wy+ji|,函数的左右侧最小值点分别为 [l,r]

  • i<L 时,随着 ji 逐渐减小,以 1 的变化量变小,但 dpy,j 斜率 1,因此 j=i 时最优,gx,i=dpy,i+wy
  • LiL+wy 时,显然 j=L 时最优,gx,i=dpx,L+wy+Li
  • L+wyiR+wy 时,显然 j=iwy 最优,gx,i=dpx,L.
  • i>R+wy 时,j 到达 iwj 时继续减到 R 最优,gx,i=dpx,R+iRwy

考虑转折点的变化情况,i>R+wy 变成了斜率为 1 的直线,[L+wy,R+wy] 变成了斜率为 0,[L,L+wy] 变成了斜率为 -1,比较发现,即是删去所有斜率 0 的点,加入 {L+wy,R+wy} 得到的新凸函数。

因此维护方式也就呼之欲出了,只需要合并两个点的转折点,将所有斜率 0 的转折点弹出,找到 L,R,加入 L+wy,R+wy 即可。最后的答案可根据 f1,0=wi 逆推得到。

如何找到斜率 0 的转折点?显然合并一个儿子最大斜率 +1,因此设点 x 儿子个数为 cnt,只需要把前 cnt1 大的点弹出,剩下的就是 L,R 了。容易用可并堆维护。

核心代码:

	inline void dfs(int x, int fa, ll w) {
		for(pii &e : adj[x]) 
		   dfs(e.fi, x, e.se), rt[x] = merge(rt[x], rt[e.fi]); if(x == 1) return ; 
		int u = adj[x].size(); while(u > 1) pop(rt[x]), --u; 
		ll R = t[rt[x]].val; pop(rt[x]); ll L = t[rt[x]].val; pop(rt[x]); 
		rt[x] = merge(rt[x], new_node(L + w)), rt[x] = merge(rt[x], new_node(R + w)); 
	}
	inline void mian() {
		n = read(), m = read(); ll ans = 0; 
		for(int i = 2; i <= n + m; ++i) {
			int f = read(), w = read(); ans += w; 
			adj[f].push_back(pii(i, w)); 
		}
		dfs(1, 0, 0); int u = adj[1].size(); while(u) pop(rt[1]), --u; 
		while(rt[1]) ans -= t[rt[1]].val, pop(rt[1]); 
		cout << ans; 
	}

本文作者:henrici3106

本文链接:https://www.cnblogs.com/henrici3106/p/17093215.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   henrici3106  阅读(642)  评论(2编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.