zoukankan      html  css  js  c++  java
  • 斜率优化

    前言

    由于某些原因,最近学习了这个优化 ( ext{DP}) 的算法——斜率优化。

    算法简介

    我们在做 ( ext{DP}) 的题目时,常常会遇到这样的一个状态转移方程:

    [dp[i]=min_{0leq j<i}left{dp[j]+f(i)+g(j)+h(i) imes r(j) ight} ]

    对于其中的 (f(i),g(j)) 我们都可以考虑通过单调队列来优化掉。

    但是对于 (h(i) imes r(j)) 这项,它与 (i,j) 同时有关,单调队列便不再适用……

    于是,我们引进一个新的优化方式——斜率优化。

    斜率优化,指的就是利用一次函数的斜率 (k) 相关性质对原来的状态转移方程进行优化。

    而这就需要我们运用数形结合的思想来解题。

    接下来将通过几道题目来加强对斜率优化的认识。

    例题讲解

    下面提供几道较为基础的题目:

    例题1:[HNOI2008]玩具装箱

    (large{ ext{Description:}})

    现在有 (n) 个玩具,第 (i) 个玩具的长度为 (c[i]) ,将 (i)(j) 连续的玩具放入一个容器,其长度为 (x=j-i+sumlimits_{k=i}^{j}c[k]) ,制作一个容器的费用为 (left(x-L ight)^{2}) ,其中 (L) 为常数。

    求将所有玩具都放入容器中的费用最小值,容器的数量不限。

    数据范围: (nleq 5 imes 10^{4},1leq L,c[i]leq 10^{7})

    (large{ ext{Solution:}})

    首先,我们不难想到用 ( ext{DP}) 来求解。

    (dp[i]) 表示将 (1)(i) 的玩具都放入容器中的最小费用,于是可以得到如下的状态转移方程:

    [dp[i]=min_{0leq j<i}left{dp[j]+left(sum_{k=j+1}^{i}c[k]+i-j-1-L ight)^{2} ight} ]

    (c) 的前缀和数组为 (s) ,即 (s[i]=sumlimits_{k=1}^{j}c[k]) ,于是方程就变为如下形态:

    [dp[i]=min_{0leq j<i}left{dp[j]+left(s[i]-s[j]+i-j-1-L ight)^{2} ight} ]

    此时,如果我们暴力去做,时间复杂度为 (Oleft(n^{2} ight)) ,并不够优秀。

    于是我们考虑对其进行斜率优化

    显然,状态 (i) 是由一个状态 (j) 转移而来,而这个 (j) 是当前最优决策,于是我们考虑两个决策 (j_1,j_2left(1leq j_1,j_2<i ight)) ,且决策 (j_1) 优于决策 (j_2) ,那么有如下式子:

    [dp[j_1]+left(s[i]-s[j_1]+i-j_1-1-L ight)^{2}<dp[j_2]+left(s[i]-s[j_2]+i-j_2-1-L ight)^{2} ]

    为了方便起见,我们设 (sum[i]=s[i]+ileft(1leq ileq n ight)) ,此时我们化简上述式子得:

    [2 imes sum[i]>frac{left(dp[j_1]+left(sum[j_1]+L+1 ight)^{2} ight)-left(dp[j_2]+left(sum[j_2]+L+1 ight)^{2} ight)}{sum[j_1]-sum[j_2]} ]

    在这里,我们把 (sum[j]) 看作横坐标 (x_j) ,把 (dp[j]+left(sum[j]+L+1 ight)^2) 看作纵坐标 (y_j) ,那么上述式子的右侧就相当于 (egin{aligned}frac{Delta y}{Delta x}=kend{aligned}) ,也就是一次函数的斜率!

    此时对于只与 (i) 有关的项,我们可以把它当成一次函数的截距 (b) ,那么所求就变成了 (b) 的最小值。

    我们是如何想到把式子化简成上面的样子呢?

    实际上,我们把所有只与 (i) 有关的项合并,当作一次函数的 (b) ;再把所有只与 (j) 有关的项合并,当作一次函数的 (y) ;最后把所有与 (i,j) 都有关的项再合并,当作一次函数的 (kx)

    在计算的过程中,我们发现 (b) 消掉了,剩下的就是上面的式子。

    当我们在求 (dp[i]) 时, (k=2 imes sum[i]) 显然不变。

    也就是说,我们平移一条斜率为定值 (k) 的直线,使得该直线过某点 (left(x_j,y_j ight)) ,那么此时的截距就是一个可行解。

    由于求的是最小值,故我们从下往上平移该直线,第一次得到的可行解就是最优解。

    显然,斜率 (k) 是单调递增的,于是我们用单调队列维护一个 (left(x_j,y_j ight)) 的下凸包。(这点容易证明是正确的)

    这个时候所得的队首就是对于 (i) 而言的最佳决策。

    (large{ ext{Code:}})

    #include<bits/stdc++.h>
    #define Re register
    using namespace std;
    
    typedef long long LL;
    typedef double db;
    
    const int N=50005;
    
    int n,L;
    LL sum[N];
    int h,t; LL q[N];
    LL dp[N];
    
    inline LL rd()
    {
    	char ch=getchar();
    	LL x=0,f=1;
    	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    	return x*f;
    }
    
    inline LL X(int x){return sum[x];}
    inline LL Y(int x){return dp[x]+(sum[x]+L)*(sum[x]+L);}
    inline LL K(int x){return 2*sum[x];}
    inline db slope(int x,int y){return double(Y(y)-Y(x))/(X(y)-X(x));}
    
    int main()
    {
    	scanf("%d%d",&n,&L); L++;
    	for(Re int i=1;i<=n;i++)
    	{
    		sum[i]=sum[i-1]+rd()+1;
    	}
    	for(Re int i=1;i<=n;i++)
    	{
    		while(h<t&&slope(q[h],q[h+1])<=K(i)) h++;
    		dp[i]=dp[q[h]]+(sum[i]-sum[q[h]]-L)*(sum[i]-sum[q[h]]-L);
    		while(h<t&&slope(q[t],q[t-1])>=slope(q[t],i)) t--;
    		q[++t]=i;
    	}
    	printf("%lld",dp[n]);
    	return 0;
    }
    

    例题2:[CEOI2004]锯木厂选址

    (large{ ext{Description:}})

    从山顶上到山底下沿着一条直线种植了 (n) 棵老树。当地的政府决定把他们砍下来,并运送到锯木厂,而且木材只能朝山下运。

    山脚下有一个锯木厂,另外两个锯木厂将新修建在山路上,你必须决定在哪里修建这两个锯木厂,使得运输的费用总和最小。

    给定 (n) 棵树的重量与位置,计算最小运输费用。

    数据范围: (nleq 20000)

    (large{ ext{Solution:}})

    这不难想到是个 ( ext{DP}) 题。

    (dp[i]) 表示第 (2) 个工厂修到第 (i) 棵树的位置时的最小花费。

    于是得出状态转移方程:

    [dp[i]=min_{0leq j<i}left{res−sd[j] imes sw[j]−sd[i] imes left(sw[i]−sw[j] ight) ight} ]

    其中, (res) 表示所有树一开始全部运送的山脚下的花费, (sd) 表示距离后缀和, (sw) 表示重量前缀和。

    然后我们将状态转移方程变形,令 (j_1,j_2) 这两种决策转移到 (i) 的时候, (j_1) 决策更优,那么我们就可以得到:

    [res−sd[j_1] imes sw[j_1]−sd[i] imes left(sw[i]−sw[j_1] ight)<res−sd[j_2] imes sw[j_2]−sd[i] imes left(sw[i]−sw[j_2] ight) ]

    化简后得到:

    [sd[i]<frac{sd[j_1] imes sw[j_1]-sd[j_2] imes sw[j_2]}{sw[j_1]-sw[j_2]} ]

    此时令:

    [egin{aligned}x_j&=sw[j]\y_j&=sd[j] imes sw[j]\k_i&=sd[i]end{aligned} ]

    之后套上斜率优化的模板即可。

    (large{ ext{Code:}})

    #include<bits/stdc++.h>
    #define Re register
    using namespace std;
    
    const int N=20005;
    
    struct Node {
    	int w,d;
    }a[N];
    
    int n,sd[N],sw[N],res;
    
    int h,t,q[N],ans;
    
    inline int rd()
    {
    	char ch=getchar();
    	int x=0,f=1;
    	while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    	while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    	return x*f;
    }
    
    inline int X(int x){return sw[x];}
    inline int Y(int x){return sd[x]*sw[x];}
    inline int K(int x){return sd[x];}
    inline double slope(int x,int y){return 1.0*(Y(y)-Y(x))/((X(y)-X(x)));}
    
    int main()
    {
    	scanf("%d",&n);
    	for(Re int i=1;i<=n;i++)
    	{
    		a[i].w=rd();
    		a[i].d=rd();
    	}
    	for(Re int i=n;i>0;i--)
    	{
    		sd[i]=sd[i+1]+a[i].d;
    	}
    	for(Re int i=1;i<=n;i++)
    	{
    		sw[i]=sw[i-1]+a[i].w;
    	}
    	for(Re int i=1;i<=n;i++)
    	{
    		res+=a[i].w*sd[i];
    	}
    	ans=0x3f3f3f3f;
    	for(Re int i=1;i<=n;i++)
    	{
    		while(h<t&&slope(q[h],q[h+1])>K(i)) h++;
    		ans=min(ans,res-sd[q[h]]*sw[q[h]]-sd[i]*(sw[i]-sw[q[h]]));
    		while(h<t&&slope(q[t],q[t-1])<slope(q[t],i)) t--;
    		q[++t]=i;
    	}
    	printf("%d",ans);
    	return 0;
    }
    

    总结

    1. 斜率单调暴力移指针

    2. 斜率不单调二分找答案

    3. (x) 坐标单调开单调队列

    4. (x) 坐标不单调用平衡树或 ( ext{cdq}) 分治

    练习题

    1. [SDOI2016]征途

    2. [ZJOI2007]仓库建设

    3. [APIO2014]序列分割

    4. [NOI2016]国王饮水记

    5. [CF311B]Cats Transport

  • 相关阅读:
    冲刺博客 五
    冲刺博客 四
    冲刺第一天
    软件工程概论第十周学习进度
    软件工程概论第九周学习进度
    找水王
    软件工程概论第八周学习进度
    软件工程概论第七周学习进度
    四则运算最终版
    二维数组最大值
  • 原文地址:https://www.cnblogs.com/kebingyi/p/14157680.html
Copyright © 2011-2022 走看看