Slope trick 学习笔记
Slope trick 学习笔记
概述
Slope trick 是一种维护凸函数优化 dp 的方式。通过记录函数的转折点和最右段的一次函数,就可以表示出一个凸函数。
一个转折点 表示在 处斜率变化量为 1(由维护的是上凸壳或下凸壳决定),若在 处斜率差 ,就放置 个 。通常在题目中,斜率和转折点都是整数,因此可以用一个堆 / 平衡树来存储所有转折点。
例如: 的转折点是 0,记录的序列是 。
性质:
两个凸函数 ,设其所有转折点分别为 ,直线为 ,则 也是凸函数,且满足 。
例 1:【CF713C】Sonya and Problem Wihtout a Legend
将 ,转化为单调不降。设 表示 时最小花费,则 。
设 为 前缀最小值,则 。 可归纳证明 为下凸函数,且所有斜率都是整数。
由于 为 前缀最小值,设最小值位置为 。则 的图像和 几乎完全相同,且 后, 为一条平行于 轴的直线。因此可以忽略 的所有转折点。
由 加入 得到,根据凸函数性质,等价于加入 。
当 时,所有斜率都应该 , 变成新的 ,因此直接加一个 。
当 时,原来的 应该被删去,新图像往上平移了 ,因此将答案加上 ,弹出 ,加入两个 。
核心代码:
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模拟】最大值
设 表示 子树中选择了 个点的最大距离和,则先由儿子背包得来,再 。
经过归纳可知, 为下凸函数,证明如下:
两个凸函数的 卷积为凸包的闵可夫斯基和,仍然是凸包, 也是凸函数,因此 是关于 的凸函数。
同时,两个凸函数的 卷积,等于其差分做归并排序,由于 ,因此用 表示 即可。初始 ,可以不加入。
加上函数 后,等价于所有 的 , 的 ,但当 为奇数时, 和 相同,因此需要用三个堆分别维护 ,当 为奇数时的 , 的 ,那么修改就可以用两个懒标记维护了。
核心代码:
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
将所有事件按照 轴排序,设 表示当前 , 的最小花费,转移 ,若存在 ,则 ,否则 。
注意到 为斜率 -1 的一次函数, 为斜率 1 的一次函数。一次操作 等价于将原凸包按照最小值点分成两半,斜率 的向左平移 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
切比雪夫距离 转曼哈顿距离 ,为了避免浮点数变成 ,最后答案再除以 2 即可。
问题变成初始在 的点,每次 , 或 。容易发现收集一个 最优的方式是当 时,代价为 ,当向上或者向下是一定都不如直接到 优。因此 ,。
虽然 不能由 转移,但容易发现能走到的 必然同奇偶,因此若走到 且 不同奇偶,则该点上一定不存在 potato,不会对答案造成影响,因此可以加上 的转移,于是就和 例 4 相同了。
例 5: [APIO2016] 烟火表演
设 表示 子树中,所有叶子节点距离 为 的最小修改量。转移显然:。
容易发现, 是下凸函数。对每个 分别考虑最小值的来源,设 ,函数的左右侧最小值点分别为 :
- 时,随着 从 逐渐减小,以 1 的变化量变小,但 斜率 ,因此 时最优,。
- 时,显然 时最优,。
- 时,显然 最优,.
- 时, 到达 时继续减到 最优,。
考虑转折点的变化情况, 变成了斜率为 1 的直线, 变成了斜率为 0, 变成了斜率为 -1,比较发现,即是删去所有斜率 的点,加入 得到的新凸函数。
因此维护方式也就呼之欲出了,只需要合并两个点的转折点,将所有斜率 的转折点弹出,找到 ,加入 即可。最后的答案可根据 逆推得到。
如何找到斜率 的转折点?显然合并一个儿子最大斜率 ,因此设点 儿子个数为 ,只需要把前 大的点弹出,剩下的就是 了。容易用可并堆维护。
核心代码:
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 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步