【总结】DP的常用优化(2/5)

1. 单调队列优化

1.1. 引入——单调队列

滑动窗口

很显然我们可以用 ST表或线段树做到 O(nlog2n),但我们可以注意到我们每次求最值的区间长度是不变的,左右端点是单调递增的,我们是否可以利用这种单调性?

对于两点 ij,若 j<i,且此时 aj<ai,那么 j 对之后的点一定不会产生贡献,我们就将它排除。我们对此可以使用一个双端队列进行维护。若当前点 i,队列最后一个点 j,一定有 j<i,若此时 aj<ai,那么 j 一定不会产生贡献,将它从队尾排除,而队首的元素不在当前点 i 的范围内,那么也一定不会对之后点产生贡献,也可以排除。还可以发现,队列维护的元素具有单调性。

1.2. 算法介绍

“当一个选手比你小还比你强,你就打不过他了”。这是对单调队列非常形象的概括。如果一个决策点不可能成为最优决策点,就将它排出。

类似滑动窗口,我们可以利用单调队列优化以下一类的 dp

fimax/minfj+Aj+Bi

1.3. 例题

I. 修剪草坪[USACO 2011 Open Gold]

我们可以定义状态 dpi,0/1,表示前 i 个奶牛,第 i 个奶牛是否被安排的最大价值,此时很容易推出状态转移方程。

dpi,0=max(dpi1,0,dpi1,1)dpi,1=maxdpj,0+sumisumj

我们可以发现对与第二种, j 的选取范围为定长,故考虑单调队列优化。

#include <cstdio>
#include <iostream>
#define int long long
using namespace std;
const int MAXN = 1e5 + 5;
int n, k;
int head, tail;
int a[MAXN], sum[MAXN];
int dp[MAXN][2];
int q[MAXN];
signed main() {
	scanf("%lld %lld", &n, &k);
	for (int i = 1; i <= n; i++)
		scanf("%lld", &a[i]), sum[i] = sum[i - 1] + a[i];
	head = tail = 1;
	for (int i = 1; i <= n; i++) {
		dp[i][0] = max(dp[i - 1][0], dp[i - 1][1]);
		while (head <= tail && q[head] < i - k)
			head++;
		dp[i][1] = dp[q[head]][0] + sum[i] - sum[q[head]];
		while (head <= tail && dp[q[tail]][0] - sum[q[tail]] < dp[i][0] - sum[i])
			tail--;
		q[++tail] = i;
	}
	printf("%lld", max(dp[n][0], dp[n][1]));
	return 0;
}

2. 斜率优化

2.1. 算法介绍

如果一类 dp 可以写为 fi=max/minaibj+cj+di,即只和 ij 有关的项和 一个ij 都有关的项的和,那么一般就可以使用斜率优化。

我们假设 ji 的最优决策点,则有 fi=aibj+cj+di,进行移项,改写形如 y=kx+b 的形式,得到 cj=aibj+fidi

{yi=ciki=aixi=bibi=fidi

2.2. 例题

I. 打印文章[HDU 3507]

我们很容易写出状态转移方程 fi=minfj+(si𝑠j)2+𝑀

对这个式子进行展开,一项得到 fj+sj2=2sisj+fisi2

就可以推出:

{yi=fi+si2ki=2sixi=sibi=fisi2

就可以进行斜率优化了

#include <cstdio>
#include <algorithm>
#include <cstring>
#define int long long
using namespace std;
const int MAXN = 5e5 + 5, INF = 1e18;
int n, m;
int a[MAXN], sum[MAXN];
int dp[MAXN];
int q[MAXN], head, tail;
long double slope(int x, int y) {
    if (sum[x] == sum[y])
        return (dp[x] + sum[x] * sum[x] >= 0) ? 1e9 : -1e9; 
    return 1.0 * (dp[x] + sum[x] * sum[x] - dp[y] - sum[y] * sum[y]) / (sum[x] - sum[y]);
}
signed main() {
	while (~scanf("%lld %lld", &n, &m)) {
		for (int i = 1; i <= n; i++)
			scanf("%lld", &a[i]), sum[i] = sum[i - 1] + a[i], dp[i] = INF;
		head = 1, tail = 1;
		q[1] = 0; 
		for (int i = 1; i <= n; i++) {
			while (head < tail && slope(q[head], q[head + 1]) <= 2 * sum[i])
				head++;
			int p = q[head];
			dp[i] = dp[p] + (sum[i] - sum[p]) * (sum[i] - sum[p]) + m;
			while (head < tail && slope(q[tail - 1], q[tail]) >= slope(q[tail], i))
				tail--;
			q[++tail] = i; 
		}
		printf("%lld\n", dp[n]);
	}
	return 0;
}

II. 玩具装箱

由题意可以得 dpfi=minfj+sumi+isumjjL1(j<i),按照套路进行斜率优化即可。

#include <cstdio>
#include <iostream>
#include <cstring>
#define int long long
using namespace std;
const int MAXN = 5e4 + 5;
int n, L;
int sum[MAXN], dp[MAXN];
int q[MAXN], head, tail;
double slope(int i, int j) {
    return 1.0 * (dp[i] + (sum[i] + i + 1 + L) * (sum[i] + i + 1 + L) - dp[j] - (sum[j] + j + 1 + L) * (sum[j] + j + 1 + L)) / (sum[i] + i - sum[j] - j);
}
signed main() {
	scanf("%lld %lld", &n, &L);
	for (int i = 1; i <= n; i++)
		scanf("%lld", &sum[i]), sum[i] += sum[i - 1];
    head = tail = 1;
    q[1] = 0;
	for (int i = 1; i <= n; i++) {
		while (head < tail && slope(q[head], q[head + 1]) <= 2 * (sum[i] + i))
			head++;
		int p = q[head];
		dp[i] = dp[p] + (sum[i] - sum[p] + i - p - 1 - L) * (sum[i] - sum[p] + i - p - 1 - L);
		while (head < tail && slope(q[tail - 1], q[tail]) >= slope(q[tail], i))
			tail--;
		q[++tail] = i;
	}
	printf("%lld", dp[n]);
	return 0;
}

III. 任务安排

发现一组任务的完成时间依赖于分成的组数,很难受,那么使用 费用提前计算 的技巧,即将启动时间 Sj+1 ~ n 的所有任务的代价计算在内,而不仅仅是对于 j+1 ~ i 的任务。

得到的式子显然可以进行斜率优化,但是 Ti 可能为负数,查询的斜率不再有单调性,所以需要二分斜率。

#include <cstdio>
#define int long long
const int MAXN = 3e5 + 5, INF = 1e9;
int n, s;
int sumt[MAXN], sumc[MAXN];
int dp[MAXN];
int q[MAXN], head, tail;
int getx(int i, int j) {
    return sumc[i] - sumc[j];
}
int gety(int i, int j) {
    return dp[i] - dp[j];
}
int find(int i) {
	if (head == tail)
		return q[head];
	int l = head, r = tail;
	while (l < r) {
		int mid = (l + r) >> 1;
		if (gety(q[mid + 1], q[mid]) <= (s + sumt[i]) * getx(q[mid + 1], q[mid]))
			l = mid + 1;
		else
			r = mid;
	}
	return q[l];
}
signed main() {
	scanf("%lld %lld", &n, &s);
	for (int i = 1; i <= n; i++)
		scanf("%lld %lld", &sumt[i], &sumc[i]), sumt[i] += sumt[i - 1], sumc[i] += sumc[i - 1];
	q[head = tail = 1] = 0;
	for (int i = 1; i <= n; i++) {
		int p = find(i);
		dp[i] = dp[p] + sumt[i] * (sumc[i] - sumc[p]) + s * (sumc[n] - sumc[p]);
		while (head < tail && gety(q[tail - 1], q[tail]) * getx(q[tail], i) >= gety(q[tail], i) * getx(q[tail - 1], q[tail]))
			tail--;
		q[++tail] = i;
	}
	printf("%lld", dp[n]);
	return 0;
}

IV. 土地购买[Usaco2008 Mar]

考虑这样的两个矩形,矩形 A 的长和宽均大于等于矩形 B,那么,矩形 B 因该矩形 A 并购,且总成本就是单独购买矩形 A 的成本,所以矩形 B 对答案没有贡献。因此,我们可以先按矩形长度升序对矩形排序,再删除不必需的矩形。剩下的矩形应当是 长度递增宽度递减 的。

我们可以得到状态转移 fi=minfj+hiwj+1

进行斜率优化即可。

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#define int long long
using namespace std;
const int MAXN = 5e4 + 5, INF = 1e18;
int n;
int dp[MAXN];
int q[MAXN], head, tail;
struct Node {
    int x, y;
    bool operator < (const Node o) {
    	return x == o.x ? y < o.y : x < o.x;
	}
} a[MAXN], b[MAXN];
int top;
signed main() {
    scanf("%lld", &n);
    for (int i = 1; i <= n; i++)
		scanf("%lld %lld", &a[i].x, &a[i].y);
    sort(a + 1, a + n + 1);
    b[0].y = INF;
    for (int i = 1; i <= n; i++) {
    	while (top && b[top].y < a[i].y)
    		top--;
    	b[++top] = a[i];
	}
	head = tail = 1;
    for (int i = 1; i <= top; i++) {
    	while (head < tail && (dp[q[head + 1]] - dp[q[head]]) <= b[i].x * (b[q[head] + 1].y - b[q[head + 1] + 1].y))
    		head++;
    	int p = q[head];
        dp[i] = dp[p] + b[p + 1].y * b[i].x;
        while (head < tail && (dp[q[tail]] - dp[q[tail - 1]]) * (b[q[tail] + 1].y - b[i + 1].y) >= (dp[i] - dp[q[tail]]) * (b[q[tail - 1] + 1].y - b[q[tail] + 1].y))
        	tail--;
        q[++tail] = i;
	}
    printf("%lld", dp[top]);
    return 0;
}

V. 仓库建设

容易得到状态转移方程 fi=minfj+xi×k=j+1ipkk=j+1ipk×xk+ci

利用前缀和 sumpi=j=1ipisumi=j=1ixi×pi

进行斜率优化即可。

#include <cstdio>
#include <iostream>
#include <cstring>
#define int long long 
using namespace std;
const int MAXN = 1e6 + 5, INF = 1e18;
int n;
int d[MAXN], sump[MAXN], c[MAXN], sum[MAXN];
int dp[MAXN];
int q[MAXN], head, tail;
signed main() {
	scanf("%lld", &n);
	for (int i = 1; i <= n; i++)
		scanf("%lld %lld %lld", &d[i], &sump[i], &c[i]), sum[i] = sum[i - 1] + d[i] * sump[i], sump[i] += sump[i - 1];
	for (int i = 1; i <= n; i++) {
		while (head < tail && (dp[q[head + 1]] + sum[q[head + 1]] - dp[q[head]] - sum[q[head]]) <= d[i] * (sump[q[head + 1]] - sump[q[head]]))
			head++;
		int p = q[head]; 
		dp[i] = dp[p] + d[i] * (sump[i] - sump[p]) - (sum[i] - sum[p]) + c[i];
		while (head < tail && (dp[q[tail]] + sum[q[tail]] - dp[q[tail - 1]] - sum[q[tail - 1]]) * (sump[i] - sump[q[tail]]) >= (dp[i] + sum[i] - dp[q[tail]] - sum[q[tail]]) * (sump[q[tail]] - sump[q[tail - 1]]))
			tail--;
		q[++tail] = i;
	}
	int ans = INF;
	for (int i = n; i >= 1; i--) {
        ans = min(ans, dp[i]);
        if (sump[i] - sump[i - 1])
			break;
    }
	printf("%lld", ans);
	return 0; 
}

VII. Cats Transport

fi,j 表示用 j 个人接到前 i 只猫的最小等待时间之和,那么有 fi,j=minfk,j1+(ik)ti(sisk),进行斜率优化即可。

#include <cstdio>
#include <algorithm>
#include <cstring>
#define int long long
using namespace std;
const int MAXN = 2e5 + 5, MAXM = 1e5 + 5, MAXP = 105, INF = 1e18;
int n, m, p;
int d[MAXN];
int h[MAXN], t[MAXN], a[MAXN], s[MAXN];
int dp[MAXP][MAXM];
int q[MAXN], head, tail;
signed main() {
	scanf("%lld %lld %lld", &n, &m, &p);
	for (int i = 2; i <= n; i++)
		scanf("%lld", &d[i]), d[i] += d[i - 1];
	for (int i = 1; i <= m; i++) {
		int h, t;
		scanf("%lld %lld", &h, &t), a[i] = t - d[h];
	}
	sort(a + 1, a + m + 1);
	for (int i = 1; i <= m; i++)
		s[i] = s[i - 1] + a[i];
	memset(dp, 63, sizeof(dp));
	dp[0][0] = 0;
	for (int i = 1; i <= p; i++) {
		head = tail = 1;
		q[1] = 0;
		for (int j = 1; j <= m; j++) {
			while (head < tail && (dp[i - 1][q[head + 1]] + s[q[head + 1]] - dp[i - 1][q[head]] - s[q[head]]) <= a[j] * (q[head + 1] - q[head]))
				head++;
			int p = q[head];
			dp[i][j] = min(dp[i - 1][j], dp[i - 1][p] + a[j] * (j - p) - (s[j] - s[p]));
			while (head < tail && (dp[i - 1][q[tail]] + s[q[tail]] - dp[i - 1][q[tail - 1]] - s[q[tail - 1]]) * (j - q[tail]) >= (dp[i - 1][j] + s[j] - dp[i - 1][q[tail]] - s[q[tail]]) * (q[tail] - q[tail - 1]))
				tail--;
			q[++tail] = j;
		}
	}
	printf("%lld", dp[p][m]);
	return 0;
}

VIII. 柠檬

推出式子 fi=maxfj1+ci(sumisumj+1)2(ci=cj,j<i)

因为我们需要维护一个上凸壳,因为 sum 单调递增,所以最优决策点在凸壳的最后一个点取到,于是维护一个单调栈进行斜率优化。

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#define int unsigned long long
using namespace std;
const int MAXN = 1e5 + 5, INF = 1e18;
int n;
int s[MAXN], c[MAXN];
int dp[MAXN];
int mp[MAXN];
int getx(int i, int j) {
	return s[i] * c[i] - s[j] * c[j];
}
int gety(int i, int j) {
	return (dp[i - 1] + s[i] * c[i] * c[i] - 2 * s[i] * c[i]) - (dp[j - 1] + s[j] * c[j] * c[j] - 2 * s[j] * c[j]);
}
vector<int> v[MAXN];
signed main() {
    scanf("%llu", &n);
    for (int i = 1; i <= n; i++)
		scanf("%llu", &s[i]), c[i] = ++mp[s[i]];
	for (int i = 1; i <= n; i++) {
		while (v[s[i]].size() >= 2 && gety(v[s[i]][v[s[i]].size() - 1], v[s[i]][v[s[i]].size() - 2]) * getx(i, v[s[i]][v[s[i]].size() - 1]) <= gety(i, v[s[i]][v[s[i]].size() - 1]) * getx(v[s[i]][v[s[i]].size() - 1], v[s[i]][v[s[i]].size() - 2]))
			v[s[i]].pop_back();
		v[s[i]].push_back(i);
		while (v[s[i]].size() >= 2 && gety(v[s[i]][v[s[i]].size() - 1], v[s[i]][v[s[i]].size() - 2]) <= 2 * c[i] * getx(v[s[i]][v[s[i]].size() - 1], v[s[i]][v[s[i]].size() - 2]))
			v[s[i]].pop_back();
		int p = v[s[i]][v[s[i]].size() - 1];
		dp[i] = dp[p - 1] + s[i] * (c[i] - c[p] + 1) * (c[i] - c[p] + 1);
	}
    printf("%llu", dp[n]);
    return 0;
}

IX. 锯木厂选址

fi 表示第二座锯木厂建在位置 i,第一座建在 jj<i,得到 fi=minsumdisjsjdisi(sisj),然后就可以斜率优化

#include <cstdio>
#include <algorithm>
#define int long long
using namespace std;
const int MAXN = 2e5 + 5, INF = 1e18;
int n, ans = INF;
int w[MAXN], d[MAXN], s[MAXN], sum;
int q[MAXN], head, tail;
double slope(int i, int j) {
	return 1.0 * (d[i] * s[i] - d[j] * s[j]) / (s[i] - s[j]);
}
signed main() {
    scanf("%lld", &n);
    for (int i = 1; i <= n; i++)
		scanf("%lld %lld", &w[i], &d[i]);
    for (int i = n; i >= 1; i--)
    	d[i] += d[i + 1];
    for (int i = 1; i <= n; i++)
		s[i] = s[i - 1] + w[i], sum += d[i] * w[i];
    for (int i = 1; i <= n; i++) {
        while (head < tail && slope(q[head + 1], q[head]) > d[i])
        	head++;
        int p = q[head];
        ans = min(ans, sum - d[p] * s[p] - d[i] * (s[i] - s[p]));
        while (head < tail && slope(q[tail - 1], q[tail]) < slope(q[tail], i))
        	tail--;
        q[++tail] = i;
    }
    printf("%lld", ans);
    return 0;
}

X. 征途

利用方差的定义以及 m2 对答案的式子进行化简,得到 s2×m2=(=1mvi)2+m×(i=1mvi2),这就是斜率优化的模版。

#include <cstdio>
#include <algorithm>
#include <cstring>
#define int long long
using namespace std;
const int MAXN = 3e3 + 5, INF = 1e9;
int n, m;
int a[MAXN], sum[MAXN];
int dp[MAXN], f[MAXN];
int q[MAXN], head, tail;
double slope(int x, int y) {
	return 1.0 * (f[y] - f[x] + sum[y] * sum[y] - sum[x] * sum[x]) / (sum[y] - sum[x]);
}
signed main() {
    scanf("%lld %lld", &n, &m);
    for (int i = 1; i <= n; i++)
    	scanf("%lld", &a[i]), sum[i] = sum[i - 1] + a[i], f[i] = sum[i] * sum[i];
    for (int i = 2; i <= m; i++) {
    	head = 1, tail = 1;
    	q[1] = 0;
    	for (int j = 1; j <= n; j++) {
    		while (head < tail && slope(q[head], q[head + 1]) < 2 * sum[j])
    			head++;
    		int p = q[head];
    		dp[j] = f[p] + (sum[j] - sum[p]) * (sum[j] - sum[p]);
    		while (head < tail && slope(q[tail], q[tail - 1]) > slope(q[tail], j))
    			tail--;
    		q[++tail] = j;
		}
		for (int j = 1; j <= n; j++)
			f[j] = dp[j];
	}
    printf("%lld", m * dp[n] - sum[n] * sum[n]);
    return 0;
}
posted @   zhou_ziyi  阅读(46)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示