李超树 学习笔记
终于有封面了(?
前情提要
主要是觉得斜率优化的时候推式子好麻烦,然后用 set 维护凸包也好麻烦(最讨厌这种 set 状物),然后就直接学李超树了,直接代替所有单调队列优化 DP。
李超树,就是支持加入直线,并且查询单点(整数点)下所有直线最值的数据结构,一个经典运用就是斜率优化 DP,传统斜率优化方法中(\(b=y-kx\)),当插入点的横坐标以及查询斜率都不单调时可以使用,并且使转移方程更直观(\(y=kx+b\))。
大神讲得好简单啊。。懂了。
李超树
李超树应用了标记永久化的 trick,就是不设懒标记,也不使用 pushup(down) 维护标记。为了保证查询返回信息的正确性,只需要在往下搜的路径上累加前面所有的标记,只要保证这个标记不漏即可,每个点维护的就是这样一个标记,代表一个直线的编号。我们只需要保证能构造一种标记方法,使得每次查询的时候结果正确即可。
接下来讲解插入直线的情况,也就是斜率优化 DP 里最常见的情况。并且维护的是最大值。
考虑以下递归函数 \(modify(l,r,f)\)。\(l\),\(r\) 代表递归区间,\(f\) 代表目前下传的直线编号。我们尝试使用直线 \(f\) 来更新 \(l\),\(r\) 这个区间:
- 首先,如果该区间的没有被任何直线标记过,直接标记上,然后返回(如果你的李超树维护的是直线,只有第一次修改才会这样)。
对于接下来的情况,我假设目前这个区间标记的线段编号为 \(g\)。
- 如果 \(f(l) \ge g(l)\) 且 \(f(r) \ge g(r)\),那么可以直接拿 \(f\) 更新掉这个区间的标记,返回即可。
否则我们钦定 \(g(mid) \ge f(mid)\),如果不满足,直接交换 \(f\),\(g\)。
-
如果 \(f(l) > g(l)\),证明在区间 \([l,mid]\) 中 \(f\) 可能有大于 \(g\) 的部分,但是 \([mid + 1,r]\) 中不可能有,于是递归 \(modify(l,mid,f)\),保留右子树和当前节点的标记不变。
-
如果 \(f(r) > g(r)\),证明在区间 \([mid + 1,r]\) 中 \(f\) 可能有大于 \(g\) 的部分,但是 \([l,mid]\) 中不可能有,于是递归 \(modify(mid + 1,r,f)\),保留左子树和当前节点的标记不变。
注意到上面的两个判定使用的是严格的大于号,于是最多只会递归一边,然后修改就是 \(\mathcal{O}(\log n)\) 的。
单点查询简单,直接一个点往下扫,路上会经过很多标记的线的编号,直接在这些线中找到使得查询位置值最大的那个就行。这样显然是正确的。
顺带说一句,如果是插入线段的话,就像普通线段树先把线段分成 \(\log n\) 个区间,然后分别按照上述过程处理,所以是两只老哥。
例题
P5785【SDOI2012】任务安排
这个是最经典的斜率优化了,把 \(c\) 和 \(t\) 前缀和,可以得到转移方程:
这个显然可以斜率优化,可以传统优化(因为这个查询不单调,所以二分斜率 + 单调栈),然后就是写写写:
//to kill a living book
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3e5 + 7;
int n, s, f[N], t[N], c[N], q[N], tl;
int x(int j){return c[j];}
int y(int j){return f[j];}
int k(int i){return s + t[i];}
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> s;
for(int i = 1; i <= n; i ++){
cin >> t[i] >> c[i];
t[i] += t[i - 1];
c[i] += c[i - 1];
}
q[tl = 1] = 0;
for(int i = 1, tar; i <= n; i ++){
if(tl == 1) tar = q[1];
else{
int l = 0, r = tl;
while(l + 1 < r){
int mid = (l + r) >> 1;
if((x(q[mid + 1]) - x(q[mid])) * k(i) > y(q[mid + 1]) - y(q[mid])) l = mid;
else r = mid;
}
tar = q[r];
}
f[i] = f[tar] + s * (c[n] - c[tar]) + t[i] * (c[i] - c[tar]);
while(tl > 1 && (y(q[tl]) - y(q[tl - 1])) * (x(i) - x(q[tl])) >= (y(i) - y(q[tl])) * (x(q[tl]) - x(q[tl - 1]))) tl --;
q[++ tl] = i;
}
cout << f[n] << "\n";
return 0;
}
当然可以直接无脑李超树(我说李超树其实是 LCT):
//to kill a living book
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 3e5 + 7, dx = N << 8;
int n, s, c[N], t[N], f[N];
struct LCT{
#define mid ((l + r) >> 1)
int v[N << 6], ls[N << 6], rs[N << 6];
int rt = 1, k[N], b[N], cnt = 0, idx = 1;
int cal(int id, int x){
return k[id] * (x - dx) + b[id];
}
void modify(int &x, int l, int r, int k){
if(!x) x = ++ idx;
if(!v[x]) return v[x] = k, void();
if(cal(k, mid) < cal(v[x], mid)) swap(k, v[x]);
if(cal(k, l) < cal(v[x], l)) modify(ls[x], l, mid, k);
if(cal(k, r) < cal(v[x], r)) modify(rs[x], mid + 1, r, k);
}
void add(int x, int y){
++ cnt, k[cnt] = x, b[cnt] = y;
modify(rt, 0, (1 << 9) * N, cnt);
}
int getmn(int x, int l, int r, int k){
if(!x) return 1e18;
if(k <= mid) return min(cal(v[x], k), getmn(ls[x], l, mid, k));
else return min(cal(v[x], k), getmn(rs[x], mid + 1, r, k));
}
} Misaka;
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> s;
for(int i = 1; i <= n; i ++){
cin >> t[i] >> c[i];
c[i] += c[i - 1];
t[i] += t[i - 1];
}
Misaka.add(-c[0], f[0] + (c[n] - c[0]) * s);
for(int i = 1; i <= n; i ++){
f[i] = Misaka.getmn(1, 0, (1 << 9) * N, t[i] + dx);
f[i] += c[i] * t[i];
Misaka.add(-c[i], f[i] + (c[n] - c[i]) * s);
}
cout << f[n] << "\n";
return 0;
}
可以看到,李超树的代码也很短(47 行),只多十行,但是思维量显著降低。易于上手,使用方便,泛用性强,可见李超树确实是一种非常有竞争力的斜率优化处理方式。
P3195 [HNOI2008] 玩具装箱
#include <bits/stdc++.h>
#define ll __int128
using namespace std;
const ll N = 5e4 + 7, M = 3e7 + 7, U = 1e12;
ll n, c[N], s[N], L, f[N];
struct LCT{
int v[M], ls[M], rs[M], idx = 0, rt = 0;
ll k[N], b[N], cnt = 0;
ll cal(int id, ll x){
return k[id] * x + b[id];
}
#define mid ((l + r) >> 1)
void modify(int &x, ll l, ll r, int k){
if(!x) x = ++ idx;
if(!v[x]) return v[x] = k, void();
if(cal(k, mid) < cal(v[x], mid)) swap(k, v[x]);
if(cal(k, l) < cal(v[x], l)) modify(ls[x], l, mid, k);
if(cal(k, r) < cal(v[x], r)) modify(rs[x], mid + 1, r, k);
}
void add(ll K, ll B){
++ cnt;
k[cnt] = K, b[cnt] = B;
modify(rt, 0, U, cnt);
}
ll get(int x, ll l, ll r, ll k){
if(!x) return 1e18;
if(k <= mid) return min(cal(v[x], k), get(ls[x], l, mid, k));
else return min(cal(v[x], k), get(rs[x], mid + 1, r, k));
}
} Misaka;
void read(ll &x){
int p; cin >> p;
x = p;
}
void write(ll x){
if(!x) cout << "0\n";
else{
stack<int> s;
while(x) s.push(x % 10), x /= 10;
while(s.size()) cout << s.top(), s.pop();
cout << "\n";
}
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
read(n), read(L), L ++;
for(int i = 1; i <= n; i ++){
read(c[i]), c[i] += c[i - 1];
s[i] = c[i] + i;
}
//f[i] = -2*s[j] * s[i] + (f[j] + s[j] ^ 2 + 2 * s[j] * L) + (s[i] ^ 2 + L ^ 2 - 2 * s[i] * L)
Misaka.add(0, 0);
for(int i = 1; i <= n; i ++){
f[i] = Misaka.get(Misaka.rt, 0, U, s[i]) + s[i] * s[i] + L * L - 2 * s[i] * L;
Misaka.add(-2 * s[i], f[i] + s[i] * s[i] + 2 * s[i] * L);
}
write(f[n]);
return 0;
}
本文来自博客园,作者:GE9x,转载请注明原文链接:https://www.cnblogs.com/GE9X/p/19757214

浙公网安备 33010602011771号