[SDOI2012]任务安排(山东省选)斜率dp

一键跳转至题目

题目描述
机器上有 $n$ 个需要处理的任务,它们构成了一个序列。这些任务被标号为 $1$ 到 $n$,因此序列的排列为 $1 , 2 , 3 \cdots n$。这 $n$ 个任务被分成若干批,每批包含相邻的若干任务。从时刻 $0$ 开始,这些任务被分批加工,第 ii 个任务单独完成所需的时间是 $T_i$ 。在每批任务开始前,机器需要启动时间 $s$,而完成这批任务所需的时间是各个任务需要时间的总和。
注意,同一批任务将在同一时刻完成。 每个任务的费用是它的完成时刻乘以一个费用系数 $C_i$。请确定一个分组方案,使得总费用最小。

输入格式
第一行一个整数 $n$。 第二行一个整数 $s$。接下来 $n$ 行,每行有一对整数,分别为 $T_i$ 和 $C_i$,表示第 ii 个任务单独完成所需的时间是 $T_i$ 及其费用系数 $C_i$ 。

输出格式
一行,一个整数,表示最小的总费用。

输入输出样例
输入 #1

5
1
1 3
3 2
4 3
2 3
1 4

输出 #1

153

说明/提示
对于 $100%$ 数据,$1 \le n \le 3 \times 10^5$ ,$1 \le s \le 2^8$ ,$\left| T_i \right| \le 2^8$ ,$0 \le C_i \le 2^8$ 。


思路(注:结尾有完整AC代码,一定要看到最后!!!)

显然,$n$的最大值是$3 \times 10^5$,暴力必然会炸
那么何为“斜率优化”呢?
答曰:用线性规划优化dp式。
顾名思义,斜率dp就是将题目给出的信息转换到坐标系中,判断斜率求解思路就这么讲完了 ,乍一看这个思路很突兀,那么我们来详细的进行分析。
首先我们根据提议可以得出这么个dp式子:

dp[i] = min(dp[i], dp[j] + tm[i] * (fm[i] - fm[j]) + (fm[n] - fm[j]) * s);

其中$dp_i$用来记录到$i$点为止的最优解,$tm_i$记录的是到$i$的$t$数组前缀和,$f_i$同理,$j$点只是中间的一个分割点,$tm_i\times(fm_i-fm_j)$是求这段区间的总耗费时间,最后一个式子至关重要,我们居然已经会在$i$这个为止分割一次,那么我们可以直接累加后面节点的$s$(机器启动耗费的时间)即为$(fm_n-fm_j)\times s$,这个思路是个人也能听懂
目前为止我们即可以得到:

for (int i = 1; i <= n; ++i) {
	for (int j = 0; j < i; ++j) {
		dp[i] = min(dp[i], dp[j] + tm[i] * (fm[i] - fm[j]) + (fm[n] - fm[j]) * s);
	}
}

这样写出来的代码即可以在洛谷得到20分,其他点全T掉。


显然,学过dp的人都能看出来,这个代码只是个普通的线性dp,那么接下来我们再利用这段代码推出斜率dp的代码。
通过上面的动态转移方程我们可以推出:

dp[i]=dp[j]-(tm[i]+s)fm[j]+tm[i]fm[i]+fm[n]*s;

拆开合并同类项而已,小学生也能推出来
学过斜率dp的大佬都能发现,这个式子符合函数的基本形式:$y=kx+b$,我们将(tm[i]+s)看作k,将其余项看作b,既可以得出这个函数的斜率为(tm[i]+s)
这样我们就可以推出:对于每一个$i$我们都可以将dp[i]看作这个节点纵坐标,将fm[i]看作这个点的很坐标,于是我们可以将每个点放到坐标系中(非样例草图,有点难看凑活看吧):
[SDOI2012]任务安排(山东省选)斜率dp
那么我们再将函数图像带入坐标系中
[SDOI2012]任务安排(山东省选)斜率dp

对于这条 dp[i]=dp[j]-(tm[i]+s)fm[j]+tm[i]fm[i]+fm[n]*s 的函数图像,要想球的dp最优解就要看这个函数图像先碰到哪个点,但是要将每个图像中的每个点进行比较那就太耗时间了,于是这里我们就会用到斜率的知识

[SDOI2012]任务安排(山东省选)斜率dp

红色的线连接的点为需要进行判断的点,上面那个蓝色的则是不需要判断的点。
[SDOI2012]任务安排(山东省选)斜率dp

显然tan∠ACD > tan∠BCD(未学过tan点击次链接
所以判断两点之间的斜率我们就只用判断对角边/斜边的值就可以了($CD=x_c-x_c$,$BD=y_b-y_d$),这里我们可以用队列来维护这些点(这段操作代码中很详细),所以针对每个$i$,队列中每个tan的值就必须<=(tm[i]+s)(函数的斜率,之前讲过),然而在插入一个元素时就要从队尾倒着找到第一个tan>=tan(当前点)的值然而<=tan(当前点)的值就直接踢出队列,因为已经没有比较的必要了,于是到现在我们就可以得到斜率优化后的代码了:

for (int i = 1; i <= n; ++i) {
		int j = 0;
		while (l < r && dp[qu[l + 1]] - dp[qu[l]] <= (tm[i] + s) * (fm[qu[l + 1]] - fm[qu[l]]))
			l++;
		j = qu[l];
		//dp[i] = min(dp[i], dp[j] - (s + tm[i]) * fm[j] + tm[i] * fm[i] + s * fm[n]);
		dp[i] = dp[qu[l]] - (s + tm[i]) * fm[qu[l]] + tm[i] * fm[i] + s * fm[n];
		//while(l<r&&)
		while (l < r && (dp[qu[r]] - dp[qu[r - 1]])* (fm[i] - fm[qu[r]]) >= (dp[i] - dp[qu[r]]) * (fm[qu[r]] - fm[qu[r - 1]]))
			r--;
		qu[++r] = i;
	}

注:代码中的判断用*是为了防止浮点运算
然而用了这个方法的oier会发现只能过$60%$的点,其余点全WA,然而在我们之前的推到中忽略了和纵坐标(纵坐标具体数值前面已提过)为负数的情况
[SDOI2012]任务安排(山东省选)斜率dp
如图中的点E那么就会导致找最近点时,比较tan出锅了,于是,在第一个while循环中,我们改用二分的思路,整体思路和之前一样,直接上代码:

int bs(int ll, int rr, int ss) {//求j
	int mid;
	int res=qu[r];
	while (ll <= rr) {
		//l++;
		mid = (ll + rr) / 2;
		if (dp[qu[mid + 1]] - dp[qu[mid]] >= ss * (fm[qu[mid + 1]] - fm[qu[mid]])) {
			res = qu[mid];
			rr = mid - 1;
		}
		else
			ll = mid + 1;
	}
	return res;
}

看到这里的oier们大概已经理解了,若还不理解可以结合完整代码进行理解:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<cstdio>
#pragma warning(disable:4996)
using namespace std;
#define int long long
int n, s, tm[1000010], fm[1000010], ans = 0x3f3f3f3f, dp[1000010], qu[1000010], l, r=0;
struct node
{
	int t, f;
}edge[1000010];
int bs(int ll, int rr, int ss) {
	int mid;
	int res=qu[r];
	while (ll <= rr) {
		//l++;
		mid = (ll + rr) / 2;
		if (dp[qu[mid + 1]] - dp[qu[mid]] >= ss * (fm[qu[mid + 1]] - fm[qu[mid]])) {
			res = qu[mid];
			rr = mid - 1;
		}
		else
			ll = mid + 1;
	}
	return res;
}
signed main() {
	scanf("%lld%lld", &n, &s);
	for (int i = 1; i <= n; ++i) {
		scanf("%lld%lld", &edge[i].t, &edge[i].f);
		tm[i] = tm[i - 1] + edge[i].t;
		fm[i] = fm[i - 1] + edge[i].f;
	}
	memset(dp, 0x3f3f3f3f, sizeof(dp));
	dp[0] = 0;
	/*for (int i = 1; i <= n; ++i) {
		for (int j = 0; j < i; ++j) {
			dp[i] = min(dp[i], dp[j] + tm[i] * (fm[i] - fm[j]) + (fm[n] - fm[j]) * s);
		}
	}*/
	//dp[i]=dp[j]-(tm[i]+s)*fm[j]+tm[i]*fm[i]+fm[n]*s;
	qu[l] = 0;
	qu[++r] = 0;
	for (int i = 1; i <= n; ++i) {
		int j = bs(l, r, (tm[i] + s));
		dp[i] = dp[j] + tm[i] * (fm[i] - fm[j]) + s * (fm[n] - fm[j]);
		while (l < r && (dp[qu[r]] - dp[qu[r - 1]])* (fm[i] - fm[qu[r]]) >= (dp[i] - dp[qu[r]]) * (fm[qu[r]] - fm[qu[r - 1]]))
			r--;
		qu[++r] = i;
	}
	cout << dp[n];
	return 0;
}

各位读到最后的oier们点个赞吧qwq
[SDOI2012]任务安排(山东省选)斜率dp

上一篇:R 多元相关与回归分析


下一篇:杰理之FM模块扩展【篇】