7号晚听了邹博一次微课,正好是本身最近正在死磕的动态规划,因此搬好小板凳听邹博讲解动态规划。现将内容整理以下:html
内容主要分为两个部分:算法
1. 动态规划和贪心的认识——工具:马尔科夫过程数组
2. 动态规划,经过3个DP中的经典问题详细讲解dom
1)最长递增子序列LISide
2)格子取数/走棋盘问题及应用工具
3)找零钱/背包问题post
正题开始。首先,人们认识事物的方法有三种:经过概念(即对事物的基本认识)、经过判断(即对事物的加深认识)、和推理(对事物的深层认识)。其中,推理又包含概括法和演绎法。(这些从初中高中一直到大学咱们都是一直在学习的,关键是理解)学习
概括法是从特殊到通常,属于发散思惟;(如:苏格拉底会死;张三会死;李四会死;王五会死……,他们都是人。因此,人都会死。)优化
演绎法是从通常到特殊,属于汇聚思惟。(如:人都会死的;苏格拉底是人。因此,苏格拉底会死。)url
那么,如何用概括法解决数学问题,进行应用呢?
已知问题规模为n的前提A,求解一个未知解B。(咱们用An表示“问题规模为n的已知条件”)
此时,若是把问题规模降到0,即已知A0,能够获得A0->B.
然而,Ai与Ai+1每每不是互为充要条件,随着i的增长,有价值的前提信息愈来愈少,咱们没法仅仅经过上一个状态获得下一个状态,所以能够采用以下方案:
上述两种状态转移图以下图所示:
下面经过分析几个经典问题来理解动态规划。
实例一:最长递增子序列(Longest Increasing Subsequence)。
问题描述。给定长度为N的数组A,计算A的最长单调递增的子序列(不必定连续)。如给定数组A{5,6,7,1,2,8},则A的LIS为{5,6,7,8},长度为4.
思路:由于子序列要求是递增的,因此重点是子序列的起始字符和结尾字符,所以咱们能够利用结尾字符。想到:以A[0]结尾的最长递增子序列有多长?以A[1]结尾的最长递增子序列有多长?……以A[n-1]结尾的最长递增子序列有多长?分析以下图所示:
显然,若是ai>=aj,则能够将ai放到b[j]的后面,获得比b[j]更长的子序列。从而:b[i] = max{b[j]}+1. s.t. A[i] > A[j] && 0 <= j < i.
因此计算b[i]的过程是,遍历b[i]以前的全部位置j,找出知足关系式的最大的b[j].
获得b[0...n-1]以后,遍历全部的b[i]找到最大值,即为最大递增子序列。 总的时间复杂度为O(N2).
我实现的Java版代码为:
publi int LIS(int[] A) { if(A == null || A.length == 0) return 0; int[] b = new int[A.length]; b[0] = 1; int result = 1; for(int i=1; i<A.length; i++) { int max = -1; for(int j=0; j<i; j++) { if(A[j] < A[i] && b[j] > max) max = b[j]; } b[i] = max + 1; result = Math.max(result, b[i]); } return result; }
进而,若是不只是求LIS的长度,而要求LIS自己呢?咱们能够经过记录前驱的方式,从该位置找到其前驱,进而找到前驱的前驱……
Java代码以下:
使用动态规划方法的到O(N2)的时间复杂度算法,可否有更优的方法呢?
最开始,缓冲区里为空;
看到了字符“1”,添加到缓冲区的最后,即缓冲区中是“1”;
看到了字符“4”,“4”比缓冲区的全部字符都大,所以将“4”添加到缓冲区的最后,获得“14”;
看到了字符“6”,“6”比缓冲区的全部字符都大,所以将“6”添加到缓冲区的最后,获得“146”;
看到了字符“2”,“2”比“1”大,比“4”小,所以将“4”直接替换成“2”,获得“126”;
看到了字符“8”,“8”比缓冲区的全部字符都大,所以将“8”添加到缓冲区的最后,获得“1268”;
看到了字符“9”,“9”比缓冲区的全部字符都大,所以将“9”添加到缓冲区的最后,获得“12689”;
看到了字符“7”,“7”比“6”大,比“8”小,所以将“8”直接替换成“7”,获得“12679”;
如今,缓冲区的字符数目为5,所以,数组A的LIS的长度就是5!
这样,时间复杂度变为每次都在一个递增的序列中替换或插入一个新的元素,因此为O(nlogn)。
代码为:
但后来我分析了这种方法只能获得长度,不能获得子序列自己。(老师上课时提示说考虑序列长度变化的时候,对于示例数组{1,4,6,2,8,9,7}来讲能够解决,即当序列变长的时候,元素1,4,6,8,9正好是最终的字长递增子序列;当若是原数组是{10,9,2,5,3,7,101,18}时,就不是这么回事了。目前我没有找到求解子序列自己的方法,留做之后思考。)
实例二:格子取数/走棋盘问题
问题描述。给定一个m*n的矩阵,每一个位置是一个非负整数,从左上角开始放一个机器人,它每次只能朝右和下走,走到右下角,求机器人的全部路径中,总和最小的那条路径。以下图所示,其中图中所示的彩色方块是已知的某些非负整数值。
考虑通常状况下位于机器人位于某点(x, y)处,那么它是怎么来的呢?只可能来自于左边或者上边。即:
dp[x, y] = min(dp[x-1, y], dp[x, y-1]) + a[x, y],其中a[x, y]是棋盘中(x, y)点的权重取值。
而后考虑位于最左边一列与左上边的一行,获得全部的状态转移方程为:
因此,代码以下:
观察状态转移方程发现,每次更新(x, y),只须要最多知道上一行便可,不必知道更早的数据。凡是知足这样条件的动态规划问题,均可以用“滚动数组”的方式作空间上的优化。
使用滚动数组的状态转移方程如上图所示。
代码以下:
实例三:找零钱问题/0-1背包问题
问题描述。给定某不超过100万元的现金总额,兑换成数量不限的100、50、20、十、五、二、1元的纸币组合,共有多少种组合?
思路:此问题涉及两个类别:面值和总额。因此咱们定义dp[i][j]表示使用小于等于i的纸币,凑成j元钱,共有多少种组合方法。好比dp[100][500]表示使用面值不大于100的纸币,凑出500块钱,共有多少种组合方法。
进一步思考,若是面值都是1元的,则不管总额多少,可行的组合数都为1.好比只用1元的纸币凑出100元,显然只有一种组合方法。那么若是多出一种面值呢?组合数有什么变化?
回到dp[100][500],既然用小于等于100的纸币凑出500块钱,则组合中只会要么包含至少一张100块的纸币,要么不包含100块的纸币。因此咱们能够分红两种状况考虑:
1)若是没有包括100元,则用到的最大面值可能为50元,即便用面值小于等于50的纸币,凑出500块钱,表示形式为:dp[50][500];
2)若是必须包含100元,怎么计算呢?既然至少包含100元,咱们先拿出100块钱,则还须要凑出400块钱便可完成。用小于或等于100元的纸币凑出400块钱,表示形式为dp[100][400];
将二者综合起来为:dp[100][500] = dp[50][500] + dp[100][400];
为了方便表示,咱们定义纸币面值为一个数组:dom[] = {1,2,5,10,20,50,100},这样dom[i]和dom[i-1]就表示相邻的纸币面额了。i的意义从面值变成了面值下标。
根据上面分析,对于通常状况,咱们有dp[i][j] = dp[i-1][j] + dp[i][j-dom[i]]. ]有了通常状况,在考虑两种特殊状况:
若是dp[i][0]应该返回啥?dp[i][0]表示用小于等于i的纸币,凑出0块钱,咱们能够定义这种状况的值为1;
若是dp[0][j]应该返回啥?dp[0][j]表示用小于等于0的纸币,凑出j块钱,咱们能够定义这种状况的值为1.
再看dp[100][78],用小于等于100元的纸币凑出78块钱,这时组合中必定不会包含100块的纸币,所以dp[100][78] = dp[50][78],即当j < dom[i]时,dp[i][j] = dp[i-1][j]。
这样整个dp的过程就出来了:
代码为:
总结,何时适合用动态规划呢?
总之,动态规划只是一种解决问题的思路,要灵活运用这种方法,多作练习,就能很快找到灵感了。