斜率优化的变种 - 凸包优化
斜率优化的变种 - 凸包优化
李超线段树
李超线段树是一个用于维护凸包的数据结构。
作为一棵线段树,它可以在 \(O(\log n)\) 的时间内筛选出修改的区间。
同时,它可以在 \(O(\log n)\) 的时间内完成对标记的处理。
斜率优化
斜率优化是数形结合的经典应用,一般的斜率优化是计算斜率,而我在初学斜率优化时,苦于无法理解斜率优化的本质,因而独立想出了一种另类的“斜率优化”,也许称之为“凸包”优化更加贴切。
“凸包”优化
对于一个一般的 \(\text{DP}\) 转移方程:
先提出每一次迭代的转移:
简单变换得:
令 \(y = f_i - A(i), k = D(j), b = f_j + B(j)\),则原式被重构为了 \(y = kx + b\) 的形式。对于固定的 \(i\),\(x\) 为常量,因此原问题被转化为:
已知由 \(n\) 个直线组成的直线集合,给定 \(x = x_0\),求直线集合中所截得的 \(y\) 的最值。
不难想到对直线集合维护其上/下凸包,查询时在凸包上截取即可。
对比传统的斜率优化,凸包优化理解简单,方便结合李超线段树,无需大量计算浮点数,因此常数小且没有舍入误差,同时实现码量与斜率优化差不多,可谓是斜率优化的上位替代。
一些 \(\text{trick}\):
- 如果插入的直线 \(k\),查询 \(x\) 满足单调性,则可以使用单调栈/队列优化,时间复杂度 \(O(n)\)。
- 如果只有插入的直线 \(k\) 满足单调性,则查询时需要二分凸包顶点,时间复杂度 \(O(n \log n)\)。
- 如果 \(k, x\) 均无单调性则需要使用李超树/平衡树维护凸包,使用李超树码量略大但不容易写挂,时间复杂度 \(O(n \log n)\)。
一些坑:
- 如果是维护单调栈/队列,则交点横坐标一定要 + eps。
例题
[JSOI2011] 柠檬
题意:给定序列 \(s_{1 \dots n}\),将序列分为任意段,对每一段选择一个数值 \(s_0\),其出现此时为 \(t\),则获得该段的贡献 \(s_0 t ^ 2\),求最大化的总贡献。
首先,设 \(c_i\) 为 \(s_i\) 在 \(i\) 处出现过的次数。
首先考虑朴素的 \(\text{DP}\),定义 \(f_i\) 为在 \(i\) 处为一段的结束分界点的最值,可以推出转移:
如何记录满足 \(s_i = s_j\) 的所有的 \(j\)?可以使用邻接表来储存 \(s_i\) 出现的每一个位置,对于每一个 \(i\),遍历所有在 \(i\) 之前被插入同一行的 \(j\) 即可。
考虑优化算法。我们对原转移做转化,可得:
从而转化为 \(y = kx + b\) 的形式。
注意到 \(k\) 与 \(x\) 单调性相反,因此对于每一个 \(s_i\),我们都用单调栈维护一个下凸包,总共维护值域个直线集合。
[NOI2007] 货币兑换
题意:TODO
洛谷的题面给出了提示:证明
必然存在一种最优的买卖方案满足:
每次买进操作使用完所有的人民币,每次卖出操作卖出所有的金券。
因此容易推出转移:
第二个转移包含了两个形如 \(D(j) \times C(i)\) 的式子,看似无法直接套用斜率优化,但只需要对式子左右同时除以 \(a_i\),则原式可化为:
从而转化为 \(y = kx + b\) 的形式。
注意到 \(x = \dfrac{b_i}{a_i}\) 可能不是整数,可以将所有的的 \(\dfrac{b_i}{a_i}\) 离散化,然后再套用李超线段树处理。
[NOI2019] 回家路线
题意:TODO
这道题朴素的 \(\text{DP}\) 都使我卡了半天,主要是我卡在了 若小猫最终在时刻 z 到达 n 号站点,则烦躁值将再增加 z。 这条限制。
实际上,只需要优化状态设计,把 \(f_i\) 设计为通过第 \(i\) 条边之后的开销最小值(不包含到达时间产生的开销),则可以推出转移:
统计答案时再处理总到达时间产生的开销:
这样我们就推导出了朴素的 \(\text{DP}\) 转移。
进一步优化 \(\text{DP}\) 可以考虑使用斜率优化。类似 [JSOI2011] 柠檬,我们对每个结点开一个邻接表,维护 \(n\) 个上凸包。
维护上凸包看似没有单调性,不方便直接套用单调队列,实际上可以把一条边拆为始末两个时间点的操作:
- 开始时,查询 \(x = x_0\) 与凸包的交点。
- 结束时,向凸包中插入一条直线。
然后原问题就可以直接对时间排序然后用单调队列维护。
[USACO08MAR] Land Acquisition G
题意:TODO
本题是 P6047 丝之割 的弱化版,\(\text{DP}\) 的推导过程大相径庭,如果对理解 P6047 丝之割 有困难可以先做做这道题目。
P6047 丝之割
题意:有 \(m\) 条弦,一条弦可以抽象为一个二元组 \((u,v)\),你可以进行任意次切割操作,一次切割操作你将选择两个下标 \(i\) 和 \(j\) 满足 \(i,j \in [1,n]\),然后所有满足 \(i < u, v < j\) 的弦 \((u, v)\) 都将被破坏,同时你将付出 \(a_i \times b_j\) 的代价。求破坏所有弦的最小代价和。
练思维的好题,所以丝之歌什么时候出(
Team Cherry,我的 Team Cherry,我只要 Team Cherry 完美的 silksong~
首先有一个重要的性质:对于两根弦 \((u_1, v_1), (u_2, v_2)\),如果满足 \(u_1 < u_2 \wedge v_2 < v_1\),也就是说 \((u_1, v_1)\) 可以切割 \((u_2, v_2)\),则 \((u_2, v_2)\) 无意义,可以忽略。
证明其实是显然的(但是其他题解写的不显然):
不要考虑乱七八糟的几何意义,只考虑代数意义。
对于一条能够切割 \((u_1, v_1)\) 的边 \((u, v)\),由定义可知 \(u < u_1 \wedge v_1 < v\),因此 \(u < u_1 < u_2 \wedge v_2 < v_1 < v\)。
这意味着能切割 \((u_1, v_1)\) 的边 \((u, v)\) 一定可以切割 \((u_2, v_2)\),所以 \((u_2, v_2)\) 可以直接忽略,只需考虑切割 \((u_1, v_1)\) 即可。换而言之,切割具有 传递性。
根据性质,我们可以先对所以弦排序去掉无意义的弦。
定义 \(f_i\) 为切割完前 \(i\) 条边的最小开销,则容易推出转移:
考虑优化。我们可以重定义 \(u_i, v_i\):
然后原转移可以被简化为:
原式是一个一眼的 \(y = kx + b\),同时考虑性质:
- 原始的 \(u_i,v_i\) 关于 \(i\) 单调增。
- \(p_i\) 关于 \(i\) 单调减。
- \(q_i\) 关于 \(i\) 单调增。
因此重定义后,\(k = u_j\) 单调减,\(x = v_i\) 单调增,单调队列维护即可,注意判定斜率相同。
[NOI2014] 购票
给定一棵 \(n\) 个节点的树,我懒得写了。
首先考虑 \(t = 0\) 的部分分,则原问题转化为了一个简单的序列上问题。
定义 \(f_i\) 为走到节点 \(i\) 的最小开销,\(d_i\) 为节点 \(i\) 到 \(1\) 的路径长,则容易推出朴素的转移:
简单转化可得:
则原式被转化为了 \(y = kx + b\) 的形式,注意此题 \(k = d_j\) 具有单调性,但 \(x = p_i\) 没有。
接下来有以下几种做法:
| 做法 | 可通过数据点 | 时间 - 空间复杂度 |
|---|---|---|
| 李超树维护凸包 | \(t = 0\) | / |
| 可持久化李超树 | \(t = 0, 1\) | / |
| 树链剖分 + 李超树 | \(t = 0, 1\) | / |
| 线段树套可持久化李超树 | \(t = 0, 1, 2, 3\) | \(O(n \log ^ 2 n) - O(n \log ^ 2 n)\) |
| 树链剖分 + 线段树套李超树 | \(t = 0, 1, 2, 3\) | \(O(n \log ^ 3 n) - O(n \log n)\) |
| 出栈序 + 线段树套李超树 | \(t = 0, 1, 2, 3\) | \(O(n \log ^ 3 n) - O(n \log n)\) |
| 二分单调队列 | \(t = 0, 1\) | / |
| 二分可持久化单调队列 | \(t = 0, 1, 2, 3\) | \(O(n \log ^ 2 n) - O(n \log n)\) |
虽然我信仰树剖神教,但我选择 出栈序 + 线段树套李超树 或者 二分可持久化单调队列。
(本人曾在 [SDOI2017] 切树游戏 这道题被卡树剖,硬控三天。)
简单补充一下出栈序的性质:
定义 \(u\) 的 出栈序 \(e_u\) 为每个节点结束 \(\text{DFS}\) 的顺序。对于 \(u\) 和 \(u\) 的祖先 \(v\),满足性质:
在新的一轮 \(\text{DFS}\) 第一次搜索到 \(u\) 时,出栈序 \([e_u, e_v]\) 区间所包含的节点有且仅有 \(u\) 到祖先 \(v\) 路径上的节点被访问到,区间内其他的节点仍未被访问到,其他被访问到的节点也不在区间内。
本题细节超级多,建议亲自上手写写。
#include <bits/extc++.h>
#define inline __always_inline
#define lson(u) (son[(u)][0])
#define rson(u) (son[(u)][1])
template <typename T> inline void chkmin(T &x, const T &y) { if (x > y) x = y; }
template <typename T> inline void read(T &x)
{
char ch;
for (ch = getchar(); !isdigit(ch); ch = getchar());
for (x = 0; isdigit(ch); ch = getchar()) x = x * 10 + ch - '0';
}
using pli = std::pair<int64_t, int>;
const int MaxN = 2e5 + 5, MaxP = 1e6 + 5, MaxL = 25, MaxIGT = MaxP * MaxL, MaxOGT = MaxN * 2;
const int64_t inf = 1e18;
int n, t;
int fa[MaxN], p[MaxN];
int64_t d[MaxN], q[MaxN], l[MaxN], f[MaxN];
std::vector<int> graph[MaxN];
struct line_t
{
int64_t k, b;
inline int64_t operator()(int x) { return k * x + b; }
};
struct inner_tree_t
{
int son[MaxIGT][2], top = 1;
line_t line[MaxIGT];
void insert(int u, int l, int r, line_t f)
{
int mid = l + r >> 1;
if (line[u](mid) > f(mid)) std::swap(line[u], f);
if (r - l == 1) return;
if (line[u](l) > f(l))
lson(u) ? insert(lson(u), l, mid, f) : (line[lson(u) = top++] = f, void());
else if (line[u](r - 1) > f(r - 1))
rson(u) ? insert(rson(u), mid, r, f) : (line[rson(u) = top++] = f, void());
}
int64_t min(int u, int l, int r, int x)
{
if (!u) return inf;
int64_t ans = line[u](x);
if (r - l == 1) return ans;
int mid = l + r >> 1;
if (x < mid) chkmin(ans, min(lson(u), l, mid, x));
else chkmin(ans, min(rson(u), mid, r, x));
return ans;
}
};
struct outer_tree_t
{
int son[MaxOGT][2], top = 2, rt[MaxOGT];
inner_tree_t tree;
void build(int u, int l, int r)
{
tree.line[rt[u] = tree.top++] = {0, inf};
if (r - l == 1) return;
build(lson(u) = top++, l, l + r >> 1), build(rson(u) = top++, l + r >> 1, r);
}
void insert(int u, int l, int r, int x, const line_t &f)
{
tree.insert(rt[u], 0, MaxP, f);
if (r - l == 1) return;
int mid = l + r >> 1;
if (x < mid) insert(lson(u), l, mid, x, f);
else insert(rson(u), mid, r, x, f);
}
int64_t min(int u, int l, int r, int pl, int pr, int x)
{
if (pl <= l && r <= pr) return tree.min(rt[u], 0, MaxP, x);
int mid = l + r >> 1; int64_t ans = inf;
if (pl < mid) chkmin(ans, min(lson(u), l, mid, pl, pr, x));
if (mid < pr) chkmin(ans, min(rson(u), mid, r, pl, pr, x));
return ans;
}
} tree;
int end[MaxN], cur = 1;
void init(int u)
{
for (auto &&v : graph[u]) init(v);
end[u] = cur++;
}
pli stack[MaxN], *top = stack;
void dfs(int u)
{
*top++ = {d[u] += d[fa[u]], u};
int v = std::lower_bound(stack, top, (pli){d[u] - l[u], 0})->second;
assert(d[u] - d[v] <= l[u]);
f[u] = tree.min(1, 1, n + 1, end[u], end[v] + 1, p[u]) + d[u] * p[u] + q[u];
line_t l = {-d[u], f[u]};
tree.insert(1, 1, n + 1, end[u], l);
for (auto &&v : graph[u]) dfs(v);
top--;
}
int main()
{
read(n), read(t);
for (int i = 2; i <= n; i++)
read(fa[i]), read(d[i]), read(p[i]), read(q[i]), read(l[i]), graph[fa[i]].push_back(i);
init(1);
tree.build(1, 1, n + 1);
tree.insert(1, 1, n + 1, end[1], {0, 0});
dfs(1);
for (int i = 2; i <= n; i++) printf("%ld\n", f[i]);
return 0;
}
[NOI2016] 国王饮水记
题意:TODO
这题好像只能用正统的斜率优化了,凸包优化无能为力。

浙公网安备 33010602011771号