動態規劃的大體思路是把一個複雜的問題轉化成一個分階段逐步遞推的過程,從簡單的初始狀態一步一步遞推,最終獲得複雜問題的最優解。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); } }