转自PomeCat

“DP的斜率优化——对不必要的状态量进行抛弃,对不优的状态量进行搁置,使得在常数时间内找到最优解成为可能。斜率优化依靠的是数形结合的思想,通过将每个阶段和状态的答案反映在坐标系上寻找解答的单调性,来在一个单调的答案(下标)队列中O(1)得到最优解。”

https://wenku.baidu.com/view/b97cd22d0066f5335a8121a3.html

“一些试题中繁杂的代数关系身后往往隐藏着丰富的几何背景,而借助背景图形的性质,可以使那些原本复杂的数量关系和抽象的概念,显得直观,从而找到设计算法的捷径。”—— 周源《浅谈数形结合思想在信息学竞赛中的应用》

斜率优化的核心即为数形结合,具体来说,就是以DP方程为基础,通过变形来使得原方程形如一个函数解析式,再通过建立坐标系的方式,将每一个DP方程代表的状态表示在坐标系中,在确定“斜率”单调性的前提下,进行形如单调队列操作的舍解和维护操作。

一个算法总是用于解决实际问题的,所以结合例题来说是最好的:

Picnic Cows(HDU3045)

题目大意: 
给出一个有N (1<= N <=400000)个正数的序列,要求把序列分成若干组(可以打乱顺序),每组的元素个数不能小于T (1 < T <= N)。每一组的代价是每个元素与最小元素的差之和,总代价是每个组的代价之和,求总代价的最小值。

样例输入包含: 
第一行 N 
第二行 N个数,如题意

样例输出包含: 
第一行 最小的总代价

分析: 
首先,审题。可以打乱序列顺序,又知道代价为组内每个元素与最小值差之和,故想到贪心,先将序列排序(用STL sort)。 
先从最简单的DP方程想起: 
容易想到:

f[i] = min( f[j] + (a[j + 1 -> i] - Min k) ) (0 <= j < i)

– –> f[i] = min( f[j] + sum[i] - sum[j] - a[j + 1] * ( i - j ) )

Min k 代表序列 j + 1 -> i 内的最小值,排序后可以简化为a[j + 1]。提取相似项合并成前缀和sum。这个方程的思路就是枚举 j 不断地计算状态值更新答案。但是数据规模达到了 40000 ,这种以O(n ^ 2)为绝对上界方法明显行不通。所以接下来我们要引入“斜率”来优化。

首先要对方程进行变形: 
f[i] = f[j] + sum[i] - sum[j] - a[j + 1] * ( i - j ) 
– –> f[i] = (f[j] - sum[j] + a[j + 1] * j) - i * a[j + 1] + sum[i] 
(此步将只由i决定的量与只由j决定的量分开) 
由于 sum[i] 在当前枚举到 i 的状态下是一个不变量,所以在分析时可以忽略(因为对决策优不优没有影响)(当然写的时候肯定不能忽略)

令 i = k 
a[j + 1] = x 
f[j] - sum[j] + a[j + 1] * j = y 
f[i] = b 
原方程变为 
– –> b = y - k * x 
移项 
– –> y = k * x + b

是不是很眼熟? 没错,这就是直线的解析式。观察这个式子,我们可以发现,当我们吧许许多多的答案放在坐标系上构成点集,且枚举到 i 时,过每一个点的斜率是一样的!! 很抽象? 看图

可以看出,我们要求的f[i]就是截距,明显,延长最右边的直线交于坐标轴可得最小值。难道只要每次提取最靠近 i 的状态就行了嘛?现实没有那么美好。

像这样的情况,过2直线的截距明显比过3直线的截距要小, 意味着更优(在找求解最小值问题时),这种情况下我们之前的猜想便行不通。

那怎么办呢?这时就要用到斜率优化的核心思想——维护凸包。 
何为凸包? 
不懂得同学还是戳这里:http://baike.baidu.com/link?url=OWX7haiZHtuKD2hjbEBVouUGxKXIMvXDnKra0xDhxuz9zGttTg_JoRwmUcbrGD9Xp2BnbCJDF8BblaQffDBvblg0xNqgIOXCVZpQ7ZNBkWG

其实我们要维护的凸包与这个凸包并无关系,只是在图中长得像罢了。 
那为什么要维护凸包呢? 
还要看图: 

这就是一个下凸包,由图可见,最前面的那个点的截距最小,也诠释了维护凸包的真正含义(想一想优先队列,是不是队首最优?)。那还是有人会提出疑问,为什么非要维护这样的凸包呢? 答案就是,f[i]明显是递增的(相比于f[j] 加上一个代价),所以会在图中自然而然地显现出 Y 随着 X增加而增加的情况,呈现出凸包的模样。

可能这个过程比较晦涩难懂,没懂的同学可以多看几遍。

现在我们回到对  的分析

现在假设我们正在枚举 j 来更新答案,有一个数 k, j < k < i 
再来假设 k 比 j 优(之所以要假设正是要推出具体情况方便舍解)

则有

  1. f[k] + sum[i] - sum[k] - a[k + ] * (i - k) <=
  2. f[j] + sum[i] - sum[j] - a[j + ] * (i - j) (k > j)

移项消项得

  1. f[k] - sum[k] + a[k+ ] * k - (f[j] - sum[j] + a[j + ] * j) <= i * (a[k + ] - a[j+ ])

将只与 i 有关的元素留下,剩下的除过去, 得到

  1. f[k] - sum[k] + a[k+ ] * k - (f[j] - sum[j] + a[j + ] * j) / (a[k + ] - a[j + ])<= i

(这里注意判断除数是否为负, 要变号,当然这里排序过后对于 k > j a[k] 肯定大于 a[j])

这个式子什么意思呢?我用人类的语言解释一下。 
设 Xi = a[i],            Yi = f[i] - sum[i] + a[i + 1] * i 
那么原式即为如下形式:

  1. (Yk - Yj) / (Xk - Xj) <= i

意思就是当有k 和 j 满足 j < k 的前提下 满足此不等式 
则证明 j 没有 k 优

而这个式子的左边数学意义是斜率, 而右边是一个递增的变量, 所以递增的 i 会淘汰队列里的元素, 而为了高效的淘汰, 我们会(在这道题里)选用斜率递增的单调队列,也就是上凸包。(再看看前面的图,是不是斜率在递增)

我们还可以从另一个角度理解维护上凸包的理由

仔细观察下面的图:

一开始,1 号点的截距比2号点更优

随着斜率的变化,两个点的截距变得一样了

然后,斜率接着变化,1号点开始没有2号点优了,所以要舍弃

后面的过程,3号点会渐渐超过2号点,并淘汰掉2号点

分析整个过程,最优点依次是 1 -> 2 -> 3,满足单调的要求

但是如果是一个上凸包会怎样呢?

这里只给出最终图(有兴趣的同学可以自己推一推),可以预见的是,在1赶超2前,3先赶超了2就破坏了顺序,因此不行

思路大概是清晰了,现在只剩下代码实现方面的问题了

下面就看看单调队列的操作

先将推出的X, Y用函数表示方便计算: 
X:

  1. dnt X( int i, int j )
  2. {
  3. return a[j + ] - a[i + ];
  4. }
  1.  
  • 1
  • 2
  • 3
  • 4

(dnt 是 typedef 的 long long)

Y:

  1. dnt Y( int i, int j )
  2. {
  3. return f[j] - sum[j] + j * a[j + ] - (f[i] - sum[i] + i * a[i + ]);
  4. }
  1.  
  • 1
  • 2
  • 3
  • 4

处理队首:

  1. for(; h + < t && Y(Q[h + ], Q[h + ]) <= i * X(Q[h + ], Q[h + ]); h++);
  1.  
  • 1

从队尾维护单调性: 
(这里是一个下凸包所以两点之间的斜率要递增,即 斜率(1, 2) < 斜率(2, 3), 前一个斜率比后一个小)

  1. for(; h + < t && Y(Q[t - ], Q[t]) * X(Q[t], cur) >= X(Q[t - ], Q[t]) * Y(Q[t], cur); t--);
  1.  
  • 1

(注意,要把除法写成乘的形式,不然精度可能会出问题)

斜率优化部分已经完结(说起来挺复杂其实代码很短),接下来就放出AC代码:

  1. #include <cstdio>
  2. #include <cstring>
  3. #include <algorithm>
  4. #include <iostream>
  5. using namespace std;
  6.  
  7. typedef long long dnt;
  8.  
  9. int n, T, Q[];
  10. dnt sum[], f[], a[];
  11.  
  12. dnt Y( int i, int j )
  13. {
  14. return f[j] - sum[j] + j * a[j + ] - (f[i] - sum[i] + i * a[i + ]);
  15. }
  16.  
  17. dnt X( int i, int j )
  18. {
  19. return a[j + ] - a[i + ];
  20. }
  21.  
  22. dnt DP( int i, int j )
  23. {
  24. return f[j] + (sum[i] - sum[j]) - (i - j) * a[j + ];
  25. }
  26.  
  27. inline dnt R()
  28. {
  29. static char ch;
  30. register dnt res, T = ;
  31. while( ( ch = getchar() ) < '' || ch > '' )if( ch == '-' )T = -;
  32. res = ch - ;
  33. while( ( ch = getchar() ) <= '' && ch >= '')
  34. res = res * + ch - ;
  35. return res*T;
  36. }
  37.  
  38. int main()
  39. {
  40. sum[] = ;
  41. while(~scanf( "%d%d", &n, &T ))
  42. {
  43. a[] = , f[] = ;
  44. for(int i = ; i <= n; i++)
  45. scanf( "%I64d", &a[i] );
  46. sort(a + , a + n + );
  47. for(int i = ; i <= n; i++)
  48. sum[i] = sum[i - ] + a[i];
  49. int h = , t = ;
  50. Q[++t] = ;
  51. for(int i = ; i <= n; i++)
  52. {
  53. int cur = i - T + ;
  54. for(; h + < t && Y(Q[h + ], Q[h + ]) <= i * X(Q[h + ], Q[h + ]); h++);
  55. f[i] = DP(i, Q[h + ]);
  56. if(cur < T) continue;
  57. for(; h + < t && Y(Q[t - ], Q[t]) * X(Q[t], cur) >= X(Q[t - ], Q[t]) * Y(Q[t], cur); t--);
  58. Q[++t] = cur;
  59. }
  60. printf( "%I64d\n", f[n] );
  61. }
  62. return ;
  63. }

我自己的版本:

  1. #include<cstdio>
  2. #include<cstdlib>
  3. #include<cstring>
  4. #include<iostream>
  5. #include<algorithm>
  6. using namespace std;
  7. const int maxn=;
  8. long long dp[maxn],q[maxn];
  9. long long a[maxn],sum[maxn];
  10. long long getdp(long long i,long long j)
  11. {
  12. return dp[j]+(sum[i]-sum[j])-a[j+]*(i-j);
  13. }
  14. long long getdy(long long j,long long k)//得到 yj-yk k<j
  15. {
  16. return dp[j]-sum[j]+j*a[j+]-(dp[k]-sum[k]+k*a[k+]);
  17. }
  18. long long getdx(long long j,long long k)//得到 xj-xk k<j
  19. {
  20. return a[j+]-a[k+];
  21. }
  22. int main()
  23. {
  24. long long i,j,n,k,head,tail,m;
  25. while(~scanf("%lld%lld",&n,&m)){
  26. head=tail=;
  27. sum[]=q[]=dp[]=q[]=;
  28. for(i=;i<=n;i++) scanf("%lld",&a[i]);
  29. sort(a+,a+n+);
  30. for(i=;i<=n;i++) sum[i]=sum[i-]+a[i];
  31. for(i=;i<=n;i++){
  32. //删去队首斜率小于目前斜率的点
  33. while(head<tail&&(getdy(q[head+],q[head])<=i*getdx(q[head+],q[head]))) head++;
  34. dp[i]=getdp(i,q[head]);
  35. j=i-m+;
  36. if(j<m) continue;
  37. //接下来是对j而不是i进行处理 ,保证了间隔大于m-1的要求
  38. while(head<tail&&(getdy(j,q[tail])*getdx(j,q[tail-])<=getdy(j,q[tail-])*getdx(j,q[tail]))) tail--;
  39. q[++tail]=j;
  40. }
  41. printf("%lld\n",dp[n]);
  42. }
  43. return ;
  44. }

HDU3045 Picnic Cows (斜率DP优化)(数形结合)的更多相关文章

  1. HDU3045 Picnic Cows —— 斜率优化DP

    题目链接:https://vjudge.net/problem/HDU-3045 Picnic Cows Time Limit: 8000/4000 MS (Java/Others)    Memor ...

  2. HDU 3045 - Picnic Cows - [斜率DP]

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3045 It’s summer vocation now. After tedious milking, ...

  3. hdu 3045 Picnic Cows(斜率优化DP)

    题目链接:hdu 3045 Picnic Cows 题意: 有n个奶牛分别有对应的兴趣值,现在对奶牛分组,每组成员不少于t, 在每组中所有的成员兴趣值要减少到一致,问总共最少需要减少的兴趣值是多少. ...

  4. hdu3507 Print Article(斜率DP优化)

    Zero has an old printer that doesn't work well sometimes. As it is antique, he still like to use it ...

  5. [ZJOI2007]仓库建设(斜率dp优化)

    前言 纪念一下我做的第二道斜率优化$dp$题,终于自己能把代码敲出来了,然而有很智障的$bug$,把$i$写成$q[i]$,找了半天QAQ.然后写$dp$公式并优化的能力稍微强了一点(自我感觉良好), ...

  6. HDU3045 Picnic Cows

    题面 HDU vjudge 题解 将权值排序,则分组一定是连续的 设$f[i]$表示前$i$头牛的最小代价,则($a[i]$为$i$的权值): $$ f[i] = f[j - 1] + sum[i] ...

  7. HDU 3045 picnic cows(斜率DP)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3045 题目大意:有n个数,可以把n个数分成若干组,每组不得小于m个数,每组的价值=除了该组最小值以外每 ...

  8. HDU 3669 Cross the Wall(斜率DP+预处理)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3669 题目大意:有n(n<=50000)个矩形,每个矩形都有高和宽,你可以在墙上最多挖k个洞使得 ...

  9. bzoj4518: [Sdoi2016]征途--斜率DP

    题目大意:把一个数列分成m段,计算每段的和sum,求所有的sum的方差,使其最小. 由方差*m可以化简得ans=m*sigma(ki^2)-sum[n]^2 很容易得出f[i][j]=min{f[i- ...

随机推荐

  1. 51nod 1270 数组的最大代价 思路:简单动态规划

    这题是看起来很复杂,但是换个思路就简单了的题目. 首先每个点要么取b[i],要么取1,因为取中间值毫无意义,不能增加最大代价S. 用一个二维数组做动态规划就很简单了. dp[i][0]表示第i个点取1 ...

  2. hdu1540 区间操作,合并,模板题

    During the War of Resistance Against Japan, tunnel warfare was carried out extensively in the vast a ...

  3. spring 内部工作机制(一)

    Spring内部机制的内容较多,所以打算多分几个阶段来写. 本章仅探索Spring容器启动做了哪些事: 前言: 都说Spring容器就像一台构造精妙的机器,此话一点不假,我们通过配置文件向机器传达控制 ...

  4. 入门VMware Workstation下的Debian学习之基本命令(二)

    本章记录如何在Linux终端进行命令操作命令下载路径,模拟终端.dkpg管理软件包.用户组和用户管理.文件属性.文件与目录管理.查看磁盘使用量. (1)命令下载路径: wegt 路径; (2)模拟终端 ...

  5. 使用微软URLRewriter.dll的url实现任意后缀名重写

    <?xml version="1.0"?> <!--先引用URLRewriter.dll,放置于Bin目录--> <configuration> ...

  6. 【学习】滚动延迟加载插件scrollLoading用法

    今天遇到一个很好用的滚动延迟加载的插件,作者是我的偶象大神张鑫旭,其博客为http://www.zhangxinxu.com/. 以前也写过这种效果,用的是lazyload,不过只能实现图片的加载.而 ...

  7. Session详解及集群共享

    Session的介绍 维基百科:会话(session)是一种持久网络协议,在用户(或用户代理)端和服务器端之间创建关联,从而起到交换数据包的作用机制,session在网络协议(例如telnet或FTP ...

  8. web容器启动后自动执行程序的几种方式比较

    1.       背景 1.1.       背景介绍 在web项目中我们有时会遇到这种需求,在web项目启动后需要开启线程去完成一些重要的工作,例如:往数据库中初始化一些数据,开启线程,初始化消息队 ...

  9. win10 uwp 使用 Geometry resources 在 xaml

    经常会遇到在 xaml 使用矢量图,对于 svg 的矢量图,一般都可以拿出来写在 Path 的 Data ,所以可以写为资源,但是写出来的是字符串,如何绑定 Geometry 到字符串资源? 假如在资 ...

  10. 张高兴的 Xamarin.Android 学习笔记:(二)“Hello World”

    完成环境配置后开始第一个简单项目.打开 Visual Studio 新建一个 Xamarin.Android 项目 "HelloAndroid".(GitHub:https://g ...