动态规划之所以比暴力算法(回溯算法等)快,是因为动态规划技巧消除了重叠子问题。
动态规划是分治思想的延伸,通俗一点来说就是一种大事化小,小事化无的艺术。在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理大问题时直接使用这些结果。
动态规划具备了以下三个特点
动态规划的本质是对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)。
动态规划问题的求解步骤:
注意:
状态定义的要求:定义的状态一定要形成递推关系。
适用场景:
最大值/最小值, 可不可行, 是不是,方案个数
大家都知道斐波那契数列,现在要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。注意:n<=39
链接:斐波那契数列
public class Solution {
public int Fibonacci(int n) {
if(n<=0){
return 0;
}
if(n==1||n==2){
return 1;
}
return Fibonacci(n-1)+Fibonacci(n-2);
}
}
缺点:递归的方法时间复杂度为O(2^n)
,随着n的增大呈现指数增长,效率低下,当输入n比较大时,可能导致栈溢出,且在递归过程中有大量的冗余计算。
状态:F(i)
状态递推:F(i)=F(i-1)+F(i-2)
初始值:F(1)=F(2)=1
返回结果:F(N)
F(n)只与它相邻的前两项有关,所以只需要保存两个子问题的解就可以了,通过不断递推就可以得到最终的解。
public class Solution {
public int Fibonacci(int n) {
if(n<=0){
return 0;
}
if(n==1||n==2){
return 1;
}
int fn=0;
int f1=1,f2=1;
for(int i=3;i<=n;i++){
fn = f1 +f2;
f1 =f2;
f2 = fn;
}
return fn;
}
}
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
链接:疯狂青蛙
通过上面的简单举例,可知通过第0层直接跳到第三层,通过第一层可以直接跳到第三层,通过第二层也可以直接跳到第三层。
F(i) = F(i-1)+F(i-2)+F(i-3)+…+F(1)+F(0);
F(i-1)= F(i-2)+F(i-3)+…+F(1)+F(0);
得:F(i) = 2*F(i-1)
状态:F(i)跳上i级台阶的方法个数
状态递推:F(i) = 2*F(i-1)
初始值:F(1)=1
返回值:F(n)
public class Solution {
public int JumpFloorII(int target) {
if(target==1){
return 1;
}
int f1=1;
int fn=0;
for(int i =2;i<=target;i++){
fn=2*f1;
f1=fn;
}
return fn;
}
}
若一次只能跳1级或者2级,那么问题求解就变成斐波那契问题。
链接:普通青蛙
状态:F(i)跳上i级台阶的方法个数
状态递推:F(i) = F(i-1)+F(i-2)
初始值:F(1)=1, F(2)=2
返回值:F(n)
public class Solution {
public int JumpFloor(int target) {
if(target==1){
return 1;
}
if(target==2){
return 2;
}
int fn=0;
int f1=1,f2=2;
for(int i=3;i<=target;i++){
fn=f1+f2;
f1=f2;
f2 = fn;
}
return fn;
}
}
我们可以用2*1
的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2*1
的小矩形无重叠地覆盖一个2*n
的大矩形,总共有多少种方法?
比如n=3时,2*3的矩形块有3种覆盖方法:
链接:矩形覆盖问题
矩形的最后部分有两种放法,一种是竖着放一个的情况,此时前面还有i-1个小正方形;一种是横着放的情况,此时前面还有i-2个小矩形。所以i个小矩形可以总结为i-1个小矩形的覆盖方法数和i-2个小矩形的覆盖方法数之和。
状态F(i):用i个2*1
无重复覆盖2*i
的大矩形的方法个数
状态递推:F(i) = F(i-1)+F(i-2)
初始值:F(1)=1, F(2)=2
返回值:F(n)
public class Solution {
public int RectCover(int target) {
if(target==1){
return 1;
}
if(target == 2){
return 2;
}
int f1 = 1, f2 = 2;
int fn=0;
for(int i=3;i<=target;i++){
fn = f1+f2;
f1 = f2;
f2 = fn;
}
return fn;
}
}
在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。给一个数组,返回它的最大连续子序列的和(子向量的长度至少是1)。
链接:最大连续子数组和
得到多个以第i个元素结尾的最大值F(0),F(1),F(2)…F(i),然后从F(i)中选出一个最大的。
状态:以第i个元素结尾的最大连续和
状态递归:F(i)=max(F(i-1)+a[i],a[i])
初始值:F(0)=a[0]
返回值:max(F(i))——返回F(i)中的最大值
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int maxNum=array[0];
int curNum = array[0];
for(int i=1;i<array.length;i++){
curNum = Math.max(curNum+array[i],array[i]);
if(maxNum<curNum){
maxNum = curNum;
}
}
return maxNum;
}
}
给定一个字符串s和一组单词dict,判断s是否可以用空格分割成一个单词序列,使得单词序列中所有的单词都是dict中的单词(序列可以包含一个或多个单词)。
例如:
给定s=“leetcode”;
dict=[“leet”, “code”].
返回true,因为"leetcode"可以被分割成"leet code".
链接:字符串分割
F(2)中F(1)代表以F(1)为界限进行分割,因为F(1)=false,所以F(1)为界限分割的F(2)肯定为false,除了考虑在F(1)的基础上的分割情况,还要考虑F(2)的整体情况。
F(4)为true,所以可以考虑除去F(4),剩余字符的匹配情况。
状态F(i):前i个字符能否被分割
状态递推:F(i): j 初始值:辅助状态F(0):true
返回值:F(n)
注意:
import java.util.*;
public class Solution {
public boolean wordBreak(String s, Set<String> dict) {
boolean[] canBreak = new boolean[s.length()+1];
// 初始化F(0) = true
canBreak[0]=true;
for(int i=1;i<=s.length();i++){
for(int j=0;j<=i-1;j++){
// 第j+1个字符的索引为j
if(canBreak[j]&&dict.contains(s.substring(j,i))){
canBreak[i] = true;
break;
}
}
}
//最后一个索引
return canBreak[s.length()];
}
}
注意:
这里需要注意一下索引,一个是canBreak的索引(下标0为引入的辅助状态),一个是字符串s的索引(从小标0开始)。if(canBreak[j]&&dict.contains(s.substring(j,i)))
。
j==0时表示匹配整个字符。
给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字。
例如,给出的三角形如下:
[ [2],
[3 , 4],
[6, 5, 7],
[4, 1, 8, 3]
]
最小的从顶部到底部的路径和是2 + 3 + 5 + 1 = 11。
注意:
如果你能只用O(N)的额外的空间来完成这项工作的话,就可以得到附加分,其中N是三角形中的行总数。
链接:三角矩阵
初始值为第一行,从上到下递推,求出每一行每一个值对应的最短路径。
状态F(i,j):从(0,0)到(i,j)的最短路径和
状态递推:
第一列:F(i,0) = F(i-1,0)+a[i][0]
中间位置:F(i,j)=min(F(i-1,j-1),F(i-1,j))+a[i][j]
最后一列:F(i,i)=F(i-1,i-1)+a[i][i]
初始值:F(0,0)=a[0][0]
返回值:在最后一行找一个最小值min(F(n-1,j))
import java.util.List;
import java.util.ArrayList;
public class Solution {
public int minimumTotal(ArrayList<ArrayList<Integer>> triangle) {
if(triangle.isEmpty()){
return 0;
}
//保存每一行,每个数值的最短路径
List<List<Integer>> minPathSum = new ArrayList<>();
for(int i = 0; i < triangle.size(); ++i) {
minPathSum.add(new ArrayList<>());
}
// F[0][0]初始化,将第0行第0列(第一个数)放入minPathSum中
minPathSum.get(0).add(triangle.get(0).get(0));
for(int i = 1; i < triangle.size(); ++i) {
int curSum = 0;
for(int j = 0; j <= i; ++j) {
// 处理左边界和右边界
if(j == 0) {
curSum = minPathSum.get(i - 1).get(0);
}
else if(j == i){
curSum = minPathSum.get(i - 1).get(j - 1);
}
else{
curSum = Math.min(minPathSum.get(i - 1).get(j),
minPathSum.get(i - 1).get(j - 1));
}
// F(i,j) = min( F(i-1, j-1), F(i-1, j)) + triangle[i][j]
minPathSum.get(i).add(triangle.get(i).get(j) + curSum);
}
}
int size = triangle.size();
// min(F(n-1, i)),将最后一行的第一个数作为最小值
int allMin = minPathSum.get(size - 1).get(0);
for(int i = 1; i < size; ++i)
{ //在第一行中选择最小值
allMin = Math.min(allMin,minPathSum.get(size - 1).get(i));
}
return allMin;
}
}
定义一个同样的三角形List集合,用来保存每个值对应的最短路径和。在List集合中的最后一行寻找全局最小路径和。
初始值为最后一行,从下到上递推,求出每一行每一个值对应的最短路径,使用这种方法不用考虑边界值,也不需要在最后寻找最小值,直接返回F(0,0)即可。
状态F(i,j):从(i,j)到最后一行的最短路径和
状态递推:F(i,j) = min(F(i+1,j),F(i+1,j+1))+a[i][j]
初始值:F(n-1,0) = a[n-1][0], F(n-1,1) =a[n-1][1] ,…, F(n-1,n-1) = a[n-1][n-1]
返回值:F(0,0)
import java.util.List;
import java.util.ArrayList;
public class Solution {
public int minimumTotal(ArrayList<ArrayList<Integer>> triangle) {
if(triangle.isEmpty()){
return 0;
}
// F[n-1][n-1],...F[n-1][0]初始化
ArrayList<ArrayList<Integer>> minPathSum = new ArrayList<>(triangle);
int row = minPathSum.size();
// 从倒数第二行开始,此时i代表的索引
for(int i = row - 2; i >= 0; --i){
for(int j = 0; j <= i; ++j){
// F(i,j) = min( F(i+1, j), F(i+1, j+1)) + triangle[i][j]
int curSum = Math.min(triangle.get(i + 1).get(j),triangle.get(i + 1).get(j + 1)) + triangle.get(i).get(j);
minPathSum.get(i).set(j, curSum);
}
}
return minPathSum.get(0).get(0);
}
}
一个机器人在m×n大小的地图的左上角(起点,下图中的标记“start"的位置)。
机器人每次向下或向右移动。机器人要到达地图的右下角。(终点,下图中的标记“Finish"的位置)。可以有多少种不同的路径从起点走到终点?
上图是3×7大小的地图,有多少不同的路径?
备注:m和n小于等于100
链接:路径总数
状态F(i,j):从(0,0)到达F(i,j)的路径数
状态递推:F(i,j) = F(i-1,j)+F(i,j-1)
初始化:F(0,i)=1 F(i,0)=1 第0行,第0列的特殊情况
返回值:F(m-1,n-1)
import java.util.List;
import java.util.ArrayList;
public class Solution {
public int uniquePaths(int m, int n) {
List<List<Integer>> pathNum = new ArrayList<>();
// 申请F(i,j)空间,初始化
for(int i = 0; i < m; ++i){
pathNum.add(new ArrayList<>());
pathNum.get(i).add(1);
}
for(int i = 1; i < n; ++i){
pathNum.get(0).add(1);
}
for(int i = 1; i < m; ++i){
for(int j = 1; j < n; ++j){
// F(i,j) = F(i-1,j) + F(i,j-1)
pathNum.get(i).add(pathNum.get(i).get(j - 1)
+ pathNum.get(i - 1).get(j));
}
}
return pathNum.get(m - 1).get(n - 1);
}
}
如果在图中加入了一些障碍,有多少不同的路径?分别用0和1代表空区域和障碍。
例如
下图表示有一个障碍在3*3
的图中央。
[
[0,0,0],
[0,1,0],
[0,0,0]
]
有2条不同的路径
备注:m和n不超过100.
链接:加入障碍的路径总数
状态F(i,j):从(0,0)到达F(i,j)的路径数
状态递推:if(a[i][j]==1){
F(i,j)=0
}else {
F(i,j)=F(i-1,j)+F(i,j-1)
}
初始值:
第一行if(a[0][j]==1) k>=j -> F(0,k)=0 else F(0,k)=1
第一列:if(a[i][0]==1) k>=i -> F(k,0) = 0 else F(k,0)=1
返回值:F(m-1,n-1)
import java.util.List;
import java.util.ArrayList;
public class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
List<List<Integer>> pathNum = new ArrayList<>();
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
// 申请F(i,j)空间,初始化
// 初始化第0列
for(int i = 0; i < m; ++i){
pathNum.add(new ArrayList<>());
//如果当前位置有障碍,则无法到达
if(obstacleGrid[i][0] == 1)
pathNum.get(i).add(0);
else{
//如果当前位置无障碍,但是前面如果到达不了,
//当前位置也达到不了
if(i > 0){
if(pathNum.get(i-1).get(0) == 1){
pathNum.get(i).add(1);
}else{
pathNum.get(i).add(0);
}
}else{
//这里考虑到第一个元素没有障碍,即i==0时的情况
//因为第一个元素,已经在初始化列时考虑了,所以初始化
//行时不用再考虑了。
pathNum.get(i).add(1);
}
}
}
//初始化第一行
for(int i = 1; i < n; ++i){
if(obstacleGrid[0][i] == 1)
pathNum.get(0).add(0);
else{
if(pathNum.get(0).get(i - 1) == 1)
pathNum.get(0).add(1);
else
pathNum.get(0).add(0);
}
}
for(int i = 1; i < m; ++i){
for(int j = 1; j < n; ++j){
// obstacleGrid[i][j] = 1 时,F(i,j)无法到达
if(obstacleGrid[i][j] == 1)
pathNum.get(i).add(0);
else
// F(i,j) = F(i-1,j) + F(i,j-1)
pathNum.get(i).add(pathNum.get(i).get(j - 1)
+ pathNum.get(i - 1).get(j));
}
}
return pathNum.get(m - 1).get(n - 1);
}
}
给定一个由非负整数填充的m x n的二维数组,现在要从二维数组的左上角走到右下角,请找出路径上的所有数字之和最小的路径。
注意:每次只能向下或向右移动。
链接: 最小路径和
状态F(i,j):从(0,0)到(i,j)最短路径和
状态递归:
F(i,j)=min(F(i-1,j),F(i,j-1))+a[i][j]
第一行:F(0,j) = F(0,j-1)+a[0][j]
第一列:F(i,0) = F(i-1,0)+a[i][0]
初始值:F(0,0) = a[0][0]
返回值:F(m-1,n-1)
public class Solution {
public int minPathSum(int[][] grid) {
int row = grid.length;
int col = grid[0].length;
//// 如果为空或者只有一行,返回0
if(row == 0 || col == 0) {
return 0;
}
// F(0,0), F(0,i), F(i,0)初始化
for(int i = 1;i < row;i++) {
grid[i][0] = grid[i - 1][0] + grid[i][0];
}
for(int i = 1;i < col;i++) {
grid[0][i] = grid[0][i - 1] + grid[0][i];
}
// F(i,j) = min{F(i-1,j) , F(i,j-1)} + (i,j)
for(int i = 1;i < row;i++) {
for(int j = 1;j < col;j++) {
grid[i][j] = Math.min(grid[i - 1][j],grid[i][j - 1]) + grid[i][j];
}
}
return grid[row - 1][col - 1];
}
}
有n 个物品和一个大小为m 的背包. 给定数组A 表示每个物品的大小和数组V 表示每个物品的价值. 问最多能装入背包的总价值是多大?
链接:背包问题
状态F(i,j):从前i个物品中选择包的大小为j时的最大值
递推状态:
如果第i个商品的大小大于j,则第i个商品不能放进去F(i,j)=F(i-1,j)
如果第i个商品的大小小于j,则第i个商品可以放进去:
(1)如果选择不放第i个商品F1(i,j) = F(i-1,j)
(2)如果选择放第i个商品F2(i,j) = F(i-1,j-A[i-1])+V[i-1]
在两种选择中选取总价值最大的:
F(i,j) = max(F1,F2)
初始值:
第一行:F(0,j) = 0 空包的价值
第一列:F(i,0) =0 没有包的价值
返回值:F(m-1,n-1)
注意:
第i个物品大小A[i-1]);第i个商品的价值V[i - 1]
public class Solution {
public int backPackII(int m, int[] A, int[] V) {
// write your code here
int num = A.length;
if(m == 0 || num == 0)
return 0;
//多加一行一列,用于设置初始条件
int[][] maxValue = new int[num + 1][m + 1];
//初始化所有位置为0,第一行和第一列都为0,初始条件
for(int i = 0; i <= num; ++i){
maxValue[i][0] = 0;
}
for(int i = 1; i <= m; ++i){
maxValue[0][i] = 0;
}
for(int i = 1; i <= num; ++i){
for(int j = 1; j <= m; ++j){
//第i个商品在A中对应的索引为i-1: i从1开始
//如果第i个商品大于j,说明放不下,所以(i,j)的最大价值和(i-1,j)相同
if(A[i - 1] > j){
maxValue[i][j] = maxValue[i - 1][j];
}
else{
//如果可以装下,分两种情况,装或者不装
//如果不装,则即为(i-1, j)
//如果装,需要腾出放第i个物品大小的空间:j - A[i-1],
//装入之后的最大价值,
//即为(i-1, j - A[i-1]) + 第i个商品的价值V[i - 1]
//最后在装与不装中选出最大的价值
int newValue = maxValue[i - 1][j - A[i - 1]]
+ V[i - 1];
maxValue[i][j] = Math.max(newValue
, maxValue[i - 1][j]);
}
}
}
//返回装入前N个商品,物品大小为m的最大价值
return maxValue[num][m];
}
}
注意:
int newValue = maxValue[i - 1][j - A[i - 1]]
+ V[i - 1];
给出一个字符串s,分割s使得分割出的每一个子串都是回文串
计算将字符串s分割成回文分割结果的最小切割数。
例如:给定字符串s=“aab”,
返回1,因为回文分割结果[“aa”,“b”]是切割一次生成的。
链接:回文串分割
public class Solution {
//判断是否回文串
public boolean isPal(String s, int start, int end){
while(start < end){
if(s.charAt(start) != s.charAt(end))
return false;
++start;
--end;
}
return true;
}
public int minCut(String s) {
int len = s.length();
if(len == 0)
return 0;
int[] minCut = new int[len + 1];
// F(i)初始化
// F(0)= -1,必要项,如果没有这一项,
//对于重叠字符串“aaaaa”会产生错误的结果
for(int i = 0; i <= len; ++i){
minCut[i] = i - 1;
}
for(int i = 1; i <= len; ++i){
for(int j = 0; j < i; ++j){
// F(i) = min{F(i), 1 + F(j)}, where j
// 从最长串判断,如果从第j+1到i为回文字符串
// 则再加一次分割,从1到j,j+1到i的字符就全部分成了回文字符串
if(isPal(s, j, i - 1)){
minCut[i] = Math.min(minCut[i], minCut[j] + 1);
}
}
}
return minCut[len];
}
}
在上面的回文串分割中,isPal(s,j,i-1)是从0开始向后分割。我们在对回文串判断进行优化时,如果从前向后分割,第i处需要用到第i+1处的信息,所以i应该从字符串末尾遍历分割。
状态F(i,j):区间[i,j]是否为回文串
状态递推:F(i,j) : s[i]==s[j] && F(i+1,j-1)
上式表示如果字符区间首尾字符相同且在去掉区间首尾字符后字符区间仍为回文串, 则原字符区间为回文串。
初始化:F(i,i)=true 单字符串为回文串
返回值:矩阵F(n,n)
public class Solution {
public boolean[][] getMat(String s){
int len = s.length();
boolean[][] Mat = new boolean[len][len];
for(int i = len - 1; i >= 0; --i){
for(int j = i; j < len; ++j){
if(j == i)
// 单字符为回文字符串
Mat[i][j] = true;
else if(j == i + 1){
// 相邻字符如果相同,则为回文字符串
if(s.charAt(i) == s.charAt(j))
Mat[i][j] = true;
else
Mat[i][j] = false;
}
else{
// F(i,j) = {s[i]==s[j] && F(i+1,j-1)
// j > i+1
Mat[i][j] = (s.charAt(i) == s.charAt(j)) && Mat[i + 1][j - 1];
}
}
}
return Mat;
}
public int minCut(String s) {
int len = s.length();
if(len == 0)
return 0;
boolean[][] Mat = getMat(s);
int[] minCut = new int[len + 1];
/ F(i)初始化
// F(0)= -1,必要项,如果没有这一项,
//对于重叠字符串“aaaaa”会产生错误的结果
for(int i = 0; i <= len; ++i){
minCut[i] = i - 1;
}
for(int i = 1; i <= len; ++i){
for(int j = 0; j < i; ++j){
// F(i) = min{F(i), 1 + F(j)}, where j
// 从最长串判断,如果从第j+1到i为回文字符串
// 则再加一次分割,从1到j,j+1到i的字符就全部分成了回文字符串
if(Mat[j][i - 1]){
minCut[i] = Math.min(minCut[i], minCut[j] + 1);
}
}
}
return minCut[len];
}
}
给定两个单词word1和word2,请计算将word1转换为word2至少需要多少步操作。
你可以对一个单词执行以下3种操作:
a)在单词中插入一个字符
b)删除单词中的一个字符
c)替换单词中的一个字符
链接:编辑距离
状态F(i,j):word1的前i个字符于word2的前j个字符的编辑距离
状态递推:
初始值:
初始化一定要是确定的值,如果这里不加入空串,初始值无法确定
F(i,0) = i : word与空串的编辑距离,删除操作
F(0,i) = i : 空串与word的编辑距离,增加操作
返回值: F(m,n)
public class Solution {
public int minDistance(String word1, String word2) {
// word与空串之间的编辑距离为word的长度
if(word1.isEmpty() || word2.isEmpty())
return Math.max(word1.length(), word2.length());
int len1 = word1.length();
int len2 = word2.length();
int[][] minDis = new int[len1 + 1][len2 + 1];
// F(i,j)初始化
for(int i = 0; i <= len1; ++i){
minDis[i][0] = i;
}
for(int i = 0; i <= len2; ++i){
minDis[0][i] = i;
}
for(int i = 1; i <= len1; ++i){
for(int j = 1; j <= len2; ++j){
// F(i,j) = min { F(i-1,j)+1, F(i,j-1) +1, F(i-1,j-1) +(w1[i]==w2[j]?
0:1) }
minDis[i][j] = 1 + Math.min(minDis[i - 1][j]
, minDis[i][j - 1]);
// 判断word1的第i个字符是否与word2的第j个字符相等
if(word1.charAt(i - 1) == word2.charAt(j - 1)){
// 字符相等,F(i-1,j-1)编辑距离不变
minDis[i][j] = Math.min(minDis[i][j]
,minDis[i - 1][j - 1]);
}
else{
// 字符不相等,F(i-1,j-1)编辑距离+ 1
minDis[i][j] = Math.min(minDis[i][j]
,minDis[i - 1][j - 1] + 1);
}
}
}
return minDis[len1][len2];
}
}
注意:
字符串类的动态规划,可引入空串进行初始化。
给定一个字符串S和一个字符串T,计算S中的T的不同子序列的个数。
字符串的子序列是由原来的字符串删除一些字符(也可以不删除)在不改变相对位置的情况下的剩余字符(例如,"ACE"is a subsequence of"ABCDE"但是"AEC"不是)。
例如:
S =“rabbbit”, T =“rabbit”
返回3
链接:不同子序列
S中有多少个字串与T相同。
状态F(i,j):S[1:i]中的子串与T[1:j]相同的个数
状态递推:F(i,j):
if(s[i-1]==t[j-1]):第i个字符和第j个字符相同
F(i,j) = F(i-1,j-1)+F(i-1,j)
else
F(i,j) = F(i-1,j)
初始值:
引入空串进行初始化,
F(i,0) = 1 —> S的子串与空串相同的个数,只有空串与空串相同
返回值:F(m,n)
public class Solution {
public int numDistinct(String S, String T) {
int sLen = S.length();
int tLen = T.length();
int[][] numDis = new int[sLen + 1][tLen + 1];
numDis[0][0] = 1;
// F(i,j),初始化第一行剩余列的所有值为0
for(int i = 1; i <= tLen; ++i){
numDis[0][i] = 0;
}
//F(i, 0) = 1
for(int i = 1; i <= sLen; ++i){
numDis[i][0] = 1;
}
for(int i = 1; i <= sLen; ++i){
for(int j = 1; j <= tLen; ++j){
// S的第i个字符与T的第j个字符相同
if(S.charAt(i - 1) == T.charAt(j - 1)){
numDis[i][j] = numDis[i - 1][j] + numDis[i - 1][j - 1];
}
else{
// S的第i个字符与T的第j个字符不相同
// 从S的前i-1个字符中找子串,使子串与T的前j个字符相同
numDis[i][j] = numDis[i - 1][j];
}
}
}
return numDis[sLen][tLen];
}
}
/*
此题也可优化空间复杂度为O(n)
f[i][j] 只和f[i - 1][j], f[i - 1][j - 1]有关
类似于背包问题,可以用一维数组保存上一行的结果,
每次从最后一列更新元素值
*/
public class Solution {
public int numDistinct(String S, String T) {
int sLen = S.length();
int tLen = T.length();
int[] numDis = new int[tLen + 1];
numDis[0]= 1;
for(int i = 1; i <= tLen; ++i){
numDis[i] = 0;
}
for(int i = 1; i <= sLen; ++i){
for(int j = tLen; j > 0; --j){
if(S.charAt(i - 1) == T.charAt(j - 1)){
numDis[j] = numDis[j] + numDis[j - 1];
}
}
}
return numDis[tLen];
}
}