动态规划总结
动态规划
问题介绍
大致做法:大问题分解为小问题,存储子问题的解避免重复计算。
最优子结构:原问题的最优解是从子问题的最优解构造来的
重叠子问题:相同的问题会被多次计算
无后效性:问题的当前状态的最优解不受其未来状态的影响
存储子问题的解:通常使用表格(数组或者哈希表)来存储子问题的解
计算顺序:子问题间存在依赖性,要保证计算一个子问题前,其依赖的所有子问题已经解决
代码实际概念
状态:子问题的定义,通常用一个或者多个变量来表示
状态转移方程:描述如何从已知子问题的解来计算另一个子问题的解的公式
边界条件:子问题的基本情况,常用于递归算法的基准情况
动态规划表:通常是一个表格,用来存储子问题的解,避免重复计算
何时应用动态规划
- 问题是否可以被分解为子问题? 这意味着问题可以递归地解决。
- 子问题之间是否存在重叠? 如果是,每个子问题是否在整个过程中被多次计算?
- 问题是否具有最优子结构? 即你可以通过组合子问题的最优解来得到原问题的最优解。
经典问题
- 背包问题:求解如何在一个限定容量的背包中放入不同物品使得价值最大化。
- 最长公共子序列问题:找到两个字符串的最长公共子序列。
- 矩阵路径问题:求解从矩阵的一个角到另一个角的最短路径、最大路径等。
- 股票买卖问题:给定一段时间的股票价格,求最大利润。
- 编辑距离问题:最少的操作次数将一个字符串转换为另一个字符串。
问题通解
第一步、描述决策、定义状态、建立dp表、
确定dp表的下标含义和大小
第二步、思考子问题条件(利用反推)推导状态转移方程、
子问题可以简单粗暴由原问题更改为中间值i
思考子问题由什么条件来,进而推导状态转移方程
第三步、确定边界条件(初始条件)
第四步、确定状态转移顺序——
备忘录法,自顶向下,递归;
dp数组法,自底向上,递归消除;
是否可能覆盖需要的数据
第五步、举例推导dp数组
个人思考
确定使用动态规划要看最优子结构,也就是每一步的决策都能依赖于前面部分解的结果,站在第i步看能够发现i的结果依赖于先前的哪些结果,能够发现问题能够被分解为子问题,子问题存在对子问题的子问题的依赖结构。即先前决策影响后续决策且依赖关系确定的题目都用动态规划。
确定状态是什么也很重要,对状态的定义一定要自己想清楚,dp表往往就是问题要求的答案。
在确定状态的定义和dp表的建立后(也就是第二步),从某个i条件开始思考,先反推什么条件会推导到i条件,确定下来什么条件能够推导到i条件后就可以依据最优情况构建状态转移方程了。
下标:由于数组第0位是有数据的,而正常数数的时候是从1开始的,所以处理下标的时候要小心,思考问题时从正常数数的角度出发即可,所以在定义dp表(经常看到dp表大小为(n+1)*(cap+1),0那位都不care)、状态转移方程的时候都是从正常数数出发开始计算的,初始化dp表的时候可能需要稍微考虑一下0那一位
背包问题-价值最大化,硬币问题-统计方案数量
经典题目
基础问题
斐波那契数
思路
这道题目已经把所有需要的条件都给我们了。
首先定义状态,建立
dp表
dp[i]
表示第i
个斐波那契数的数值定义子问题,推导递推公式
dp[i]
由dp[i-1]
和dp[i-2]
转移而来,题目已经给出**dp[i] = dp[i - 1] + dp[i - 2]
**确定边界条件
题目已经给出,dp[0]=0,dp[1]=1
确定转移顺序
dp[i]
由dp[i-1]
和dp[i-2]
转移而来,转移顺序为从前往后空间优化
发现
dp[i]
只依赖于前两次计算出来的dp[i-1]
和dp[i-2]
,先前的计算结果都不需要,因此可以用两个变量来承接
代码
dp表
1 |
|
空间优化
1 |
|
递归,没有记忆就、状态,重复计算
1 |
|
爬楼梯
思路
建立
dp
表dp[i]
表示爬到第i
台阶的方法数推导状态转移方程
要爬到第
i
级台阶,可以从i-2
和i-1
爬上来,方法数应该取两者累和,dp[i]=dp[i-2]+dp[i-1]
确定边界条件
跳上第一、二个台阶的方法数,
dp[1]=1,dp[2]=2
确定状态转移顺序
从0开始往后
空间优化
发现
dp[i]
只依赖于前两次计算出来的dp[i-1]
和dp[i-2]
,先前的计算结果都不需要,因此可以用两个变量来承接
代码
一维空间的dp表
1 |
|
空间优化,同时把n较小(1,2,3)的情况进行了合并
1 |
|
使用最小花费爬楼梯
思路
建立
dp
表dp[i]
表示登上第i
个台阶的最低花费,dp
表长度为n+1
,从0一直到n推导状态转移方程
登上第
i
个阶梯,可以由第i-2
个阶梯支付第i-2个台阶的费用,爬2个阶梯;也可以由第i-1
个阶梯,支付第[i-1]个阶梯的费用,爬1个阶梯。fp表的定义可得,两者应该取最小的,dp[i]=min(dp[i-2]+cost[i-2], dp[i-1]+cost[i-1])
确定边界条件
可以选择从下标为 0 或 1 的元素作为初始阶梯,则dp[0]=0,dp[1]=0
确定转移顺序
从0开始往后
空间优化
发现
dp[i]
只依赖于前两次计算出来的dp[i-1]
和dp[i-2]
,先前的计算结果都不需要,因此可以用两个变量来承接
其实还有其他角度改变这道题的思路,如反向爬楼梯(从楼梯顶爬到0或1),逆序思想挺厉害的,有时候能够简化问题。
代码
按照题意实现的动态规划
1 |
|
空间优化
1 |
|
反向爬楼梯,这题不太推荐
1 |
|
不同路径
思路
dfs搜索,先略过
动态规划
确定dp表
dp[i][j]
表示从(0,0)
出发到(i,j)
的路径数,dp表大小为m*n
,最终到达(m-1,n-1)
。此处没有在左上方采用多添加一行和一列来简化初始条件的做法。推导状态转移方程
(i,j)
可以由(i-1,j)
和(i,j-1)
走一步到达,路径数为两者之和,即dp[i][j]=dp[i-1][j]+dp[i][j-1]
确定临界条件
要想到达首行的位置,只能从原点不断向右移动;同样要想到达首列的位置,只能从原点不断向下移动。所以首行和首列的位置只有一种到达路径,即
dp[i][0]=1,dp[0][j]=1
确定状态转移顺序
dp[i][j]
是从其左方和上方推导而来,只要从左到右一层一层遍历即可空间优化
dp[i][j]
只依赖于dp[i-1][j]
和dp[i][j-1]
,因此可以只用一维数组来承接每一层的计算结果。在计算新的一层时,上层的结果都还保留在数组中,也就是说dp[i-1][j]
已经存在dp[j]
了,根据状态转移方程,还差dp[i][j-1]
,dp[i][j-1]
刚好刚计算过是dp[j-1]
,故dp[j]+=dp[j-1]
。临界条件是计算第一层
dp[i]=1
排列组合
C(m+n-2,m-1)秒了
代码
动态规划
1 |
|
排列组合
1 |
|
不同路径2
多了障碍物,但是思路类似,数学用不了了,用动态规划
思路
确定dp表
dp[i][j]
表示从(0,0)
出发到(i,j)
的路径数,dp表大小为m*n
,最终到达(m-1,n-1)
。此处没有在左上方采用多添加一行和一列来简化初始条件的做法。推导状态转移方程
当
(i,j)
不是1即没有阻挡时,(i,j)
可以由(i-1,j)
和(i,j-1)
走一步到达,路径数为两者之和,即dp[i][j]=dp[i-1][j]+dp[i][j-1]
当
(i,j)
是1,有阻挡时,永远不可能到达,dp[i][j]=0
确定临界条件
要想到达首行的位置,只能从原点不断向右移动;同样要想到达首列的位置,只能从原点不断向下移动。所以首行没障碍的点
dp[i][0]=1
,有障碍的该点及后续的位置dp[i][0]=0
。首列同理,没障碍的点dp[0][j]=1
,有障碍的该点及后续的位置dp[0][j]=0
状态转移顺序
dp[i][j]
是从其左方和上方推导而来,只要从左到右一层一层遍历即可空间优化
dp[i][j]
只依赖于dp[i-1][j]
和dp[i][j-1]
,因此可以只用一维数组来承接每一层的计算结果。在计算新的一层时,上层的结果都还保留在数组中,也就是说dp[i-1][j]
已经存在dp[j]
了,根据状态转移方程,还差dp[i][j-1]
,dp[i][j-1]
刚好刚计算过是dp[j-1]
,故dp[j]+=dp[j-1]
。临界条件是计算第一层时,无障碍
dp[i]=1
,有障碍后续dp[i]=0
代码
动态规划
1 |
|
空间优化
1 |
|
整数拆分
从拆分的角度想,一个数被拆成i和j后,i和j仍然可以往下拆解,因此就形成了子问题,可以采用动态规划来实现
思路
动态规划
确定dp表
dp[i]
表示分拆数字i可以得到的最大乘积推导状态转移方程
一个数可以被拆分成两个数,也就是
(i-j)*J
,j能被继续拆分,取最大的就是(i-j)*dp[j]
,为满足比较时的存储问题,故dp[i] = max(dp[i], (i - j) * j, dp[i - j] * j)
确定边界条件
dp[0]和dp[1]其实都毫无意义,不纠结让它为0,只初始化dp[2]=1
确定转移顺序
从小到大,j从1开始枚举到i-1,但可以根据数学规律优化为i/2,拆成m个近似数组的子数 相乘才是最大的,至少拆成两个数,j最差取一半。
贪心
以后再说
代码
1 |
|
背包问题
0-1背包
给定n
个物品,第i
个物品的重量为wgt[i-1]
,价值为val[i-1]
,和一个容量为cap
的背包,每个物品只能选择一次,问在不超过背包容量下能放入物品的最大价值。
- 首先定义状态,建立
dp表
1 |
|
- 定义子问题、推导状态转移方程
1 |
|
小小的细节,数组不能越界
1 |
|
- 确定边界条件、状态转移顺序
1 |
|
- 空间优化
1 |
|
实际代码
1 |
|
空间优化代码
1 |
|
分割等和数组
问题可以转化为0-1背包,把要素对齐一下。
- 背包容量为sum/2
- 放入的物品的重量和价值都是元素的数值
- 背包刚好装满说明找到了sum/2的子集
- 每个物品不能重复放入
思路
确定dp表
dp[j]表示背包容量为j能装的最大重量(价值),当dp[target]==target时背包装满了
推导状态转移方程
01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。
所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
确定临界条件
dp[0]=0。如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。
确定状态转移顺序
使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!
其余处理
要先遍历求取数组总和sum
sum%2==0才能分割成两个等和子集
代码
1 |
|
最后一块石头的重量2
不要陷入纠结过程的漩涡中,模拟太难了
还是和分割等和子集类似,将石头分成尽量一样大的两部分,这样对撞后剩下的石头最小
物品重量为stones[i],价值也为stones[i]
背包最大容量为sum/2
思路
确定dp表
dp[j]表示容量为j的背包可以装入的石头的总和,dp表的大小为target+1,target是sum/2,sum是所有石头重量总和
推导状态转移方程
装入第i块石头和不装入第i块石头两种状态转移而来,要装入第i块石头,要考虑当前能装得下
dp[j]=max(dp[j],dp[j-stones[i]]+stones[i])
确定临界条件
初始化
dp[j]=0
,就能保证dp[j]=max(dp[j],dp[j-stones[i]]+stones[i])
的dp[j]
正常计算确定状态转移顺序
石头从前到后,背包容量从后往前
最后返回结果是
sum-dp[target]-dp[target]
。target一定是sum一半向下取整,dp[target]<=target
,因此sum-dp[target]>=sum/2
,sum-dp[target]
表示分出来的另一堆石头,和dp[target]
相减得到对撞后剩余结果
代码
1 |
|
目标和
思路
问题转化,把加和减的数分别看成集合,left-right=target
,所有数的总和为sum
,left+right=sum
故left=(sum+target)/2
,只要找出能构成left
的组合的数量即可。
根据题意,left是正数,而且这里整形除以2必然关心取整问题,所以sum+target
是奇数或负数时无解。
因此采用动态规划就转化为了背包容量为left,装满有多少种方法。注意是方法数,这道题会出现元素是0,特别恶心
回溯
动态规划
确定dp表
dp[i][j]
表示使用下标为0到i的nums[i]
能够凑满容量j
的背包的方法数,数组大小为len(nums)*(left+1)
推导状态转移方程
j<nums[i]
时装满背包dp[i][j]=dp[i-1][j]
j>=nums[i]
时装满背包,可以用nums[i]
,也可以不用,dp[i][j]=dp[i-1][j-nums[i]]+dp[i-1][j]
确定临界条件
nums[0]<left
时dp[0][nums[0]]=1
若有
nums[i]=0
,假设n为统计到nuns[i]时0的数量,dp[i][0]=2^n
*题目允许0的出现,因此对于0放入背包非常灵活,即使是背包容量为0,可以放也可以不放0,它可以叠加到所有的装入情况中。就有出现物品重量为0时,有2的n次方中选择,n为这个物品及之前重量为0的物品的次数。
确定状态转移顺序
i从1开始,j从1开始
代码
1 |
|
一和零
这道题是0-1背包的变种,有两个背包,分别是装0和装1的背包,物品是字符串数组中的元素,我们要确定的是把字符串数组中的元素放入到背包中最多能放多少个。字符串元素–物品,m和n对应两个背包的容量。
实际上最简单的考量是建立dp[i][j][k]
,物品一维,背包两维,不断考虑一个又一个物品是否放入背包。
思路
确定dp表
dp[i][j]
表示在i个0和j个1的情况下,考虑字符串数组中的所有元素,字符串数组的最大子集的大小(该子集中 最多 有i个0和j个1),dp表的大小为(m+1)*(n+1)
。其实这里已经压缩了一维空间了,但是计算的时候不要忘记了背包问题的计算顺序是一件件地考虑物品能否放入。推导状态转移方程
遍历字符串数组,考虑是否将该元素放入到背包中
对于字符串数组的当前元素,可以考虑将其放入背包,也可以不放入,这取决于当前元素有zeors个0和ones个1
若放入背包,则考虑容量足够,方法数加1,
dp[i][j]=dp[i-zeros][j-ones]+1
若不放入背包,则
dp[i][j]
与先前一个字符串数组元素保持一致方法数应该取最大
故
dp[i][j]=max(dp[i][j],dp[i-zeros][j-ones]+1)
确定临界条件
初始情况下没有字符串数组的元素纳入考虑,没有元素,dp表初始化为0
确定状态转移顺序
0-1背包空间压缩后,需要从后往前遍历背包容量,所以i和j都要从后往前遍历,以确保在计算时不会覆盖所需的上一层的计算结果
代码
1 |
|
完全背包
给定n
个物品,第i
个物品的重量为wgt[i-1]
,价值为val[i-1]
,和一个容量为cap
的背包,每个物品可以重复选取,问在不超过背包容量下能放入物品的最大价值。
分析其实和0-1背包类似,只需要小小地修改一下状态转移方程和条件。
- 首先定义状态,建立dp表
1 |
|
- 定义子问题、推导状态转移方程
1 |
|
小小的细节,数组不能越界
1 |
|
- 确定边界条件、状态转移顺序
1 |
|
空间优化
可以使用一维滚动数组来优化空间,因为允许重复选择一个物品,所以c的遍历要从前往后来覆盖dp
实际代码
1 |
|
空间优化代码
1 |
|
硬币问题
给定n
种硬币,第i
种硬币的面值为coin[i-1]
,目标金额为amt
,每种硬币可以重复选取,问能够凑出目标金额的最少硬币个数,如果无法凑出目标金额则返回-1
本题是要求最少硬币数量,硬币是组合数还是排列数都无所谓!所以两个for循环先后顺序怎样都可以!
- 首先定义状态,建立
dp
表
1 |
|
定义子问题、推导状态转移方程
目标变成了最少硬币个数,限制要刚刚好凑到金额,其他转移条件和多重背包类似
1 |
|
小小的细节,数组不能越界
1 |
|
- 确定边界条件、状态转移顺序
1 |
|
实际代码
1 |
|
空间优化代码
1 |
|
给定n
种硬币,第i
种硬币的面值为coin[i-1]
,目标金额为amt
,每种硬币可以重复选取,问在凑出目标金额的硬币组合数量
- 首先定义状态,建立
dp
表
1 |
|
- 定义子问题、推导状态转移方程
1 |
|
- 确定边界条件、状态转移顺序
1 |
|
求取组合数,外循环硬币,内循环金额;若反过来则求取的是排列数。原因在于外循环是金额时,对同一金额,不断枚举各种硬币的情况,硬币之间的出现产生了顺序,并且得到了记录。
单词拆分
回溯
动态规划
思路
子序列问题
最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
- 输入:nums = [10,9,2,5,3,7,101,18]
- 输出:4
- 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
- 输入:nums = [0,1,0,3,2,3]
- 输出:4
示例 3:
- 输入:nums = [7,7,7,7,7,7,7]
- 输出:1
提示:
- 1 <= nums.length <= 2500
- -10^4 <= nums[i] <= 104
特点
首先明确子序列定义——”子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序”。也就是说子序列可以是不连续的,但又有顺序关系。这一点对后续搜索形成指导意义。
解决这类序列问题,很关键的是在dp状态的定义上下功夫,如果确认dp[i]为到i为止的最长严格递增子序列的长度,那么就很难进行状态的转移,对于一个新的子序列,很难确定计算如何转移。这个转移信息很难被记录,因为dp的定义不允许记录新的子序列的计算信息。那么就需要改变dp的定义,使得能够记录新的子序列,再多使用一个变量记录最长严格递增子序列的长度。
设置dp[i]表示以i结尾的子序列的最长严格递增子序列的长度,这样便于开启新的子序列时计算的转移。
思路
确定状态,确定dp表
dp[i]表示以i结尾的子序列的最长严格递增子序列的长度,长度为n
定义子问题,推导递推公式
因为子序列可以不连续,只要
nums[i]
够大,nums[i]
可以加在任何一个前面已经求取过的子序列,所以dp[i]
可能由j
从0取到i-1
的所有dp[j]
转移而来。即位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值1
2
3if nums[i] > nums[j] {
dp[i] = max(dp[i], dp[j]+1)
}确定边界条件
dp[i] = 1,每个数自己就是一个最长递增子序列
确定转移顺序
从左往右遍历,i从0到n-1,j从0到i-1
代码
1 |
|
最长连续递增子序列
与最长递增子序列的差别在于要求连续了,连续了就不用再来个for内循环去检查了,直接从dp[i-1]就能转移。
1 |
|
最长重复子数组
给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
示例:
输入:
- A: [1,2,3,2,1]
- B: [3,2,1,4,7]
- 输出:3
- 解释:长度最长的公共子数组是 [3, 2, 1] 。
提示:
- 1 <= len(A), len(B) <= 1000
- 0 <= A[i], B[i] < 100
思路
因为强调连续性,dp状态转移时会因为非连续而与先前状态断开传递关系,所以设置dp表时注意以i/j结尾这个条件。
dp[i][j]
表示nums1数组中以i结尾的子数组和nuns2数组中以j结尾的子数组,两者公共的、长度最长的子数组的长度
dp[i][j] = dp[i-1][j-1] + 1
,比较的时候i和j至少从1开始,所以第一行和第一列需要提前初始化号,在初始化的时候比较麻烦
1 |
|
避免复杂初始化,那就把第一行第一列给空出来,将dp全部后移一个格子
1 |
|
空间优化,采用滚动数组,且我们发现dp[i][j]
只依赖于dp[i-1][j-1]
,为了避免覆盖,内循环倒序,同时注意覆盖
1 |
|
最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,”ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
- 输入:text1 = “abcde”, text2 = “ace”
- 输出:3
- 解释:最长公共子序列是 “ace”,它的长度为 3。
示例 2:
- 输入:text1 = “abc”, text2 = “abc”
- 输出:3
- 解释:最长公共子序列是 “abc”,它的长度为 3。
示例 3:
- 输入:text1 = “abc”, text2 = “def”
- 输出:0
- 解释:两个字符串没有公共子序列,返回 0。
提示:
- 1 <= text1.length <= 1000
- 1 <= text2.length <= 1000 输入的字符串只含有小写英文字符。
思路
本题对连续性以及元素之间的关系都没有先前连续、单调要求那么高,只要求最长公共子序列,只要先前匹配上的字符,后续都能用得到,因此可以直接定义状态dp[i][j]
表示text1前i-1个字符、text2前j-1个字符,两者共同子序列的长度。
状态转移上,两字符相等,纳入最长公共子序列;两字符不相等,则分别考虑text上一位的重叠情况——看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。(这位不匹配,那上一位匹配吗
同样采取优化,i-1和j-1来减少初始化开销
1 |
|
不相交的线
线不能相交,一开始去想怎么判断线不相交,然后就做不出来了。要学会转化问题,连成线不相交其实都是保证一个相对顺序,直接将不能相交转化为最长公共子序列,确保了顺序就不会出现连线相交。代码几乎一模一样。
最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4]
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
思路
这道题的思路不难想,贪心也可以,动态规划特别直观,抓住连续以及最大和两个条件,直接给出套路.答案要另外使用个变量res来记录。
确定状态,确定dp表
dp[i]表示以i结尾的连续子数组的最大和,长度为n
定义子问题,推导递推公式
dp[i]只能从dp[i-1]和nums[i]两种情况进行转移: dp[i]= max(dp[i-1] + nums[i], nums[i] )
- dp[i-1] + nums[i],即nums[i]加入当前子数组
- nums[i],即nums[i]作为新的子数组开头开始计算
确定边界条件
dp[0] = nums[0]
确定转移顺序
从左往右遍历,i从0到n-1
空间压缩
i只依赖于i-1,且依赖于左,使用一个滚动变量来替代就行
1 |
|
判断子序列
给定字符串 s 和 t ,判断 s 是否为 t 的子序列。
字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,”ace”是”abcde”的一个子序列,而”aec”不是)。
示例 1:
- 输入:s = “abc”, t = “ahbgdc”
- 输出:true
示例 2:
- 输入:s = “axc”, t = “ahbgdc”
- 输出:false
提示:
- 0 <= s.length <= 100
- 0 <= t.length <= 10^4
两个字符串都只由小写字符组成
本题可以算是编辑距离的简化入门题,还是有点难想,涉及到删除的字符概念,即考虑是否使用当前字符来组成子序列。
动态规划