一文学懂动态规划

前言

动态规划(dynamic programming,简称 dp)是工程中非常重要的解决问题的思想,从我们在工程中地图软件上应用的最短路径问题,再在生活中的在淘宝上如何凑单以便利用满减券来最大程度地达到我们合理薅羊毛的目的 ,很多时候都能看到它的身影。不过动态规划对初学者来说确实比较难,dp状态,状态转移方程让人摸不着头脑,网上很多人也反馈不太好学,其实就像我们之前学过的递归一样,任何算法的学习都是有它的规律和套路的,只要掌握好它的规律及解题的套路,再加上大量的习题练习,相信掌握它不是什么难事,本文将会用比较浅显易懂地讲解来帮助大家掌握动态规划这一在工程中非常重要的思想,相信看完后,动态规划的解题套路一定能手到擒来(文章有点长,建议先收藏再看,看完后一定会对动态规划的认知上升到一个台阶!)

本文将会从以下角度来讲解动态规划:

  • 什么是动态规划
  • 动态规划从入门到进阶
  • 再谈动态规划

什么是动态规划

以下是我综合了动态规划的特点给出的动态规划的定义:动态规划是一种多阶段决策最优解模型,一般用来求最值问题,多数情况下它可以采用自下而上的递推方式来得出每个子问题的最优解(即最优子结构),进而自然而然地得出依赖子问题的原问题的最优解。

  1. 多阶段决策,意味着问题可以分解成子问题,子子问题,......,也就是说问题可以拆分成多个子问题进行求解
  2. 最优子结构,在自下而上的递推过程中,我们求得的每个子问题一定是全局最优解,既然它分解的子问题是全局最优解,那么依赖于它们解的原问题自然也是全局最优解。
  3. 自下而上,怎样才能自下而上的求出每个子问题的最优解呢,可以肯定子问题之间是有一定联系的,即迭代递推公式,也叫「状态转移方程」,要定义好这个状态转移方程, 我们就需要定义好每个子问题的状态(DP 状态),那为啥要自下而上地求解呢,因为如果采用像递归这样自顶向下的求解方式,子问题之间可能存在大量的重叠,大量地重叠子问题意味着大量地重复计算,这样时间复杂度很可能呈指数级上升(在下文中我们会看到多个这样重复的计算导致的指数级的时间复杂度),所以自下而上的求解方式可以消除重叠子问题。

简单总结一下,最优子结构,状态转移方程,重叠子问题就是动态规划的三要素,这其中定义子问题的状态与写出状态转移方程是解决动态规划最为关键的步骤,状态转移方程如果定义好了,解决动态规划就基本不是问题了。

既然我们知道动态规划的基本概念及特征,那么怎么判断题目是否可以用动态规划求解呢,其实也很简单,当问题的定义是求最值问题,且问题可以采用递归的方式,并且递归的过程中有大量重复子问题的时候,基本可以断定问题可以用动态规划求解,于是我们得出了求解动态规划基本思路如下(解题四步曲)

  1. 判断是否可用递归来解,可以的话进入步骤 2
  2. 分析在递归的过程中是否存在大量的重复子问题
  3. 采用备忘录的方式来存子问题的解以避免大量的重复计算(剪枝)
  4. 改用自底向上的方式来递推,即动态规划

可能不少人看了以上的动态规划的一些介绍还是对一些定义如 DP 状态,状态转移方程,自底而上不了解,没关系 ,接下来我们会做几道习题来强化一下大家对这些概念及动态规划解题四步曲的理解,每道题我们都会分别用递归,递归+备忘录,动态规划来求解一遍,这样也进一步帮助大家来巩固我们之前学的递归知识

动态规划从入门到进阶

入门题:斐波那契数列

接下来我们来看看怎么用动态规划解题四步曲来解斐波那契数列

注:斐波那契数列并不是严格意义上的动态规划,因为它不涉及到求最值,用这个例子旨在说明重叠子问题与状态转移方程

1、判断是否可用递归来解 显然是可以的,递归代码如下

int fibonacci(int n) {
    if (n == 0||n==1) return 1;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

2、分析在递归的过程中是否存在大量的重复子问题

怎么分析是否有重复子问题,画出递归树

可以看到光是求 f(6),就有两次重复的计算, f(4) 求解了两次,f(3) 求解了两次,时间复杂度是指数级别,递归时间复杂度怎么看,解决每个子问题需要的时间乘以子问题总数,每个子问题需要的时间即 f(n) = f(n-1) + f(n-2) 只做了一次加法运算,子问题的个数有多少呢,每个问题一分为二,是个二叉树,可以看到第一层 1 个,第二层 2 个,第三层 4 个,即 1 + 2 + 2^2 + .... 2^n,所以总的来说时间复杂度是)O(2^n),是指数级别

注:自顶向下: 这种从 原问题展开子问题进行求解的方式

3、采用备忘录的方式来存子问题的解以避免大量的重复计算 既然以上中间子问题中存在着大量的重复计算,那么我们可以把这些中间结果给缓存住(可以用哈希表缓存),如下

vector<int> m(n+1,0);
int fibonacci(int n) {
    if (n ==0||n==1) return 1;
    if (m[n]) 
        return m[n];
    return m[n] = fibonacci(n - 1) + fibonacci(n - 2);
}

这么缓存之后再看我们的递归树

 

可以看到通过缓存中间的数据,做了大量地剪枝的工作,同样的f(4),f(3),f(2),都只算一遍了,省去了大量的重复计算,问题的规模从二叉树变成了单链表(即 n),时间复杂度变成了 O(n),不过由于哈希表缓存了所有的子问题的结果,空间复杂度是 O(n)。

4、改用自底向上的方式来递推,即动态规划 我们注意到如下规律:

只要依次自底向上求出 f(3),f(4),...,自然而然地就求出了 f(n)

 注:自底向上:从最终地不能再分解的子问题根据递推方程(f(n) = f(n-1) + f(n-2))逐渐求它上层的问题,上上层问题,最终求得一开始的问题

f(n) 就是定义的每个子问题的状态(DP 状态),f(n) = f(n-1) + f(n-2) 就是状态转移方程,即 f(n) 由 f(n-1), f(n-2) 这两个状态转移而来,由于每个子问题只与它前面的两个状态,所以我们只要定义三个变量,自底向上不断循环迭代即可,如下

int f(int n) {
    if (n == 0||n==1)
       return 1;
    if(n == 2)
       return 2;
    int result = 0;
    int pre = 1;
    int next = 2;
  
    for (int i = 3; i < n + 1; ++i) {
        result = pre + next;
        pre = next;
        next = result;
    }
    return result;
}

 这样时间复杂度虽然还是O(n),但空间复杂度只由于只定义了三个变量(result,pre,next)所以是常量 O(1)。

通过简单地斐波那契的例子,相信大家对自底向上,DP 状态, DP 转移方程应该有了比较深入地认识,细心的同学一定发现了最优子结构怎么没有,因为前面我们也说了,斐波那契数列并不是严格意义上的动态规划,只是先用这个简单地例子来帮助大家了解一下一些基本的概念。在之后的习题中我们将会见识到真正的动态规划。

经典入门:三角形的最小路径和

 如图示,以上三角形由一连串的数字构成,要求从顶点 2 开始走到最底下边的最短路径,每次只能向当前节点下面的两个节点走,如 3 可以向 6 或 5 走,不能直接走到 7。

如图示:从 2 走到最底下最短路径为  2+3+5+1 = 11,即为我们所求的

首先我们需要用一个二维数组来表示这个三个角形的节点,用二维数组显然可以做到, 第一行的 2 用 a[0][0] 表示,第二行元素 3, 4 用 a[1][0],a[1][1],依此类推。

定义好数据结构之后,接下来我们来看看如何套用我们的动态规划解题套路来解题

1、 判断是否可用递归来解

如果用递归,就要穷举所有的路径和,最后再求所有路径和的最小值,我们来看看用递归怎么做。

对于每个节点都可以走它的左或右节点,假设我们定义 traverse(i, j) 为节点 a[i][j] 下一步要走的节点,则可以得出递归公式的伪代码如下


traverse(i, j) =min {
    traverse(i+1, j);    向节点i,j 下面的左节点走一步
    traverse(i+1, j+1);    向节点i,j 下面的右节点走一步
}

对于每个节点,要么向左或向右,每个问题都分解成了两个子问题,和斐波那契数列一样,如果画出递归树也是个二叉树,所以时间复杂度是 O(2^n),也是指数级别。

2、分析在递归的过程中是否存在大量的重复子问题

为啥时间复杂度是指数级别呢,我们简单分析一下:

 对于节点 3 和 4 来说,如果节点 3 往右遍历, 节点 4 往左遍历,都到了节点 5,节点 5 往下遍历的话就会遍历两次,所以此时就会出现重复子问题

3、改用自底向上的方式来递推,即动态规划

重点来了,如何采用自底向上的动态规划来解决问题呢? 我们这么来看,要求节点 2 到底部边的最短路径,只要先求得节点 3 和 节点 4 到底部的最短路径值,然后取这两者之中的最小值再加 2 不就是从 2 到底部的最短路径了吗,同理,要求节点 3 或 节点 4 到底部的最小值,只要求它们的左右节点到底部的最短路径再取两者的最小值再加节点本身的值(3 或 4)即可。

我们知道对于三角形的最后一层节点,它们到底部的最短路径就是其本身,于是问题转化为了已知最后一层节点的最小值怎么求倒数第二层到最开始的节点到底部的最小值了。先看倒数第二层到底部的最短路径怎么求

同理,第二层对于节点 3 ,它到最底层的最短路径转化为了 3 到 7, 6 节点的最短路径的最小值,即 9, 对于节点 4,它到最底层的最短路径转化为了 4 到 6, 10 的最短路径两者的最小值,即 10。

 接下来要求 2 到底部的路径就很简单了,只要求 2 到节点 9 与 10 的最短路径即可,显然为 11。

于是最终的 11 即为我们所求的值,接下来我们来看看怎么定义 DP 的状态与状态转移方程。 我们要求每个节点到底部的最短路径,于是 DP 状态 DP[i,j] 定义为 i,j 的节点到底部的最小值,DP状态转移方程定义如下:

DP[i,j] = min(DP[i+1, j], D[i+1, j+1]) + triangle[i,j]

 这个状态转移方程代表要求节点到最底部节点的最短路径只需要求左右两个节点到最底部的最短路径两者的最小值再加此节点本身!

DP 状态 DP[i,j] 有两个变量,需要分别从下而上,从左到右循环求出所有的 i,j, 有了状态转移方程求出代码就比较简单了,如下

int traverse(vector<vector<int>> triangle) {
    int ROW=triangle.size();
    for (int i = ROW - 2; i >= 0; --i) {
        for (int j = 0; j < triangle[j].size();++j) {
             triangle[i][j] += min(triangle[i+1][j],triangle[i+1][j+1]);
        }
    }
    return triangle[0][0];
}

我们再来谈谈最优子结构,在以上的推导中我们知道每一层节点到底部的最短路径依赖于它下层的左右节点的最短路径,求得的下层两个节点的最短路径对于依赖于它们的节点来说就是最优子结构,最优子结构对于子问题来说属于全局最优解,这样我们不必去求节点到最底层的所有路径了,只需要依赖于它的最优子结构即可推导出我们所要求的最优解,所以最优子结构有两层含义,一是它是子问题的全局最优解,依赖于它的上层问题只要根据已求得的最优子结构推导求解即可得全局最优解,二是它有缓存的含义,这样就避免了多个依赖于它的问题的重复求解(消除重叠子问题)。

总结:仔细回想一下我们的解题思路,我们先看了本题是否可用递t归来解,在递归的过程中发现了有重叠子问题,于是我们又用备忘录来消除递归中的重叠子问题,既然我们发现了此问题可以用递归+备忘录来求解,自然而然地想到它可以用自底向上的动态规划来求解。是的,求解动态规划就按这个套路来即可,最重要的是要找出它的状态转移方程,这需要在自下而上的推导中仔细观察。

进阶:凑零钱

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。输入: coins = [1, 2, 5], amount = 11,输出: 3  解释: 11 = 5 + 5 + 1 输入: coins = [2], amount = 3,输出: -1
由以上的四步曲我们可以得出其状态转移方程如下:

DP[i] =  min{ DP[ i - coins[j] ] + 1 } = min{ DP[ i - coins[j] ]} + 1,  其中 j 的取值为 0 到 coins 的大小,i 代表取了 coins[j] 这一枚硬币。

由此可以写出如下动态规划代码:

int exchangeDP(int amount, vector<int> coins) {
    vector<int> dp(amount+1,0);
    // 初始化每个值为 amount+1,这样当最终求得的 dp[amount] 为 amount+1 时,说明问题无解
    for (int i = 0; i < amount + 1; i++) {
        dp[i] = amount + 1;
    }

    // 0 硬币本来就没有,所以设置成 0
    dp[0] = 0;
    for (int i = 0; i < amount + 1; ++i) 
        for (int j = 0; j < coins.size(); ++j) 
            if (i >= coins[j])
                dp[i] = min(dp[i- coins[j]], dp[i]) + 1;

    if (dp[amount] == amount + 1) 
        return -1;
    return dp[amount];
}

凑零钱这道题还可以用另外一道经典的青蛙跳台阶的思路来考虑,从最底部最少跳多少步可以跳到第 11 阶,一次可以跳 1,2,5步 。由此可知最后一步一定是跳 1 或 2 或 5 步,于是如果用 f(n) 代表跳台阶 n 的最小跳数,则问题转化为了求 f(n-1),f(n-2) ,f(n-5)的最小值。

 如图示:最后一跳一定是跳 1 或 2 或 5 步,只要求  f(n-1),f(n-2) ,f(n-5)的最小值即可


写出递推表达式, 即:

 f(n) = min{ f(n-1),f(n-2),f(n-5)} + 1 (1代表最后一跳)

我们的 DP 状态转移方程对比一下,可以发现两者其实是等价的,只不过这种跳台阶的方式可能更容易理解。

总结

本文通过几个简单的例子强化了大家动态规划的三要素:最优子结构,状态转移方程,重叠子问题的理解,相信大家对动态规划的理解应该深刻了许多,怎么看出是否可以用动态规划来解呢,先看题目是否可以用递归来推导,在用递归推导的过程如果发现有大量地重叠子问题,则有两种方式可以优化,一种是递归 + 备忘录,另一种就是采用动态规划了,动态规划一般是自下而上的, 通过状态转移方程自下而上的得出每个子问题的最优解(即最优子结构),最优子结构其实也是穷举了所有的情况得出的最优解,得出每个子问题的最优解后,也就是每个最优解其实是这个子问题的全局最优解,这样依赖于它的上层问题根据状态转移方程自然而然地得出了全局最优解。动态规划自下而上的求解方式还有一个好处就是避免了重叠子问题,因为依赖于子问题的上层问题可能有很多,如果采用自顶而下的方式来求解,就有可能造成大量的重叠子问题,时间复杂度会急剧上升。

更多动态规划题:

九大背包问题:可见背包九讲,绝对牛

最长公共子序列

最长不下降子序列

买卖股票的最佳时机

以及poj上:

1015 Jury Compromise 
1029 False coin 
1036 Gangsters 
1037 A decorative fence 
1038 Bugs Integrated, Inc. 
1042 Gone Fishing 
1050 To the Max 
1062 昂贵的聘礼 
1074 Parallel Expectations 
1080 Human Gene Functions 
1088 滑雪 
1093 Formatting Text 
1112 Team Them Up! 
1141 Brackets Sequence 
1143 Number Game 
1157 LITTLE SHOP OF FLOWERS 
1159 Palindrome 
1160 Post Office 
1163 The Triangle 
1170 Shopping Offers 
1178 Camelot 
1179 Polygon 
1180 Batch Scheduling 
1185 炮兵阵地 
1187 陨石的秘密 
1189 钉子和小球 
1191 棋盘分割 
1192 最优连通子集 
1208 The Blocks Problem 
1239 Increasing Sequences 
1240 Pre-Post-erous! 
1276 Cash Machine 
1293 Duty Free Shop 
1322 Chocolate 
1323 Game Prediction 
1338 Ugly Numbers 
1390 Blocks 
1414 Life Line 
1432 Decoding Morse Sequences 
1456 Supermarket 
1458 Common Subsequence 
1475 Pushing Boxes 
1485 Fast Food 
1505 Copying Books 
1513 Scheduling Lectures 
1579 Function Run Fun 
1609 Tiling Up Blocks 
1631 Bridging signals 2分+DP NLOGN 
1633 Gladiators 
1635 Subway tree systems 
1636 Prison rearrangement 
1644 To Bet or Not To Bet 
1649 Market Place 
1651 Multiplication Puzzle 
1655 Balancing Act 
1661 Help Jimmy 
1664 放苹果 
1671 Rhyme Schemes 
1682 Clans on the Three Gorges 
1690 (Your)((Term)((Project))) 
1691 Painting A Board 
1692 Crossed Matchings 
1695 Magazine Delivery 
1699 Best Sequence 
1704 Georgia and Bob 
1707 Sum of powers 
1712 Flying Stars 
1714 The Cave 
1717 Dominoes 
1718 River Crossing 
1722 SUBTRACT 
1726 Tango Tango Insurrection 
1732 Phone numbers 
1733 Parity game 
1737 Connected Graph 
1740 A New Stone Game 
1742 Coins P 
1745 Divisibility 
1770 Special Experiment 
1771 Elevator Stopping Plan 
1776 Task Sequences 
1821 Fence 
1837 Balance 
1848 Tree 
1850 Code 
1853 Cat 
1874 Trade on Verweggistan 
1887 Testing the CATCHER 
1889 Package Pricing 
1920 Towers of Hanoi 
1926 Pollution 
1934 Trip 
1936 All in All 
1937 Balanced Food 
1946 Cow Cycling 
1947 Rebuilding Roads 
1949 Chores 
1952 BUY LOW, BUY LOWER 
1953 World Cup Noise 
1958 Strange Towers of Hanoi 
1959 Darts 
1962 Corporative Network 
1964 City Game 
1975 Median Weight Bead 
1989 The Cow Lineup 
2018 Best Cow Fences 
2019 Cornfields 
2029 Get Many Persimmon Trees 
2033 Alphacode 
2039 To and Fro 
2047 Concert Hall Scheduling 
2063 Investment 
2081 Recaman's Sequence 
2082 Terrible Sets 
2084 Game of Connections 
2127 Greatest Common Increasing Subsequence 
2138 Travel Games 
2151 Check the difficulty of problems 
2152 Fire 
2161 Chandelier 
2176 Folding 
2178 Heroes Of Might And Magic 
2181 Jumping Cows 
2184 Cow Exhibition 
2192 Zipper 
2193 Lenny's Lucky Lotto Lists 
2228 Naptime 
2231 Moo Volume 
2279 Mr. Young's Picture Permutations 
2287 Tian Ji -- The Horse Racing 
2288 Islands and Bridges 
2292 Optimal Keypad 
2329 Nearest number - 2 
2336 Ferry Loading II 
2342 Anniversary party 
2346 Lucky tickets 
2353 Ministry 
2355 Railway tickets 
2356 Find a multiple 
2374 Fence Obstacle Course 
2378 Tree Cutting 
2384 Harder Sokoban Problem 
2385 Apple Catching 
2386 Lake Counting 
2392 Space Elevator 
2397 Spiderman 
2411 Mondriaan's Dream 
2414 Phylogenetic Trees Inherited 
2424 Flo's Restaurant 
2430 Lazy Cows 
2915 Zuma 
3017 Cut the Sequence 
3028 Shoot-out 
3124 The Bookcase 
3133 Manhattan Wiring 
3345 Bribing FIPA 
3375 Network Connection 
3420 Quad Tiling ?

发布了393 篇原创文章 · 获赞 20 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40991687/article/details/104436921
今日推荐