精华内容
下载资源
问答
  • 动态规划算法经典例题

    热门讨论 2012-05-16 13:14:50
    一些动态规划,最新最典型的算法!如,背包问题,钢管切割问题,最长子序列问题等等。
  • 在之前的文章当中,我们一起探讨了二分、贪心、排序和搜索算法,今天我们来看另一个非常经典算法——动态规划。在acm-icpc竞赛领域,动态规划是一个非常大的范畴,当中包含了许多变种,而且很多变种难度极大。比如...

    今天是周三算法与数据结构专题的第12篇文章,动态规划之零一背包问题。

    在之前的文章当中,我们一起探讨了二分、贪心、排序和搜索算法,今天我们来看另一个非常经典的算法——动态规划

    在acm-icpc竞赛领域,动态规划是一个非常大的范畴,当中包含了许多变种,而且很多变种难度极大。比如在各种树上和图上以及其他数据结构上做动态规划,这会使得问题非常复杂。好在非竞赛选手并不需要了解到那么深入,一般来说,吃透背包九讲,就足够笑傲各种面试了。所以周三的算法专题我们开始全新的篇章——背包系列,今天和大家分享背包九讲中的第一讲,也是最简单的零一背包问题。

    背包和零一背包

    没有竞赛经验的同学在看到这个标题的时候可能会一头雾水,动态规划和背包有什么关系。其实没有关系,我也不是陈奕迅的粉丝,只是当初最经典的动态规划问题用背包做了题面,还引发出了各种变种。后来在教学的时候为了方便,于是沿用了前人的名称。

    之前我们在怪盗基德偷宝石的问题当中提到过背包问题,其实很简单,就是说我们当下有一个容量是V的背包,和n个体积分别是v[i],价值是w[i]的物品。请问,在背包容量允许的前提下,我们最多能够获得多少价值的物品

    由于每种物品只有一个,也就是物品只有拿和不拿两种状态,所以这个问题被称为零一背包问题

    贪心与反例

    这种问题我们最先想到的就是贪心法,比如优先拿价值大的物品,或者是性价比高的物品,但是我们很容易构思出反例。

    举个例子,比如背包的容量是10,我们有3个物品,体积分别是6,5,5,价值是10,8,8。这个反例可以证明两种贪心策略都不生效,因为价值最大的是10,它的体积是6,我们一旦拿了它就没有空间再继续获取其他物品,而显然拿两个5的情况是最优的。同样,体积是6的物品也是性价比最高的,性价比优先的贪心策略同样不生效。

    实际上不仅这两种贪心策略不生效,所有能够想到的贪心策略都不生效。这个问题看起来简单,但是并不是那么容易解决。实际上这个问题一直困扰着计算学家,直到上世纪六十年代,动态规划算法横空出世,完美地解决了这个问题。

    动态规划

    动态规划算法的英文是dynamic programming,算是很直白的翻译了。规划我们都很好理解,但是动态应该怎么理解呢?又怎么来动态地规划呢?关于这个问题的思考直接关系到算法的本质。

    动态规划算法的本质是状态的记录和转移,我们结合刚才的问题,有没有想过为什么贪心算法不可行?其实很简单,因为我们没办法确定背包什么状态是完美的。虽然我们知道背包的容量是V,但是我们并不知道最优的情况下我们能装多少,最优的结束状态是什么。我们把空间V看成了一个状态来进行贪心,贪心得到的结果是最优的,但是只是贪心能达到的状态的最优解,并不是全局的最优解,因为背包容量的限制,很有可能我们贪心策略下无法达到真正最优的状态。

    用刚才的例子解释一下上面这段话,在贪心算法下,我们会选取容量是6,价值是10的物品,这个物品拿取了之后背包的状态是6,获取的价值是10。这个状态是贪心能够达到的最终状态,对于这个状态而言,它是最优解,但是这个状态并不是整体最优的情况,因为在贪心策略下,无法达到容量10全用完的状态。

    理解了这个问题之后,再去推导解法就顺其自然了。贪心策略可以获取一些状态最优的情况,那么我们能不能记录下所有状态能够达到的最优的情况,最后在这些最优的情况当中选取一个最优的,它不就是整体最优解了吗?

    动态规划正是基于上述思路展开的,它解决的不是一个状态的最优解,而是所有状态的最优解。

    状态与转移

    看到这里,你肯定还没理解动态规划算法,但是应该已经有一些大概的感觉了。这是对的,有正确的感觉是正确认识的前提。我们循序渐进,再来看状态这个概念。

    我们刚才提了这么多次,究竟状态是什么呢?这是一个比较抽象的概念,在不同的问题当中它有着不同的含义。在背包问题当中,状态指就的是背包容量的使用情况。由于背包问题中物品的体积是整数,显然背包容量的可能性是有限的,这点不起眼,但是很重要,如果状态不是整数,那么虽然存在动态规划的可能,但是代码实现可能比较麻烦。

    明白了背包的容量是状态之后,我们可以进一步想明白,背包的容量是会变化的。变化的原因是因为我们往里面放了东西,放了东西之后,背包的状态会发生变化,会从一个状态转移到另一个状态。状态的转移中间伴随着我们放入东西,我们放的物品并不是固定的,而是有多种选择的,我们决定放入A而不是BC,这是一种决策,决策会带来状态的转移,不同的决策会带来不同的转移。

    比如当前有一个背包,它的容量是10,我们在其中已经放入了一个体积是3,价值是7的物品。如果这个时候,我们经过选择再放入一个体积是4,价值是5的物品。那么显然,背包占用的容量会变成7,价值会变成12。这个过程就是一个经典的状态转移过程,这也是整个动态规划算法的核心。

    基本的概念和思想已经介绍完了,接下来就是用这些概念来解决实际问题了。

    最优状态

    我们前文说了,动态规划最后会获取所有状态的最优解,再从中选取全局最优的。那么它是怎么获取局部最优解的呢?

    在回答这个问题之前,我们先来思考两个问题。

    首先,假如我们已经知道了背包体积是3时的最大价值是5,这时候我们决定放入一个体积是4,价值是5的物品,那么背包的体积会增加到7,那么这个时候获得的是体积6的最优解吗?

    这个问题不难回答,我们稍微想想就知道,很有可能不是。举个最简单的例子,假如我们有一个体积是7,价值是20的物品。那么显然要比放这两个物品更优。虽然状态3最多能获得价值5,状态7也可以由状态3转移得到,但是这并不一定是最优的。也就是说最优的状态转移出去,并不一定也能得到其他状态的最优值

    我们把问题反过来就不一样了,如果我们知道了体积6的最优解,并且还知道它是由体积等于4转移得到的,那么我们能不能确定体积4的状态也是对应的最优解?

    这次的答案就变了,是正确的,因为如果体积4时还有更好的解法,那么体积6理应也会变得更好才对,这和我们的假设矛盾了。

    我们总结一下上面的两个结论,也就是说局部最优的情况转移出去并不一定是最优的,但是局部最优一定也是由其他局部最优的状态转移得到的。这句话有点像绕口令,但是我觉得应该不难解释。就好比学霸去考试,不一定能考第一,但是考到第一的一定是学霸。局部最优就是学霸,转移就是考试。局部最优转移出去并不一定是转移之后状态的最优,有可能还有其他更好的转移策略,但是对于某个状态最优的情况而言,它一定也是从之前的某个最优状态转移得到的。

    并且状态的转移也是有顺序的,比如在这题当中,背包当中放入了物品体积只可能增加,不可能减小,意味着状态只能从小的转移到大的。

    我们捋一捋思路,已经很明确了,状态可以转移,状态的转移有顺序,局部最优一定是由其他局部最优转移得到的。由于我们并不知道当前的转移能否达到最优状态,所以我们需要用一个数组或者是容器来记录所有状态历史上曾经达到过的最值。最后从所有的最值当中再选出一个最值来,就是最后问题的解。

    到这里,如果是一般的动态规划问题,已经解决了,但是零一背包还有一个细节需要考虑。

    无后效性

    我们先来看下整个的计算流程,首先我们需要从最初状态开始,这个最初的状态很好办,就是背包是空的时候,这时候的价值是0,体积也是0,这也是它的最优状态,这个很好理解,因为我们不能无中生有。

    所以我们从0开始转移状态,状态转移伴随着决策,在这题当中体现在选取不同的物品上。我们遍历物品,作为决策,再遍历能够应用这些决策的状态,就拿到了所有的状态转移。最后,我们用一个容器记录一下所有状态转移过程当中达到的局部最优解,于是就结束了。

    这个过程看起来非常正常,没有任何异常,但实际上,问题来了。

    我们还用刚才的题面举例,背包容量是10,3个物品,体积分别是6,5,5,价值是10,8,9。我们从0开始拿取第三个物品,转移到了状态5,此时的价值是9。这个时候,我们继续往后遍历的话还会遍历到状态5,它已经是拿取了物品3,价值9的信息了。因为一个物品只能拿一次,所以我们不能再用物品3转移状态5,否则就违反了题意

    你可能会说这个问题不难,我们可以在状态当中也记录之前做过的决策嘛,只要在决策的时候加一个判断就好了。

    表上面看是因为物品不能重复拿的限制,实际上是因为我们的状态之间会有影响。也就是说我们前面做的决策很有可能影响后面的状态做决策,这种状态之间的前后影响称为后效性。显然在有后效性的场景下我们是不能使用动态规划算法的,并不是所有问题都可以通过加上判断解决,我们需要解决后效性这个本质问题,也就是说我们要想办法消除后效性。

    在这个问题当中,这一点很容易做到。我们只需要控制一下状态和决策的遍历顺序,将之前的决策与之后的决策分开,使它们互不影响即可。如果我们先遍历状态,再遍历每个状态可以采取的措施,这样必然会造成前后影响。因为前面做了的决定,后面就不能再做。但是后面并不能感知前面究竟做了什么决定。所以比较好的方法是先遍历决策,再来遍历可以采取这个决策的状态。为了避免决策前后的互相影响,我们采取倒序的方式遍历状态

    我们举个例子,假设背包容量还是10,我们枚举的第一个物品体积是3,价值是5。我们倒叙遍历状态7到0,因为对于大于7的状态而言,并不能采取这个决策(总体积会超)。因为对于大于7的状态而言,我们不能采取这个决策(总体积会超过限制),对于状态7而言,我们可以采取这个决策,转移到状态10。我们并不知道这样转移会不会达成最优,所以我们这样来记录:dp[10] = max(dp[10], dp[3] + 5).

    我们接着遍历体积6,可以转移到状态9。

    由于我们是倒序遍历,所以当我们用状态7更新状态10时,状态7本身并没有被这个决策更新过。即使后面我们在遍历到状态4时更新了状态7,也不会影响状态10的结果。因为是倒序遍历的,我们不会再用同一个策略更新到状态10了。如果是正序遍历,则无法避免。同样的物品,我们很有可能会出现,用状态1更新状态4,再用状态4更新状态7,再用状态7更新状态10的情况出现。而这种情况其实对应了使用了多个同样的物品,这就和题意矛盾了。

    举个例子,假设有一个物品体积是2,它的价值是5。我们遍历状态0的时候,会更新状态2,我们遍历到了状态2,又用同样的物品更新了状态4,得到了10。那么对于状态4而言,它其实相当于拿了2个这个物品,也就是说被同一个决策更新了两次。但是我们的物品最多只有一个,显然就不对了。

    81bbea1f7d091233bc78f3b1ca7c52b6.png

    动态规划当中因为无法判断当前枚举的状态的来源,所以不允许出现后效性,如果解决不了则不能使用动态规划。这也是动态规划最基本的原则,在这题当中,我们是巧妙变换了决策和状态枚举的过程,消除了后效性。在其他题目当中未必相同,我们需要根据实际情况进行判断。

    如果你在做题思考的过程当中忘记了动态规划的前提,就想想零一背包当中拿取物品的情况。物品只有一个,只能拿一次。前面拿过了后面还能拿,就违反了后效性。

    状态转移方程

    我们整理一下刚才关于状态转移的思路,有以下几点:

    1. 我们从状态0开始,状态0的最优价值是0.
    2. 考虑后效性的问题,确保没有后效性
    3. 执行决策的时候,会发生状态转移,记录状态对应的最优解

    在这个问题当中,决策就是获取物品,状态就是背包容量。由于拿取物品会引起背包容量变化,并且每个物品只有一个,为了避免产生后效性,我们需要先枚举决策,再枚举状态,保证每个决策只在每个状态上最多应用一次。在此过程当中,需要一直记录每个状态的最优解。

    由于背包的容量是V,我们只需要用一个容量是V的数组就足够记录所有的状态。

    1bd62ddc955ccb6cd69bc6ba2dfe30ca.png

    dp记录的是所有的状态,我们用max(dp[v+i.v], dp[v] + i.w)来更新dp[v+i.v]状态的值,由于当前的决策不一定比之前的更好,所以要加上max操作,保证每个状态记录下来的结果都是它最优的。当所有的状态的最优解都有了之后,显然整个问题的最优解也在其中了。

    上面这个记录状态转移过程的式子叫做状态转移方程,它也是动态规划算法的核心概念。很多时候,在我们解动态规划问题的时候,会在草稿纸上推演状态转移方程。如果状态转移方程能清楚地列出来,距离写出代码也就不远了。

    代码

    上面的转移方程已经非常接近最后的代码了,真正写出来也就只有几行而已:

    750eb7d823658ba057996e6fbbc98472.png

    总结

    关于零一背包的前后推导以及当中所有的概念始末就算是介绍完了,虽然我们用了这么多篇幅来介绍这个算法,但是真正写成代码也就只有短短几行。单从代码行数来看,动态规划可以说是实现代码最短的算法了,只是虽然它代码不长,但是思路并不简单,尤其是当中的下标以及循环的顺序等细节,希望大家不要掉以轻心。

    今天零一背包的问题到这里就结束了,下周的算法专题我们继续背包问题,来看看01背包的进阶版——完全背包和多重背包问题,敬请期待。

    如果觉得有所收获,请顺手点个关注或者转发吧,你们的举手之劳对我来说很重要。

    展开全文
  • 动态规划算法的核心就是记住已经解决过的子问题的解。记住求解的方式有两种:①自顶向下的备忘录法②自底向上。动态规划的经典模型线性模型线性模型的是动态规划中最常用的模型,上文讲到的钢条切割问题就是经典的...

    动态规划算法的核心就是记住已经解决过的子问题的解。

    记住求解的方式有两种:①自顶向下的备忘录法自底向上。

    动态规划的经典模型

    线性模型

    线性模型的是动态规划中最常用的模型,上文讲到的钢条切割问题就是经典的线性模型,这里的线性指的是状态的排布是呈线性的。【例题1】是一个经典的面试题,我们将它作为线性模型的敲门砖。

    【例题1】在一个夜黑风高的晚上,有n(n <= 50)个小朋友在桥的这边,现在他们需要过桥,但是由于桥很窄,每次只允许不大于两人通过,他们只有一个手电筒,所以每次过桥的两个人需要把手电筒带回来,i号小朋友过桥的时间为T[i],两个人过桥的总时间为二者中时间长者。问所有小朋友过桥的总时间最短是多少。

    4430d3fe6ff3ba503185b58451a4ba74.png

    每次过桥的时候最多两个人,如果桥这边还有人,那么还得回来一个人(送手电筒),也就是说N个人过桥的次数为2*N-3(倒推,当桥这边只剩两个人时只需要一次,三个人的情况为来回一次后加上两个人的情况…)。有一个人需要来回跑,将手电筒送回来(也许不是同一个人,realy?!)这个回来的时间是没办法省去的,并且回来的次数也是确定的,为N-2,如果是我,我会选择让跑的最快的人来干这件事情,但是我错了…如果总是跑得最快的人跑回来的话,那么他在每次别人过桥的时候一定得跟过去,于是就变成就是很简单的问题了,花费的总时间:

    T = minPTime * (N-2) + (totalSum-minPTime)

    来看一组数据 四个人过桥花费的时间分别为 1 2 5 10,按照上面的公式答案是19,但是实际答案应该是17。

    具体步骤是这样的:

    第一步:1和2过去,花费时间2,然后1回来(花费时间1);

    第二歩:3和4过去,花费时间10,然后2回来(花费时间2);

    第三部:1和2过去,花费时间2,总耗时17。

    所以之前的贪心想法是不对的。我们先将所有人按花费时间递增进行排序,假设前i个人过河花费的最少时间为opt[i],那么考虑前i-1个人过河的情况,即河这边还有1个人,河那边有i-1个人,并且这时候手电筒肯定在对岸,所以opt[i] = opt[i-1] + a[1] + a[i] (让花费时间最少的人把手电筒送过来,然后和第i个人一起过河)如果河这边还有两个人,一个是第i号,另外一个无所谓,河那边有i-2个人,并且手电筒肯定在对岸,所以opt[i] = opt[i-2] + a[1] + a[i] + 2*a[2] (让花费时间最少的人把电筒送过来,然后第i个人和另外一个人一起过河,由于花费时间最少的人在这边,所以下一次送手电筒过来的一定是花费次少的,送过来后花费最少的和花费次少的一起过河,解决问题)
    所以 opt[i] = min{opt[i-1] + a[1] + a[i] , opt[i-2] + a[1] + a[i] + 2*a[2] }

    区间模型

    区间模型的状态表示一般为d[i][j],表示区间[i, j]上的最优解,然后通过状态转移计算出[i+1, j]或者[i, j+1]上的最优解,逐步扩大区间的范围,最终求得[1, len]的最优解。

    【例题2】给定一个长度为n(n <= 1000)的字符串A,求插入最少多少个字符使得它变成一个回文串。
    典型的区间模型,回文串拥有很明显的子结构特征,即当字符串X是一个回文串时,在X两边各添加一个字符’a’后,aXa仍然是一个回文串,我们用d[i][j]来表示A[i…j]这个子串变成回文串所需要添加的最少的字符数,那么对于A[i] == A[j]的情况,很明显有 d[i][j] = d[i+1][j-1] (这里需要明确一点,当i+1 > j-1时也是有意义的,它代表的是空串,空串也是一个回文串,所以这种情况下d[i+1][j-1] = 0);当A[i] != A[j]时,我们将它变成更小的子问题求解,我们有两种决策:

    1、在A[j]后面添加一个字符A[i];

    2、在A[i]前面添加一个字符A[j];

    根据两种决策列出状态转移方程为:

    d[i][j] = min{ d[i+1][j], d[i][j-1] } + 1; (每次状态转移,区间长度增加1)

    空间复杂度O(n^2),时间复杂度O(n^2), 下文会提到将空间复杂度降为O(n)的优化算法。

    背包模型

    背包问题是动态规划中一个最典型的问题之一。由于网上有非常详尽的背包讲解,这里只将常用部分抽出来。

    【例题3】有N种物品(每种物品1件)和一个容量为V的背包。放入第 i 种物品耗费的空间是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值总和最大。f[i][v]表示前i种物品恰好放入一个容量为v的背包可以获得的最大价值。决策为第i个物品在前i-1个物品放置完毕后,是选择放还是不放,状态转移方程为:

    f[i][v] = max{ f[i-1][v], f[i-1][v – Ci] +Wi }

    时间复杂度O(VN),空间复杂度O(VN) (空间复杂度可利用滚动数组进行优化达到O(V) )。

    本文出处:

    算法-动态规划 Dynamic Programming--从菜鸟到老鸟blog.csdn.net
    634d32069ded80a79b0f12255bf5648a.png
    展开全文
  • 相信很多初学者,在刚刚接触动态规划,都花费了...c动态规划精简例题1. 单序列型爬楼梯算法思想: 1、定边界体条件,因为有两种走法,所以dp[0]=cost[0],dp[1]=cost[1] 2、我们假设第i个为最后一步,则有两种情况...

    相信很多初学者,在刚刚接触动态规划,都花费了不少时间。网上有很多人写如何去做动态规划的题目,但是很少有整理适合初学者做的习题。这里是我自己考研期间,复习整理的。希望对大家有帮助。

    c动态规划精简例题

    1. 单序列型

    1. 爬楼梯
    算法思想:
             1、定边界体条件,因为有两种走法,所以dp[0]=cost[0],dp[1]=cost[1]
             2、我们假设第i个为最后一步,则有两种情况
                min{dp[i-1],dp[i-2]}+sonst[i]
    //1.爬楼梯
    
    int minCostClimbingStairs(int cost[], int n)
    {
        int dp[n];
        dp[0] = cost[0];
        dp[1] = cost[1];
        for (int i = 2; i < n; i++)
        {
            dp[i] = min{dp[i - 1], dp[i - 2]} + cost[i];
        }
        return min{dp[n - 2], dp[n - 1]};
    }
    1. House Robber
    算法思想:
        1、边界条件:如果没有房子,则return 0;
        2、如果只有一个房子,则返回这一个房子
        3dp[0]=nums[0];
           dp[1]=max{nums[0],nums[1]};
        4、比较第i个房子偷dp[i - 2] + nums[i])和不偷(dp[i - 1])的时候的大小
    //2.House Robber
    
    int rob(int nums[], int n)
    {
        if (n == 0)
            return 0;
        if (n == 1)
            return nums[0];
        int dp[n];
        dp[0] = nums[0];
        dp[1] = max{nums[0], nums[1]};
        for (int i = 2; i < n; i++)
        {
    
            dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[n - 1];
    }
    1. 最长上升子序列
    算法思想:这里用到的是一个自底向上的寻找最优子结构的的思想。粗俗来说:如果你想要得到七个数里面的最长子序列,你可以先找前6个数里面的最长子序列,同理,你又必须得找前5个数里面的最长子序列,直到子序列为1
        1、我们先令dp[i]=1//初始化,假如刚开始前面没有比它小的元素只有它自己
        2、我们在0~i之间,与dp[i]进行比较,如果,比dp[i]大,则dp[i]=dp[k]+1
        3、一趟内for循环结束,确定一个空,这个时候和max进行比较。因为最长上升子序列不一定是最后一个,所以每次都要比较下
    //3.最长上升子序列
    
    int lengthOfLIS(int nums[], int n)
    {
        if (n == 0)
            return 0;
        int dp[n];
        dp[0] = 1;
        max = 1;
        for (int i = 1; i < n; i++)
        {
            dp[i] = 1;//一开始最长上升子序列只有自己1个
            for (int k = 0; k < i; k++)
            {
                if (nums[k] < nums[i])//与比nums[i]小的之前的所有的最长上升子序列进行比较
                    dp[i] = max{dp[i], dp[k] + 1};
            }
            max = max{max, dp[i]};//最长上升子序列不一定是最后一个,所以每次都要比较下
        }
        return max;
    }
    1. (模拟卷上的例题)

    8d2c882c1e3a5fd5c4346a69bcdc9afb.png

    .JPG)

    算法思想:
        1、我们用一个一维数组来统计输入的数据的个数
        2、我们a来存储最大值,作为后面for循环的终止条件
        3、我们每次到i的时候都是假设i为最后一个元素,与前面的状态进行比较
               a.如果第i个数据我们不选择,则dp[i-1]
               b.如果我们选择,则dp[i-2]+vis[i]*i
        4、找里面最大值
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    define N 1000 int main()
    {
        int n, a, m = 0;
        int dp[N];   //dp[i]为数字1~i能获得的最大价值
        int vis[dp]; //用来统计序列中数字i的个数
        memset(vis, 0, sizof(vis));
        scanf("%d", &n);
        for (int i = 0; i < n; i++)
        {
            scanf("%d", &a);
            vis[a]++;
            m = max(m, a);
        }
        dp[0] = 0;
        dp[1] = vis[1];
        for (int i = 2; i < m; i++)
        {
            dp[i] = max{dp[i - 1], dp[i - 2] + vis[i] * i};
        }
        printf("%dn", dp[m]);
        return 0;
    }

    2.双序列型

    1. 最长公共子序列

    转移方程: $$ dpleft[ i right] left[ j right] =begin{cases} 0 & & {i=0text{或}j=0} dpleft[ i-1 right] left[ j-1 right] +1 & & {Aleft[ i right] =Bleft[ j right]} 0 & & {Aleft[ i right] ne Bleft[ j right]} end{cases} $$

    核心算法:
        if (A[i] == B[j]) //如果最后一个元素相同
            dp[i][j] = dp[i - 1][j - 1] + 1;
        else
            dp[i][j] = max{dp[i - 1][j], dp[i][j - 1]};
    void LCSLength(int m, int n, char *x, char *y, int dp[][])
    {
        int i, j;
        for (i = 1; i <= m; i++)
            dp[i][0] = 0;
        for (i = 1; i <= n; i++)
            dp[0][i] = 0;
        for (i = 1; i <= m; i++)
        {
            for (j = 1; j <= n; j++)
            {
                if (x[i] == y[j])
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                else
                {
                    dp[i][j] = max{dp[i - 1][j], dp[i][j - 1]};
                }
            }
        }
        return dp[m][n];
    }
    1. Edit Distance
    int minDistance(string word1, string word2)
    {
        int n = word1.length;
        int m = word2.length;
    
        int dp[n][m];
        //边界条件
        for (int i = 0; i < n + 1; i++)
            dp[i][0] = i;
        for (int j = 0; j > m + 1; j++)
            dp[0][j] = j;
    
        for (int i = 0; i < n + 1; i++)
        {
            for (int j = 0; j < m + 1; j++)
            {
                dp[i][j] = min{dp[i - 1][j], dp[i][j - 1]} + 1; //delet和insert中找最小的
                if (word1.charAt(i - 1) == word2.charAt(j - 1))
                {
                    dp[i][j] = min{dp[i][j], dp[i - 1][j - 1]};//如果最后一个元素相同,则找前一个和这个,谁最小
                }
                else
                {
                    dp[i][j] = min{dp[i], dp[i - 1][j - 1] + 1};//如果不相同,则和replace进行比较
                }
            }
        }
    }

    ![IMG_1504(20201127-154444)](C:Users渐明DocumentsTencent Files834422139FileRecvMobileFileIMG_1504(20201127-154444).JPG)

    算法思想:
            dp[i][0/1/2]分别表示第i天选择休息/健身/上网比赛的最少休息天数,因为不能连续两天做同样的事情,所以今天选择健身的话,从昨天选择了休息或者上网比赛的状态进行转移
    #include <algorithm>
    #include <stdio.h>
    #include <stdlib.h>
    using namespace std;
    
    const int INF = 10000;
    const int mx = 110;
    int dp[mx][3];
    
    int main()
    {
        int n, a;
        scanf("%d", &n);
        memset(dp, INF, sizeof(dp));
        //dp[i][0/1/2] rest/gym/match
        dp[0][0] = 0;
        for (int i = 1; i <= n; i++)
        {
            scanf("%d", &a);
            int minn = min(dp[i - 1][1], dp[i - 1][2]);
            dp[i][0] = min(dp[i - 1][0], minn) + 1;
            if (!a)
                continue;
            if (a != 2) //choose to join match
            {
                dp[i][2] = min(dp[i - 1][0], dp[i - 1][1]);
            }
            if (a != 1) //choose to gym
            {
                dp[i][1] = min(dp[i - 1][0], dp[i - 1][2]);
            }
        }
        printf("%dn", min(dp[n][0], min(dp[n][1], dp[n][2])));
        return 0;
    }

    3. 0-1背包问题

    0-1背包的意思是:要么拿,要么不拿;拿只能拿一整个,不存在只是拿部分;物品只有一个,拿了之后就没了
    //问题描述:
    Input:
    n items
    v[i]: ith item 的价值
    w[i]: ith item 的重量
    C:knapsack 的最大承重量
    Note: at most one copy of each item
    Output: 能带走的最大价值总和
    //问题分析:
    
    f(i,j)=max{f(i+1,j),f(i+1,j-w[i])+v[i]}  //第i个物品不选,第i个物品选,从中选出最大的一项出来
    
    Base Case:  f(0,j)=0   //房子内没有物品
                f(i,0)=0   //没有带袋子/或者是袋子已经满了
    public int knapsack(int &v,int &w,int C)
    {
        int n=v.length;
        int[][] dp=new int[n+1][C+1];
        for(int r=1;r<=n;r++)
        {
           for(int c=1;c<=C;c++)
            {
               dp[r][c]=dp[r-1][c];    //表示未放入这个商品之前,最优解
                if(w[r-1]<=c)
                {
                    dp[r][c]=Math.max(dp[r][c],dp[r-1][c-w[r-1]]+v[r-1]);
                }
            } 
        }        
    }

    4. 完全背包问题

    完全背包是在N种物品中选取若干件(同一种物品可多次选取)放在空间为V的背包里,每种物品的体积为C1,C2,…,Cn,与之相对应的价值为W1,W2,…,Wn.求解怎么装物品可使背包里物品总价值最大。

    状态转移方程: $$ dpleft[ i right] left[ j right] =max left{ dpleft[ i-1 right] left[ j-kcdot vleft[ i right] right] +kcdot vleft[ i right] right} ,, 0leqslant k&kcdot wleft[ i right] ll j $$

    //问题描述:
    Input:
          n items
          v[i]: ith item的价值
          w[i]: ith item的重量
          CKnapsack的最大承重重量
          Note: infinite copies of each item(每一种物品不限制数量)
          Output: 能带走的最大价值总和
    #include <iostream>
    #include <string.h>
    using namespace std;
    #define Max_n 105
    #define Max_w 10005
    
    int n,W;
    int w[Max_n],v[Max_n];
    int dp[Max_n][Max_w];
    
    //初始化
    void initialize(int n,int m)
    {
        for(int i=0; i<=n; i++)
            dp[i][0]=0;
        for(int j=0; j<=W; j++)
            dp[0][j]=0;
    }
    
    //dp[i][j]=max⁡{dp[i-1][j-k*w[i]]+k*v[i]|0≤k&k*w[i]≤j}
    void solve_1()
    {
        for(int i=1; i<=n; i++)
            for(int j=1; j<=W; j++)
            {
                dp[i][j]=-1;
                for(int k=0; k*w[i]<=j; k++)
                    dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+k*v[i]);
            }
    }

    注:完全背包和0-1背包的两个for循环的位置代表的东西不同,主要是由于i用来表示每一种物品。

    展开全文
  • 那么今天继续讲解算法题第55题, 也是一道经典的面试题目,那我们来一起看一看吧.Given an array of non-negative integers, you are initially positioned at the first index of the array.Each element in the ...

    Hello, 刚刚结束完一天的工作,忙着把算法题写完.那么今天继续讲解算法题第55题, 也是一道经典的面试题目,那我们来一起看一看吧.

    Given an array of non-negative integers, you are initially positioned at the first index of the array.

    Each element in the array represents your maximum jump length at that position.

    Determine if you are able to reach the last index.

    「Example1」

    Input: nums = [2,3,1,1,4]
    Output: true
    Explanation: Jump 1 step from index 0 to 1, then 3 steps to the last index.

    「Example2」

    Input: nums = [3,2,1,0,4]
    Output: false
    Explanation: You will always arrive at index 3 no matter what. Its maximum jump length is 0, which makes it impossible to reach the last index.

    来简单描述一下这道题目的意思, 大致上就是给定一个数组, 数组中的元素代表你可以往后跳的一个步数,比方nums[0]=2 代表它可以选择跳到nums[1] 和 nums[2] 这两步

    我们来画个图,举个例子给大家看看

    4eb08fcbda02dda817b31130cf9b244a.png

    我用四种不同的颜色代表每个元素可以跳跃的步数, 这下子是不是就很清楚了

    接着我们来回顾一下「递归」「动态规划」的思想

    1. 「创建一个数组 存储动态规划的过程」
    2. 「寻找递归结束的条件(可以先画个树图出来帮助理解)」

    我们先来看一下 动态规划我们要准备什么? 其实我们就是要保存, 每个点是否能跳跃到终点的记录

    我们用-1,0,1来代表3种状态,分别是「不能到达」,「初始化」,「可以到达」三个阶段

    31015b92909b74df2bd04f68eca1098b.png

    我们先初始化dp的数组, 并且将终点设置为1, 因为终点肯定能到终点 接下来,我们来思考一下

    「递归的结束条件应该是什么?」

    我们先把递归图画出来

    45d89ee7cb4f64d2d3931b640917e89e.png

    从这道例子中, 我们递归树应该是这样子

    4ba9cef0c1a92871417d5eb704ea4dba.png

    我们的递归顺序,就是从左边的1开始,往右边数,最后才做2自己本身, 在这个例子中你会发现, 你无论选那一条路 你都能到终点对吧, 那我们就换一题来讲

    6237f4f4c6452a49a4a56dcbc3c55513.png

    我们对着下面这个题目, 画一下动态规划表

    3ea05596101dce7c9491176bf639b1a4.png

    所以, 我们的动态规划表应该是上图那样的结果. 1代表可到达的路径, -1代表不可到达

    所以说, 我们递归结束的条件 应该是 「dp[i]==-1」「dp[i]==1」 为结束条件

    如果 「dp[i]==0」 说明该节点尚未被计算,需要再次把这个节点进行递归 如果递归到了死胡同了, 我们就把该点标记成为-1退出则可

    让我们来看一下递归代码

    func recurrentJump(position int,nums []int, dp *[]int) bool{
     if (*dp)[position] == 1{
      return true
     }else if (*dp)[position] == -1{
      return false
     }
     maxJump := min(position + nums[position], len(nums)-1)
     for i:= position + 1; i<=maxJump; i++{
      result:=recurrentJump(i, nums, dp)
      if result {
       (*dp)[position] = 1
       return true
      }
     }
     (*dp)[position] = -1
     return false
    }
    func min(x int, y int) int{
     if (x < y){
      return x
     }
     return y
    }
    

    递归的主函数就在上面, 和我们描述的一致, 如果说「dp[i]==-1」「dp[i]==1」直接可以返回否则说明该节点尚未被计算

    我们来看一下节点未被计算的时候需要做的循环

    maxJump := min(position + nums[position], len(nums)-1)
    for i:= position + 1; i<=maxJump; i++{
     result:=recurrentJump(i, nums, dp)
     if result {
      (*dp)[position] = 1
      return true
     }
    }
    

    首先 求最小的跳跃 次数 这里的「position + nums[i]」「nums长度」比较,取小的一位代表着该节点,最大能跳到哪里 然后我们再循环递归, 把该节点能跳的位置, 全部再递归一次

    如果说返回值等于 「true」 则代表本节点可以跳跃到终点, 记录本节点为 「dp[position]=1」 然后返回 「true」

    告诉上一层的节点, 你也可以通过我到达终点, 如下图所示

    efa3434a0ddd4929902f3c5f2920faef.png

    然后我们在看一下主函数

    func canJump(nums []int) bool {
     var dp = make([]int, len(nums))
     for i:=0; i<len(nums); i++ {
      dp[i]  = 0
     }
     dp[len(nums)-1] = 1
     ans:= recurrentJump(0, nums, &dp)
     return ans
    }
    

    这里, 没什么好讲的 就是初始化DP数组, 然后从下标0开始递归,

    ae0b9fd88e2ee77d4202310fe739f8a0.png

    递归以及动态规划的例子, 那么很明显的问题就是...效率极低 那么我们也没有什么思路可以让效率更高一些呢?

    顺便附带上GitHub的代码地址:LeetCode代码集合-持续更新中

    那么就来讲解另外2种思路「1. 动态规划」「2. 优化版动态规划(优化的是空间复杂度)」
    那我们就顺着这个思路往下走, 先来了解一下动态规划的算法去取代掉递归的算法

    动态规划版

    大家应该忘记,我们动态规划是需要一个表来存储以往的记录的吧,在这个算法里, 我们照样需要使用到这个数组

    b0d1cb6db6f01fbb9d7b1a01149c732d.png

    那么这个做法就是不使用递归, 用for循环做, 我们继续看代码

    func nonRecurrentJump(nums[] int, dp *[]int) bool{
     for i:= len(nums)-2;i>=0;i-- {
      maxJump := min(i+nums[i], len(nums)-1) //获取跳跃的步数
      for j := i + 1; j<=maxJump; j++{
       if (*dp)[j] == 1{
        (*dp)[i] = 1
        break
       }
      }
     }
     if (*dp)[0] == 1{
      return true
     }
     return false
    }
    

    在这个算法中, 跟递归的很像,唯一的差别就是在于在第二个For的循环中,我们不在是进入递归了, 而是直接查看「dp[j]==1」是否成立, 而这段代码中我们的思路其实转变了, 递归的思想是从头开始找, 而这段代码是从后往前

    dbabda6b188884adfea39c2aa61a93f4.png

    我们把问题简化一下, 从需要到达「dp[len(nums)-1]」 变成 「dp[len(nums)-i]」
    因为只要我们能到达 「2」 这个元素,说明我们可以达到终点, 那么从2往前的元素,只需要知道自己能不能到达2

    这就是这个算法的核心所在, 如果可以到达 则把 「dp[i]=1」 并且跳出内层循环,直到最后的结果应该是这样

    ba563e8d2ae183f0f649dd5017b69fc3.png

    「中间的 1 和 0 是不会被赋值的, 因为它们无法到达 2」

    最后, 我们判断 「dp[0]==1」 是否成立,如果成立代表从第0个出发,可以到达终点, 很明显我们比起递归而言要快将近「5倍」的时间

    优化动态规划

    我们在想深一层次, 我们真的需要这个数组吗?

    其实不需要对吧, 为什么? 因为我们一直都是在与 「最新」 的一个元素做对比, 那我们为什么还需要一个数组去存储呢?
    也就是说,我们可以去掉数组,将代码简化成这样...

    func optimizeNonRecurrentJump(nums[] int) bool{
    	aim := len(nums)-1 //终点
    	for i:= len(nums)-2;i>=0;i-- {
    		maxJump := min(i+nums[i], len(nums)-1)
    		for j := i + 1; j<=maxJump; j++{
    			if j == aim{
    				aim = i //更新最近的终点
    			}
    		}
    	}
    	if aim == 0{
    		return true
    	}
    	return false
    }

    新建 一个变量叫aim 永远拿它指向最后一个终点, 如果有比它更前的元素接近终点, 则更新它
    最后我们判断 「aim==0」 是否成立,就知道我们第一个元素是否可以到达终点拉

    f387ba51de2f725933c3f8c8c746910a.png

    这个方案比之前的省下了「0.2MB」的空间 又一次的进步。

    创作不易, 希望留下你们的小赞赞,是最大的鼓励
    展开全文
  • 动态规划什么是动态规划动态规划的大致思路是把一个复杂的问题转化成一个分阶段逐步递推的过程,从简单的初始状态一步一步递推,最终得到复杂问题的最优解。基本思想与策略编辑:由于动态规划解决的问题多数有重叠...
  • 五大常用算法——动态规划算法详解及经典例题

    万次阅读 多人点赞 2018-02-27 23:12:16
    一、基本概念 动态规划是运筹学中用于求解决策过程中的最优化数学方法。当然,我们在这里关注的是作为一种算法设计技术,作为一种使用多阶段决策过程最优的通用方法。 动态规划过程是:每次决策依赖于当前状态,又...
  • 一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。 假设问题是由交叠的子问题所构成,我们就能够用动态规划技术来解决它。一般来说,这种子问题出自对给定...
  • 动态规划什么是动态规划动态规划的大致思路是把一个复杂的问题转化成一个分阶段逐步递推的过程,从简单的初始状态一步一步递推,最终得到复杂问题的最优解。基本思想与策略编辑:由于动态规划解决的问题多数有重叠...
  • 动态规划是最重要、最经典算法之一,学好动态规划对我们十分重要,掌握动态规划对解决某些问题会起到事半功倍的效果。 动态规划: 特点: ①可以把原始问题划分为一系列子问题 ②求解每个子问题仅一次,并将其结果...
  • 动态规划课件及动态规划经典例题讲解和分析。主要介绍动态规划算法的思想和例题的分析。
  • 前言:各大公司关于动态规划的笔试题太多了,必须得掌握。上篇文章提过,使用动态规划的五大步骤: 1. 判题题意是否为找出一个问题的最优解 2. 将原问题分解为子问题 3. 从下往上分析问题 ,找出这些问题之间的...
  • 动态规划算法(Dynamic Programming)——从入门到精(fang)通(qi) 一、动态规划方法的基本步骤 1- 描述问题的最优解(optimal solution)结构特征; 2- 递归定义最优解值; 3- 自底向上计算最优解值; 4- 从已计算...
  • 文章目录 一、从实例的角度理解动态规划 二、动态规划经典例题...最终可得动态规划算法实现如下: def solution2(seq): res = local_min = local_max = seq[0] for i in range(1, len(seq)): local_max = max(local_...
  • 题目描述: 有N种物品,第i种物品的体积是c[i],价值是w[i],每种物品...朴素算法: 实际上01背包问题是特殊的多重背包问题。 代码如下: #include<iostream> #include<cstring> using namespace...
  • 动态规划经典例题java实现

    千次阅读 2018-01-16 10:07:21
    动态规划算法通常基于一个递推公式及一个或多个初始状态。当前子问题的解将由上一次子问题的解推出。使用动态规划来解题只需要多项式时间复杂度,因此它比回溯法、暴力法等要快许多。  解决动态规划问题的关键是要...
  • 合并石子-动态规划经典例题【问题描述】【输入格式】【输入样例】【输出样例】【算法分析】【源代码】 【问题描述】 在一个操场上一排地摆放着N堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆...
  • 动态规划(Dynamic Programming,DP)是一种解决一类最优问题的算法思想。简单来说,动态规划将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。需要注意的是,动态规划会将每个...
  • 算法竞赛入门经典动态规划初步 例题9-6 UVa11400 【算法竞赛入门经典动态规划初步 例题9-6 UVa11400 例题UVa11400 分析 样例实现代码 结果 例题UVa11400 Input Output For each test ...

空空如也

空空如也

1 2 3 4 5 ... 8
收藏数 160
精华内容 64
关键字:

动态规划算法经典例题