做为考察范围最广,考察次数最多的算法,固然要开一篇博客来复习啦。算法
子曰:温故而知新,能够为师矣数组
我复习DP时有一些本身对DP的理解,也就分享出来吧。学习
——正片开始——优化
动态规划算法,即Dynamic Programming(如下简称为DP),是解决多阶段决策过程最优化问题的高效数学方法。自从1999年IOI出了一道名为"数字三角形"的题后,DP题就在OI竞赛中广为流传。而上面提到的"数字三角形",如今就是DP的一道入门题。spa
递推和DP的关系:设计
不少人会混淆递推和DP,递推只是DP的一种实现方式。咱们提到的DP是一种高效的算法,实现方式主要是递推和记忆化搜索,可是DP和递推很像的一点就是,它们都是利用子问题来搞定原问题。code
我举一个例子,斐波那契数列,相信你们都不陌生。blog
咱们知道Fib的递推式:F(x) = F(x-1) + F(x-2),能够经过第x-1项和第x-2项推出第x项。教程
若是咱们从DP的角度来看Fib,咱们能够把求第x项当作原问题,而后咱们把求第x-1项和求第x-2项当作子问题,咱们就把"求第x项"这个原问题划分红了"求第x-1项"和"求第x-2项"两个子问题,而后继续划分子问题并求解,最后利用全部的子问题获得原问题的解。递归
不少DP的入门博客都会仔细详解DP的三个基本性质,解题步骤和使用DP的特征。
不明白这些的,我从@mengmengdastyle的blog中摘取了这一段给大家看:
(2) 动态规划包含三个重要的概念:
- 最优子结构
- 边界
- 状态转移方程
(3)解题的通常步骤是:
1. 找出最优解的性质,刻画其结构特征和最优子结构特征;
2. 递归地定义最优值,刻画原问题解与子问题解间的关系;
3. 以自底向上的方式计算出各个子问题、原问题的最优值,并避免子问题的重复计算;
4. 根据计算最优值时获得的信息,构造最优解。
(4)使用动态规划特征:
1. 求一个问题的最优解
2. 大问题能够分解为子问题,子问题还有重叠的更小的子问题
3. 总体问题最优解取决于子问题的最优解(状态转移方程)
4. 从上往下分析问题,从下往上解决问题
5. 讨论底层的边界问题
以上。
本文注重思路和解题技巧,对于基本知识很少赘述。
咱们先来分析一道题,经过这道题让你明白DP是用来干什么的。
给你一个高度为x(1<=x<=2x10^5)的楼梯,每次能够向上走1级或者2级,问你走到顶一共有多少种走法。
输入样例:
10
输出样例:
89
咱们来分析一下这道题,假如x=1,答案你能够很轻松的获得,等于1,若是x=2呢?答案是2,也很容易获得。
可是咱们的x是能够很大的,咱们不可能对于每一个x都手算出结果存下来。
因而咱们考虑分解问题,将原问题分解成若干子问题,而后对于每一个子问题也分解下去。
“大事化小,小事化了。”
那咱们怎么分解这个问题呢?若是咱们要求走3级的走法数,首先咱们看看可否用走1级和走2级的方案数凑出走3级的方案数。因而,咱们发现,确实能够。咱们走3级的走法数就是走1级的走法数+走2级的走法数。
至此这个问题得解了:走x级的走法数=走x-1级的走法数+走x-2级的走法数。
这题就是求一个斐波那契数列,咱们利用递推获得答案。
介绍更困难的题目前,我打算讲一讲DP自己。
在OI竞赛中,咱们使用计算机计算答案。可是咱们用计算机只能记录下一些状态,咱们须要利用记录下来的状态去求解,因而咱们要尝试把问题定义成一个状态。不少人在讲DP的时候,说须要“递归”的定义状态。对于这种说法,咱们通常用两个字来形容:扯淡。咱们确实须要定义状态,可是,每一个问题均可以被定义成状态,咱们要作的首先就是思考怎么去定义它,通常来讲就是思考咱们存什么变量,算什么变量,用这些变量怎么得出解。
咱们通常把问题划分红不一样阶段,每一个阶段有不一样状态。可是,咱们并不须要算出全部状态存下来,咱们每一步只须要存最优的答案就好了,这就是DP用来求最优解的缘由。既然问题都是能够划分红阶段和状态的,那么,某一阶段的最优解就必定能够经过以前阶段的最优解获得。
可是,若是咱们仅经过前一个阶段的答案算不出当前阶段的答案呢?若是咱们须要前面全部阶段的答案呢?
若是在问题的每一个阶段,一个状态均可以转移到下一个阶段的多个状态,那咱们计算解的时间复杂度就是指数级别的,也就是说咱们并不能用DP来解决这个问题。这种,前面的决策会影响后面的状况,就被称为有后效性。
还记得DP的三个要素之一吗?无后效性。
记得搜索的入门题吗?01迷宫。
若是咱们要找到从起点到终点的最短路径,咱们能够只保存当前阶段的状态吗?显然不行。想一想咱们当时作的时候保存的状态?{x,y,step},以及一个vis数组。因为题目要求咱们求的路径最短,咱们必须知道以前走过的全部位置。
即便咱们当前在同一个位置,咱们以前走的路线不一样,也是会影响到咱们后面选择走的路径的,由于咱们不会走已经标记vis过的格子了。
因此咱们必须保存每一个阶段经历过的全部状态才能获得下一个阶段的解。
这就是有后效性的问题的一个例子。
若是咱们须要记录以前全部的状态,咱们的复杂度就是指数级的,可是DP呢?
咱们并不须要记录以前的全部状态,咱们当前的决策并不受以前状态的组合的影响了,就能够多项式时间内出答案了。
引用一段@X丶dalao的blog:
每一个阶段只有一个状态->递推;
每一个阶段的最优状态都是由上一个阶段的最优状态获得的->贪心;
每一个阶段的最优状态是由以前全部阶段的状态的组合获得的->搜索;
每一个阶段的最优状态能够从以前某个阶段的某个或某些状态直接获得而无论以前这个状态是如何获得的->动态规划。
每一个阶段的最优状态能够从以前某个阶段的某个或某些状态直接获得
这个性质叫作最优子结构;
而无论以前这个状态是如何获得的
这个性质叫作无后效性。
好了,如今咱们讲题。
网上各类什么,让你完全学懂DP啊,特别的DP入门教程啊,其实都不如本身多写点DP题来的实在...
下面我将从几道例题开始,从易到难慢慢打开DP的大门。
1. 石子合并:
有n堆石子排成一列,每堆石子有一个重量w[i], 每次合并能够合并相邻的两堆石子,一次合并的代价为两堆石子的重量和w[i]+w[i+1]。问安排怎样的合并顺序,可以使得总合并代价达到最小。
首先咱们来划分阶段,咱们有一坨长度为n的石子堆,咱们每次合并后,石子堆的数量都会减小,那咱们就从这里切入。
直观地想,咱们可能会这样划分阶段:
咱们要合并石子,确定就要找一个地方,把它两边的石子合并起来。
设f[x]表示合并了x次的最小总代价。马上就能发现不对...咱们选定不一样的地方来合并,每次的答案时不一样的,也就是说f[x]的值不定,这时确定是得不到最优解的。有人可能会有疑问,f[x]不是定义成了最小定义的代价了吗?
那你回去仔细看看上面说的关于状态定义的内容。
因此咱们须要从新定义状态,这里给出一种划分方法,咱们用f[i,j]表示合并区间左端点为i,右端点为j的这段区间合并成一堆石子的最优值。
为何这么定义呢?这就涉及到一类问题:区间DP。
对于区间DP,咱们利用区间长度做为阶段,用左右端点表示状态。这种定义方法能够解决大部分的区间DP问题了,可是遇到一些难题,咱们还须要加维度来解决。
咱们上面提到过,要合并两堆石子,咱们就要循环一个分界点,咱们定义一个分界点k,枚举这个分界点找最优解。这个过程咱们称之为——决策!
而后咱们利用决策转移状态(用子问题求解出原问题):
下面这个式子就是咱们常听到的“状态转移方程”:
f[i,j] = f[i][k] + f[k+1][j] + cost(i,j),其中cost(i,j)表示合并两堆石子的代价。
而后咱们思考一下状态的可选范围,i表示左端点,j表示右端点。
i: 1~n-len+1,j:i+len-1,这样咱们就保证了既不超出边界,又能保证咱们的阶段是区间长度len。
阶段就是len:2~n
这种作法时间复杂度是O(n^4),咱们发现无法简化定义了,因而咱们O(n^3)预处理出cost(i,j),再O(n^3)DP得出答案。
伪代码大概是这样的:
for(i,1,n) for(j,i,n) for(k,i,j) cost(i,j)+=w[k] for(len,2,n) for(i,1,n-len+1) j=i+len-1 for(k,i,j) f[i][j]=min(f[i][j],f[i][k]+f[k+1][r]+cost(i,j))
若是仍是不太理解的能够仔细去看看这道题的题解,博主这一篇博客只打算讲思路,不仔细讲例题。接下来咱们看这样一道题:没有上司的舞会
题目描述已经说了,它们的关系像一棵有根树,那咱们就在树上DP。这种依赖树形结构的DP咱们也把它们划分为一类:
树形DP。
这时候可能就有人想问了,既然也是一类DP,它的阶段划分是否是也和区间DP同样,有套路呢?
没错。树形DP依赖树形结构,那么咱们很容易想到树的性质,父亲和子节点的关系一一对应,咱们能够经过子节点的信息计算父节点的信息。也就是,这类题已经帮咱们划分好阶段了,节点从深到浅的顺序就是咱们的阶段,咱们用一个从上到下的遍从来进行DP,对于每一个子节点x先往下递归在它的每一个子节点进行DP,再在回溯的时候从子节点向节点x转移状态。这样咱们须要作的就是定义状态了。定义状态也很容易,咱们通常选择每一个节点的编号x做为状态的第一维,再根据不一样题目的需求加维进行DP。
回到这道题目上来,咱们根据上面的内容,先定义第一维为节点编号,而后咱们会发现,一个节点的信息值只与它参不参加舞会有关系,因而咱们定义f[x][0/1]为它参加/不参加时的值。
题目也明确说了,若是一我的的直接上司参加了舞会他就不会参加,那咱们就能够轻松的获得状态转移方程:
f[x][0]+=max(f[y][0],f[y][1]),f[x][1]+=f[y][0],y∈son(x)
而后递归求解就行了。
写到这里我发现我实在是讲不完全部的DP类型了,因而咱们后面会跳过几种DP分为下一篇来谈。
放到下一篇讲的DP(状压DP,计数DP,数位DP,几率与指望DP,全部优化方法)
那么咱们就回过来说DP中一类很特殊的问题:背包问题
什么是背包问题?咱们先从基础的0/1背包开始,逐步分析背包问题的模型。
给你n个物品,其中,第i个物品的体积为wi,价值为vi。再给你一个容积为m的背包,如今让你在不超过容积的范围内选出一些物品装入背包让价值尽量大。
首先咱们可能会想到用贪心来解决这道题目,可是贪心很显然是错误的。
咱们贪心的策略很显然是每次选择“性价比”最高的物品,也就是wi/vi最大的物品。
可是,对于0/1背包问题,贪心选择之因此不能获得最优解是由于:
它没法保证最终能将背包装满,部分闲置的背包空间使每公斤背包空间的价值下降了。
明白这一点后,咱们考虑用DP求解。
如何划分阶段呢?很显然,咱们依次考虑是否选择每件物品,而后咱们还须要知道如今背包用了多少容积。
那咱们就设f[i][j]为前i件物品中装了j体积的物品的最大价值。
这里咱们用另一种思路来想转移方程式,咱们不分解这个问题,咱们直接考虑状态怎么推动。
咱们前i件物品中装j体积的最大价值很显然是由前i-1件物品装了某体积的物品,再考虑选不选择当前这件物品转移过来的。
那咱们很容易就能够获得以下的转移方程:
f[i][j]=max(
f[i-1][j],//选这件物品
if(j>=wi)
f[i-1][j-wi]+v[i] //不选这件物品
else
f[i-1][j] //选不了这件物品
)
阶段:前i件物品(i∈[1,n])
状态:当前的容积(j∈[m,0])
这样咱们的空间复杂度是O(n^2)的,考虑优化空间。
咱们发现第一位是能够省略的(咱们按顺序依次考虑每一个物品便可)。
因而...就变成了这样:
for(int i=1;i<=n;i++) for(int j=m;j>=w[i];j--) f[j]=max(f[j],f[j-w[i]]+v[i]);
咱们每一个物品只能放一次,而咱们的f[j]要经过f[j-w[i]]计算获得。
若是,咱们使用正序循环,从w[i]到m,那么咱们可能出现这种状况:
f[j]被f[j-wi]+vi更新过,当咱们j增长到j+w[i]时,f[j+w[i]]又有可能被f[j]+vi更新,而同时它们都处于阶段i,也就是说,咱们在一个阶段内的两个状态间发生了转移,至关于第i个物品被使用了屡次(若是后面又更新了),不符合0/1背包的要求。
因此咱们要倒序循环,这样咱们的j会一直缩小,不会出现“同阶段间转移”的状况,因此,问题至此得解。
接下来咱们要讲彻底背包,思考这样一个问题:
给你n种物品,每种物品有无数个,其中,第i种物品的体积为wi,价值为vi。再给你一个容积为m的背包,如今让你在不超过容积的范围内选出一些物品装入背包让价值尽量大。
注意它和0/1背包问题的区别,0/1背包每种物品只有一个,而彻底背包有无数个。
细心的读者可能发现了,咱们上面说,当咱们正序循环时,至关于一个物品被使用了屡次,符合彻底背包的要求...那咱们只要正序循环,是否是就?
仍是太想固然了啊,固然没错。
而后咱们讲讲多重背包吧,仍是一个相似的问题:
给你n种物品,每种物品有ci个,其中,第i种物品的体积为wi,价值为vi。再给你一个容积为m的背包,如今让你在不超过容积的范围内选出一些物品装入背包让价值尽量大。
每种物品从无数个变成了ci个,也就是有了限制,怎么作呢?
水题啊!咱们把每种物品当作ci个不一样的物品不就行了?而后跑一遍0/1背包,问题不久得解了咩?
天真啊,这样的时间复杂度但是 $O(m*\sum\limits_{i=1}^nc_i)$ 的啊(第一次用Latex有点不习惯)
那咋整啊?咱们又延伸出了:单调队列优化DP。
DP的种类真是数不胜数...不过优化DP是下一篇的内容,这里再也不叙述。
不想用单调队列优化DP来解决多重背包的话,咱们能够二进制拆分多重背包。
咱们大概是这样一个拆分思路,把每一种物品拆成log个不一样物品。
大概是这样拆:
int cnt=0; for(int i=1;i<=n;i++){ int a,b,c; cin>>a>>b>>c; for(int j=1;j<=c;j<<=1){ v[++cnt]=j*a,w[cnt]=j*b; c-=j; } if(c)//剩下拆不掉的部分,直接当新物品 v[++cnt]=c*a,w[cnt]=c*b; }
思路仍是很简单的,可是很巧妙。拆完就是一个0/1背包了,很水。
我佛了...还有个分组背包没讲...这篇博客都写了2天了QuQ,DP真难讲。
限于篇幅和时间,树上的背包问题我留到下一篇的开头...
给出分组背包的模型:
给你n组物品,每组物品有ci个,其中,第i组第j个物品的体积为wi,j,价值为vi,j。再给你一个容积为m的背包,如今让你在不超过容积的范围内每组至多选一个物品装入背包让价值尽量大。
分组背包是一类树形DP的很重要的组成Part,因此熟练掌握它仍是很重要滴。
这个问题咱们怎么作呢?
考虑用线性DP解决(...雾),咱们要知足“每组至多选择一个物品”的要求,就能够利用“阶段”线性增加的特性,把物品组数做为阶段,只要选了一个第i组的物品,就转移状态到下一个阶段就行了^-^。而后仿照0/1背包的作法,设f[i][j]表示从前i组中选出整体积为j的物品放入了背包,物品的最大价值和。
f[i,j]=max{
f[i-1,j],//不选第i组的物品喵
max{f[i-1,j-wik]+vik},(1<=k<=ci)//选第i组的某个物品k喵->是作决策哒
}
上面那东西不是我敲的...可是她不让我删掉
咱们仍是能够省略掉第一维,别问,问就是这是背包问题。为何呢?由于咱们能够用j的倒序循环来控制阶段i的状态只能由阶段i-1转移获得。
至此问题得解,给出代码:
for(int i=1;i<=n;i++) for(int j=m;j>=0;j--) for(int k=1;k<=c[i];k++) if(j>=w[i][k]) f[j]=max(f[j],f[j-w[i][k]+v[i][k]);
总结一下,这篇博客咱们接触并初步学习了动态规划算法,并对DP的本质有了必定的了解,明白了设计DP算法求解问题的通常思路。没错,设计DP算法,DP算法迷人的地方就在于,对于每道DP题,都须要本身去设计一个合理且高效的DP算法去解决问题,这也是DP难的地方。除此以外,咱们还学习了几种常见的DP模型,加深了对“阶段,状态,决策”的理解。DP题要难能够难上天,要简单能够一眼秒,可是本质上看它们都是考察同一个东西:脑子。DP题其实不难个鬼,只要你理解了DP的基本实现方法,稍加思考,把问题转化一下,就很容易想到如何用DP去求解答案。
对于这种依靠思惟的题目,其实不用写不少题。虽然刷题是必不可缺的,可是对于写过的每道DP题,都确保本身理解了思路,明白了为何这样设计DP,就能够总结出一套本身应对DP题的方法和技巧,每一个人写DP题的方法都是不尽相同的。但愿经过这篇博客,能让你喜欢上动态规划算法。
更深层次难度更高的DP,我会在下一篇博客里讨论。不过就连这篇博客我都写了整整两天,下一篇可能我要写挺久了。除非我够肝。
大家的点赞就是我最大的动力(其实我就是想本身整理整理...),感谢大家的支持。