斜率优化

Problem:任务安排

有N个任务排成一个序列在一台机器上等待执行,顺序不得改变。要把N个任务分成若干批,每一批当然是连续的若干个任务。从0时刻开始加工(只有一个机器,单线程),执行第i个任务所需时间为T[i]。在每批开始前,机器需要S的时间来开机。且一批中的任务是等这一批全被处理完才一起结束。易得,若j属于第x批任务(k为这批任务的最后一个),那他要等的时间为S*j + sum(1 ~ k)。每个任务有费用系数C[i],所花费用为等待时间*C[i]。请规划一个分批方案,使得费用最小。

我们为了方便表述,定义sumT,sumC,为T,C的前缀和。

Task 1(tyvj1098)

数据范围:1 ≤ N ≤ 5000,1 ≤ S ≤ 50,1 ≤ T[i],C[i] ≤ 100。

解法一:

        F[i][j]表示把前i个任务分成j批的最小费用,则有转移方程:

                                F[i][j] = min{F[k][j - 1] + (S * j + sumT[i]) * (sumC[i] - sumC[k])} k∈[0, i)   复杂度O(n³)。

        明显无法得到满分。

解法二:

        解法一中我们把用到了一个参数,表示批数,就是为了给乘S提供系数。事实上,我们每次新建一批任务,都可以给后面的累加上一倍的S,省去了这一维,把复杂度优化到O(n²)。

                                F[i] = min{F[j] + sumT[i] * (sumC[i] - sumC[j]) + S * (sumC[N] - sumC[j])}   j∈[0, i)

        这是一种名为“费用提前计算”的经典思想。

long long f[5010], sumt[5010], sumc[5010];
int n, s;
int main() {
	scanf("%d%d", &n, &s);
	for(int i = 1; i <= n; ++i) {
		int t, c;
		scanf("%d%d", &t, &c);
		sumt[i] = sumt[i - 1] + t;
		sumc[i] = sumc[i - 1] + c;
	}
	memset(f, 0x3f, sizeof(f));
	for(int i = 1; i <= n; ++i)
		for(int j = 0; j < i; ++j)
			f[i] = min(f[i], f[j] + sumt[i] * (sumc[i] - sumc[j]) + s * (sumc[n] - sumc[j]));
	printf("%lld", f[n]);
	return 0;
}

Task 2(IOI2002/POJ1180)

数据范围:1 ≤ N ≤ 3e5,1 ≤ S,T[i],C[i] ≤ 512。

由于N范围的剧增,我们要对上面解法二的转移方程进行优化。把带j的量看作自变量。

 F[i] = min{F[j] - (sumT[i] + S) * sumC[j] + sumT[i] * sumC[i] + S * sumC[N]}   j∈[0, i)

然后我们研究min内的式子。由于F[i]肯定由某个j得到,所以不妨去掉min。直接使:

F[i] = F[j] - (sumT[i] + S) * sumC[j] + sumT[i] * sumC[i] + S * sumC[N]。

然后移项:(以F[j]为纵坐标,sumC[j]为横坐标)

F[j] = (sumT[i] + S) * sumC[j] + F[i] - sumT[i] * sumC[i] - S * sumC[N]。

这是一条以sumT[i] + S为斜率,F[i] - sumT[i] * sumC[i] - S * sumC[N]为截距的直线。

所以我们的最优子选项就是一个坐标系下的点集。

除了F[i],斜率与截距都是定值。想使F[i]尽量小,则是让截距尽量小。

既然斜率固定,那么我们用一条斜率固定的直线从下往上平移,第一个碰到的就是最优解(此时截距最小)。

对于任意三个决策点(sumC[j1], F[j1]),(sumC[j2], F[j2]),(sumC[j3], F[j3]),不妨设 j1 < j2 < j3,因为T,C均为正整数(这点很重要),亦有sumC[j1] < sumC[j2] < sumC[j3]。

则,若j1,j2,j3构成上凸形状,j2一定不是最优决策。若构成下凸,则有可能是最优决策。

所以,j2成为最优决策的必要条件是:

(F[j2] - F[j1])/(sumC[j2] - sumC[j1]) < (F[j3] - F[j2])/(sumC[j3] - sumC[j2])   (即,一下凸壳)

通俗地讲,我们应该维护一个“连接相邻两点得到线段斜率单调递增”的下凸壳。

对于一个i,我们记k[i]为sumT[i] + S,则最优解应该取最小的那个j,j满足 k(j, j + 1) > k[i](画图就能得知了,如果k[i]再大一点,那么直接就可以够到靠后的那个点了)。

#include <cstdio>
#define N 300010
typedef long long ll;
int n, q[N]; ll s, sumt[N], sumc[N], f[N];
inline int read() {
	int x = 0; char c = getchar();
	while(c < '0' || c > '9') c = getchar();
	while(c >= '0' && c <= '9') {x = x * 10 + c - 48; c = getchar();}
	return x;
}
int main() {
	n = read(); s = read();
	for(int i = 1; i <= n; ++i) {
		int t = read(), c = read();
		sumt[i] = sumt[i - 1] + t; sumc[i] = sumc[i - 1] + c;
	}
	f[0] = 0; int l = 1, r = 1; q[1] = 0;
	for(int i = 1; i <= n; ++i) {
		while(l < r && (f[q[l + 1]] - f[q[l]]) <= (s + sumt[i]) * (sumc[q[l + 1]] - sumc[q[l]])) ++l;
		f[i] = f[q[l]] - (s + sumt[i]) * sumc[q[l]] + sumt[i] * sumc[i] + s * sumc[n];
		while(l < r && (f[q[r]] - f[q[r - 1]]) * (sumc[i] - sumc[q[r]]) >= (f[i] - f[q[r]]) * (sumc[q[r]] - sumc[q[r - 1]])) --r;
		q[++r] = i;
	}
	printf("%lld", f[n]);
	return 0;
}

Task 3(BZOJ2726)

数据范围:1 ≤ N ≤ 3e5,0 ≤ S,C[i] ≤ 512,-512 ≤ T[i] ≤ 512。

T[i]可以取负数,这意味着sumT不再具有单调性,从而,斜率S+sumT[i]不再具有单调性。我们就不能++l(删队头就是因为斜率的单调性,现在用不上,以后也用不上)了。所以维护整个凸壳(决策斜率该单增还是要单增的)。

那么,队头不一定是最佳决策,所以我们在队列中二分,求出一个位置p,使得k(p - 1, p) ≤ k[i] ≤ k(p, p + 1),p就是最优决策了。

#include <cstdio>
#define N 300010
typedef long long ll;
int n, q[N], l, r; ll s, sumt[N], sumc[N], f[N];
inline int read() {
	int x = 0, f = 1; char c = getchar();
	while(c < '0' || c > '9') {if(c == '-') f = -1; c = getchar();}
	while(c >= '0' && c <= '9') {x = x * 10 + c - 48; c = getchar();}
	return x * f;
}
inline int binary_search(int k) {
	if(l == r) return q[l];
	int L = l, R = r;
	while(L < R) {
		int mid = (L + R)>>1;
		if(f[q[mid + 1]] - f[q[mid]] <= k * (sumc[q[mid + 1]] - sumc[q[mid]])) L = mid + 1;
		else R = mid;
	}
	return q[L];
}
int main() {
	n = read(); s = read();
	for(int i = 1; i <= n; ++i) {
		int t = read(), c = read();
		sumt[i] = sumt[i - 1] + t; sumc[i] = sumc[i - 1] + c;
	}
	f[0] = 0; l = 1, r = 1; q[1] = 0;
	for(int i = 1; i <= n; ++i) {
//		while(l < r && (f[q[l + 1]] - f[q[l]]) <= (s + sumt[i]) * (sumc[q[l + 1]] - sumc[q[l]])) ++l;
		int p = binary_search(s + sumt[i]);
		f[i] = f[p] - (s + sumt[i]) * sumc[p] + sumt[i] * sumc[i] + s * sumc[n];
		while(l < r && (f[q[r]] - f[q[r - 1]]) * (sumc[i] - sumc[q[r]]) >= (f[i] - f[q[r]]) * (sumc[q[r]] - sumc[q[r - 1]])) --r;
		q[++r] = i;
	}
	printf("%lld", f[n]);
	return 0;
}

Task 4

若T为正,C可能为负,如何处理?


Task 5

若T和C均可能为负,如何处理?

明显,利用平衡树动态插点就行,依旧维护整个凸壳,二分查找。


明天补T4解法与T4、T5代码。

猜你喜欢

转载自blog.csdn.net/richard_for_oi/article/details/79671660