动态规划
为了方便表示,题面中的费用系数改用 (v_i)
确定 (f_i) 表示到第 (i) 个位置的最优解
每次转移就是钱 (i) 个加上一段 ([j+1,i]) 的花费
很显然,可以用前缀和优化
但因为有 (s) 的存在,使得直接加上多开一维需记录分组数,却会增加巨大的开销,方程给出,感受一下 (n^3) 算法:
这个方程出现了大量无用状态,因为最终只需要求出一个位置上的最优解
那我们反过来想,每一次的分组,都会使得时间拖延 (s) ,那么事先加上就行了
即在分为 (k) 组时,前面已经加上了 (k) 次 (s) 所带来的影响值
也就是 (s imes sum_{l=j+1}^n v_l)
给出最终的时间复杂度为 (mathcal{O}(n^2)) 转移方程:
将所有的求和用前缀和维护
然后就能愉快的敲代码了
代码给出:
#include<bits/stdc++.h>
using namespace std;
const int maxn = (int)5e3+7;
int n,s,f[maxn],t[maxn],v[maxn];
int main() {
scanf("%d%d",&n,&s);
for(int i=1;i<=n;++i) {
scanf("%d",t+i);t[i] += t[i-1];
scanf("%d",v+i);v[i] += v[i-1];
}
memset(f,0x3f,sizeof f);
f[0] = 0;
for(int i=1;i<=n;++i)
for(int j=0;j<i;++j) {
f[i] = min(f[i],f[j]+t[i]*(v[i]-v[j])+s*(v[n]-v[j]));
}
printf("%d",f[n]);
return 0;
}
Upd 2020.10.30
以上是 P2365 的 (mathcal{O}(n^2)) 做法,接下来介绍该题 (mathcal{O}(n)) 的斜率优化 DP
注: 下文中 (sumt_i gets sum_{j=1}^i t_j) , (sumv_i gets sum_{j=1}^i v_j)
把原方程中的 (min) 去掉得
展开,移项得
tips: 斜率优化拆项时, (x,y) 必须与 (j) 相关, (k) 必须与 (i) 相关
问题转化为斜率 (k=sumt_i+s) 时,取一个点使得直线在 (y) 轴上的截距最大,其中点就为 ((sumv_j,f_j-s imes sumv_j))
我们用线性规划的思想,将斜率为 (k=sumt_i+s) 的直线从下往上移动,碰到的第一个点即能得到最小截距
此题中, (k) 单调上升, (x) 单调上升,坐标系上的点长这样
上图中,用线性规划的取点方式,红点是无论如何都取不到的,我们要维护蓝点所形成的几何图形,称之为凸包
我们用一个单调队列来维护凸包
因为 (x) 是单增的,所以每次加入新点都在后方,我们考虑下图的情况
其中红点是新加入的点, tail 表示队尾, tail-1 表示队列的队尾的前一个,此时加入新点后凸包会被破坏,于是我们就弹出队尾的这个点
为了每次都能 (mathcal{O}(1)) 取到最优点,我们用队头来维护
如上图所示,当队头和队头后一个点组成直线的斜率小于 (k=sumt_i+s) 时,又因为 (k) 单增,此时队头将不会被取到,弹出即可
tips: 1. (frac{y_1-y_2}{x_1-x_2} leq frac{y_3-y_4}{x_3-x_4}) 可转化为 ((y_1-y_2) imes(x_3-x_4)leq(y_3-y_4) imes(x_1-x_2)) 避免精度问题
2. 不同题目中的斜率优化时的斜率可能是单调下降的,应另画图分析
代码实现如下
#include<bits/stdc++.h>
#define forn(i,s,t) for(int i=(int)s;i<=(int)t;++i)
using namespace std;
const int R = (int)3e5+7;
int N,Q[R];
long long s,T[R],V[R],f[R];
int main() {
scanf("%d%lld",&N,&s);
forn(i,1,N) scanf("%lld%lld",&T[i],&V[i]),
T[i] += T[i-1],V[i] += V[i-1]; // 前缀和
int h=0,t=0;
forn(i,1,N) {
while(h<t&&f[Q[h+1]]-f[Q[h]]<=(T[i]+s)*(V[Q[h+1]]-V[Q[h]])) // 弹出队头
++h;
f[i] = f[Q[h]] + T[i]*(V[i]-V[Q[h]]) + s*(V[N]-V[Q[h]]); // 转移
while(h<t&&(f[i]-f[Q[t]])*(V[Q[t]]-V[Q[t-1]])<=(f[Q[t]]-f[Q[t-1]])*(V[i]-V[Q[t]])) // 弹出队尾
--t;
Q[++t] = i;
}
printf("%lld
",f[N]);
return 0;
}
Upd 2020.10.31
以上是朴素的斜率优化的实现,我们来看下一题【SDOI2012】的加强版
乍一看好像没什么区别,但注意,该题的 (t_i) 可以为负数,也就是说上文中的 (k=sumt_i+s) 不再单调递增了,但 (x) 仍单调递增
也就是说我们无法和上题一样直接维护队头为答案了,但队尾还能一样维护凸包,我们观察下下凸包(向下凸的凸包)的性质
仔细观察上图,可以发现 (k_f<k_g<k_h) ,即相邻两个点的斜率是单调递增的
有了这个性质,我们就可以用二分查找的方式找出第一条斜率 (kgeq sumt_i+s) 的直线的后一个点,并进行转移
时间复杂度 (mathcal{O}(nlog n))
具体实现见代码
#include<bits/stdc++.h>
#define forn(i,s,t) for(int i=(int)s;i<=(int)t;++i)
using namespace std;
const int R = (int)3e5+7;
int N,Q[R];
long long s,T[R],V[R],f[R];
int main() {
scanf("%d%lld",&N,&s);
forn(i,1,N) scanf("%lld%lld",&T[i],&V[i]), // 前缀和
T[i] += T[i-1],V[i] += V[i-1];
int h=0,t=0,l,r,mid,res;
forn(i,1,N) {
l = h,r = t,res = Q[r];
while(l<=r) { // 二分找点
mid = l+r >> 1;
if((f[Q[mid+1]]-f[Q[mid]])>(T[i]+s)*(V[Q[mid+1]]-V[Q[mid]])) r = mid-1,res = Q[mid];
else l = mid+1;
}
f[i] = f[res] + T[i]*(V[i]-V[res]) + s*(V[N]-V[res]); // 转移
while(h<t&&(f[i]-f[Q[t]])*(V[Q[t]]-V[Q[t-1]])<=(f[Q[t]]-f[Q[t-1]])*(V[i]-V[Q[t]]))
--t; // 弹出队尾维护凸包
Q[++t] = i; // 加入新点
}
printf("%lld
",f[N]);
return 0;
}