一、穷举法思想
在有限条件里,将所有符合条件的元素一一带入问题进行检验,从而得到检验通过的精确解。
举例一、
输出一个字符串里最长回文子串长度
思路:根据定义,根据条件(输入的字符串子串)带入问题一一检验,并且得到检验通过(最长的回文子串)的精确解。
拆解思路分析:
1、假设输入字符串:abcdcbc
2、检验所有子串
2.1、由字符a作为子串首字母遍历出所有的子串,并对每一个子串进行校验,判断是否为回文字符串。由此可以判断第一轮循环时间复杂度是O(n*m).注:m设为回文子串的检验时间复杂度。 如下图2-1
图2-1
2.2、由字符b作为子串首字母遍历出所有的子串,并对每一个子串进行校验,判断是否为回文字符串。由此可以判断第二轮轮时间复杂度是O(n*m).注:m设为回文子串的检验时间复杂度。 如下图2-2
图2-2
2.3、知道以最后一个字符为首字母遍历出所有的子串,并对每一个子串进行校验,判断是否为回文字符串,由此可见总体上需要有n次首字母为首遍历所有的子串,故总体的时间复杂度是O(n*n*m)
3、校验是否是回文字符串,验证方式如图3-1
每验证一个字符串,需要左右指针分别标识字符串的最左端和字符串的最右端的字符,当左右指针表示的字符相等,则左右指针相向运动一格,当左右指针标识的字符串不想等时则其不是回文字符串,当左指针位置 > 右指针位置则该字符串事回文字符串,所以可以认为验证回文字符串的时间复杂度为O(n/2) = O(n)。故m = O(n)。
可以看到穷举法做这道题整个的时间复杂度是O(n^2 * m) = O(n^3)。借助的额外空间也就是左右指针,故空间复杂度O(1)。
穷举法引发的思考:
穷举法虽然简单简单易懂,但是时间复杂度太高了,所以有什么方式可以将时间复杂度给降低?当然要思考这个问题,就需要去看时间都花在哪了
第一:遍历所有子串,花费时间复杂度:O(n ^ 2)。只有拿到子串才能知道是不是回文字符串,所以这一步看似少不了
第二:校验子串是否为回文字符串,花费时间复杂度:O(n)。由于校验子串bcdcb和子串cdc都花费了O(n)的时间复杂度,但是我们可以先验证cdc是回文子串,设为串A,那么字符串bcdcb则为bAb,由此可见只需要去校验首字符和尾字符是否相等就能知道bcdcb是否为回文字符串。不需要重新校验cdc串是否为回文字符串,故可以采取空间换时间的概念,先处理长度短的子串,再处理长度加1的子串,依次处理越来越长的子串。将每次处理结果记录下来,那么校验长串是否为回文子串,只需要去校验头、尾字符是否相等和短串处理的结果即可知道长串是否为回文字符串。所以时间可以缩减到O(1),当然代价就是空间复杂度提升。这也引出来了另一个算法的思想,就是动态规划。
二、动态规划法
当一个问题呈现某种规律求最优解,一个问题可以被拆解成多层子问题,并且上层问题可以依托所有下层问题的最优解从而得到上层最优解,如此下去,直到得到顶层也就是原问题的最优解。
还以输出一个字符串里最长回文子串长度为例子,来说明动态规划法的定义。 如图3所示
图3
红色标记:第四层abcd串由第二层的bc串是否是回文字符串和a,d字符是否相等来判断abcd是否是回文字符串,所以bc串不是回文字符串,那么abcd肯定不是回文字符串。
蓝色标记:第三层cdc是否是回文字符串,依靠第一层字符串d和首、尾字符是否相等判断是否为回文字符串,是则长度为1+2。第五层字符串bcdcb是否是回文字符串,依靠第三层字符串cdc和首、尾字符是否相等判断是否为回文字符串,如果是,则长度为cdc长度加2。第七层abcdcbc是否是回文字符串,依靠第五层字符串bcdcb和首、尾字符是否相等判断是否为回文字符串。
重要因素
"规律",当有符合一定规律的事物,一定可以通过表达式方程表示出来。所以列出表达式方程是使用动态规划最重要的一环。
使用动态规划解例题一
第一步:列出表达式方程。注,通过前面的穷举法可以分析出来,判断子串是否是回文字符串这一步是优化点,而优化的思想就是动态规范。所以isHw返回的就是是否是回文字符串,i表示子串的头,j表示子串的尾,故方程如图4所示
图4
第二步:实现方程(不展示代码,通过图来描述整个过程,重在理解,以下图中"T"表示true,"F"标识FALSE),s表示输入字符串假设为“abcdcbc”
动态规划还有一个关键点就是 空间换时间。图中方程的值isHW(i, j)是具有两个变量i,j(从第i个位置到第j个位置的子串)。所以该动态规划可以定义成二维的动态规划,可以使用二维数组来存储isHW(i, j)的值
所以代码实现先初始化一个二维数组
图5-1
根据方程进行数组的填充
4.1、首先填充i == j的情况,填充后如下图。
图5-2
最长的回文子串长度是1
4.2、再填充i == j -1的情况,此时代码中需要判定s[j] 是否等于s[i]。也就是判断图3中第二层每个字符串的左右两个字符是否相等,填充完毕可以得到下图
图5-3
最长的回文子串长度是1
4.3、此后接着填充 填充i == j - 2,i == j - 3的情况,此时会走到方程是的isHW(i + 1, j - 1) &&
s[i] == s[j] 。
当i == j - 2时 , 此时 i + 1 == j - 1。 故isHW(i + 1, j - 1)为4.1步所得到的值,都为ture,所以需要判断s[i] 是否等于s[j]
当i == j - 3时, 此时 (j - 1) - (i + 1) == 1。故isHW(i + 1,j - 1)为4.2步所得到的值,都为false,所以isHW(i, j)都为false
如下图所示
图5-4
最长的回文子串长度是3
4.4 、通过4.3的处理方式扫尾处理剩下的所有子串,可以得到下图
图5-5
最长的回文子串长度是5
由此通过动态规划求解出来的例题一最长回文子串长度是5
复杂度
时间复杂度O(n ^ 2)空间复杂度O(n ^ 2)
动态规划写法的优化
通过例题1可以看到使用动态规划的思想去代替穷举法的思想,确实时间复杂度降低了,由O(n^3) 降到了O(n^2),但是空间复杂度的开销由O(1)升到了O(n^2),看起来并不划算。
那么如何去优化呢?
动态规划方程式通过代码实现后,可以将一个问题通过平面图抽象出来代码的运行过程,那么就可以通过平面图去找到优化点,从而使问题更加直观。
我们知道动态规划一个问题可以被拆解成多层子问题,如何去减少子问题解的存储是动态规划优化的重要一环。
可以想一下能否得到最底层一个子问题的,就求出所有与该层相关的上层问题的解呢?
有,不妨将思路转变一下,如图6
图6
可以看到优化后可以由左下角的箭头一直遍历到右上角的箭头,可以设三个变量y表示isHW(i, j),z表当前回文子串长度,max表示回文子串的最大长度,s表示字符串。注:下面所说的元素指代途中二维数组中下标为:(i,j)标识的空间。
实力字符串:"abcdcbc"
分析步骤(由左下角的箭头一个个遍历到右上角的箭头,每个箭头的遍历方向跟随箭头方向)
第一个箭头:涉及到一个元素,那么 y = T, z = 1。max = 1
第二个箭头:涉及到一个元素,那么 y = F, z = 0。max = 1
第三个箭头:涉及到两个元素,那么处理第一个元素:x = T, z = 1,处理第二个元素时:x = x && s[0] == s[2] ,x = F, z = 1。max = 1
第四个箭头:涉及到两个元素,处理第一个元素:x = F,z = 0,后续元素不需要处理,都是F。max = 1
第五个箭头:涉及到三个元素,处理第一个元素:x = T,z = 1,处理第二个元素时:x = x && s[1] == s[3],x = F,z = 1。后续元素不需要处理,都是F。max = 1
第六个箭头:涉及到三个元素,处理第一个元素:x = F,z = 0,后续元素不需要处理,都是F。max = 1
第七个箭头:涉及到四个元素,处理第一个元素:x = T,z = 1,处理第二个元素时:x = x && s[2] == s[4],x = T,z = z + 2 = 3,max = max > z ? max : z, max = 3 。处理第三个元素时: x = x && s[1] == s[5],x = T,z = z + 2 = 5,max = max > z ? max : z, max = 5 。处理第四个元素时: x = x && s[0] == s[6],x = F,z = 5,max = max > z ? max : z, max = 5 。
........(依赖上面处理箭头的思路处理剩下的箭头)
最后可得Max = 5。
优化后,辅助空间上的成本就是x , z, max。空间复杂度:O(1),时间复杂度O(n)。
练习两道经典动态规划问题
1、爬楼梯问题
一个人爬楼梯,一次只能爬一阶或两阶,当总共有n阶楼梯时,有几种爬法。
1、分析题目,想想轮询法怎么实现:轮询法是从第一层楼梯开始走走一步或者两步的排列组合都记录了(可有可无)
2、轮询法的优化点:运动到某一点的所有走法记录下来,从而以该点为起点去算更高层的楼梯,从而不需要从父计算低点的走法。
3、列出方程表达式:该题与例题一的区别是,例题一是通过两个点i,j确定子串的,所以拥有两个变量决定子问题。而该题固定是从第一层到第n层,通过一个点n确定子串,所以拥有一个变量,故表达式是依据这一个变量进行展开的
4、解题思路:由于该方程式仅有一个变量n,所以通过一个一位数组进行处理即可
假设n = 10,则需要使用一维数组作为一个辅助空间
5、优化:根据抽象出来的一维数组和方程式,可以知道想求到第n层的爬法,只需要知道到第n-1层的爬法,和到第n - 2层的爬法相加即可。故只需要三个辅助空间j , k , l即可,j代表n- 2层的爬法,k代表 n - 1层的爬法,l代表n层的爬法。
可以理解为:
当计算第i层时,2 < i < n - 1。那么 j = k, k = l , l = k + l;
当计算第i + 1层时,3 < i + 1 < n。那么 j = k, k = l , l = k + l;
故最后可求得爬法为l。
最终时间复杂度为O(n),空间复杂度为O(1)。
2、背包问题(讲解完全背包)
有N种物品和一个容量为V的背包,每种物品都有无限件可用,第i件物品消耗的容量为Ci,价值为Wi,求解放入哪些物品可以使得背包中总价值最大。
1、分析题目,想想轮询法怎么实现:选择一个物品放进背包,有放和不放两种状态,故通过轮询法将背包填入n个物品的时间复杂度是O(2^n)。
2、可优化点,当选择放置一个物品i时,其可再放入的最大容量则为V-Ci,就相当于需要求解一个最大容量为V-Ci的背包放置的最大价值。所以V-Ci 容量的背包可以看成一个子问题。
3、列出表达式方程:可以看到背包背的最大价值取决于背包的容量V,和放置物品的数量,当数量越多背包容量越少,所以背包的容量和物品的重量是变量。列出表达式方程