动态规划——路径问题(64,62)、数组区间(303,413)、分割整数(343,279,91)、最长递增子序列(300,646,376)、最长公共子序列(1143

路径问题

一、64. 最小路径和

1.1 题目描述

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:每次只能向下或者向右移动一步。

1.2 代码

题解链接

一般遇到这些路径问题,一般有两种解法:搜索和动态规划

但是搜索是计算出所有的结果,然后选取最优的,时间复杂度很高,m*n的矩阵时间复杂度O(2^mn)。

因此我们使用动态规划算法O(m*n)。

解题思路:此题是典型的动态规划题目。

状态定义:设 dp 为大小 m×n 矩阵,其中dp[i][j] 的值代表直到走到(i,j) 的最小路径和。
转移方程:题目要求,只能向右或向下走,换句话说,当前单元格 (i,j) 只能从左方单元格(i−1,j) 或上方单元格 (i,j−1) 走到,因此只需要考虑矩阵左边界和上边界。走到当前单元格(i,j) 的最小路径和为 “从左方单元格 (i-1,j)与 从上方单元格 (i,j-1) 走来的两个最小路径和中较小的 ” +当前单元格值 grid[i][j] 。具体分为以下 4 种情况:

  • 当左边和上边都不是矩阵边界时: 即当i!=0 && j!=0,dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
  • 当只有左边是矩阵边界时: 只能从上面来,即当i = 0, j!= 0时,dp[i][j]=dp[i][j−1]+grid[i][j] ;
  • 当只有上边是矩阵边界时: 只能从左面来,即当i != 0, j = 0时,dp[i][j]=dp[i−1][j]+grid[i][j] ;
  • 当左边和上边都是矩阵边界时: 即当i=0,j=0时,其实就是起点, dp[i][j]=grid[i][j];

初始状态:dp 初始化即可,不需要修改初始0 值。
返回值:返回dp 矩阵右下角值,即走到终点的最小路径和。

其实我们完全不需要建立 dp 矩阵浪费额外空间,直接遍历grid[i][j] 修改即可。这是因为:grid[i][j] = min(grid[i - 1][j], grid[i][j - 1]) + grid[i][j] ;原grid 矩阵元素中被覆盖为dp 元素后(都处于当前遍历点的左上方),不会再被使用到。

复杂度分析:
时间复杂度 O(m×n) : 遍历整个grid 矩阵元素。
空间复杂度 O(1) : 直接修改原矩阵,不使用额外空间。

public int minPathSum(int[][] grid) {
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                if (i==0 && j==0) continue;
                else if (i==0) grid[i][j]+=grid[i][j-1];
                    else if (j==0) grid[i][j]+=grid[i-1][j];
                        else   grid[i][j]=Math.min(grid[i-1][j],grid[i][j-1])+grid[i][j];
            }
        }
        return grid[grid.length-1][grid[0].length-1];

二、62. 不同路径

2.1 题目描述

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

   

2.2 代码

2.2.1 动态规划

题解链接

思路与算法

我们用 f(i, j)表示从左上角走到(i,j) 的路径数量,其中 i 和 j的范围分别是 [0, m)和 [0, n)。

由于我们每一步只能从向下或者向右移动一步,因此要想走到(i,j),如果向下走一步,那么会从(i−1,j) 走过来;如果向右走一步,那么会从(i,j−1) 走过来。

因此我们可以写出动态规划转移方程:f(i,j)  = f(i−1,j) + f(i,j−1)

需要注意的是,如果 i=0,那么f(i−1,j) 并不是一个满足要求的状态,我们需要忽略这一项;同理,如果j=0,那么f(i,j−1) 并不是一个满足要求的状态,我们需要忽略这一项。

初始条件:f(0,0)=1,即从左上角走到左上角有一种方法。

最终返回的答案: f(m-1,n-1)。

细节:为了方便代码编写,我们可以将所有的f(0,j) 以及f(i,0) 都设置为边界条件,它们的值均为 1。

 public int uniquePaths(int m, int n) {
    int[][] grid = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j <n ; j++) {
             if(i==0) grid[i][j]=1;
              else if (j==0) grid[i][j]=1;
                    else  grid[i][j]=grid[i-1][j]+grid[i][j-1];
            }
        }
        return grid[m-1][n-1];
    }

复杂度分析

时间复杂度:O(m*n)。

空间复杂度:O(m*n),即为存储所有状态需要的空间。注意到 f(i,j) 仅与第 i 行和第i−1 行的状态有关,因此我们可以使用滚动数组代替代码中的二维数组,使空间复杂度降低为O(n)。此外,由于我们交换行列的值并不会对答案产生影响,因此我们总可以通过交换 m 和 n 使得m≤n,这样空间复杂度降低至O(min(m,n))。

2.2.2 排列组合

因为机器到底右下角,向下几步,向右几步都是固定的,比如,m=3, n=2,我们只要向下 1 步,向右 2 步就一定能到达终点。

  public int uniquePaths(int m, int n) {
        long ans = 1;
        for (int x = n, y = 1; y < m; ++x, ++y) {
            ans = ans * x / y;
        }
        return (int) ans;
    }

复杂度分析

时间复杂度:O(m)。由于我们交换行列的值并不会对答案产生影响,因此我们总可以通过交换 m 和 n 使得 m≤n,这样空间复杂度降低至O(min(m,n))。

空间复杂度:O(1)

数组区间

三、303. 区域和检索 - 数组不可变

3.1 题目描述

给定一个整数数组  nums,求出数组从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点。

实现 NumArray 类:

NumArray(int[] nums) 使用数组 nums 初始化对象
int sumRange(int i, int j) 返回数组 nums 从索引 i 到 j(i ≤ j)范围内元素的总和,包含 i、j 两点(也就是 sum(nums[i], nums[i + 1], ... , nums[j]))

3.2 代码

题解链接

思路:

要求一个区间范围内的数之和,如果采用现找现加的方式,将会导致复杂度跟求和范围紧密相关。

我们能做的更好的,就是让检索结果的时候,将其复杂度降为常量级,也就是单纯的数学计算。

所以我们可以先创建一个数组,用来存放从原数组每项累加的和,我们称其为前缀和数组

这样,我们可以利用减法,直接得出结果,公式:sumRange(i,j)=sums[j+1]−sums[i]。

注意前缀和数组设置为len=n+1,
若计算[1,2],return sum[right]-sum[left-1],ok

但若要计算nums的[0,2]时,return sum[right]-sum[left-1],left越界。
为了复用代码,将前缀和数组sum长度设置为len=n+1,sum[0]=0;

class NumArray {
  int[] sum;
    public NumArray(int[] nums) {//{-2, 0, 3, -5, 2, -1}
        int len= nums.length;
        sum=new int[len+1];
        for (int i = 0; i < len; i++) {
        sum[i+1]=sum[i]+nums[i];
        }
    }
    public int sumRange(int left, int right) {
        return sum[right+1]-sum[left];
        }
}

复杂度分析

时间复杂度:初始化 O(n),每次检索O(1),其中 n是数组nums 的长度。

空间复杂度:O(n),需要创建一个长度为n+1 的前缀和数组。

 

四、413. 等差数列划分

4.1 题目描述

如果一个数列至少有三个元素,并且任意两个相邻元素之差相同,则称该数列为等差数列。

例如,以下数列为等差数列:

数组 A 包含 N 个数,且索引从0开始。数组 A 的一个子数组划分为数组 (P, Q),P 与 Q 是整数且满足 0<=P<Q<N 。

如果满足以下条件,则称子数组(P, Q)为等差数组:元素 A[P], A[p + 1], ..., A[Q - 1], A[Q] 是等差的。并且 P + 1 < Q 。

函数要返回数组 A 中所有为等差数组的子数组个数。

4.2 代码

题解链接

dp[i] 用来存储在区间 (k,i), 而不在区间 (k,j) 中等差数列的个数,其中 j<i。

对于第 i 个元素,判断这个元素跟前一个元素的差值是否和等差数列中的差值相等。如果相等,那么新区间中等差数列的个数即为1+dp[i−1]。sum 同时也要加上这个值来更新全局的等差数列总数。

    

    //使用动态规划
    public int numberOfArithmeticSlices(int[] nums) {
        int len=nums.length;
        int count=0;
        int[] dp = new int[len];
        if (len<3) return 0;
        for (int i = 2; i < len; i++) {
            if (nums[i]-nums[i-1]==nums[i-1]-nums[i-2]){
                dp[i]=dp[i-1]+1;
            }
            count+=dp[i];
        }
        return count;
    }

复杂度分析

  • 时间复杂度: O(n)只需遍历数组 A 一次,其大小为 n。

  • 空间复杂度: O(n),一维数组dp 大小为 n。

优化:空间复杂度变为O(1),只是将上面的dp数组变为了int 变量

public int numberOfArithmeticSlices(int[] A) {
        int sum = 0;
        int dp=0;
        int n = A.length;
        for (int i = 1; i < n - 1; i++) {
            if (A[i] - A[i - 1] == A[i + 1] - A[i]) {
                dp=dp + 1;
                sum =sum+ dp;
            }
            else dp=0;
        }
            return sum;
    }

方法二:

    public int numberOfArithmeticSlices1(int[] nums) {
        int len=nums.length;
        int count=0;
        if (len<3) return 0;
        for (int i = 2; i < len; i++) {
            for (int j = i-2; j >=0 ; j--) {
                if (nums[j+2]-nums[j+1]==nums[j+1]-nums[j]){
                    count++;
                }else break;
            }
        }
        return count;
    }

分割整数

五、343. 整数拆分

5.1 题目描述

5.2 代码

5.2.1 数学方法

题解链接

     

  public int integerBreak(int n) {
        if (n<=3) return n-1;
        int a=n/3,b=n%3;
        if (b==0) return (int) Math.pow(3,a);
        if (b==1) return (int) Math.pow(3,a-1)*4;
         return (int) Math.pow(3,a)*2;
    }

复杂度分析

时间复杂度 :O(1),仅有求整、求余、次方运算。求整和求余运算:查阅资料,提到不超过机器数的整数可以看作是 O(1);幂运算:提到浮点取幂为 O(1)。

空间复杂度 :O(1) : a 和 b 使用常数大小额外空间。

5.2.2 动态规划

题解链接

 public int integerBreak(int n) {
        int[] dp = new int[n+1];
        dp[1]=1;
        for (int i = 2; i <n+1 ; i++) {
            for (int j = 1; j < i; j++) {
                dp[i]= Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));
            }
        }
        return dp[n];
    }

复杂度分析

时间复杂度:O(n^2),其中 n 是给定的正整数。对于从 2 到 n 的每一个整数都要计算对应的dp 值,计算一个整数的 dp 值需要O(n) ,因此总时间复杂度是 O(n^2)

空间复杂度:O(n),其中 n 是给定的正整数。创建一个数组dp,其长度为n+1。

六、279. 完全平方数

BFS(1091. 二进制矩阵中的最短路径、279. 完全平方数、127. 单词接龙)_kww_的博客-CSDN博客

此篇博文有提及

七、91. 解码方法

7.1 题目描述

一条包含字母 A-Z 的消息通过以下映射进行了 编码 :

'A' -> 1
'B' -> 2
...
'Z' -> 26

要 解码 已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106" 可以映射为:

"AAJF" ,将消息分组为 (1 1 10 6)
"KJF" ,将消息分组为 (11 10 6)
注意,消息不能分组为  (1 11 06) ,因为 "06" 不能映射为 "F" ,这是由于 "6" 和 "06" 在映射中并不等价。

给你一个只含数字的 非空 字符串 s ,请计算并返回 解码 方法的 总数 。

题目数据保证答案肯定是一个 32 位 的整数。

7.2代码

动态规划就是重复利用了结果,举个例子,有n个阶梯,一个人每一步只能跨一个台阶或是两个台阶,问这个人一共有多少种走法?

  

 

 public int numDecodings(String s) {
		if(s==null || s.length()==0) return 0;
		int n= s.length();
		int dp[] = new int[n+1];
		dp[0]=1;
		dp[1]= s.charAt(0)=='0' ? 0 : 1;
		for (int i = 2; i < dp.length; i++) {
			int one=Integer.valueOf(s.substring(i-1,i));
			if (one!=0) {
				dp[i]+=dp[i-1];
			}
			if (s.charAt(i-2)=='0') {
				continue;
			}
			int two =Integer.valueOf(s.substring(i-2,i));
			if (two <= 26) {
				dp[i]+=dp[i-2];
			}
		}
		return dp[n];
	}

最长递增子序列

八、300. 最长递增子序列

8.1 题目描述

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

8.2 代码

题解链接

public int lengthOfLIS(int[] nums) {
		if (nums.length==0) return 0;
		int dp[] = new int[nums.length];
		Arrays.fill(dp, 1);
		int res=0;
		for (int i = 0; i < nums.length; i++) {
			for (int j = 0; j < i; j++) {
				if (nums[j] < nums[i]) {
					dp[i]=Math.max(dp[i], dp[j]+1);
				}	
			}
            res=Math.max(res,dp[i]);
		}
		return res;//选出dp中最大的
	}

九、646. 最长数对链

9.1 题目描述

给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。

现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。

给定一个数对集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。

9.2 代码

此题和300题很相似,只是需要先排序

public static void main(String[] args) {
		// TODO Auto-generated method stub
		int[][] pairs= {{1,2}, {2,5},{4,8},{3,4}};
		Arrays.sort(pairs, (a, b) -> (a[0] - b[0]));//升序
		//Arrays.sort(pairs, (a, b) -> (b[0] - a[0]));//降序
		//System.out.println(Arrays.toString(pairs));//使用Java标准库提供的Arrays.toString()输出数组元素
		for (int[] n:pairs) {
			System.out.println(Arrays.toString(n));
		}
	}

public static int findLongestChain(int[][] pairs) {
		Arrays.sort(pairs, (a, b) -> (a[0] - b[0]));//升序
		int n= pairs.length;
		int dp[] = new int[n];
		Arrays.fill(dp, 1);
		for (int j = 1; j < dp.length; j++) {
			for (int i = 0; i < j; i++) {
				if (pairs[j][0] > pairs[i][1]) {
					dp[j]=Math.max(dp[j], dp[i]+1);
				}
			}		
		}
//		int ans=0;
//		for(int d:dp) {
//			if (d>ans) {
//				ans=d;
//			}
//		}
//		return ans;
		//Arrays.stream将数组转换成流
		return Arrays.stream(dp).max().orElse(0);//返回值如果存在,则返回;否则返回orElse里的0
    }
Arrays.stream(dp).max().orElse(0);//返回值如果存在,则返回;否则返回orElse里的0

十、376. 摆动序列

10.1题目描述

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。

相反,[1, 4, 7, 2, 5] 和 [1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。
子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

10.2 代码

题解链接

注意到 down 和 up 只和前一个状态有关,所以我们可以优化存储,分别用一个变量即可。

    public int wiggleMaxLength(int[] nums) {
		if (nums==null || nums.length==0) {
			return 0;
		}
		int up=1,down=1;
		for (int i = 1; i < nums.length; i++) {
			if (nums[i]> nums[i-1]) {
				up=down+1;
			}
			else if (nums[i] < nums[i-1]) {
				down=up+1;
			}
		}
		 return Math.max(down, up);
	    }

最长公共子序列

十一、1143. 最长公共子序列

11.1题目描述

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

11.2 代码

题解链接

求两个数组或者字符串的最长公共子序列问题,肯定是要用动态规划的。

定义 dp[i][j] 表示 text1[0:i-1] 和 text2[0:j-1] 的最长公共子序列。 (注:text1[0:i-1] 表示的是 text1 的 第 0 个元素到第 i - 1 个元素,两端都包含)

状态转移方程

  • 当 text1[i - 1] == text2[j - 1] 时,说明两个子字符串的最后一位相等,所以最长公共子序列又增加了 1,所以 dp[i][j] = dp[i - 1][j - 1] + 1;举个例子,比如对于 ac 和 bc 而言,他们的最长公共子序列的长度等于 a 和 b 的最长公共子序列长度 0 + 1 = 1。
  • 当 text1[i - 1] != text2[j - 1] 时,说明两个子字符串的最后一位不相等,那么此时的状态 dp[i][j] 应该是 dp[i - 1][j] 和 dp[i][j - 1] 的最大值。
public int longestCommonSubsequence(String text1, String text2) {
		int len1=text1.length();
		int len2=text2.length();
		int dp[][]=new int[len1+1][len2+1];
		for (int i = 1; i <= len1; i++) {
			for (int j = 1; j <= len2; j++) {
				if (text1.charAt(i-1)==text2.charAt(j-1)) {//最后一位相等,则加一
					dp[i][j]=dp[i-1][j-1]+1;
				}
				//当 text1[i - 1] != text2[j - 1] 时,说明两个子字符串的最后一位不相等,
				//那么此时的状态 dp[i][j] 应该是 dp[i - 1][j] 和 dp[i][j - 1] 的最大值
				else
				dp[i][j]=Math.max(dp[i-1][j], dp[i][j-1]);
			}
		}
		return dp[len1][len2];
	}

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值