这个利用了线段树标记永久化的思想 , 支持查询很多条直线 \(y=kx+b\) (线段)在 \(x=k\) 的最值 .
常常可以在一些最优化问题中 优化时间复杂度 , 增强程序效率 .
算法简述
假设我们当前维护最大值 (最小值同理) .
用线段树维护每一个区间的一个 优势线段 (暴露在最上面的线段 , 也就是不会被别的线段在这个区间完全盖住)
对于网上那些说暴露最多的 , 他们程序都似乎不能体现 , 故个人理解是这样 .
可以证明 , 对于 \(x=k\) 在这些直线上的最大值 , 肯定是所有包含这个点的所有区间 优势线段 对应 \(y\) 的最大值 .
暴露在最上面线段 是个抽象化的过程 , 我们可以把这个看成在 \([l, r]\) 区间中 \(\displaystyle f(mid = \frac{l + r}{2})\) 的最大的那条直线 .
我们插入的时候讨论四种情况 qwq
- 这个区间本来没有线段 , 直接放在这里就行了 .
- 新的线段完全被旧的线段盖住 , 这种情况直接退出就行了 .
- 新的线段把旧的线段完全盖住 , 直接修改然后退出就行了 .
- 在这个区间中有交点 , 先改 , 然后把劣势的放入交点的那一侧 .
这个说起来容易 , 但实现起来有点细节 .
我们先比较 \(f(mid)\) 大小 , 如果新的大 , 那我们先交换 , 也就是说 把当前较为优势的先放在此处 , 劣势的拿下来等待安排 .
然后我们看新旧的交点 也就是 \(\displaystyle x= -\frac{b_1-b_2}{k_1-k_2}\) (注意 如果 \(k_1=k_2\) 要判断掉 , 直接退出就行了)
\(x\) 如果不在此段区间内 , 那么意味着优势的在这个区间都优于劣势 , 那么直接退出 , 否则 把当前劣势的下方入标点那侧继续递归处理 .
有时候判断交点不行 , 会错掉 , 其实最好的是判断斜率 .
因为有时候交点刚好在 \(mid\) 处时候 , 你可能会存在瞎走的情况 .
这时候斜率能帮助你判断接下来应该向哪里走 , 也就是它接下来哪里会最优 .
然后查询的话 , 在线段树上一直向下走 , 直到走入端点所处的区间 , 然后一路把存在的优势线段的 \(f(x)\) 取 \(\max\) .
分析一波时间复杂度qwq ...
插入的时候每个线段最多被分成 \(O(\log n)\) 个区间 , 然后继续下放也需要 \(O(\log n)\) 的复杂度 , 插入的时候复杂度就是 \(O(\log^2 n)\) . (如果插入直线的话就是 \(O(\log n)\) 的复杂度)
然后查询的话和普通单点查询的复杂度是一样的 \(O(\log n)\) .
代码实现
const int N = 5e4 + 1e3;
struct Line {
int l, r, id; double k, b;
Line (int xl = 0, int xr = 0, int yl = 0, int yr = 0, int id = 0) {
this -> id = id; l = xl, r = xr;
if (xl != xr) k = 1.0 * (yr - yl) / (xr - xl), b = yl - k * xl;
else k = .0, b = max(yl, yr);
}
double func(int x) { return k * x + b; }
} ;
const double eps = 1e-8;
inline int Sgn(double x) { return (x > eps) - (x < -eps); }
inline bool Cmp(Line a, Line b, int x) {
if (!a.id) return true;
int dir = Sgn(a.func(x) - b.func(x));
return (dir != 0) ? dir < 0 : a.id < b.id;
}
#define lson o << 1, l, mid
#define rson o << 1 | 1, mid + 1, r
struct Chao_Segment_Tree {
Line Adv[N << 2];
void Down(int o, int l, int r, Line up) {
int mid = (l + r) >> 1;
if (Cmp(Adv[o], up, mid)) swap(Adv[o], up);
if (l == r || Sgn(Adv[o].k - up.k) == 0) return ;
double x = (Adv[o].b - up.b) / (up.k - Adv[o].k);
if (x < l || x > r) return ;
if (x <= mid) Down(lson, up); else Down(rson, up);
}
void Insert(int o, int l, int r, int ul, int ur, Line up) {
if (ul <= l && r <= ur) { Down(o, l, r, up); return ; }
int mid = (l + r) >> 1;
if (ul <= mid) Insert(lson, ul, ur, up);
if (ur > mid) Insert(rson, ul, ur, up);
}
Line Query(int o, int l, int r, int qp) {
if (l == r) return Adv[o];
int mid = (l + r) >> 1; Line tmp;
tmp = (qp <= mid ? Query(lson, qp) : Query(rson, qp));
return Cmp(Adv[o], tmp, qp) ? tmp : Adv[o];
}
} T;
例题讲解
-
要求在平面直角坐标系下维护两个操作:
- 在平面上加入一条线段。记第 \(i\) 条被插入的线段的标号为 \(i\) 。
- 给定一个数 \(k\) ,询问与直线 \(x = k\) 相交的线段中,交点最靠上的线段的编号。
这个题就是模板题啦 qwq
但是网上好多都像我这种 , 把线段基本上视作一条直线 , 从顶至底更新的时候 , 没有判断当前更新的线段 是否覆盖了 \(x=k\) 这个范围 就导致答案失真... (ps : 这个一拍就错啦) 但是官方数据好像很神奇 竟然过啦 qwq
我太菜啦 , 懒得改啦 , 注意下这个问题就行了 ....
Codeforces Round #463 F. Escape Through Leaf
又来骗访问啦 qwq 一道好题 2333