李超线段树
直线型
先看一种题型:
- 给出 \(k, b\),加入一条 \(y = k \cdot x + b\) 的直线。
- 给出 \(a\), 查询与 \(x = a\) 相交的直线中纵坐标最大/最小值。
似乎是一种区间修改,单点查询?考虑使用线段树维护。
但对于不同的 \(x\),不同的 \(k, b\) 造成的效果不一定是呈现出一种“压制”的态势,可以是类似于对于 \(x < mid\) 时 \(k_1 \cdot x + b_1 > k_2 \cdot x + b_2\),反之 \(k_1 \cdot x + b_1 < k_2 \cdot x + b_2\)。
这种情况下就需要使用另外一种线段树——李超线段树。
基本构成
在每个节点存储一组 \(k,b\),表示在 \(x=mid\) 的情况下,\(k\cdot x + b\) 可以取到最值。
但注意我这里的 \(k,b\) 不作为懒标记下传,采用的是永久化标记的写法。
可以使用动态开点来优化。
操作二
和传统线段树差别不大。由于是永久化标记的写法,只需要把从根节点到 \([x,x+1)\) 的所有节点上的 \(k,b\) 都去更新一下答案即可。
为了避免查询无效的节点,如果当前节点没法继续走下去(即对应节点的 \(b\) 取到了极值),则直接退出即可。
操作一
假设当前节点为 \(id\),区间对应 \([l,r)\),注意是左闭右开。
- 如果当前节点没有存储任何的 \(k,b\) (可以采取给 \(b\) 初始化为极大/极小值来解决),则直接赋值即可。
- 否则,看对于 \(x=mid\) 时,当前节点和修改的 \(k,b\) 那种情况更优,将更优的存储在节点 \(id\) 上,更劣的作为修改的 \(k,b\),注意不是直接舍弃。这时有三种情况。
- \(k=tr_{id}.k\),那么无论如何,也不可能用手上的 \(k,b\) 来更新答案,直接退出即可。
- \(k>tr_{id}.k\),如果是求最大值,则去试图更新 \(id \times 2 + 1,[mid, r)\),否则去试图更新\(id \times 2,[l,mid)\)。
- \(k<tr_{id}.k\),如果是求最小值,则去试图更新 \(id \times 2 + 1,[mid, r)\),否则去试图更新\(id \times 2,[l,mid)\)。
复杂度
加上上面所说的所有优化后,可以发现每条线段最多影响 \(1\) 个节点,所以空间复杂度是 \(O(n)\)。
但查询仍有可能跑满 \(\log V\) 层,所以时间复杂度是 \(O(n \log V)\) 的。
线段型
如果加入的不是直线而是一条在 \([l,r)\) 的线段呢?
其实和上面差不多,但你需要找到那条线段对应每个的区间,再在这个区间内做一下直线型修改即可。
时间负杂度和空间复杂度都比上面多乘一个 \(\log V\)。
例题
点击查看代码
// https://vjudge.d0j1a1701.cc/problem/Yosupo-segment_add_get_min
#include <bits/stdc++.h>
//#define _1 (__int128)1
//#define int long long
using namespace std;
using ll = long long;
void FileIO (const string s) {
freopen(string(s + ".in").c_str(), "r", stdin);
freopen(string(s + ".out").c_str(), "w", stdout);
}
const int P = 1e9;
const ll INF = 4e18;
struct SegTree {
int ls, rs, k;
ll b = INF;
} ;
int n, q, ndcnt = 1, op, x, l, r;
ll ans, y, b;
vector<SegTree> tr;
int ls (int id) {
if (!tr[id].ls) tr.push_back({}), tr[id].ls = ++ndcnt;
return tr[id].ls;
}
int rs (int id) {
if (!tr[id].rs) tr.push_back({}), tr[id].rs = ++ndcnt;
return tr[id].rs;
}
void modify (int id, int l, int r, int k, ll b, int x, int y) {
if (l > y || r <= x || b == INF) return ;
if (l >= x && r <= y + 1 && tr[id].b == INF) {
tr[id].k = k, tr[id].b = b;
return ;
}
int mid = l + (r - l) / 2;
if (l >= x && r <= y + 1 && 1ll * tr[id].k * mid + tr[id].b > 1ll * k * mid + b)
swap(tr[id].k, k), swap(tr[id].b, b);
if ((tr[id].k == k && tr[id].b <= b) || l + 1 == r)
return ;
if (!(l >= x && r <= y + 1) || tr[id].k < k) modify(ls(id), l, mid, k, b, x, y);
if (!(l >= x && r <= y + 1) || tr[id].k > k) modify(rs(id), mid, r, k, b, x, y);
}
void Query (int x) {
int id = 1, l = -P, r = P + 1;
while (1) {
ans = min(ans, 1ll * tr[id].k * x + tr[id].b);
int mid = l + (r - l) / 2;
if (mid > x && tr[id].ls) id = ls(id), r = mid;
else if (mid <= x && tr[id].rs) id = rs(id), l = mid;
else break;
}
}
signed main () {
ios::sync_with_stdio(0), cin.tie(0);
// FileIO("");
cin >> n >> q, tr.push_back({}), tr.push_back({});
for (int i = 1, k, x, y; i <= n; i++) {
cin >> x >> y >> k >> b;
modify(1, -P, P + 1, k, b, x, y - 1);
}
while (q--) {
cin >> op >> l;
if (op) {
ans = INF;
Query(l);
if (ans == INF) {
cout << "INFINITY\n";
continue;
}
cout << ans << '\n';
} else {
cin >> r >> x >> y;
modify(1, -P, P + 1, x, y, l, r - 1);
}
}
return 0;
}