Loading

李超树 学习笔记

终于有封面了(?

前情提要

主要是觉得斜率优化的时候推式子好麻烦,然后用 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\) 前缀和,可以得到转移方程:

\[f_i = f_j + (c_i - c_j) t_i + (c_n - c_j) s \]

这个显然可以斜率优化,可以传统优化(因为这个查询不单调,所以二分斜率 + 单调栈),然后就是写写写:

//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;
}
posted @ 2026-03-23 21:53  GE9x  阅读(37)  评论(0)    收藏  举报