动态规划的大体思路是把一个复杂的问题转化成一个分阶段逐步递推的过程,从简单的初始状态一步一步递推,最终获得复杂问题的最优解。android
因为动态规划解决的问题多数有重叠子问题这个特色,为减小重复计算,对每个子问题只解一次,将其不一样阶段的不一样状态保存在一个二维数组中。算法
1. 拆分问题:根据问题的可能性把问题划分红经过递推或者递归一步一步实现。数组
关键就是这个步骤,动态规划有一类问题就是从后往前推到,有时候咱们很容易知道 : 若是只有一种状况时,最佳的选择应该怎么作.而后根据这个最佳选择往前一步推导,获得前一步的最佳选择dom
2. 定义问题状态和状态之间的关系:用一种量化的形式表现出来,相似于高中学的推导公式,由于这种式子很容易用程序写出来,也能够说对程序比较亲和(也就是最后所说的状态转移方程式)优化
3. 动态规划算法的基本思想与分治法相似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各类可能的局部解,经过决策保留那些有可能达到最优的局部解,丢弃其余局部解。依次解决各子问题,最后一个子问题就是初始问题的解。spa
个人理解是:好比咱们找到最优解,咱们应该讲最优解保存下来,为了往前推导时可以使用前一步的最优解,在这个过程当中不免有一些相比于最优解差的解,此时咱们应该放弃,只保存最优解,设计
这样咱们每一次都把最优解保存了下来,大大下降了时间复杂度。3d
动态规划解决问题的过程分为两步:code
1.寻找状态转移方程式blog
2.利用状态转移方程式自底向上求解问题
(1)子序列:一个序列X = x1x2...xn,中任意删除若干项,剩余的序列叫作A的一个子序列。也能够认为是从序列A按原顺序保留任意若干项获得的序列。
例如:对序列 1,3,5,4,2,6,8,7来讲,序列3,4,8,7 是它的一个子序列。对于一个长度为n的序列,它一共有2^n 个子序列,有(2^n – 1)个非空子序列。在这里须要提醒你们,子序列不是子集,它和原始序列的元素顺序是相关的。
(2)公共子序列:若是序列Z既是序列X的子序列,同时也是序列Y的子序列,则称它为序列X和序列Y的公共子序列。空序列是任何两个序列的公共子序列。
(3)最长公共子序列:X和Y的公共子序列中长度最长的(包含元素最多的)叫作X和Y的最长公共子序列。
这个问题若是用穷举法时间,最终求出最长公共子序列时,时间复杂度是Ο(2mn),是指数级别的复杂度,对于长序列是不适用的。所以咱们使用动态规划法来求解。
设X=x1x2…xm和Y=y1y2…yn是两个序列,Z=z1z2…zk是这两个序列的一个最长公共子序列。
1. 若是xm=yn,那么zk=xm=yn,且Zk-1是Xm-1,Yn-1的一个最长公共子序列;
2. 若是xm≠yn,那么zk≠xm,意味着Z是Xm-1,Y的一个最长公共子序列;
3. 若是xm≠yn,那么zk≠yn,意味着Z是X,Yn-1的一个最长公共子序列。
从上面三种状况能够看出,两个序列的LCS包含两个序列的前缀的LCS。所以,LCS问题具备最优子结构特征。
从最优子结构能够看出,若是xm=yn,那么咱们应该求解Xm-1,Yn-1的一个LCS,而且将xm=yn加入到这个LCS的末尾,这样获得的一个新的LCS就是所求。
若是xm≠yn,咱们须要求解两个子问题,分别求Xm-1,Y的一个LCS和X,Yn-1的一个LCS。两个LCS中较长者就是X和Y的一个LCS。
能够看出LCS问题具备重叠子问题性质。为了求X和Y的一个LCS,咱们须要分别求出Xm-1,Y的一个LCS和X,Yn-1的一个LCS,这几个字问题又包含了求出Xm-1,Yn-1的一个LCS的子子问题。(有点绕了。。。晕没晕。。。。)
根据上面的分析,咱们能够得出下面的公式;
根据上面的,咱们很容易就能够写出递归计算LCS问题的程序,经过这个程序咱们能够求出各个子问题的LCS的值,此外,为了求解最优解自己,咱们好须要一个表dp,dp[i,j]记录使C[i,j]取值的最优子结构。
package cn.itcast.recursion; public class LCS { public int findLCS(String A, String B) { int n = A.length(); int m = B.length(); //返回一个字符数组,该字符数组中存放了当前字符串中的全部字符 //返回的是字符数组char[]a
char[] a = A.toCharArray(); char[] b = B.toCharArray(); //建立一个二维矩阵,用来推到公共子序列
int[][] dp = new int[n][m]; for (int i = 0; i < n; i++) { //若是找到第一列其中一个字符等于第一行第一个字符
if (a[i] == b[0]) { //找到第一列与第一行b[0]的相等的值,把其变成1
dp[i][0] = 1; //并将其后面的字符都变成1
for (int j = i + 1; j < n; j++) { dp[j][0] = 1; } break; } } for (int i = 0; i < m; i++) { //若是找到第一列其中一个字符等于第一行第一个字符
if (b[i] == a[0]) { //则把第一列后面的字符都变成1
dp[0][i] = 1; for (int j = i + 1; j < m; j++) { dp[0][j] = 1; } break; } } //从1开始是由于横向和纵向下标为0的都遍历过了
for (int i = 1; i < n; i++) { for (int j = 1; j < m; j++) { //横向和纵向有相等的值
if (a[i] == b[j]) { //当前位置左边的值+1
dp[i][j] = dp[i - 1][j - 1] + 1; } else { //取当前位置(左边的值,上边的值)的最大值
dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); } } } for (int i = 0; i < n - 1; i++) { for (int j = 0; j < m; j++) { System.out.print(dp[i][j] + " "); } System.out.println(); } return dp[n - 1][m - 1]; } public static void main(String[] args) { LCS lcs = new LCS(); int findLCS = lcs.findLCS("android", "random"); System.out.println("最长子序列长度:" + findLCS); } }