斜率优化

李超线段树

李超线段树通常维护两个操作:

  • 插入一个一次函数

  • 查询直线 \(x = k\) 处的先前插入的函数最值

流程

插入

考虑因为插入的都是直线,所以函数在区间 \([L, R]\) 具有单调性。

李超线段树维护的叫做最优势线段,也就是线段树上区间 \([L, R]\),维护的是取 \(mid = \lfloor \frac{L + R}{2} \rfloor\),所有插入到这一个节点的线段中,\(k·mid + b\) 取到最值的线段。

然后在此过程后,一定会有一条直线会被刷掉。由于先前所述的函数单调性,我们只需要比较剩下的直线的端点处的函数值是否比当前线段树上区间最优势线段在端点处的函数值便可以继续递归。

值得一提的是,如果插入的是线段,则可以按普通线段树区间修改一样划分成若干个线段树上的区间去插入。

查询

跟普通线段树的大差不差,具体的见代码。

例题

P4097 【模板】李超线段树 / [HEOI2013] Segment

要求插入线段和查询最值。

插入代码:

inline void insert(int &p, int L, int R, int x) {
	if(! x) return ;
	if(! p) p = ++ tot;

	int y = id[p];

	if(x > y) swap(x, y);
	if(F(a[y], mid) > F(a[x], mid)) swap(x, y);

	id[p] = x;

	if(F(a[y], L) > F(a[x], L) || fabs(F(a[y], L) - F(a[x], L)) < eps) insert(lson, y);
	if(F(a[y], R) > F(a[x], R) || fabs(F(a[y], R) - F(a[x], R)) < eps) insert(rson, y);

	return ;
}

inline void add(int l, int r, int x, int &p, int L = 1, int R = INF) {
	if(! p) p = ++ tot;

	if(l <= L && R <= r) {
		insert(p, L, R, x);

		return ;
	}

	if(l <= mid) add(l, r, x, lson);
	if(r > mid) add(l, r, x, rson);

	return ;
}

查询代码:

inline int query(int k, int &p, int L = 1, int R = INF) {
	if(L == R) return id[p];

	int x = id[p], res = 0;

	if(k <= mid) res = query(k, lson);
	else res = query(k, rson);

	if(x > res) swap(x, res);
	if(F(a[res], k) > F(a[x], k)) swap(x, res);

	return x;
}

基于维护凸壳的优化

假设有转移:

\[dp_i = \max \{ dp_j + (w_i - w_j) ^ 2 + M \} \]

假设有两个决策点 \(k, j\),且 \(k < j\)\(j\) 为更优的决策点。
则等价于:

\[dp_k + (w_i - w_k) ^ 2 + M \le dp_j + (w_i - w_j) ^ 2 + M \]

再化简:

\[dp_k + w_i ^ 2 - 2 \times w_i \times w_k + w_k ^ 2 \le dp_j + w_i ^ 2 - 2 \times w_i \times w_j + w_j ^ 2 \]

\[dp_k + w_k ^ 2 - (dp_j + s_j ^ 2) \le 2 \times w_i \times w_k - 2\times w_i \times w_j \]

同乘 \(-1\)

\[(dp_j + w_j ^ 2) - (dp_k + w_k ^ 2) \geq 2 \times w_i \times (w_j - w_k) \]

\[\frac{(dp_j + w_j ^ 2) - (dp_k + w_k ^ 2)}{w_j - w_k} \geq 2 \times w_i \]

当满足上述不等式时,对于状态 \(dp_i\)\(k\) 是一个更劣的决策点。
因为 \(w_i\) 单调不降,所以 \(k\) 一定不会被后面的状态所用。

因此,决策集合一定是一个凸壳。

代码实现

  1. 维护单调队列,其中相邻决策点形成的斜率应该递增。
  2. 对于 \(dp_i\),若 \(head_2\)\(head_1\) 形成的斜率小于等于 \(2 \times w_i\),循环将 \(head_1\) 删除。
  3. \(dp_i = dp_{head_1} + (s_i - s_{head_1}) ^ 2 + M\)
  4. \(i\) 作为决策点要插入队尾,若不构成凸壳,循环删除队尾直至合法。

斜率优化

通常是出现形如 \(dp_i \times dp_j\) 的转移。

例题

HDU3507 Print Article

题意:给定长度为 \(n\) 的序列,可以划分若干子段,每一段的代价为 \(\displaystyle (\sum_{i = l} ^ {r} a_i) ^ 2 + M\)。求划分的最小代价。

  • \(dp_i = \min \{ dp_j + (pre_i - pre_j) ^ 2 + m \}\)

用单调队列实现的。

代码:

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N = 5e5 + 5;
int n, m, l, r, q[N], dp[N], pre[N];

inline int top(int x, int y) {
	return (dp[x] + pre[x] * pre[x]) - (dp[y] + pre[y] * pre[y]);
}

inline int down(int x, int y) {
	return pre[x] - pre[y];
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);
	
	while(cin >> n >> m) {
		for(int i = 1 ; i <= n ; ++ i)
			cin >> pre[i], pre[i] += pre[i - 1];
			
		l = 1, r = 0;
		q[++ r] = 0;
		dp[0] = pre[0] = 0;
		
		for(int i = 1 ; i <= n ; ++ i) {
			while(l + 1 <= r && top(q[l + 1], q[l]) <= 2 * pre[i] * down(q[l + 1], q[l])) ++ l;
			dp[i] = dp[q[l]] + m + (pre[i] - pre[q[l]]) * (pre[i] - pre[q[l]]);
			while(l + 1 <= r && top(i, q[r]) * down(q[r], q[r - 1]) <= top(q[r], q[r - 1]) * down(i, q[r])) -- r;
			q[++ r] = i;
		}
		
		cout << dp[n] << '\n';
		
		for(int i = 1 ; i <= n ; ++ i)
			dp[i] = 0;
	}
	
	return 0;
}

P4655 [CEOI2017] Building Bridges

首先要拆除的柱子一定是连续的,因此我们想到可以用前缀和去维护这一部分。

\(dp_i\) 为已经考虑到第 \(i\) 位,且第 \(i\) 个位置要为桥梁柱的最小代价。

答案:\(dp_n\)

转移:

考虑枚举断点并由断点 dp 值转移过来,则有:

\[dp_i = \min \{ dp_j + (h_i - h_j) ^2 + pre_{i - 1} - pre_j \} \]

注意第 \(i, j\) 号柱子不能被拆除。

这样时间复杂度是 \(O(n ^2)\) 的,考虑优化。

假设当前考虑到的断点 \(j\) 已经是最优的,再拆出平方项,式子化为:

\[dp_i = dp_j + h_i ^ 2 - 2h_i·h_j + h_j ^ 2 + pre_{i - 1} - pre_j \]

将下标为 \(i\) 的项提到一边,下标为 \(j\) 的项和下标为 \(i, j\) 的交叉项提到另一边,就有:

\[dp_i - h_i ^ 2 - pre_{i - 1} = -2h_i·h_j + dp_j + h_j ^ 2 - pre_j \]

不妨将右边的 \(h_i\) 看作自变量,\(-2h_j\) 看作斜率,\(dp_j + h_j ^ 2 - pre_j\) 看作截距,那么李超线段树就可以维护其最小值。

又因为左侧的 \(-h_i ^ 2 - pre_{i - 1}\) 一定是定值,所以当右侧式子最小时,\(dp_i\) 一定最小。

代码:

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N = 1e5 + 5;
int n, x, cnt, h[N], f[N], dp[N], pre[N];
struct Line {
	int k, b;
	Line(int kk = 0, int bb = 0) {
		k = kk, b = bb;
	}
} a[N];

inline int F(Line l, int x) {
	return l.k * x + l.b;
}

namespace Segment_Tree {
	#define mid (L + R) >> 1
	#define son p, L, R
	#define lson ls[p], L, (L + R) >> 1
	#define rson rs[p], ((L + R) >> 1) + 1, R

	int tot, root, ls[N << 2], rs[N << 2], id[N << 2];

	inline void insert(int x, int &p, int L = 0, int R = 1e6) {
		if(! p) p = ++ tot;

		int y = id[p];
		
		if(F(a[y], mid) < F(a[x], mid)) swap(x, y);

		id[p] = x;

		if(F(a[y], L) < F(a[x], L)) insert(y, lson);
		if(F(a[y], R) < F(a[x], R)) insert(y, rson);

		return ;
	}

	inline int query(int x, int &p, int L = 0, int R = 1e6) {
		if(L == R) return F(a[id[p]], x);

		int res = F(a[id[p]], x);

		if(x <= mid) res = min(res, query(x, lson));
		else res = min(res, query(x, rson));

		return res;
	}

	#undef mid
	#undef son
	#undef lson
	#undef rson
}

using namespace Segment_Tree;

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);

	cin >> n;
	for(int i = 1 ; i <= n ; ++ i)
		cin >> h[i];
	for(int i = 1 ; i <= n ; ++ i)
		cin >> x, pre[i] = pre[i - 1] + x;

	a[0] = Line(0, 1e18);

	a[++ cnt] = Line(-2 * h[1], h[1] * h[1] - pre[1]);
	insert(cnt, root);

	for(int i = 2 ; i <= n ; ++ i) {
		// for(int j = 1 ; j <= i - 1 ; ++ j)
		// 	f[i] = min(dp[i], f[j] + (h[i] - h[j]) * (h[i] - h[j]) + pre[i - 1] - pre[j]);

		dp[i] = query(h[i], root) + h[i] * h[i] + pre[i - 1];
		a[++ cnt] = Line(-2 * h[i], dp[i] + h[i] * h[i] - pre[i]);
		insert(cnt, root);
	}

	cout << dp[n];

	return 0;
}
  • UB:更新一个 struct 类型的变量时一定要调用构造函数!

P3195 [HNOI2008] 玩具装箱

  • \(dp_i = \min \{ dp_j + pre[i] - pre[j] + j - i - 1 - L \}\)

于是就好优化了。

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N = 5e4 + 5;
const int V = 1e9;
int n, L, x, cnt, f[N], dp[N], pre[N];
struct Node {
  int k, b;
  Node(int kk = 0, int bb = 1e18) {
    k = kk, b = bb;
  }
} a[N];

inline int F(Node a, int x) {
  return a.k * x + a.b;
}

namespace SGT {
  #define mid ((L + R) >> 1)
  #define son p, L, R
  #define lson ls[p], L, mid
  #define rson rs[p], mid + 1, R

  int tot, root, ls[N << 2], rs[N << 2], id[N << 2];

  inline void insert(int x, int &p, int L = 0, int R = V) {
    if(! p) p = ++ tot;

    if(F(a[x], mid) < F(a[id[p]], mid)) swap(id[p], x);

    if(F(a[x], L) < F(a[id[p]], L)) insert(x, lson);
    if(F(a[x], R) < F(a[id[p]], R)) insert(x, rson);

    return ;
  }

  inline int query(int x, int p, int L = 0, int R = V) {
    if(L == R) return F(a[id[p]], x);

    int res = F(a[id[p]], x);

    if(x <= mid) res = min(res, query(x, lson));
    else res = min(res, query(x, rson));

    return res;
  }

  #undef mid 
  #undef son
  #undef lson  
  #undef rson
}

using namespace SGT;

signed main() {
	// freopen("xcy.in", "r", stdin);
//	freopen("asd.txt", "w", stdout);
	
  ios_base :: sync_with_stdio(NULL);
  cin.tie(nullptr);
  cout.tie(nullptr);

  cin >> n >> L;
  ++ L;
  for(int i = 1 ; i <= n ; ++ i) {
    cin >> x, pre[i] = pre[i - 1] + x;
    
    dp[i] = (pre[i] - L + i) * (pre[i] - L + i);
//    dp[i] = 1e18;
  }

  a[++ cnt] = Node(2ll * (-1ll -pre[1] - L), (-1ll - pre[1] - L) * (-1ll - pre[1] - L) + dp[1]);
//  a[++ cnt] = Node(2 * (-1 - L), (-1 - L) * (-1 - L) + dp[1]);
  insert(cnt, root);

//	memset(f, 0x3f, sizeof f);
	
	// for(int i = 1 ; i <= n ; ++ i)
	// 	f[i] = (pre[i] - L + i) * (pre[i] - L + i);

	// for(int i = 1 ; i <= n ; ++ i) {
	// 	for(int j = 1 ; j < i ; ++ j)
	// 		f[i] = min(f[i], f[j] + (pre[i] - pre[j] + i - j - L) * (pre[i] - pre[j] + i - j - L));
	// }

  for(int i = 2 ; i <= n ; ++ i) {
    dp[i] = min(dp[i], query(i + pre[i], root) + (i + pre[i]) * (i + pre[i]));
    a[++ cnt] = Node(2ll * (-i - pre[i] - L), (-i - pre[i] - L) * (-i - pre[i] - L) + dp[i]);
//    a[++ cnt] = Node(2 * (-i - pre[i - 1] - L), (-i - pre[i - 1] - L) * (-i - pre[i - 1] - L) + dp[i]);
    insert(cnt, root);
  }

	// for(int i = 1 ; i <= n ; ++ i)
	// 	cout << f[i] << ' ';
	// cout << '\n';
//  for(int i = 1 ; i <= n ; ++ i)
//    cout << dp[i] << ' ';
//  cout << '\n';

  cout << dp[n];

  return 0;
}

P3628 [APIO2010] 特别行动队

P2900 [USACO08MAR] Land Acquisition G

  1. \(w_i \le w_j\)\(c_i \le c_j\),那么买 \(j\) 的时候把 \(i\) 带上,则 \(i\) 不花钱。
  2. 如此,可以认为 \(i\) 不做贡献,删除即可。
  3. 预处理无效的土地后,剩下的土地任意两块互不包含。
  4. \(w, c\) 为第一、二关键字排序。
  5. 排序后任意子段 \([l, r]\) 一定是 \(w_l\) 最宽,\(w_r\) 最长,否则 \(r\) 会被左边某个土地包含。
  6. 问题转化为一个序列划分子段使得代价最小,斜率优化即可。
  7. 注意分母部分的正负性。

代码:

#include <bits/stdc++.h>
#define int long long
using namespace std;

const int N = 5e4 + 5;
int n, tot, l = 1, r, q[N], dp[N];
bool vis[N];
struct Node {
    int w, l;
} a[N], b[N];

inline int top(int x, int y) {
    return dp[x] - dp[y];
}

inline int down(int x, int y) {
    return a[x + 1].l - a[y + 1].l;
}

signed main() {
    ios_base :: sync_with_stdio(NULL);
    cin.tie(nullptr);
    cout.tie(nullptr);

    cin >> n;
    for(int i = 1 ; i <= n ; ++ i)
        cin >> a[i].w >> a[i].l;

    sort(a + 1, a + 1 + n, [&](Node a, Node b) {
        if(a.w == b.w) return a.l < b.l;
        
        return a.w < b.w;
    });

    for(int i = 1 ; i <= n ; ++ i) {
        while(tot && a[i].l >= b[tot].l) -- tot;

        b[++ tot] = a[i];
    }
    
    n = tot;
    
    for(int i = 1 ; i <= n ; ++ i)
        a[i] = b[i];

    sort(a + 1, a + 1 + n, [&](Node a, Node b) {
        if(a.w == b.w) return a.l < b.l;

        return a.w < b.w;
    });

    q[++ r] = 0;

    cerr << "array\n";
    for(int i = 1 ; i <= n ; ++ i)
        cerr << a[i].w << ' ' << a[i].l << '\n';

    for(int i = 1 ; i <= n ; ++ i) {
        while(l + 1 <= r && top(q[l + 1], q[l]) <= a[i].w * down(q[l], q[l + 1])) ++ l;

        dp[i] = dp[q[l]] + a[q[l] + 1].l * a[i].w;

        while(l + 1 <= r && top(i, q[r]) * down(q[r - 1], q[r]) <= top(q[r], q[r - 1]) * down(q[r], i)) -- r;

        q[++ r] = i;
    }

    cerr << "dp\n";
    for(int i = 1 ; i <= n ; ++ i)
        cerr << dp[i] << ' ';
    cerr << '\n';

    cout << dp[n];

    return 0;
}
posted @ 2025-01-16 20:55  endswitch  阅读(15)  评论(0)    收藏  举报