文章目录

前言

除了去年11月份以及今年近几月的算法刷题之外,只有在当时20年蓝桥杯准备的时候才刷过一些题,在当时就有接触到一些动归、递归回溯、贪心等等,不过那会也还是一知半解,做的题目也特别少,因为考虑到之后面试有算法题以及数据结构算法对于一个程序员十分重要,我也开始了刷题之路。

我目前的学习数据结构与算法及刷题路径:

1、学习数据结构的原理以及一些常见算法。

2、​​代码随想录​​:跟着这个github算法刷题项目进行分类刷,在刷题前可以学习一下对应类别的知识点,而且这里面每道题都讲的很详细。

3、牛客网高频面试101题:​​牛客网—面试必刷101题​​,在刷的过程中可以在leetcode同步刷一下。

4、接下来就是力扣上的专栏​​《剑指offer II》​​​、​​《程序员面试金典(第 6 版)》​​…有对应的精选题单来对着刷即可。

5、大部分的高频面试、算法题刷完后,就可以指定力扣分类专栏进行一下刷题了。

刚开始刷的时候真的是很痛苦的,想到去年一道题可能就需要好几小时,真的就很难受的,不过熬过来一切都会好起来,随着题量的增多,很多题目你看到就会知道使用什么数据结构或者算法来去求解,并且思考对应的时间空间复杂度,并寻求最优解,我们一起加油!

我的刷题历程

截止2022.8.18:

1、牛客网101题(其中1题是平台案例有问题):

13数据结构与算法刷题之【动态规划】篇_空间复杂度

2、剑指offerII:

13数据结构与算法刷题之【动态规划】篇_动态规划_02

力扣总记录数:

13数据结构与算法刷题之【动态规划】篇_动态规划_03

加油加油!

知识点

13数据结构与算法刷题之【动态规划】篇_贪心算法_04

知识点:递归

递归是一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解。因此递归过程,最重要的就是查看能不能讲原本的问题分解为更小的子问题,这是使用递归的关键。

知识点:贪心思想

贪心思想属于动态规划思想中的一种,其基本原理是找出整体当中给的每个局部子结构的最优解,并且最终将所有的这些局部最优解结合起来形成整体上的一个最优解。

剑指offer

剑指 Offer 62. 圆圈中最后剩下的数字【简单】

和约瑟夫环题目类似,是历史上的一个问题

题目链接:​​剑指 Offer 62. 圆圈中最后剩下的数字​

题目内容:0,1,···,n-1这n个数字排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字(删除后从下一个数字开始计数)。求出这个圆圈里剩下的最后一个数字。

例如,0、1、2、3、4这5个数字组成一个圆圈,从数字0开始每次删除第3个数字,则删除的前4个数字依次是2、0、4、1,因此最后剩下的数字是3。

思路:动规。从最后1人往前进行推导。

f(1)最后活下来的是索引0。也就是我们从最后活的那人位置开始不断的进行往前推导
f(2) = (f(1) + ?) % 2, 这个?就是我们题目中说明的m => f(n) = (f(n - 1) + m) % n

1、动规

复杂度分析:时间O(n)、空间O(1)

class Solution {
public int lastRemaining(int n, int m) {
//动规
//f(n)表示在n个人中,值表示能够活下来的索引
//初始化f(1) = 0
//转移方程:f(n) = (f(n - 1) + m) % n
int x = 0;
for (int i = 2; i <= n; i++) {
x = (x + m) % i;
}
return x;
}
}

剑指 Offer 46. 把数字翻译成字符串【中等,与牛客网12题一致】

题目链接:​​剑指 Offer 46. 把数字翻译成字符串​

class Solution {
public int translateNum(int num) {
//[0, 25]的符合情况,实际上只需要看[10,25] dp[i] = dp[i - 1] + dp[i - 2]
//>25情况 dp[i] = dp[i - 1]
String str = "" + num;
int[] dp = new int[str.length() + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= str.length(); i++) {
//第一位数是否不为0
//不为0 ①若二位数符合 dp[i] = dp[i - 1] + dp[i - 2] ②二位数不符合 dp[i] = dp[i - 1]
//为0,二位数此时不符合 dp[i] = dp[i - 1]
//判断当前二位数的数字第一位是否为0
if (str.charAt(i - 2) != '0') {
//获取二位数
String numStr = str.substring(i - 2, i);
int n = Integer.valueOf(numStr);
if (n >= 0 && n <= 25) {
dp[i] = dp[i - 1] + dp[i - 2];
}else {
dp[i] = dp[i - 1];
}
}else {
dp[i] = dp[i - 1];
}
}
return dp[str.length()];
}
}

剑指 Offer 14- I. 剪绳子【中等】

学习:​​剑指 Offer 14- I. 剪绳子的优质题解​

题目链接:​​剑指 Offer 14- I. 剪绳子​

题目内容:给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

思路:

1、动态规划

复杂度分析:时间复杂度O(n2),空间复杂度O(n)

class Solution {

//长度为n的可能被剪成m段,这个m没有限制
//f(n) = val,n表示长度为n,val表示减m段最大的一个乘积
//1(1)、2(1x1=1,2)、3(1x2=2,3)、4(1x3=3,2x2=4)、5(1X4=4,2X3=6,5)
//转移方程:f(n) = Math.max(f(n), f(i)xf(n-i)...,f(i+1)xf(n-i-1))
//此时我们可以将f(i)...来进行记忆化,也就是f(n)为最大情况
public int cuttingRope(int n) {
int[] dp = new int[n + 1];
dp[1] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 1; j <= i / 2; j++) {
//计算得到左半部分最长的绳子、右半部分最长,取得积
int temp = Math.max(dp[j], j) * Math.max(dp[i - j], i - j);
dp[i] = Math.max(temp, dp[i]);
}
}
return dp[n];
}
}

2、数学推导

复杂度分析:时间复杂度O(1)、空间复杂度O(1)

class Solution {

//根据数学公式推出x=3的乘积最大
public int cuttingRope(int n) {
if(n <= 3) return n - 1;
int a = n / 3, b = n % 3;
//余数为0时,可计算3的a倍
if(b == 0) return (int)Math.pow(3, a);
//余数为1 【3的a次方*1 < a的(a-1)次方*4】,肯定是选择后者
if(b == 1) return (int)Math.pow(3, a - 1) * 4;
//余数为2 【3的a次方*2 > a的(a-1)次方*5】,选择前者
return (int)Math.pow(3, a) * 2;
}

}

剑指 Offer 14- II. 剪绳子 II【中等】

题目链接:​​剑指 Offer 14- II. 剪绳子 II​

题目内容:给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m - 1] 。请问 k[0]k[1]…*k[m - 1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

与题目I中的区别:①n的边界变大了,此时2<=n<=1000。②最终的结果值需要进行%运算。

思路:

1、数学推导(推导的过程实际上与I中一致,只不过这里的pow运算需要我们自己来进行手动去来进行,过程中需要不断模运算)

复杂度分析:时间复杂度O(n);空间复杂度O(1)

class Solution {

//核心:剪绳子成m段,最大的乘积
public int cuttingRope(int n) {
if (n <= 3) return n - 1;
int a = n / 3, b = n % 3;
int rem = 1000000007;
//这一个过程实际上就是得到【pow(3, a-1)】的一个过程,在进行乘积的过程中,不断进行模运算,仅此而已
//注意:res再进行倍乘的过程里可能会越int的界,所以我们需要将其定义为【long】类型
long res = 1;
for (int i = 1; i <= a - 1; i++) {
res = res * 3 % rem;
}
//根据%的结果值来进行列举
if (b == 0) {
return (int)(res * 3 % rem);
}else if (b == 1) {
return (int)(res * 4 % rem);
}
return (int)(res * 6 % rem);
}
}

13数据结构与算法刷题之【动态规划】篇_算法_05

我们可以对遍历操作来进行优化:

复杂度分析:时间复杂度O(logn);空间复杂度O(1)

class Solution {

private int rem = 1000000007;

//核心:剪绳子成m段,最大的乘积
public int cuttingRope(int n) {
if (n <= 3) return n - 1;
int a = n / 3, b = n % 3;
//这一个过程实际上就是得到【pow(3, a-1)】的一个过程,在进行乘积的过程中,不断进行模运算,仅此而已
//注意:res再进行倍乘的过程里可能会越int的界,所以我们需要将其定义为【long】类型
long res = 1;
// for (int i = 1; i <= a - 1; i++) {
// res = res * 3 % rem;
// }
res = pow(3, a - 1);
//根据%的结果值来进行列举
if (b == 0) {
return (int)(res * 3 % rem);
}else if (b == 1) {
return (int)(res * 4 % rem);
}
return (int)(res * 6 % rem);
}

public long pow(int n, int k) {
if (k == 0) {
return 1;
}
if (k == 1) {
return n;
}
long num = pow(n, k / 2);
if (k % 2 == 1) {
return num * num * n % rem;
}else {
return num * num % rem;
}
}
}

13数据结构与算法刷题之【动态规划】篇_动态规划_06

剑指 Offer 63. 股票的最大利润【中等,等同牛客网的第6题】

题目链接:​​剑指 Offer 63. 股票的最大利润​

题目内容:假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?

思路:

1、贪心:维护一个最大收益以及一个最低价买入

复杂度分析:时间复杂度O(n)、空间复杂度O(1)

class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
int max = Integer.MIN_VALUE, minPrice = -prices[0];
for (int i = 1; i < prices.length; i++) {
max = Math.max(max, minPrice + prices[i]);
minPrice = Math.max(minPrice, -prices[i]);
}
return max < 0 ? 0 : max;
}
}

2、动态规划:也可以解决多次买入卖出问题

复杂度分析:时间复杂度O(n)、空间复杂度O(n)

class Solution {
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) {
return 0;
}
//状态定义:dp[i][2],dp[i][0]表示的是卖出股票的最大收益,dp[i][1]表示买股票的最低点
//初始状态:dp[0][0],dp[0][1] = -prices[0]
//转移方程:dp[i][0] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][0]),dp[i][1] = Math.min(dp[i - 1][1], -prices[i])
//返回:dp[prices.length - 1][0]
int[][] dp = new int[prices.length][2];
dp[0][0] = dp[0][1] = -prices[0];
for (int i = 1; i < prices.length; i++) {
dp[i][0] = Math.max(dp[i - 1][1] + prices[i], dp[i - 1][0]);
dp[i][1] = Math.max(dp[i - 1][1], -prices[i]);
}
//若是所有的方案都亏钱,那么就不进行方案操作直接返回0
return dp[prices.length - 1][0] < 0 ? 0 : dp[prices.length - 1][0];
}
}

剑指 Offer 60. n个骰子的点【中等】

题目链接:​​剑指 Offer 60. n个骰子的点数​

题目内容:把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出s的所有可能的值出现的概率。

你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。

思路:

1、递归【在leetcode上超时,n=9时】

复杂度分析:时间复杂度 O(6n);空间复杂度O(1)

class Solution {
public double[] dicesProbability(int n) {
//暴力求法,可能投到的点数
List<Integer> counts = new ArrayList<>();
int sum = 0;//总次数
for (int i = n; i <= n * 6; i++) {
int num = dfs(i, n);
sum += num;
counts.add(num);
}
//定义概率数组
double[] dp = new double[5 * n + 1];
for (int i = 0; i < dp.length; i++) {
dp[i] = counts.get(i) * 1.0 / sum * 1.0;
}
return dp;
}

//sum表示投得的点数
//n表示当投筛子的次数
public int dfs(int sum, int n) {
//若是sum为小于0
if (sum < 0 || n < 0) {
return 0;
}
//若是最终扣减的值为0且投掷的次数也为0表示是一种方案
if (sum == 0 && n == 0) {
return 1;
}
int res = 0;
for (int i = 1; i <= 6; i++) {
res += dfs(sum - i, n - 1);
}
return res;
}
}

2、动态规划【推荐】

复杂度分析:时间复杂度O(n2);空间复杂度O(n)

class Solution {
public double[] dicesProbability(int n) {
//状态定义:dp[i][j]=val,表示i个筛子投到点数j的概率
//状态转移方程:dp[i][j] = dp[i][j] + dp[i - 1][j - 1] / 6.0
double[] dp = new double[6];//初始化6个,因为1个筛子能够投到6
Arrays.fill(dp, 1.0 / 6.0);
//筛子的数量作为遍历的次数
for (int i = 2; i <= n; i++) {
//初始化对应新的筛子数可能投中的点数
double[] temp = new double[5 * i + 1];
//前i - 1个筛子投之后数的大小
for (int j = 0; j < dp.length; j++) {
//最大是6个点
for (int k = 0; k < 6; k++) {
temp[j + k] += dp[j] / 6.0;//新的一轮的话,就需要x6倍,例如1个筛子可以投1-6,也就是6次几率,两个筛子就是6x6=36次
}
}
dp = temp;
}
return dp;
}
}

剑指 Offer 47. 礼物的最大价值【中等】

题目链接:​​剑指 Offer 47. 礼物的最大价值​

题目内容:在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?

思路:

1、动态规划

复杂度分析:时间复杂度O(n2),空间O(1)

class Solution {
public int maxValue(int[][] grid) {
//状态定义:dp[i][j]=val,移动到第第i,j位置时可取得的最大价值
//转移方程:dp[i][j] = Math.max(dp[i - 1][j] + grid[i][j], dp[i][j - 1] + grid[i][j])(i > 0, j > 0)
//初始化:i=0,j=0
int n = grid.length;
int m = grid[0].length;
int[][] dp = new int[n][m];
dp[0][0] = grid[0][0];
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int j = 1;j < m; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
dp[i][j] = Math.max(dp[i - 1][j] + grid[i][j], dp[i][j - 1] + grid[i][j]);
}
}
return dp[n - 1][m - 1];
}
}

剑指 Offer 43. 1~n 整数中 1 出现的次数【困难】

题目链接:​​剑指 Offer 43. 1~n 整数中 1 出现的次数​

题目内容:输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

0-9    【1】
10-99 19
10 11-19 21 31 41 - 91 19个
0-99 【20】
0-999 【300】
结论:从[0, pow(10, i) - 1]的个数为i*pow(10, i - 1),i指的是位数。

假设n,若是n > (pow(10, i) - 1),那么来进行累加,n - (pow(10, i) - 1),循环累减,直到 n <= (pow(10, i) - 1)

举例:n=234
dp[i] = val,i表示的是位数,val即为值
dp[0] = 0 dp[1] = 1 dp[2] = 20 dp[3]=300
234根据区间来拆分为:[0,99]、[100,199]、[200,234]
[0,99]、[100,199]:dp[2] * (234/100) + pow(10,2) = 40 + 100 = 【140】
[200, 234]:中1的个数之和等于[0, 34]之间,
[0, 9], [10, 19], [20, 29], [30, 34]进行拆分:
[0, 9], [10, 19], [20, 29]:(34/10) * dp[1] + pow(10, 1) = 3 + 10 = 【13】
[30, 34]: (4/1)*dp[0] + pow(10, 0) = 【1】
最终合并起来就是:140+13+1=154
[0,99]、[100,199] => (234/100) * dp[2] + pow(10,2) = 40 + 100 = 【140】
[0, 9], [10, 19], [20, 29] => (34/10) * dp[1] + pow(10, 1) = 3 + 10 = 【13】
[30, 34] => (4/1)*dp[0] + pow(10, 0) = 【1】

思路1:找规律

复杂度分析:时间复杂度O(logn);空间复杂度O(1)

class Solution {

//234(所有例子都是ans += b * dp[i] + temp)
//i = 0 a = 0 b = 4 temp = 1 ans+=4 * dp[0] + 1= 4 * 0 + 1 = 1 实际上计算的是[0,4]
//i = 1 a = 4 b = 3 temp = 10 ans+=3 * dp[1] + 10 = 3 + 10 = 14 实际上计算机的是[0, 34] => [0,4]、[10, 19]、[20,29]、[30,34]的个位数,对于10-19的十位数有10个
// 3个个位上的,10个十位上的数以及之前的个人数
//i = 2 a = 3 b = 2 temp = 100 ans+=2 * dp[2] + 100 = 40 + 100 = 154

//214
//到b==1时(该例子是ans += a + dp[i] + 1;)
//i = 1 a = 4 b = 1 temp = 10 ans+=4 + dp[1] + 1
// 4+1=>[10,14]的十位数、dp[1]=1表示的是[10,14]的个位数
public int countDigitOne(int n) {
//dp准备10位数的所有情况
int[] dp = new int[10];
dp[0] = 0;
dp[1] = 1;
//开始提前进行计算
//0 1 20 200+200/2
for (int i = 2; i < 10; i++) {
//等同于i * pow(10, i - 1)
dp[i] = dp[i - 1] * 10;
dp[i] = dp[i] + dp[i] / (i - 1);
}
//ans表示结果集、i表示位数
int ans = 0, i = 0;
long temp = 1;//表示各段的进位数
while (temp <= n) {
//a表示当前temp的取模结果,b表示各个temp的对应的位数
int a = (int)(n % temp), b = (int)((n % (temp * 10)) / temp);
//根据b的值来进行推断
if (b > 1) {
ans += b * dp[i] + temp;
}else if (b == 1) {
ans += a + dp[i] + 1;
}
temp *= 10;
i++;
}
return ans;
}
}

牛客网

跳台阶【简单】

题目链接:​​ 跳台阶​

题目内容:一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法(先后次序不同算不同的结果)。

思路1:迭代法(动态规划)。

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
class Solution {
//来自leetcode:https://leetcode.cn/problems/qing-wa-tiao-tai-jie-wen-ti-lcof/submissions/
public int numWays(int n) {
if (n <= 1) {
return 1;
}
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = (dp[i-1] + dp[i - 2]) % 1000000007;
}
return dp[n];
}
}

优化动态规划:通过使用数组的方法,会有很多空间会被浪费掉,其实使用三个变量就ok了,空间复杂度大大优化。

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
public class Solution {
//迭代法
public int jumpFloor(int target) {
if (target <= 1) {
return 1;
}
int res = 0;
int a = 1;
int b = 1;
for (int i = 2;i <= target; i++) {
res = a + b;
a = b;
b = res;
}
return res;
}
}

思路2:递归

复杂度分析:

  • 时间复杂度:O(2n)
  • 空间复杂度:O(1)
public class Solution {

//迭代法
public int jumpFloor(int target) {
if (target == 0 || target == 1) {
return 1;
}
return jumpFloor(target - 1) + jumpFloor(target - 2);
}
}

优化:上述递归会出现大量重复的计算,我们可以使用一个dp数组来另其具备记忆化。

public class Solution {

//记忆化数组
private int dp[] = new int[40];

//迭代法
public int jumpFloor(int target) {
if (target == 0 || target == 1) {
return 1;
}
//来进行记忆化判断,若是之前计算过,那么就无需再重复计算
if (dp[target] != 0) {
return dp[target];
}
return jumpFloor(target - 1) + jumpFloor(target - 2);
}
}

最小花费爬楼梯【简单】

题目链接:​​最小花费爬楼梯​

题目内容:给定一个整数数组 cost cost ,其中 cost[i]*cos**t*[i] 是从楼梯第i *i* 个台阶向上爬需要支付的费用,下标从0开始。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

思路:采用动态规划。使用dp数组来保存每个格子最小花费,推出递归公式为:​​dp[i] = Math.min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])​

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
import java.util.*;

public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param cost int整型一维数组
* @return int整型
*/
public int minCostClimbingStairs (int[] cost) {
//设置dp数组:下标表示第几层,值为最小花费
//dp[i] = Math.min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
int[] dp = new int[cost.length + 1];
for (int i = 2; i <= cost.length;i++) {
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[cost.length];
}
}

不同路径的数目(一)【简单】

题目链接:​​不同路径的数目(一)​

题目内容:一个机器人在m×n大小的地图的左上角(起点)。机器人每次可以向下或向右移动。机器人要到达地图的右下角(终点)。

可以有多少种不同的路径从起点走到终点?

思考区

约束条件:

1、机器人每次只能往右或者往下走。

2、机器人不能越界。

解题

思路1:动态规划

定义状态方程:

dp[i][j] = val  i表示行,j表示列,val表示方案数量
当i>1 && j>1 dp[i][j] = dp[i][j-1] + dp[i-1][j]
当i=0时,dp[i][j] = dp[i][j-1]
当j=0时,dp[i][j] = dp[i-1][j]

复杂度分析:

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

代码:

import java.util.*;


public class Solution {
/**
*
* @param m int整型
* @param n int整型
* @return int整型
*/
public int uniquePaths (int m, int n) {
//定义dp数组
int[][] dp= new int[m][n];

for (int i = 0;i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0) {
dp[i][j] = 1;
continue;
}
if (j == 0) {
dp[i][j] = 1;
continue;
}
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}

思路2:递归

import java.util.*;


public class Solution {


/**
*
* @param m int整型
* @param n int整型
* @return int整型
*/
public int uniquePaths (int m, int n) {
if (m == 1 || n == 1) {
return 1;
}
return uniquePaths(m - 1, n) + uniquePaths(m, n - 1);
}
}

上面的时间复杂度是2n,如何优化呢?进行记忆化状态方程

class Solution {
private int[][] dp;

public int uniquePaths(int m, int n) {
if (dp == null) {
dp = new int[m + 1][n + 1];
}
if (m == 1 || n == 1) {
return 1;
}
if (dp[m][n] == 0) {
dp[m][n] = uniquePaths(m - 1, n) + uniquePaths(m, n - 1);
}
return dp[m][n];
}
}

此时无论是时间还是空间复杂度与思路1一致。

连续子数组的最大和【简单】

题目链接:​​连续子数组的最大和​

题目内容:输入一个长度为n的整型数组array,数组中的一个或连续多个整数组成一个子数组,子数组最小长度为1。求所有子数组的和的最大值。

思路

关键词:连续、最大。

dp[i] = val:i表示当前数组下标索引,val表示截止到目前连续的最大和

列出dp方程组:dp[i] = Math.max(array[i], dp[i-1] + array[i]);

解题

思路1:动态规划

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int[] dp = new int[array.length];
dp[0] = array[0];
int max = dp[0];
//dp[i] = Math.max(array[i], dp[i-1] + array[i]);
//下标:到index时最大的值位置 值;最大连续和
for (int i = 1; i < array.length; i++) {
dp[i] = Math.max(array[i], dp[i-1] + array[i]);
if(max < dp[i]) {
max = dp[i];
}
}
return max;
}
}

买卖股票的最好时机(一)【简单】

题目链接:​买卖股票的最好时机(一)

题目内容:假设你有一个数组prices,长度为n,其中prices[i]是股票在第i天的价格,请根据这个价格数组,返回买卖股票能获得的最大收益。

思路1:暴力法(超时)

复杂度分析:

  • 时间复杂度:O(n2)
  • 空间复杂度:O(1)
import java.util.*;


public class Solution {
/**
*
* @param prices int整型一维数组
* @return int整型
*/
public int maxProfit (int[] prices) {
int ans = 0;
for (int i = 0; i < prices.length;i++) {
for (int j = i + 1;j < prices.length; j++){
ans = Math.max(ans, prices[j] - prices[i]);
}
}
return ans;
}
}

思路2:贪心算法,维护一个最小值来进行不断比较。

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
import java.util.*;


public class Solution {
/**
*
* @param prices int整型一维数组
* @return int整型
*/
public int maxProfit (int[] prices) {
int min = Integer.MAX_VALUE;
int ans = 0;
for (int i = 0; i < prices.length;i++) {
//维护一个最小值
min = Math.min(prices[i], min);
//不断的比对(截止今天之前的最大收益,当前价格-最小值)
ans = Math.max(ans, prices[i] - min);
}
return ans;
}
}

买卖股票的最好时机(二)【中等】

与第6题的区别在于:这里可以多次买卖股票,让你求得一个最大收益。

区别在于可以多次买入卖出。 但是对于每天还是有到此为止的最大收益和是否持股两个状态,因此我们照样可以用动态规划

思路:动态规划。

复杂度分析:

  • 时间复杂度:
  • 空间复杂度:
import java.util.*;


public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
* 计算最大收益
* @param prices int整型一维数组 股票每一天的价格
* @return int整型
*/
public int maxProfit (int[] prices) {
//dp动态规划。0下标:表示未持有时最大收益。1下标:表示持有时的一个最大收益。
//dp[i][0] = Math.max(dp[i - 1][0], (dp[i - 1][1] + prices[i]))
//dp[i][1] = Math.max(dp[i - 1][1], (dp[i - 1][0] - prices[i]))
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for (int i = 1; i < prices.length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
}
return dp[prices.length - 1][0];
}
}

买卖股票的最好时机(三)【困难】

区别:最多可以对该股票有两笔交易操作,一笔交易代表着一次买入与一次卖出,但是再次购买前必须卖出之前的股票.

设置一个五维数组,然后分别来进行记录。

13数据结构与算法刷题之【动态规划】篇_动态规划_07

思路:动态规划

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
import java.util.*;


public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
* 两次交易所能获得的最大收益
* @param prices int整型一维数组 股票每一天的价格
* @return int整型
*/
public int maxProfit (int[] prices) {
//dp[i][j] (j在0-4中), i表示第几天。j的0-4分别表示:未买入、第一次买入、第二次卖出、第二次买入、第二次卖出
//状态方程定义与初始化
int n = prices.length;
int[][] dp = new int[n][5];
//第一天
Arrays.fill(dp[0], -10000);//最大股票10000元
for (int i = 0; i < n; i++) {
dp[i][0] = 0;
}
dp[0][1] = -prices[0];
for (int i = 1; i < n; i++) {
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]);
dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]);
dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]);
}
//可能两次交易收益最后或者一次交易最多
return Math.max(dp[n - 1][4], Math.max(0, dp[n - 1][2]));
}
}

兑换零钱【中等】

学习视频:​​Leetcode 322 零钱兑换 【动态规划解法】​​​(讲的不太清楚)、​​动态规划19:例题:零钱兑换问题的分析​​(推荐)

题目地址:​​兑换零钱(一)​

题目描述:给定数组arr,arr中所有的值都为正整数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个aim,代表要找的钱数,求组成aim的最少货币数。如果无解,请返回-1.

思路1:采用动态规划,列出状态方程组。

1、dp数组​​下标[1,amount]表示金额数​​​,dp的​​值表示对应金额的最小硬币数​​。

2、​​dp[i] = min(dp[i], dp[i-arr[j] + 1])​​。

3、初始化每个数组值为​​最大硬币数+1​​。

4、迭代遍历​​所有金额情况​​​及​​对应面值情况​​。

5、设置符合的条件(​​coins[j] <= i,面额要小于对应总额​​)来进行状态方程推导。

class Solution {
//dp数组下标[1,amount]表示金额数,dp的值表示对应金额的最小硬币数
//dp[i] = min(dp[i], dp[i-arr[j] + 1])
public int coinChange(int[] coins, int amount) {
//边界条件
if (amount < 1) {
return 0;
}
//定义dp数组,数量为金额数
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1);//硬币数的范围肯定是0 - amount,所以这里的话是最大硬币数
//当目标金额为0时,0个硬币数
dp[0] = 0;
//迭代所有金额
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
//最后返回对应的一个dp[amount]即可
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
}

最长系列【中等】

最长公共子序列(二)【中等】

题目链接:​​最长公共子序列(二)​

题目内容:给定两个字符串str1和str2,输出两个字符串的最长公共子序列。如果最长公共子序列为空,则返回"-1"。目前给出的数据,仅仅会存在一个最长的公共子序列

思路:dp+递归。①nxn遍历,来进行计算dp中每个格子的可连接

示例:把思路理清楚了就ok。

"1A2C3D4B56", "B1D23A456A"
结果:123456

下图中每个格子的左边是dp的值,右边红色的是方向数组b的值。左下角包含有思路解决:

13数据结构与算法刷题之【动态规划】篇_i++_08

复杂度分析:

  • 空间复杂度:O(n2)
  • 时间复杂度:O(n2)
import java.util.*;


public class Solution {

private String x;
private String y;

/**
* longest common subsequence
* @param s1 string字符串 the string
* @param s2 string字符串 the string
* @return string字符串
*/
public String LCS (String s1, String s2) {
this.x = s1;
this.y = s2;
char[] sArr1 = s1.toCharArray();
char[] sArr2 = s2.toCharArray();
int[][] dp = new int[sArr1.length + 1][sArr2.length + 1];
int[][] d = new int[sArr1.length + 1][sArr2.length + 1];
for (int i = 1; i <= sArr1.length; i++) {
for (int j = 1; j <= sArr2.length; j++) {
//比较两个字符
if (sArr1[i - 1] == sArr2[j - 1]) {
//若是相同
dp[i][j] = dp[i - 1][j - 1] + 1;
d[i][j] = 1;
}else {
if (dp[i - 1][j] > dp[i][j - 1]) {
dp[i][j] = dp[i - 1][j];
d[i][j] = 2;
}else {
dp[i][j] = dp[i][j - 1];
d[i][j] = 3;
}
}
}
}
String ans = ans(s1.length(), s2.length(), d);
if (ans.isEmpty()) {
return "-1";
}
return ans;
}

//递归获取最长子序列
public String ans(int i, int j, int[][] d) {
String res = "";
if (i == 0 || j == 0) {
return res;
}
if (d[i][j] == 1) {
res += ans(i - 1,j - 1, d);
res += x.charAt(i - 1);
}else if (d[i][j] == 2) {
res += ans(i - 1,j, d);
}else {
res += ans(i, j - 1, d);
}
return res;
}
}

最长公共子串【中等】

题目链接:​​最长公共子串​

题目内容:给定两个字符串str1和str2,输出两个字符串的最长公共子串。题目保证str1和str2的最长公共子串存在且唯一。

思路1:动态规划,使用二维数组来进行记忆。

示例:

DABCE
ABCD
结果:ABC

13数据结构与算法刷题之【动态规划】篇_动态规划_09

复杂度分析:

  • 时间复杂度:O(n*m)
  • 空间复杂度:O(n*m)
import java.util.*;


public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* longest common substring
* @param str1 string字符串 the string
* @param str2 string字符串 the string
* @return string字符串
*/
public String LCS (String str1, String str2) {
char[] arr1 = str1.toCharArray();
char[] arr2 = str2.toCharArray();
int[][] dp = new int[arr1.length + 1][arr2.length + 1];
int maxLastIndex = 0;
int maxLength = 0;
for (int i = 1; i <= arr1.length; i++) {
for (int j = 1; j <= arr2.length; j++) {
if (arr1[i - 1] == arr2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
if (dp[i][j] > maxLength) {
maxLength = dp[i][j];
maxLastIndex = i;
}
}else {
dp[i][j] = 0;
}
}
}
if (maxLength == 0) {
return "";
}
return str1.substring(maxLastIndex - maxLength, maxLastIndex);
}
}

思路2:使用一维数组来进行dp动态规划

13数据结构与算法刷题之【动态规划】篇_算法_10

import java.util.*;


public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* longest common substring
* @param str1 string字符串 the string
* @param str2 string字符串 the string
* @return string字符串
*/
public String LCS (String str1, String str2) {
char[] arr1 = str1.toCharArray();
char[] arr2 = str2.toCharArray();
int[] dp = new int[arr2.length + 1];
int maxLastIndex = 0;
int maxLength = 0;
for (int i = 1; i <= arr1.length; i++) {
for (int j = arr2.length; j >= 1; j--) {
if (arr1[i - 1] == arr2[j - 1]) {
dp[j] = dp[j - 1] + 1;
if (dp[j] > maxLength) {
maxLength = dp[j];
maxLastIndex = i;
}
}else {
dp[j] = 0;
}
}
}
if (maxLength == 0) {
return "";
}
return str1.substring(maxLastIndex - maxLength, maxLastIndex);
}
}

最长上升子序列(一)【中等】

题目链接:​​最长上升子序列(一)​

题目内容:给定一个长度为 n 的数组 arr,求它的最长严格上升子序列的长度。所谓子序列,指一个数组删掉一些数(也可以不删)之后,形成的新数组。例如 [1,5,3,7,3] 数组,其子序列有:[1,3,3]、[7] 等。但 [1,6]、[1,3,5] 则不是它的子序列。我们定义一个序列是 严格上升 的,当且仅当该序列不存在两个下标 i 和 j 满足 i < j,i < j 且 arr_i geq arr_ arr i ≥ arr j。

思路:动规。dp数组中的值表示的是当前最长严格上升序列的长度。

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
import java.util.*;

public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 给定数组的最长严格上升子序列的长度。
* @param arr int整型一维数组 给定的数组
* @return int整型
*/
public int LIS (int[] arr) {
//dp[i] = dp[j] + 1;
int res = 0;
int[] dp = new int[arr.length];
Arrays.fill(dp, 1);
for (int i = 1; i < arr.length; i++) {
for (int j = 0; j < i; j++) {
//若是后者i>j的元素,且dp数组当前
if (arr[i] > arr[j] && dp[i] < dp[j] + 1) {
dp[i] = dp[j] + 1;
res = Math.max(res, dp[i]);
}
}
}
return res;
}
}

打家劫舍(一)【中等】

题目链接:​​打家劫舍(一)​

题目内容:你是一个经验丰富的小偷,准备偷沿街的一排房间,每个房间都存有一定的现金,为了防止被发现,你不能偷相邻的两家,即,如果偷了第一家,就不能再偷第二家;如果偷了第二家,那么就不能偷第一家和第三家。给定一个整数数组nums,数组中的元素表示每个房间存有的现金数额,请你计算在不被发现的前提下最多的偷窃金额。

思路:采用dp来进行收益最大化。

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
import java.util.*;


public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param nums int整型一维数组
* @return int整型
*/
public int rob (int[] nums) {
//dp[i] = Math.max(dp[i - 1], nums[i] + dp[i - 2])
int[] dp = new int[nums.length + 1];
dp[1] = nums[0];
for (int i = 2; i <= nums.length; i++) {
dp[i] = Math.max(dp[i - 1], nums[i - 1] + dp[i - 2]);
}
return dp[nums.length];
}
}

打家劫舍(二)【中等】

题目链接:​​打家劫舍(二)​

题目内容:你是一个经验丰富的小偷,准备偷沿湖的一排房间,每个房间都存有一定的现金,为了防止被发现,你不能偷相邻的两家,即,如果偷了第一家,就不能再偷第二家,如果偷了第二家,那么就不能偷第一家和第三家。沿湖的房间组成一个闭合的圆形,即第一个房间和最后一个房间视为相邻。

给定一个长度为n的整数数组nums,数组中的元素表示每个房间存有的现金数额,请你计算在不被发现的前提下最多的偷窃金额。

思路:dp动态规划

多了个条件为:沿湖的房间组成一个闭合的圆形,即第一个房间和最后一个房间视为相邻

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
import java.util.*;


public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param nums int整型一维数组
* @return int整型
*/
public int rob (int[] nums) {
//在打家劫舍(一)中进行升级:第一家与最后一家是邻居。
//那么此时第一家与最后一家为邻里关系
//那么两种方案来进行对比:①偷第一家,然后到最后一家之前,最后一家不偷。②不偷第一家,然后一直到最后一家。
int[] dp = new int[nums.length + 1];
dp[1] = nums[0];
for (int i = 2;i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1], nums[i - 1] + dp[i - 2]);
}
int res = dp[nums.length - 1];//由于少了最后一家,所以这里取前一位
//开启第二轮比较
Arrays.fill(dp, 0);
dp[1] = 0;
for (int i = 2; i <= nums.length; i++) {
dp[i] = Math.max(dp[i - 1], nums[i - 1] + dp[i - 2]);
}
return Math.max(res, dp[nums.length]);
}
}

数字字符串转化成IP地址【中等】

同题目:​​leetcode_剑指 Offer II 087. 复原 IP ​

题目链接:​​数字字符串转化成IP地址​

题目内容:现在有一个只包含数字的字符串,将该字符串转化成IP地址的形式,返回所有可能的情况。

例如:给出的字符串为"25525522135",返回[“255.255.22.135”, “255.255.221.35”]. (顺序没有关系)

题解一:枚举

复杂度分析:

  • 时间复杂度:O(n3)
  • 空间复杂度:O(n)
import java.util.*;


public class Solution {
/**
*
* @param s string字符串
* @return string字符串ArrayList
*/
public ArrayList<String> restoreIpAddresses (String s) {
ArrayList<String> res = new ArrayList<>();
int n = s.length();
if (s.length() > 12) {
return res;
}
//三层遍历
for (int i = 1; i < 4 && i < n - 2; i++) {
for (int j = i + 1; j < i + 4 && j < n - 1; j++) {
for (int k = j + 1; k < k + 4 && k < n; k++) {
//计算最后一段长度
if (n - k >= 4) {
continue;
}
String a = s.substring(0, i);
String b = s.substring(i, j);
String c = s.substring(j, k);
String d = s.substring(k, n);
//筛选前导0
if ((a.length() > 1 && a.charAt(0) == '0') ||
(b.length() > 1 && b.charAt(0) == '0') ||
(c.length() > 1 && c.charAt(0) == '0') ||
(d.length() > 1 && d.charAt(0) == '0')
){
continue;
}
//筛选每个数字要<255
if (Integer.valueOf(a) > 255 ||
Integer.valueOf(b) > 255 ||
Integer.valueOf(c) > 255 ||
Integer.valueOf(d) > 255
){
continue;
}
String ip = a + "." + b + "." + c + "." + d;
res.add(ip);
}
}
}
return res;
}
}

题解二:递归+回溯

复杂度分析:

  • 时间复杂度:O(3n),3个分枝的树型递归
  • 空间复杂度:O(n),递归栈深度为nnn
import java.util.*;


public class Solution {
/**
*
* @param s string字符串
* @return string字符串ArrayList
*/
public ArrayList<String> restoreIpAddresses (String s) {
ArrayList<String> res = new ArrayList<>();
dfs(res, s, 0, 0);
return res;
}

private String nums = "";

//递归
public void dfs(ArrayList<String> res,String str, int step, int index) {
//表示一个数字
String num = "";
//递归出口
if (step == 4) {
//若是当前的索引不是最后一个,那么说明不符合条件
if (index != str.length()) {
return;
}
res.add(nums);
}else {
//全排列
for (int i = index; i < index + 3 && i < str.length(); i++) {
num += str.charAt(i);
int n = Integer.valueOf(num);
//记录当前的总字符串来方便回溯
String temp = nums;
//进行筛选num,若是num不符合规则
if (n <= 255 && (num.length() == 1 || num.charAt(0) != '0')) {
//根据当前的数字个数来判断是否是最后一个
if (step != 3) {
nums += num + ".";
}else {
nums += num;
}
//进行递归
dfs(res, str, step + 1, i + 1);
//回溯
nums = temp;
}
}
}
}

}

优化:对于个位0提前进行剪枝

class Solution {
/**
*
* @param s string字符串
* @return string字符串ArrayList
*/
public ArrayList<String> restoreIpAddresses (String s) {
ArrayList<String> res = new ArrayList<>();
dfs(res, s, 0, 0);
return res;
}

//记录数组
int[] segments = new int[4];

//递归
public void dfs(ArrayList<String> res,String s, int step, int index) {
if (step == 4) {
if (index == s.length()) {
StringBuilder str = new StringBuilder();
for (int i = 0; i < segments.length; i++) {
str.append(segments[i]);
if (i != segments.length - 1) {
str.append(".");
}
}
res.add(str.toString());
}
return;
}

//提前剪枝
if (index == s.length()) {
return;
}

//前导0快速截止
if (s.charAt(index) == '0') {
segments[step] = 0;
dfs(res, s, step + 1, index + 1);
}

//进行枚举情况
int num = 0;
for (int i = index; i < index + 3 && i < s.length(); i++) {
num = num * 10 + (s.charAt(i) - '0');
//筛选(要将个位0排除掉)
if (num > 0 && num <= 255) {
segments[step] = num;
//注意注意:这里是i+1
dfs(res, s, step + 1, i + 1);
}else {
//提前结束
break;
}
}

}

}

13数据结构与算法刷题之【动态规划】篇_贪心算法_11

矩阵的最小路径和【中等】

​矩阵的最小路径和​

思路:递归+记忆化dp。

这种方式的话会有大量的重复递归操作,为了简化重复递归的情况,使用dp来进行记忆化保存。

import java.util.*;


public class Solution {
/**
*
* @param matrix int整型二维数组 the matrix
* @return int整型
*/
public int minPathSum (int[][] matrix) {
this.dp = new int[matrix.length][matrix[0].length];
return dfs(matrix, 0, 0);
}

private int min = Integer.MAX_VALUE;
//记忆化dp
private int[][] dp;

public int dfs(int[][] matrix, int i, int j) {
if (i == matrix.length - 1 && j == matrix[i].length - 1) {
return matrix[i][j];
}
if (dp[i][j] != 0) {
return dp[i][j];
}
if (i == matrix.length - 1) {
return matrix[i][j] + dfs(matrix, i, j + 1);
}
if (j == matrix[i].length - 1) {
return matrix[i][j] + dfs(matrix, i + 1, j);
}
//找到左右路径最小的一个值
int right = dfs(matrix, i + 1, j);
int down = dfs(matrix, i, j + 1);
dp[i][j] = matrix[i][j] + Math.min(right, down);
return dp[i][j];
}

}

思路2:动态规划

复杂度分析:

  • 时间复杂度:O(n2)
  • 空间复杂度:O(n2)
import java.util.*;

public class Solution {

int[][] dp;

/**
*
* @param matrix int整型二维数组 the matrix
* @return int整型
*/
public int minPathSum (int[][] matrix) {
int rows = matrix.length;
int columns = matrix[rows - 1].length;
this.dp = new int[rows][columns];
dp[0][0] = matrix[0][0];
//处理第一行
for (int i = 1; i < matrix[0].length; i++) {
dp[0][i] = dp[0][i - 1] + matrix[0][i];
}
//处理第一列
for (int j = 1; j < rows; j++) {
dp[j][0] = dp[j - 1][0] + matrix[j][0];
}
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
dp[i][j] = Math.min(dp[i - 1][j] + matrix[i][j], dp[i][j - 1] + matrix[i][j]);
}
}
return dp[rows - 1][columns - 1];
}

}

把数字翻译成字符串【中等】

题目链接:​​把数字翻译成字符串​

题目内容:有一种将字母编码成数字的方式:‘a’->1, ‘b->2’, … , ‘z->26’。我们把一个字符串编码成一串数字,再考虑逆向编译成字符串。

由于没有分隔符,数字编码成字母可能有多种编译结果,例如 11 既可以看做是两个 ‘a’ 也可以看做是一个 ‘k’ 。但 10 只可能是 ‘j’ ,因为 0 不能编译成任何结果。现在给一串数字,返回有多少种可能的译码结果

'a'->1, 'b->2', ... , 'z->26'
对于11,可能是aa,也可能是k,也就是说2位数的数字有两种情况

思路1:动态规划。

  • 直接开始找规律,可以看到每增加一个时,可以拆分为单个数字以及多个数字情况,那么我们可以列出动规式子,使用dp来表示当前的一个字符串翻译数量,总共有四种情况。
//个位数不符合(= 0) && 两位数不符合(>=26)  dp[i] = 0
//个位数符合 && 两位数不符合 dp[i] = dp[i - 1]
//个位数不符合(=0) && 两位数符合 dp[i] = dp[i - 2]
//个位数符合 && 两位数符合 dp[i] = dp[i - 2] + dp[i - 1]

13数据结构与算法刷题之【动态规划】篇_算法_12

import java.util.*;


public class Solution {
/**
* 解码
* @param nums string字符串 数字串
* @return int整型
*/
public int solve (String nums) {
int[] dp = new int[nums.length() + 1];
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= nums.length(); i++) {
//dp[i] = dp[i - 1] + dp[i - 2]
//dp[i] = dp[i - 1]
//两位数
String str = nums.substring(i - 2, i);
int num = Integer.valueOf(str);
//各位数
int ge = nums.charAt(i - 1) - '0';
boolean expr1 = num >= 10 && num <= 26;
boolean expr2 = ge == 0;
//两位数不符合情况
if (!expr1) {
if (expr2) {
//个位数不符合(= 0) && 两位数不符合(>=26) dp[i] = 0
dp[i] = 0;
}else {
//个位数符合 && 两位数不符合 dp[i] = dp[i - 1]
dp[i] = dp[i - 1];
}
}else{
//两位数符合情况
//个位数不符合(=0) && 两位数符合 dp[i] = dp[i - 2]
if (ge == 0) {
dp[i] = dp[i - 2];
}else {
//个位数符合 && 两位数符合 dp[i] = dp[i - 2] + dp[i - 1]
dp[i] = dp[i - 2] + dp[i - 1];
}
}
}
return dp[nums.length()];
}
}

最长回文子串【较难】

文章:​​最长回文子串(四种方法)​

视频:​​使用【动态规划】求解最长回文子串​

题目链接:​​最长回文子串​

题目内容:对于长度为n的一个字符串A(仅包含数字,大小写英文字母),请设计一个高效算法,计算其中最长回文子串的长度。

思路:动态规划

dp数组含义:

  • ​dp[i][j]​​:字符串s[i…j]是否是回文。

状态转移方程:

13数据结构与算法刷题之【动态规划】篇_算法_13

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
import java.util.*;


public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param A string字符串
* @return int整型
*/
public int getLongestPalindrome (String A) {
boolean[][] dp = new boolean[A.length()][A.length()];
for (int i = 0; i < A.length(); i++) {
dp[i][i] = true;
}
char[] arr = A.toCharArray();
int max = 1;
//i - j + 1>=3,dp[i][j] = A[i] == A[j] && dp[i + 1][j - 1]
//i - j + 1 < 3,dp[i][j] = A[i] == A[j]
for (int j = 1; j < A.length(); j++) {
for (int i = 0; i < j; i++) {
if (j - i + 1 >= 3) {
dp[i][j] = arr[i] == arr[j] && dp[i + 1][j - 1];
}else {
dp[i][j] = arr[i] == arr[j];
}
//来进行计算最长回文串了
if (dp[i][j]) {
max = Math.max(max, j - i + 1);
}
}
}
return max;
}
}

编辑距离(一)【较难】

视频:​​leetcode 72. 编辑距离 #动态规划​​ ,讲的特别好。

文章:​​【动态规划】(一)编辑距离​​,还行,建议是先看视频,接着看文章。

题目内容:给定两个字符串 str1 和 str2 ,请你算出将 str1 转为 str2 的最少操作数。

你可以对字符串进行3种操作:

1.插入一个字符

2.删除一个字符

3.修改一个字符。

字符串长度满足 1 \le n \le 1000 \1≤n≤1000 ,保证字符串中只出现小写英文字母。

思路:动态规划

dp数组含义:dp[i][j]表示a串的[0,i]字符串替换为b串的[0,j]
例如:abc->ab,即为dp[3][2]。示例:abd abcd
' ' a b c d
'' 0 1 2 3 4
a 1 0 1 2 3
b 2 1 0 1 2
d 3 2 1 1 1 此时最后一个dp[3][4]就表示abd=>abcd的最终情况
计算dp[1][2] 实际上就是a => ab,由于a!=b,那么此时可以进行三种情况:
①添加操作dp[1][2] = dp[1][1]+1,实际上就是将a替换为a,然后添加b,构成ab。
②删除操作dp[1][2] = dp[0][2]+1,实际上就是空字符串替换为ab,此时为abb,删除最后一个,此时构成ab。
③替换操作dp[1][2] = dp[0][1]+1,实际上空字符串替换得到a,此时为aa,那么替换最后一个a为b,此时构成ab。

转移方程:
1、dp[i][j] = dp[i - 1][j - 1], a[i] == b[j]
2、dp[i][j] = min(dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]) + 1 , a[i] != b[j]

边界值:
dp[i][0] = i, dp[0][j] = j

代码:

import java.util.*;


public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param str1 string字符串
* @param str2 string字符串
* @return int整型
*/
public int editDistance (String str1, String str2) {
int rows = str1.length();
int columns = str2.length();
int[][] dp = new int[rows + 1][columns + 1];
char[] arr1 = str1.toCharArray();
char[] arr2 = str2.toCharArray();
//初始填充:每个位置就是对应的一个长度
for (int i = 0; i <= columns; i++) {
dp[0][i] = i;
}
for (int i = 0; i <= rows; i++) {
dp[i][0] = i;
}
//动归
for (int j = 1; j <= columns; j++) {
for (int i = 1; i <= rows; i++) {
if (arr1[i - 1] == arr2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
}else {
dp[i][j] = Math.min(dp[i][j - 1], Math.min(dp[i - 1][j], dp[i - 1][j - 1])) + 1;
}
}
}
return dp[rows][columns];
}
}

正则表达式匹配【较难】

相同leetcode:​​剑指 Offer 19. 正则表达式匹配​

学习:​​评论题解​

13数据结构与算法刷题之【动态规划】篇_i++_14

题目链接:​​正则表达式匹配​

题目内容

请实现一个函数用来匹配包括’.‘和’*'的正则表达式。

1.模式中的字符’.'表示任意一个字符

2.模式中的字符’*'表示它前面的字符可以出现任意次(包含0次)。

在本题中,匹配是指字符串的所有字符匹配整个模式。例如,字符串"aaa"与模式"a.a"和"abaca"匹配,但是与"aa.a"和"ab*a"均不匹配

推导

状态定义:f(i, j):s[0,i]与p[0,i]两个字符串是否匹配

状态转移:
1、若是普通字符情况:f(i, j) = f(i - 1, j - 1) && s[i] == p[j]
举例:aaa aab
2、若是'.':f(i, j) = f(i - 1, j - 1) && p[i] == '.'
举例:aab aa.
3、若是'*':
当匹配为 0 个:f(i,j) = f(i, j - 2)
举例:aa aaa* f(i, j - 2),此时就看前两个是否匹配
当匹配为 1 个: f(i,j) = f(i - 1, j - 2) && (s[i] == p[j - 1] || p[j - 1] == '.')
举例:aaa aaa* => s[i] == p[j - 1] aaa aa.* => p[j - 1] == '.'
当匹配为 2 个:f(i, j) = f(i - 2, j - 2) && ((s[i - 1] == p[i - 1] && s[i] == p[i - 1]) || p[j - 1] == '.')
举例:aaaa aaa* => s[i - 1] == p[i - 1] && s[i] == p[i - 1]
aaaa aa.*
当匹配为3个:....
当匹配为n个

由于上面的状态转移方程中*情况,可能相同的有n个,那么需要对其进行优化。

解法一:动态规划

下面推导整个思路过程

sss     sss*

'' s s s *
'' T F F F F
s F T F F F
s F F T F //核心来了
s F

'' s s s *
'' T F F F F
s F T F F F
s F F T F T //ss => sss* 匹配0个情况 成立 true
s F F F T //sss => sss* 匹配1个情况 (i - 1 >= 0 && f[i - 1][j] && (s[i] == p[j - 1] || p[j - 1] == '.')) 思路:①匹配0个情况为False。②接着会去推导s[0, i - 1]字符串是否匹配p[0, j],由于ss => sss*成立,此时来看s串的最后一个s与p串的最后前一个s是否匹配,若是相等,成立√;若是不相等(sst => ss.*),由于.是任意一个字符,则此时就会匹配

'' s s s *
'' T F F F F
s F T F F F
s F F T F T
s F F F T T

代码:

import java.util.*;


public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
*
* @param str string字符串
* @param pattern string字符串
* @return bool布尔型
*/
public boolean match (String str, String pattern) {
//状态方程:dp[i][j] i表示s串的[0,i],j表示p串的[0, j]
//转移方程:
//情况1:若是普通字符:dp[i][j] = dp[i - 1][j - 1] && s[i] == p[j]
//情况2:若是'.'情况:dp[i][j] = dp[i - 1][j - 1] && p[j] == '.'
//情况3:若是*的情况:
// 匹配0个字符时(如ss => sss*)dp[i][j] = dp[i][j - 2]
// 匹配n个字符时(如ssss => ssss*):dp[i][j] = dp[i - 1][j] && (s[i] == p[j - 1] || p[j - 1] == '.')
int rows = str.length();
int columns = pattern.length();
//每个字符串加上一个空格前缀,此时就与dp数组的对应位置保持一致
str = " " + str;
pattern = " " + pattern;
char[] s = str.toCharArray();
char[] p = pattern.toCharArray();
//状态方程
boolean[][] dp = new boolean[rows + 1][columns + 1];
//状态方程初始化
dp[0][0] = true;
//转移方程
for (int i = 0; i <= rows; i++) {
for (int j = 1; j <= columns; j++) {
//对于下一个是*的直接下一步
if (j + 1 <= columns && p[j + 1] == '*') {
continue;
}
//判断是普通字符或. (由于j本身就是从1开始,无需担心 j - 1的问题)
if (i - 1 >= 0 && p[j] != '*') {
dp[i][j] = dp[i - 1][j - 1] && (s[i] == p[j] || p[j] == '.');
}
//若是当前为*
if (p[j] == '*'){
//若是当前为'*'。匹配0个字符情况或n个字符情况
dp[i][j] = (j - 2 >= 0 && dp[i][j - 2]) || (i - 1 >= 0 && dp[i - 1][j] && (s[i] == p[j - 1] || p[j - 1] == '.'));
}
}
}
return dp[rows][columns];
}
}

最长的括号子串【较难】

学习:​​b站视频:LeetCode每日打卡.32.最长有效括号​​,看完秒懂

题目链接:​​最长的括号子串​

题目内容:给出一个长度为 n 的,仅包含字符 ‘(’ 和 ‘)’ 的字符串,计算最长的格式正确的括号子串的长度。

例1: 对于字符串 “(()” 来说,最长的格式正确的子串是 “()” ,长度为 2 .

例2:对于字符串 “)()())” , 来说, 最长的格式正确的子串是 “()()” ,长度为 4 .

解法1:动态规划

状态方程:dp[i] i表示[0,i]范围的括号有效的个数

状态转移方程:
1、若是结尾是'(',此时有效数为0
2、若是结尾是')', dp[i] = 2 + dp[i - 1] + dp[i - dp[i - 1] - 2], dp[i - dp[i - 1] - 1] = '('
( ) ( ( ) )
0 2 0 0 2 6

13数据结构与算法刷题之【动态规划】篇_贪心算法_15

复杂度分析:

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
import java.util.*;


public class Solution {
/**
*
* @param s string字符串
* @return int整型
*/
public int longestValidParentheses (String s) {
int n = s.length();
s = " " + s;
char[] arr = s.toCharArray();
int[] dp = new int[n + 1];
int max = 0;
for (int i = 1; i <= n; i++) {
//当前元素为(
if (arr[i] != '(') {
//状态方程:dp[i] = 2 + dp[i - 1] + dp[i - dp[i - 1] - 2]
//条件:dp[i - dp[i - 1] - 1] == '('
if (i - 1 >= 0 && arr[i - dp[i - 1] - 1] == '(') {
dp[i] = 2 + dp[i - 1] + (i - dp[i - 1] - 2 >= 0 ? dp[i - dp[i - 1] - 2] : 0);
}
}
max = Math.max(max, dp[i]);
}
return max;
}
}

leetcode

70. 爬楼梯【简单】

学习:​​leetcode题解​

题目链接:​​70. 爬楼梯​

题目内容:假设你正在爬楼梯。需要 ​​n​​ 阶你才能到达楼顶。

每次你可以爬 ​​1​​​ 或 ​​2​​ 个台阶。你有多少种不同的方法可以爬到楼顶呢?

速记:

①递归,超时,不建议。(若是不超时可能要做额外的保存操作)。
②动规 *f*(*x*)=*f*(*x*−1)+*f*(*x*−2),(斐波那契数列规律)。

思路:

1、动规

思路:动规,规律符合斐波那契数列。这里使用两个变量来进行临时存储f(x-1)与f(x-2)对应的方案数,而不是使用数组来进行保存,空间复杂度优化至O(n)。

f(x)=f(x−1)+f(x−2):f(x)表示的是爬到x阶的方案。

复杂度分析:时间复杂度O(n)、空间复杂度O(1)

public int climbStairs(int n) {
int p = 0;
int q = 1;
int ans = 1;
for (int i = 2; i <= n; i++){
p = q;
q = ans;
//动规
ans = p + q;
}
return ans;
}

13数据结构与算法刷题之【动态规划】篇_空间复杂度_16


5. 最长回文子串【中等】

学习:​​leetcode题解​

题目链接:​​5. 最长回文子串​

题目内容:给你一个字符串 ​​s​​​,找到 ​​s​​ 中最长的回文子串。

思路:

1、暴力法

思路:暴力枚举法,从第一个字符串开始从取最大子串来进行判定是否为回文,若是没取到范围缩减进行进行比对。中间会有最大子串的长度校验操作,若是当前匹配的长度<已经找到最大子串长度则直接结束。

复杂度分析:时间复杂度O(n3)、空间复杂度O(1)

public String longestPalindrome(String s) {
if (s.length() == 1){
return s;
}
int maxLength = 0;
int maxi = 0;
int maxj = 0;
for (int i = 0; i < s.length(); i++) {
for (int j = s.length()-1; j >= i; j--) {
//当前要计算的回文长度
int len = j - i + 1;
if (len < maxLength){
break;
}

int left = i;
int right = j;
//比对是否为回文
while (left < right){
if (s.charAt(left) == s.charAt(right)){
left++;
right--;
}else{
break;
}
}
if (left >= right){
if (len > maxLength) {
maxLength = len;
maxi = i;
maxj = j;
}
}
}
}
return s.substring(maxi, maxj + 1);
}

13数据结构与算法刷题之【动态规划】篇_i++_17

2、动态规划

思路:​​S[i:j]​​​作为一个子串,判断其本身是回文串可以通过​​s[i+1:j-1]​​​其内部子串与其该两个字符​​Si==Sj​​是否相等来进行判断。

动态状态表达式:​​P(i,j)=P(i+1,j−1)∧(Si==Sj)​​,其本身状态与前子串、当前i位置字符、j位置是否相等有关。

代码:时间复杂度O(n2)、空间复杂度O(n2)

13数据结构与算法刷题之【动态规划】篇_动态规划_18

public String longestPalindrome(String s) {
boolean[][] dp = new boolean[s.length()][s.length()];
//单独的为true
for (int i = 0; i < s.length(); i++) {
dp[i][i] = true;
}

//列式依次向下求得状态
for (int j = 1; j < s.length(); j++) {
for (int i = 0; i <= j - 1; i++) {
boolean eq = s.charAt(i) == s.charAt(j);
if (Math.abs(i - j) == 1 && eq){
dp[i][j] = true;
continue;
}
//动规方程式
if (dp[i + 1][j - 1] && eq){
dp[i][j] = true;
}
}
}

//取得最大长度
int maxLen = 0;
int start = 0;
//每行从右往左依次寻找为true的值
for (int i = 0; i < s.length(); i++) {
for (int j = s.length() - 1; j >= i; j--) {
int length = j - i + 1;
//提前结束该行遍历判断
if (length <= maxLen){
break;
}
if (dp[i][j]){
maxLen = j - i + 1;
start = i;
}
}
}
return s.substring(start, start + maxLen);
}

13数据结构与算法刷题之【动态规划】篇_贪心算法_19