题外话:(发现原定的周一更新咕了)
此文是用于传授"动态规划"的本质,公司面试题目可能会略低于本篇文章所讲,会提到各类优化算法到底"应该去优化什么"。阅读时间约10分钟。(如滚动数组、各种数据结构/CDQ分治优化等)
问题开始之前,先谈谈一些定义:
有向图
,
点的集合为
,
边的集合为
。
边的表示形式为三元组的形式
:代表
到
有一条边,边权为
。
注意无向图其实是有向图的一种特殊形式,只要建立边
,有向图就是无向图了。
路径:多条边连成的一条长链,
无环:对于任意点对
如果存在一条路径使得
能到达
,那么不允许存在其它路径使得
到
。
某个点的入度:进入这个点的边的个数。
简单路径:路径上的顶点都不相同的路径。
有向无环图:有向图+无环条件,简称为
。
好了,让我们进入正题。
给定一个有向无环图
,一个起点
,一个终点
。
现在要求
到
的最短路,
的入度为0。
由于最短路满足性质:如果
在
到
的最短路上,那么
到
的这条路线也是最短路。
这里不要想着任何最短路算法。
让我们直接用简单的代码,描述这个"求
到
的最短路"的算法:
用
代表点
到
的最短距离,这个距离是动态更新的。
void dfs(Point u)
{
for all edge(u, v, w) in G:
if dist[v] > dist[u] + w
dist[v] := dist[u] + w
dfs(v)
}
可是这样复杂度肯定太高了,因为存在这种情况:
- 点 目前的值是最优解,点 的"当前最优解"更新了一些点的dist.
- 之后,点 被其它点(在 之前更新的点)的解所更新,导致 需要继续更新"之前它更新过的那些点的dist"。
- 这造成了冗余,那能不能存在一种更新顺序,使得 用 去更新其它点的情况下,就已经是最优解呢?(也就是说,在dfs(v)执行时,保证 dist[v]是从点S出发的最短路的长度)
既然说了它是一个有向无环图,不如我们用拓扑排序的做法:每次取出入度为0的点
,取出从它出发的每条边
, 更新最短路的同时,删掉这些边(这会使得点
的入度减小 ),不断操作直到找不出可以删除的边为止。
void
显而易见地,按照这种顺序,在每次加入一个点
到队列后,可以更新
的那些点对于点
的影响已经施加完毕了,
保证了入队时,
dist[v]是从点S出发的最短路的长度。
参考一下这张图片:
默认边权为1
考虑一下进入队列的顺序,那么可以是{S,1,2,3,4,T}或者{S,2,1,3,4,T}
任意一种更新最短路的顺序,都能保证当前进入队列的点的dist值为最短路的长度。
比如现在已经做完了{S,1,2},删除掉了从{S,1,2}出发的每条边,图是这样的:
此时,点3的入度为0,所以点3入队。
由于已经没有能进入到点3的边了,也就意味着"能更新点3的最短路的点已经不存在了"。
此时dist[3]的值,就是最终的值,它已经走到了最优解。这和原始dfs代码中能得到的dist[3]的值是完全相同的。
那么继续用3去更新4的最短路。
结论:如果按照一定顺序(拓扑序)对图上每个点进行更新,当这个点去用来更新其它点的最短路时,该点的最短路已经被算出,且为最优解。
也就是说,按照拓扑序进行计算最短路,就可以保证每一步都是最优解,每一步都是正确的。
废话了这么半天,来看题吧。
给定一个序列
,希望你找出一段连续的
,使得这段序列的和最小,求出最小的和。
啥?刚刚不是还在说图嘛?怎么跳到数字上来了?
别急。让我们对着序列傻傻地建立一张有
个点的图:我们把刚刚的有向图的边权设置为
,第
个点的点权设置为
。不妨新建一个虚拟点
,其点权也是
。
令
代表从点
出发的最小距离。
建立以下的边
代表连续选择
和
,从
点引出
条边分别到点
,边权为0。
void Build()
{
dist[0] = 0
i = 0
while(i <= n)
if i < n then addedge(i, i + 1, 0);
addedge(0, i, 0);
}
求这张图中,0到所有点
的最短路就对应了选择出来的序列的最小和。
这个图显然是一个有向无环图,也就可以通过上面的拓扑排序+求最短路做。
什么?你讲了这么半天,到底啥是动态规划啊?
"如果一个题可以用图论思想,建图之后用最短/最长路算法去解决,并且这个图是一个有向无环图,那么这个问题就是动态规划。" (Dynamic Programming,简称dp)
但这类题目真的需要用图论方法做嘛?答案是不需要。
只要你设计出合理的"状态",再找出一个合适的更新的"顺序"(使得轮到这个点更新其它的点时,保证这个点是最优解)以及可以更新状态的递推式,就能够跳出图论方法去解这道题。
这道题的主流解法是这样的,令
为以
结尾的序列的最小的和,那么
可以通过这样两种方式得到:
1.通过和
及之前的一些数字组成一个子序列。
2.以自己一个元素结尾。
(其实f[i]就对应之前的dist[i]。)
那么不难写出递推式:
。
状态就是
代表以
结尾的序列的最小的和
"顺序"就是从1到n,并不是从n到1,也不是其它奇奇怪怪的顺序,这样的顺序递推能保证f[i]是最优解。 递推式呢,就是
了。
讲到这里,其实已经很明确了:
思路如下:
首先设计状态,也就是"xx代表xx",随便举一些例子:"f[i]代表前i个数字的最优解" "g[i][j]代表第i天还剩j元钱的最优解" "h[i][j][k]代表投掷了i次筛子,j次是1,k次小于等于5的概率"
之后不需要设计更新状态的顺序
然后设计递推式:
如果设计好了递推式,比如长成这个样子:
。
显然从n到1的顺序更新就能直接得到答案。
如果设计出来的递推式有一个奇怪的形式:
.
似乎"没有一个可以入手的点"。脑袋里建出图来看看:这个图形成了一个环!
那这时候一般有两种情况:1. 这个题根本就不是一个动态规划的题 2. 状态设计的不好,导致递推式形成了环。 解决方法很暴力:推翻一开始设计的状态,重新来过。
事实上动态规划的难点就是在于1. 设计出来一个正确的状态 2. 设计一个正确的递推式
(其实练多了就会发现永远有一大票做不出来的动态规划题)
上面这些就是有向无环图与动态规划的联系了,接下来随便做几道题冷静一下:
当然动态规划题目的特征一般就是:
- 最优解问题 2. 计数问题
那些网上说的几类"树形dp,区间dp,背包dp,……"都是不同的dp题目,但其实只要设计好状态就可以做,所以你会的dp题目和你做过的dp题目类型(不是数目)有多少并没有任何联系,只和数目有关。
并不是说你做了一道树形dp的题,之后所有树形dp的题就都会了,恰恰相反,如果你只做了一道树形dp的题,那么下次考树形dp一定是不会的。
好了鸡汤就说这么多,让我们看一些题目:
这里有背包问题的讲解blog.csdn.net
题目1:旅行商简化版
有n个二维平面的点,一个人从西边的某个点出发,到达最东边的某个点,然后返回,但是返回的时候不能经过原来经过的点,所有点(除了起点)都需要走恰好一遍。求最短的距离。 (
)
解法:
设
是已经目前到了
的最短距离,这时候忽然发现解决不了两个问题:
- 解决不了"检验每个点是否走过"
- 检验不了回去怎么不经过同一个点。
这是完全错误的状态设计,一眼就可以排除掉的垃圾状态。
好了,考虑正确的状态:
因为可以把路程拆分成"向东"和"向西"两部分,设
代表从西的部分当前所在的点为
,向东的部分当前所在的点为
时,目前走过的最短距离之和。
设计递推式:
设k = max(i,j) + 1,则有:
f[i][k] = min(f[i][k],f[i][j] + dis[j,k]);
f[k][j] = min(f[k][j],f[i][j] + dis[i,k]);
这当然也是递推式……因为这样做能算出最终答案,也能保证答案是最优的。
题目2:vijos 1243 (摘自我自己的博客)
注意这个题目是可以用单调队列优化的,不要忘记。
题目3: BZOJ 3036
Description
随着新版百度空间的下线,Blog宠物绿豆蛙完成了它的使命,去寻找它新的归宿。
给出一个有向无环的连通图,起点为1终点为N,每条边都有一个长度。绿豆蛙从起点出发,走向终点。
到达每一个顶点时,如果有K条离开该点的道路,绿豆蛙可以选择任意一条道路离开该点,并且走向每条路的概率为 1/K 。
现在绿豆蛙想知道,从起点走到终点的所经过的路径总长度期望是多少?
Input
第一行: 两个整数 N, M,代表图中有N个点、M条边
第二行到第 1+M 行: 每行3个整数 a b c,代表从a到b有一条长度为c的有向边
Output
从起点到终点路径总长度的期望值,四舍五入保留两位小数。
--------
题解很简单,考虑状态
代表点
走到终点的期望距离,那么转移方程(递推式)也相对好写一些:
至于顺序的选取,采用记忆化搜索/拓扑排序都可以。
其实还有其他的一些dp题目还在整理……但是就不放上来了,接下来讲讲dp的优化部分。
1.斜率优化,也是似乎大家最不会的一个优化。
其实很简单啦~如果这里听不懂以后还会开专栏的。
记得BJWC2017出现过一个奇妙的题目:
其中
是一个单调增加的数组。
(遇到这类题目,凡是遇到一个j一个i一个平方,往斜率优化的方向猜我就没输过)
展开。
由于
只和
相关,由于枚举了
之后,
和
也只是一个常量。
所以我们可以简单地化成这样的形式:
求
。注意一个事情,因为
是只取决于
的,所以可以在算
之前就已经有了
的信息。把
看作一个二维平面上的点,每次计算
的值就转化成了:给定一个固定的斜率
,求
这个斜率在之前每个点上的截距的最大值。
这就是斜率优化。
接下来维护之前的点的凸包,以及在凸包上二分即可。
这里可以考虑用CDQ分治。(这些名词是啥以后会讲到……)
2.各种数据结构优化:
一般很容易就能看出来的。 比如这个:
由于是一个二维偏序的形式,而且由于按照动态规划的顺序是能保证
的,所以其实就只需要保证一个维度即可。
用各种数据结构都能迅速解决偏序问题,最典型的就是平衡树、线段树、树状数组balabala……
总之暂时是告一段落了,题目并不是很多,但本质地说明了有向无环图和动态规划之间的联系,也稍微总结了各种动态规划里面的优化……
但是看懂这篇文章对于学会做题应该没什么帮助,还是要多做题,独立去设计状态及状态的递推式。