动态规划超清晰易懂的教程

题目描述

https://leetcode.com/problems/distinct-subsequences/算法

这道题的分类是动态规划,动态规划很难掌握,对于没搞过信息学竞赛的人来讲更是难上加难,接下来我会用一个接地气的方法教你们如何推导出这道题的动态规划递推公式。数组

题目大意是给定一个字符串S和子串T,请问S中有多少个T的字串?缓存

Given a string S and a string T, count the number of distinct subsequences of S which equals T.函数

A subsequence of a string is a new string which is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (ie, "ACE" is a subsequence of "ABCDE" while "AEC" is not).优化

例子1: Input: S = "rabbbit", T = "rabbit" Output: 3 Explanation: ​ As shown below, there are 3 ways you can generate "rabbit" from S. (The caret symbol ^ means the chosen letters) ​ rabbbit ^^^^ ^^ rabbbit ^^ ^^^^ rabbbit ^^^ ^^^ 例子2: Input: S = "babgbag", T = "bag" Output: 5 Explanation: ​ As shown below, there are 5 ways you can generate "bag" from S. (The caret symbol ^ means the chosen letters) ​ babgbag ^^ ^ babgbag ^^    ^ babgbag ^    ^^ babgbag ^  ^^ babgbag ^^^

 

 

回溯法

这题的标签是动态规划,可是我对动态规划还不是很熟练,而动态规划是回溯的优化版本,有位大师说过一切动态规划问题均可以由回溯问题转换而来。因为我对回溯更加熟练,所以我决定先实现一个回溯版本的题解。spa

思路

从左到右遍历S的每一个字符,若是T的字符在S中出现了,则这两个字符对碰消掉,接着考虑这两个字符不存在的状况。code

在S遍历的循环中须要考虑两个状况:orm

  • 若是S当前的的字符等于T的第一个字符,则删除S和T中的这两个对应的字符,接着递归调用。例如func("abc","ac"),两个字串的第一个字母相等,下一次递归调用的入参则应该是func("bc","c")对象

  • 若是S当前的字符不等于T的第一个字符,则继续遍历S。blog

函数递归的中止条件为T串为空,意味着子串T的全部字符都被S的字符抵消掉,所以是一个子串组合。

每一个递归的尽头意味着是一个子串组合的解,所以咱们将全部的递归函数的返回值累加起来便可获得全部可能的组合数量。

代码实现

咱们对函数添加入参,表示字符串S和字符串T的开始下标,以此达到删除某个字符的目的,而不用频繁建立字符串对象,例如调用substring()。

class Solution { public static void main(String[] args) { System.out.println(new Solution().numDistinct("babgbag","bag")); } public int numDistinct(String s, String t) { return find(s, 0, t, 0); } ​ private int find(String s, int sIndex, String t, int tIndex) { System.out.println("find("+sIndex+","+tIndex+")"); if (s.length() == 0 && t.length() == 0) { return 1; } if (tIndex == t.length()) { return 1; } int sum = 0; for (int i = sIndex; i < s.length(); i++) { if (s.charAt(i) == t.charAt(tIndex)) { sum += find(s, i + 1, t, tIndex + 1); } } return sum; } }

 

输出结果

find(0,0) find(1,1) find(2,2) find(4,3) find(7,3) find(6,2) find(7,3) find(3,1) find(6,2) find(7,3) find(5,1) find(6,2) find(7,3) 5

 

缺点

这个解法超时了,从输出结果中能够看出,find方法有很多重复的调用,例如find(7,3)调用了4次,可是得到的结果应该没什么不一样,毕竟入参都同样,所以咱们须要缓存已经算出来的结果。

咱们使用备忘录法,将每次find()调用的结果保存起来存在一个二维数组中,递归调用以前先查询是否已经计算过,若是计算过则跳过没必要要的重复计算,这个方法已是动态规划的雏形了,具体实现再也不赘述。

动态规划法

从回溯法怎么优化为动态规划法呢?从上述回溯法的解法中咱们能够知道,find(0,0)的返回值就是答案,即从S串、T串的下标0开始逐个字符匹配,咱们用一个二维数组dp表明find()的计算结果。dp[0] [0]则意味着字符串S从下标0开始,子串T从下标0开始,S有多少种T的子串。

初始化

观察回溯函数find()的终止条件,可知当T的下标等于T的长度时,获得一个子串组合,此时为一个解。因为T的下标会等于T的长度,所以所以dp数组的长度须要加1。初始化为

int[][] dp=new int[S.length()+1][T.length()+1]; for(int i=0;i<=S.length;i++) dp[i][T.length()]=1;

 

从回溯的角度分析,上述初始化对应的回溯函数的入参为find("ab...bc",""),即子串T为空。

递推式

接下来思考递推式

若是S的某个字符等于T的某个字符,则将这两个字符同时消掉,当作不存在,所以有:

  • dp[i] [j]=dp[i+1] [j+1] if S.charAt(i) == T.charAt(j)

或者只消掉S的对应字符,至关于忽略S的这个字符,看看S后面还有没有可能出现这个字符,组成一个不同的子串:

  • dp[i] [j]=dp[i+1] [j] if S.charAt(i) == T.charAt(j)

若是S的字符不等于T的字符,说明不匹配,继续在S的后面的字符寻找能和T匹配的字符:

  • dp[i] [j]=dp[i+1] [j] if S.charAt(i) != T.charAt(j)

代码实现

根据上述分析,写出通俗易懂的动态规划算法,有理有据,使人信服。

public int dp(String s, String t) { int[][] dp = new int[s.length() + 1][t.length() + 1]; for (int i = 0; i <= s.length(); i++) { dp[i][t.length()] = 1; } for (int i = s.length() - 1; i >= 0; i--) { for (int j = t.length() - 1; j >= 0; j--) { if (s.charAt(i) == t.charAt(j)) { dp[i][j] = dp[i + 1][j + 1] + dp[i + 1][j]; } else { dp[i][j] = dp[i + 1][j]; } } } return dp[0][0]; }
相关文章
相关标签/搜索