• 斜率优化 DP :Luogu P2365 P5785「SDOI2012」任务安排 & 弱化版


    题面

    动态规划

    为了方便表示,题面中的费用系数改用 (v_i)

    确定 (f_i) 表示到第 (i) 个位置的最优解

    每次转移就是钱 (i) 个加上一段 ([j+1,i]) 的花费

    很显然,可以用前缀和优化

    但因为有 (s) 的存在,使得直接加上多开一维需记录分组数,却会增加巨大的开销,方程给出,感受一下 (n^3) 算法:

    [f_{i,k} = min_{1leq jleq i-1}f_{j,k-1}+sum_{l=j}^i v_l imes (sum_{l=1}^i t_l+s imes k) ]

    这个方程出现了大量无用状态,因为最终只需要求出一个位置上的最优解

    那我们反过来想,每一次的分组,都会使得时间拖延 (s) ,那么事先加上就行了

    即在分为 (k) 组时,前面已经加上了 (k)(s) 所带来的影响值

    也就是 (s imes sum_{l=j+1}^n v_l)

    给出最终的时间复杂度为 (mathcal{O}(n^2)) 转移方程:

    [f_i = min_{1leq jleq i-1} f_j + sum_{l=j}^i v_l imes sum_{l=1}^i t_l + s imes sum_{l=j+1}^n v_l ]

    将所有的求和用前缀和维护

    然后就能愉快的敲代码了

    代码给出:

    #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) 去掉得

    [f_i = f_j + (sumv_i-sumv_j) imes sumt_i + s imes (sumv_n-sumv_j) ]

    展开,移项得

    [underline{f_j-s imes sumv_j}_{ y} = underline{(sumt_i+s)}_{ k} imes underline{sumv_j}_{ x} + underline{f_i-sumv_i imes sumt_i-s imes sumv_n}_{ b} ]

    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;
    }
    
  • 相关阅读:
    Monkeyrunner环境搭建
    学习Monkeyrunner过程
    uiautomatorviewer使用报错
    安装JMeter
    如何测试网页的访问速度
    安装Android studio
    软件测试工程师具备技能
    WinRAR去除广告弹框(精华在末尾)
    android studio adb连接不上手机
    DOM的理解
  • 原文地址:https://www.cnblogs.com/Ax-Dea/p/12532852.html
Copyright © 2020-2023  润新知