路径问题
一、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];
}