斜率优化dp

image

HNOI2008 玩具装箱

这个题很容易写出一维的状态和 \(\mathcal O(n^2)\) 的转移。
\(f[i]\) 表示前 \(i\) 个数里在 \(i\) 这里分一段需要多少费用。
\(f[i] = \displaystyle\max_{j < i}\{f[j] + (sum[i] - sum[j] + i - j - 1 - l)^2\}\)
其中 \(sum\) 表示 \(c\) 的前缀和。

\(How\ to\) 优化?

我们令 \(s[i] = sum[i] + i, t[j] = sum[j] + j + l + 1\)
也就是说 \(f[i] = \displaystyle\max_{j < i}\{f[j] + (s[i] - t[j])^2\}\)
假设我们选取了某一个 \(j\) 进行转移。
那么 \(f[i] = f[j] + s[i]^2 + t[j]^2 - 2s[i]t[j]\)
也就是 \(f[j] + t[j]^2 = 2s[i]t[j] + f[i] - s[i]^2\)
也就是说 \((t[j], f[j] + t[j]^2)\)\(y = 2s[i]x + f[i] - s[i]^2\) 上。
\(f[i] - s[i]^2\) 是这条直线的 \(y\) 轴截距。
我们将 \((t[j], f[j] + t[j]^2)\) 称为一个决策点。
\(s[i]^2\) 是个定值,所以我们如果想让 \(f[i]\) 最小的话就是让 \(y\) 轴截距最小。
那么可能进行转移的决策点们一定构成一个一个下凸壳。
因为 \(t[j]\) 是单调递增的,所以我们可以用单调队列维护这个下凸壳。
而我们对于一个 \(i\) 要找的就是斜率为 \(2s[i]\) 的直线与该下凸壳的切点。
又因为 \(s[i]\) 也是单调递增的,所以我们的决策具有单调性,直接在单调队列里不断删掉队首直到找到满足条件的决策点即可。
复杂度 \(\mathcal O(n)\)

点击查看代码
#include <iostream>

using std::cin;
using std::cout;
const int N = 5e4 + 10;
typedef long long ll;
typedef long double ld;

int c[N];
int q[N];
ld s[N];
ld t[N];
ld f[N];
ld sum[N];

ld X(int x)
{
	return t[x];
}
ld Y(int x)
{
	return f[x] + t[x] * t[x];
}
ld slope(int x, int y)
{
	return (Y(y) - Y(x)) / (X(y) - X(x));
}

int main()
{
	int n;
	int l;
	cin >> n >> l;
	for (int i = 1; i <= n; ++i)
		cin >> c[i];
	for (int i = 1; i <= n; ++i)
		sum[i] = sum[i - 1] + c[i];
	for (int i = 0; i <= n; ++i)
	{
		s[i] = i + sum[i];
		t[i] = i + sum[i] + 1 + l;
	}
	int hd = 1, tl = 0;
	q[++tl] = 0;
	for (int i = 1; i <= n; ++i)
	{
		while (hd < tl && slope(q[hd], q[hd + 1]) <= s[i] * 2)
			++hd;
		f[i] = f[q[hd]] + (s[i] - t[q[hd]]) * (s[i] - t[q[hd]]);
		while (hd < tl && slope(q[tl - 1], q[tl]) >= slope(q[tl - 1], i))
			tl--;
		q[++tl] = i;
	}
	cout << (ll)f[n] << '\n';
	return 0;
}

注意到我这里是用的斜率来维护下凸壳,但是实际上用向量的叉积可以避免损失精度。
这种情况称为 \(k\) 单调, \(x\) 单调 的斜率优化。


对于 \(x\) 单调,\(k\) 不单调,如这道题:

SDOI2012 任务安排

我们依旧用单调队列维护下凸壳,但是我们不删点,而是每一次要转移的时候在下凸壳上二分即可。
时间复杂度 \(\mathcal O(n\log n)\)

点击查看代码
#include <iostream>

using std::cin;
using std::cout;
const int N = 3e5 + 10;
typedef long long ll;
typedef __int128 i1;

int s;
int q[N];
ll f[N];
ll sumt[N];
ll sumc[N];

i1 X(int x)
{
	return sumc[x];
}
i1 Y(int x)
{
	return f[x] - sumc[x] * s;
}
i1 get(int a, int b, int c)
{
	// a -> b, a -> c
	// x2 * y1 - x1 * y2;
	return (X(c) - X(a)) * (Y(b) - Y(a)) - (X(b) - X(a)) * (Y(c) - Y(a));
}
int qie(int l, int r, ll k)
{
	int ans = -1;
	while (l <= r)
	{
		int mid = (l + r) >> 1;
		if ((Y(q[mid + 1]) - Y(q[mid])) >= (X(q[mid + 1]) - X(q[mid])) * k)
		{
			ans = mid;
			r = mid - 1;
		}
		else
			l = mid + 1;
	}
	if (ans == -1)
		return r + 1;
	else
		return ans;
}

int main()
{
	int n;
	cin >> n >> s;
	for (int i = 1; i <= n; ++i)
	{
		int t, c;
		cin >> t >> c;
		sumt[i] = sumt[i - 1] + t;
		sumc[i] = sumc[i - 1] + c;
	}
	int hd = 1, tl = 0;
	q[++tl] = 0;
	for (int i = 1; i <= n; ++i)
	{
		int j = q[qie(hd, tl - 1, sumt[i])];
		f[i] = f[j] + (sumc[i] - sumc[j]) * sumt[i] + (sumc[n] - sumc[j]) * s;
		while (hd < tl && get(q[tl - 1], q[tl], i) >= 0)
			tl--;
		q[++tl] = i;
	}
	cout << f[n] << '\n';
	return 0;
}

注意到我写的并不是向量的叉积,而是其相反数,不过也没有问题。
还有就是,我写的是判断 \(q[tl - 1]\to i\)\(q[tl - 1]\to q[tl]\) 的关系,而不是 \(q[tl - 1]\to q[tl]\)\(q[tl]\to i\) 的关系。
我这么写也是可以的。


再有就是 \(x\) 不单调,如这个题:

CEOI 2017 Building Bridges

这类题我们直接搞个类似cdq分治的东西即可。
时间复杂度 \(\mathcal O(n\log n)\)
具体看代码。

点击查看代码
#include <iostream>
#include <algorithm>

using std::cin;
using std::cout;
const int N = 1e5 + 10;
typedef long long ll;
typedef __int128 i1;
const ll oo = 1e18;
struct Node
{
	ll x, y;
	int id, k;
	friend bool operator<(const Node &a, const Node &b)
	{
		return a.k < b.k;
	}
} p[N], t[N];

int q[N];
int h[N], w[N];
ll f[N];
ll sum[N];

i1 X(int x)
{
	return p[x].x;
}
i1 Y(int x)
{
	return p[x].y;
}
i1 get(int a, int b, int c)
{
	// a -> b, a -> c
	// x2 * y1 - x1 * y2
	return (X(c) - X(a)) * (Y(b) - Y(a)) - (X(b) - X(a)) * (Y(c) - Y(a));
}
void merge(int l, int r)
{
	if (l == r)
	{
		if (l == 1)
			f[l] = 0;
		p[l].x = h[l];
		p[l].y = f[l] - sum[l] + 1ll * h[l] * h[l];
		return;
	}
	int mid = (l + r) >> 1;
	int p1 = l, p2 = mid + 1;
	for (int i = l; i <= r; ++i)
	{
		if (p[i].id <= mid)
			t[p1++] = p[i];
		else
			t[p2++] = p[i];
	}
	for (int i = l; i <= r; ++i)
		p[i] = t[i];
	merge(l, mid);
	int hd = 1, tl = 0;
	for (int i = l; i <= mid; ++i)
	{
		while (hd < tl && get(q[tl - 1], q[tl], i) >= 0)
			tl--;
		q[++tl] = i;
	}
	for (int i = mid + 1; i <= r; ++i)
	{
		while (hd < tl && (Y(q[hd + 1]) - Y(q[hd])) <= p[i].k * (X(q[hd + 1]) - X(q[hd])))
			hd++;
		int nw = p[i].id;
		int j = p[q[hd]].id;
		f[nw] = std::min(f[nw], f[j] + 1ll * (h[nw] - h[j]) * (h[nw] - h[j]) + sum[nw - 1] - sum[j]);
	}
	merge(mid + 1, r);
	std::inplace_merge(p + l, p + mid + 1, p + r + 1, [&](const Node &a, const Node &b){return (a.x ^ b.x ? a.x < b.x : a.y < b.y);});
}

int main()
{
	int n;
	cin >> n;
	for (int i = 1; i <= n; ++i)
		cin >> h[i], p[i].k = (h[i] << 1), p[i].id = i;
	for (int i = 1; i <= n; ++i)
		cin >> w[i], sum[i] = sum[i - 1] + w[i];
	for (int i = 1; i <= n; ++i)
		f[i] = oo;
	std::sort(p + 1, p + n + 1);
	merge(1, n);
	cout << f[n] << '\n';
	return 0;
}

注意到我在 inplace_merge 函数中的lambda函数里以 \(y\) 为排序的第二关键字,这里很重要的。
如果不写的话,就有可能出现一个点正上方的点把它给替换掉的错误。

练习题

注意到已经有大佬整理了斜率优化的题单:https://www.luogu.com.cn/training/5352#problems

posted @ 2026-06-16 13:16  SigmaToT  阅读(3)  评论(0)    收藏  举报