当前位置: 首页 > article >正文

从递归入手一维动态规划

从递归入手一维动态规划

image-20250408174704496

1. 509. 斐波那契数

1.1 思路

递归

image-20250408175748056

F(i) = F(i-1) + F(i-2)

每个点都往下展开两个分支,时间复杂度为 O(2n)

在上图中我们可以看到 F(6) = F(5) + F(4)。

计算 F(6) 的时候已经展开计算过 F(5)了。而在计算 F(7)的时候,还需要再一次展开计算 F(5)。


记忆化搜索

image-20250408181523413

我们可以使用一张缓存表记录已经展开计算的结果。

上图右侧就是缓存表。

仔细看,我们主要是沿着这棵树的左边一直计算,计算好后将结果记录缓存表中。轮到计算右边的时候就可以直接返回。

例如我们一直沿着左边计算 F(i-3)。

F(i-3) = F(i-4) + F(i-5)。这个过程计算完后就会把每个函数各自的结果记录到缓存表中。

将F(i-3)的结果返回给F(i-2)。

F(i-2) = F(i-3) + F(i-4)。F(i-3)的结果已经返回,接着计算F(i-4)。因为F(i-4)之前计算过,我们直接从缓存表查F(i-4)的结果,返回给F(i-3)即可。

这种做法时间复杂度为O(n)


自底向上动态规划
[0		1		1		2		3						 ]0		1		2		3		4		5		6		7

初始情况:arr[0] = 0,arr[1] = 1

arr[2] = arr[1] + arr[0] = 1 + 0 = 1

arr[3] = arr[2] + arr[1] = 1 + 1 = 2

这样从底部不断向上推,时间复杂度也为O(n)


滚动数组
[0			1	]
lastLast   last  

设置两个变量,lastLast = 0,last = 1。

cur = lastLast + last = 0 + 1 = 1。

之后分别向后移动lastLast 、 last。

lastLast = last

last = cur


[0			1		 	1 ]lastLast   last  

这样就节省了额外空间复杂度O(n)


1.2 代码

import java.util.Arrays;/*** @Title: Fib* @Author Wood* @Package leetcode.DynamicProgramming.class66.lc509* @Date 2025/4/8 18:58* @description: https://leetcode.cn/problems/fibonacci-number/*/
public class Fib {// 递归public int fib1(int n) {return f1(n);}// 递归private int f1(int n) {if (n == 0){return 0;}if (n == 1){return 1;}return f1(n-1) + f1(n-2);}// 记忆化public int fib2(int n) {int[] dp = new int[n+1];Arrays.fill(dp,-1);return f2(dp,n);}// 记忆化private int f2(int[] dp, int n) {if (n == 0){return 0;}if (n == 1){return 1;}if (dp[n] != -1){return dp[n];}int ans = f2(dp,n-1) + f2(dp,n-2);dp[n] = ans;return ans;}// 自底向上动态规划public int fib3(int n) {if (n == 0){return 0;}if (n == 1){return 1;}int[] dp = new int[n+1];dp[1] = 1;for (int i = 2; i <= n ; i++) {dp[i] = dp[i-1] + dp[i-2];}return dp[n];}// 滚动数组public int fib4(int n) {if (n == 0){return 0;}if (n == 1){return 1;}int lastLast = 0;int last = 1;int cur = 0;for (int i = 2; i <= n; i++) {cur = last + lastLast;lastLast = last;last = cur;}return cur;}
}

2. 983. 最低票价

2.1 思路

递归
days [3		4		9		20		50	...	]0		1		2		3		4costs [a		b		c]0		1		2

递归函数 f(days,costs,i) 。该函数返回的是从days数组 索引i 的日期开始旅行,所需的最小花费。(days 和 costs 是不变的,以下用 f(i) 代指递归函数)

i = 0,days[i] = 3。从第三天开始旅行,有下面三种方案。

1. 买为期 1 天的通行证(a元)    + f(1)
1. 买为期 7 天的通行证(b元)    + f(3)
1. 买为期 30 天的通行证(c元)   + f(4)

如果选择方案1,f(0) = a + f(1)。f(1) 又可以选择三种方案,就这样递归遍历下去了。不断记录递归过程中的最小值即可。

如果选择方案2,f(0) = b + f(3)。f(3) 继续递归。

如果选择方案3,f(0) = c + f(4)。f(4) 继续递归。


记忆化搜索
days [3		4		9		20		50	...	]0		1		2		3		4costs [a		b		c]0		1		2

上面的递归方法会存在重复计算。

days[0]、days[1]、days[2]、days[3] 都买1天车票,价格 = 4a + f(4)。

days[0] 买7天车票,days[3] 买1天车票,价格 = b + a + f(4)。

days[0] 买30天车票,价格 = c + f(4)。

以上三种情况都重复计算了 f(4)。

用缓存数组记录结果即可。


自底向顶的动态规划

我们想知道从 days[0] 出发的最低费用,需要依赖后面days索引的返回值。

如果要从简单状态填到复杂状态,应该是从后向前的顺序。

days数组的长度为n。

f(n) = 0。因为n索引越界,没有旅行,也就没有费用,直接返回0。即dp[n] = 0。

dp[n-1] 依赖 dp[n],dp[n-2] 依赖 dp[n-1] 和 dp[n]。

由此,不断向前推,能推到dp[0]。而dp[0] 就是从days[0] 出发的最低费用。

2.2 代码

import java.util.Arrays;/*** @Title: MincostTickets* @Author Wood* @Package leetcode.DynamicProgramming.class66.lc983* @Date 2025/4/8 19:14* @description: https://leetcode.cn/problems/minimum-cost-for-tickets/*/
public class MincostTickets {// 每种方案能管几天public static int[] durations = {1,7,30};//递归public static int mincostTickets1(int[] days, int[] costs) {return f1(days,costs,0);}//递归private static int f1(int[] days, int[] costs, int i) {if (i == days.length){// 后续没有旅行了,也就没有花费return 0;}int ans = Integer.MAX_VALUE;// k 是方案编号for (int k = 0, j = i; k < 3; k++) {// j是你当前选了方案之后,方案能管到的下一天的days索引// days[i] 是出发旅行的日期// durations[k] 表示你选中的该方案的车票能管几天// days[i] + durations[k] 表示车票能管到第几天// days[j] 是车票能管到的最后一天的下一天// 下一次递归遍历从索引j开始,即f1(days,costs,j)while (j < days.length && days[i] + durations[k] > days[j]){j++;}ans = Math.min(ans,costs[k] + f1(days,costs,j));}return ans;}// 记忆化public static int mincostTickets2(int[] days, int[] costs) {int[] dp = new int[days.length];Arrays.fill(dp, Integer.MAX_VALUE);return f2(days,costs,0,dp);}// 记忆化private static int f2(int[] days, int[] costs, int i,int[] dp) {if (i == days.length){// 后续没有旅行了,也就没有花费return 0;}if (dp[i] != Integer.MAX_VALUE){return dp[i];}int ans = Integer.MAX_VALUE;// k 是方案编号for (int k = 0, j = i; k < 3; k++) {while (j < days.length && days[i] + durations[k] > days[j]){j++;}ans = Math.min(ans,costs[k] + f2(days,costs,j,dp));}dp[i] = ans;return ans;}public static int MAXN = 366;public static int[] dp = new int[MAXN];// 自底向顶的动态规划public static int mincostTickets3(int[] days, int[] costs){int n = days.length;Arrays.fill(dp,0,n+1,Integer.MAX_VALUE);dp[n] = 0;for (int i = n - 1; i >= 0; i--) {for (int k = 0,j = i; k < 3; k++) {while (j < days.length && days[i] + durations[k] > days[j]){j++;}dp[i] = Math.min(dp[i], costs[k] + dp[j]);}}return dp[0];}
}

3. 91. 解码方法

3.1 思路

递归
"1	1	0	6"i   

递归函数 f(char[] s,int i) ,s是字符串转换后得到的数组(不会变),该函数的返回结果是从索引i位置开始,i 及其它之后的位置能够返回多少种解码方式。以下直接用 f(i) 表示递归函数。

一共有三种情况:

  1. 索引i 的元素是0,没有办法转换,直接返回0。
  2. 将索引i 上的数字转换为字母,再调用 f(i+1)

​ 以上面的字符串为例,1 -> A,f(i+1)

  1. 将索引 ii + 1上的数字转换为字母(前提是 i 与 i+1 所组成的数字 <=26),再调用 f(i+2)

    11 -> k,f(i+2)

记忆化搜索

i 的变化范围是 0 ~ n。(n 是字符串长度)

那我们的dp数组就准备 n + 1 大小的。

记录每次的返回结果。

自底向顶的动态规划

i 从 0开始,i位置的结果依赖于 i +1 与 i+2 的结果。

那我们先填n位置的结果,也就是 1。再填 n-1、n-2,从右往左推断。

滚动数组

求 i 位置 依赖 i +1 和 i+2 。

求 i -1 位置 依赖 i 和 i+1 。

这样的话每必要整一张表,直接两个变量滚动更新即可。

next 记录 i + 1 的结果。

nextNext记录 i + 2 的结果。

i 的 结果就是 next + nextNext。

下一步 nextNext 记录 i + 1 的结果。next 记录 i 的结果。就能得出 i - 1 的结果。

3.2 代码

import java.util.Arrays;/*** @Title: NumDecodings* @Author Wood* @Package leetcode.DynamicProgramming.class66.lc91* @Date 2025/4/9 13:55* @description: https://leetcode.cn/problems/decode-ways/*/
public class NumDecodings {// 递归public static int numDecodings1(String s) {return f1(s.toCharArray(),0);}// 递归private static int f1(char[] s, int i) {if (i == s.length){// 证明之前所选的方案可以形成一种有效编码return 1;}int ans;if (s[i] == '0'){ans = 0;}else {// 索引i上的数字自己转ans = f1(s,i+1);// i 和 i+1 一起转// 以 '11' 为例// (s[i] - '0') * 10 = ('1' - '0') * 10 = 1 * 10 = 10// (s[i+1] - '0') = ('1' - '0') = 1// 10 + 1 = 11if (i + 1 < s.length && (s[i] - '0') * 10 + (s[i+1] - '0') <= 26){ans += f1(s,i+2);}}return ans;}// 记忆化搜索public static int numDecodings2(String s){int[] dp = new int[s.length()];Arrays.fill(dp,-1);return f2(s.toCharArray(),0,dp);}// 记忆化搜索private static int f2(char[] s, int i, int[] dp) {if (i == s.length){return 1;}if (dp[i] != -1){return dp[i];}int ans;if (s[i] == '0'){ans = 0;}else {ans = f2(s,i+1,dp);if (i+1 < s.length && (s[i] - '0') * 10 + (s[i+1] - '0') <= 26){ans += f2(s,i+2,dp);}}dp[i] = ans;return ans;}// 自底向顶的动态规划public static int numDecodings3(String str){char[] s = str.toCharArray();int n = s.length;int[] dp = new int[n + 1];Arrays.fill(dp,-1);dp[n] = 1;for (int i = n-1; i >= 0; i--) {if (s[i] == '0'){dp[i] = 0;}else {dp[i] = dp[i+1];if (i+1 < s.length && (s[i] - '0') * 10 + (s[i+1] - '0') <= 26){dp[i] += dp[i+2];}}}return dp[0];}// 滚动数组public static int numDecodings4(String s){// dp[n] = 1int next = 1;// dp[n+1]不存在int nextNext = 0;for (int i = s.length(),cur; i >= 0; i--) {if (s.charAt(i) == '0'){cur = 0;}else {cur = next;if (i+1 < s.length() && (s.charAt(i) - '0') * 10 + (s.charAt(i+1) - '0') <= 26){cur += nextNext;}}nextNext = next;next = cur;}return next;}
}

4. 639. 解码方法 II

4.1 思路

递归
  1. 如果 i 位置是0,没办法转,直接返回0。
  2. i 位置 不是0 (i 位置的字符单独转,f(i+1)后续能有多少种转换情况)
  • i 位置不是 ‘*’ ,那么 i 上的数字就是 1 ~ 9 ,继续递归调用 f(i+1)
  • i 位置是 ‘*’ ,而 ‘*’ 可以转换 1 ~ 9,那结果就是 9 * f(i+1)
  1. ii +1 一起转 (f(i + 2) 后续有多少种转换情况)
    • ii +1 都是数字
      • 如果它们合起来的数字<= 26,继续调用f(i + 2) 。否则返回0
    • i 是数字, i +1‘*’
      • i 是 1, i +1‘*’ ,结果 9 * f(i + 2)。(从11到19,总共是9种情况)
      • i 是 2, i +1‘*’ ,结果 6 * f(i + 2)。(从21到26,总共是6种情况)
      • i位置的数字 > 2,直接返回0。
    • i 是**‘*’**, i +1 是 数字
      • i 是**‘i +1 是 6,结果 2 * f(i + 2)。(’** 只能是1,2)
      • i 是**‘i +1 > 6,结果 1 * f(i + 2)。(’** 只能是1)
    • i‘*’i +1‘*’
    • 结果 15 * f(i + 2)。(从11 ~ 19,以及 21 ~ 26 ,总共15种情况)

4.2 代码

import java.util.Arrays;/*** @Title: NumDecodingsII* @Author Wood* @Package leetcode.DynamicProgramming.class66.lc639* @Date 2025/4/9 15:38* @description: https://leetcode.cn/problems/decode-ways-ii/*/
public class NumDecodingsII {public static int MOD = 1000000007;// 递归public static int numDecodings1(String s) {return f1(s.toCharArray(),0);}// 递归private static int f1(char[] s, int i) {if (i == s.length){return 1;}if (s[i] == '0'){return 0;}// s[i] != '0'// 2) i想单独转int ans = f1(s,i+1) * (s[i] == '*' ? 9 : 1);// 3) i i+1 一起转if (i + 1 < s.length){if (s[i] != '*'){if (s[i+1] != '*'){// num  num//  i   i+1if ((s[i] - '0') * 10 + (s[i+1] - '0') <= 26){ans += f1(s,i+2);}}else {// num  *//  i  i+1if (s[i] == '1'){ans += f1(s,i+2) * 9;}if (s[i] == '2'){ans += f1(s,i+2) * 6;}}}else {if (s[i+1] != '*'){// *  num// i  i+1if (s[i+1] <= '6'){ans += f1(s,i+2) * 2;}else {ans += f1(s,i+2);}}else {// *  *// i  i+1ans += f1(s,i+2) * 15;}}}return ans % MOD;}// 记忆化public static int numDecodings2(String str){char[] s = str.toCharArray();long[] dp = new long[s.length];Arrays.fill(dp,-1);return (int) f2(s,0,dp);}// 记忆化private static long f2(char[] s, int i, long[] dp) {if (i == s.length){return 1;}if (s[i] == '0'){return 0;}if (dp[i] != -1){return (int) dp[i];}long ans = f2(s,i+1,dp) * (s[i] == '*' ? 9 : 1);// 3) i i+1 一起转if (i + 1 < s.length){if (s[i] != '*'){if (s[i+1] != '*'){// num  num//  i   i+1if ((s[i] - '0') * 10 + (s[i+1] - '0') <= 26){ans += f2(s,i+2,dp);}}else {// num  *//  i  i+1if (s[i] == '1'){ans += f2(s,i+2,dp) * 9;}if (s[i] == '2'){ans += f2(s,i+2,dp) * 6;}}}else {if (s[i+1] != '*'){// *  num// i  i+1if (s[i+1] <= '6'){ans += f2(s,i+2,dp) * 2;}else {ans += f2(s,i+2,dp);}}else {// *  *// i  i+1ans += f2(s,i+2,dp) * 15;}}}ans %= MOD;dp[i] = ans;return ans;}// 自底向顶的动态规划public static int numDecodings3(String str){char[] s = str.toCharArray();int n = s.length;long[] dp = new long[n+1];dp[n] = 1;for (int i = n-1; i >= 0; i--) {if (s[i] != '0'){dp[i] = dp[i+1]  * (s[i] == '*' ? 9 : 1);if (i + 1 < n) {if (s[i] != '*') {if (s[i + 1] != '*') {// num  num//  i   i+1if ((s[i] - '0') * 10 + (s[i + 1] - '0') <= 26) {dp[i] += dp[i + 2];}} else {// num  *//  i  i+1if (s[i] == '1') {dp[i] += dp[i + 2] * 9;}if (s[i] == '2') {dp[i] += dp[i + 2] * 6;}}} else {if (s[i + 1] != '*') {// *  num// i  i+1if (s[i + 1] <= '6') {dp[i] += dp[i + 2] * 2;} else {dp[i] += dp[i + 2];}} else {// *  *// i  i+1dp[i] += dp[i + 2] * 15;}}}dp[i] %= MOD;}}return (int) dp[0];}// 空间压缩public static int numDecodings4(String str){char[] s = str.toCharArray();int n = s.length;long cur = 0, next = 1, nextNext = 0;for (int i = n-1; i >= 0 ; i--) {if (s[i] != '0') {cur = next * (s[i] == '*' ? 9 : 1);if (i + 1 < n) {if (s[i] != '*') {if (s[i + 1] != '*') {// num  num//  i   i+1if ((s[i] - '0') * 10 + (s[i + 1] - '0') <= 26) {cur += nextNext;}} else {// num  *//  i  i+1if (s[i] == '1') {cur += nextNext * 9;}if (s[i] == '2') {cur += nextNext * 6;}}} else {if (s[i + 1] != '*') {// *  num// i  i+1if (s[i + 1] <= '6') {cur += nextNext * 2;} else {cur += nextNext;}} else {// *  *// i  i+1cur += nextNext * 15;}}}cur %= MOD;}nextNext = next;next = cur;cur = 0;}return (int) next;}
}

5. 264. 丑数 II

5.1 思路

最暴力直接的方法就是从1开始,对每一个数字判断其的质因子是否是2、3、5。

对于遍历每一个数字,我们可以尝试求出下一个丑数是多少。

从1开始,分别乘以2、3、5,得2、3、5,其中最小的是2。则下一个丑数是2。

2 分别乘以2、3、5,得4、6、10,包括之前的乘法得出的结果,最小的是3。

3 分别乘以2、3、5,得6、9、15,包括之前的乘法得出的结果,最小的是4。


在此基础上我们可以改进。

image-20250409190559780

准备三个指针,* 2、* 3、 * 5。它们最初都指向1。

1* 2 = 2、1 * 3 = 3、1 * 5 = 5。

2 最小,因此下一个丑数是2。然后 * 2指针向后移动。


image-20250409191356026

上面三个指针所计算出来的数字,3最小,下一个丑数是3。* 3指针向后移动。


image-20250409191602729

下一个丑数是4,* 2指针向后移动。


image-20250409192010115

下一个丑数是5,* 5指针向后移动。


image-20250409192126715

下一个丑数是6,* 2、* 3指针都向后移动。


image-20250409192246010

5.2 代码

/*** @Title: NthUglyNumber* @Author Wood* @Package leetcode.DynamicProgramming.class66.lc264* @Date 2025/4/9 17:14* @description: https://leetcode.cn/problems/ugly-number-ii/*/
public class NthUglyNumber {public int nthUglyNumber(int n) {int[] dp = new int[n+1];dp[1] = 1;for (int i = 2,i2 = 1,i3 = 1, i5 = 1; i <= n; i++) {int a = dp[i2] * 2;int b = dp[i3] * 3;int c = dp[i5] * 5;int cur = Math.min(a,Math.min(b,c));if (cur == a){i2++;}if (cur == b){i3++;}if (cur == c){i5++;}dp[i] = cur;}return dp[n];}
}

6. 32. 最长有效括号

6.1 思路

image-20250409193613561

i 从字符串的索引0开始,从 i 位置 往左看,其有效的括号长度。

i = 0,是左括号,长度为0。

i = 1,是右括号,向左看能形成一个有效括号,长度为2。

i = 2,是右括号,但其左边还是个右括号,长度为0。

i = 3,是左括号,长度为0。

i = 4,是右括号,向左看能形成一个有效括号,长度为2。

i = 5,是左括号,长度为0。

i = 6,是右括号,向左看能形成两个有效括号,长度为4。

以此类推,返回最长长度4。


动态规划的含义也就出来了,dp[i] 代表子串以 i 位置结尾,往左最多推多远能整体有效。

我们按照上面的流程求出dp表中每个位置的答案,然后再求dp表中的max,这个就是结果。


image-20250409201245878

我们求 dp[i] 的时候,证明 i 之前的位置,都得出结果了。

如果 i 位置是左括号,直接返回0即可。dp[i] 之前的数值有跟没有都无所。


image-20250409201910830

i 位置是右括号的话,看 dp[i-1] = 4。证明 i - 4i - 1 都是有效括号。

i - 5 位置,如果 i - 5 上是右括号,说明不能配对,dp[i] = 0。


image-20250409203624104

如果 i - 5 上是右括号,说明可以配对,dp[i] 至少是 6。即dp[i-1] + 2 = 4 + 2 = 6。

为什么说至少是6?因为我们目前并不清楚 dp[i-6] 是多少。


image-20250409203852436

如果dp[i - 6] = 4,那么dp[i] = 10。

这个时候不用再往i - 9前看了,因为dp[i-6] 就是 i - 6 往左最多推多远能整体有效的数值。

过程详解

image-20250409204506785

索引0 与 索引 1都是左括号,dp[0] = 0,dp[1] = 0。

i = 2时,是右括号。dp[i-1] = 0。那么p位置就是 i位置往前跳0个,再跳1个,即 p = 1。而p位置是左括号,相当于中了图上 2 -> b分支。

那么dp[i] = dp[2] = dp[i-1] + 2 + dp[p-1] = 0 + 2 + 0 = 2。


image-20250409205203906

i = 3,是左括号,dp[3] = 0。

i = 4,是右括号,dp[i-1] = dp[3] = 0,p位置 是 i位置往前跳0个,再跳1个,即 p = 3。

p 位置是 左括号,满足2 -> b分支。

dp[4] = dp[i-1] + 2 + dp[p-1] = 0 + 2 + 2 = 4。


image-20250409205650823

i = 5,是右括号。

dp[i-1] = 4,p = 0。p 是 左括号,满足 2 -> b分支。

dp[5] = dp[i-1] + 2 + dp[p-1] = dp[4] + 2 + dp[-1] = 4 + 2 + 0 = 6。

p - 1 = -1 不存在,所以dp[-1] 直接返回0。


image-20250409210111032


image-20250409210241839


image-20250410140649326


image-20250410140719152


image-20250410141542498


image-20250410141625029


image-20250410141743045

6.2 代码

/*** @Title: LongestValidParentheses* @Author Wood* @Package leetcode.DynamicProgramming.class66.lc32* @Date 2025/4/9 19:30* @description: https://leetcode.cn/problems/longest-valid-parentheses/*/
public class LongestValidParentheses {public int longestValidParentheses(String str) {char[] s = str.toCharArray();int[] dp = new int[s.length];int ans = 0;for (int i = 1,p; i < s.length; i++) {if (s[i] == ')'){p = i - dp[i-1] - 1;if (p >= 0 && s[p] == '('){dp[i] = dp[i-1] + 2 + (p-1>=0 ? dp[p-1] : 0);}}ans = Math.max(ans,dp[i]);}return ans;}
}

7. 467. 环绕字符串中唯一的子字符串

7.1 思路

s: 		"zabpxyzab"base:	"ab...xyzabcd"	

以 ‘a’ 结尾的 s 的子串,在base中出现的最长子串:‘xyza’ ,长度为4。(不用再看 ‘za’ 了,长度长的一定包含长度短的)

以 ‘b’ 结尾:‘xyzab’ 长度为5

以 ‘p’ 结尾:‘p’ 长度为1

以 ‘z’ 结尾:‘xyz’ 长度为3

以 ‘y’ 结尾:‘xy’ 长度为2

以 ‘x’ 结尾:‘x’ 长度为1

最大长度是用来去重的。每个长度累加的结果就是返回值。

具体操作

image-20250410151733262

s 的 0位置 是 ‘z’z 向左不能延伸,len = 1 。dp[‘z’] = 1。

len代表当前字符能向左延伸的最长长度,dp记录的是每个字符向左延伸的最长长度。


image-20250410153357464

1位置是 ‘a’a 前面是 z ,以 a 结尾子串向左延伸的最长长度为2,len = 2,dp[‘a’] = 2。


image-20250410153556887

2位置是 ‘b’b 前面是 a ,以 b 结尾子串向左延伸的最长长度为3,len = 3,dp[‘b’] = 3。


image-20250410153733144

3位置是 ‘p’p 前面是 b ,而在base串中p 前面不应该是 b。以 p 结尾子串向左延伸的最长长度为1,len = 1,dp[‘p’] = 1。


image-20250410153937225

4 位置是 ‘x’,len = 1,dp[‘p’] = 1。


image-20250410154056588

5 位置是 ‘y’,len = 2,dp[‘y’] = 2。


image-20250410154344956

6 位置是 ‘z’‘z’ 前面是**‘y’**,我们可以复用dp[‘y’]的结论。len =dp[‘y’] + 1 = 2 + 1 = 3,dp[‘z’] = 3。


image-20250410154512349

7 位置是 ‘a’‘a’ 前面是**‘z’**,我们可以复用dp[‘z’]的结论。len =dp[‘z’] + 1 = 3 + 1 = 4,dp[‘a’] = 4。


image-20250410154548522

8 位置是 ‘b’‘b’ 前面是**‘a’**,我们可以复用dp[‘a’]的结论。len =dp[‘a’] + 1 = 4 + 1 = 5,dp[‘b’] = 5。


7.2 代码

public int findSubstringInWraproundString(String str) {int n = str.length();int[] s = new int[n];for (int i = 0; i < n; i++) {s[i] = str.charAt(i) - 'a'; // 将字符转换成数字}// 记录26个字母中以每个字符作结尾延伸的最长长度int[] dp = new int[26];// 第一个字符的长度初始值一定为1dp[s[0]] = 1;for (int i = 1,cur,pre,len = 1; i < n; i++) {cur = s[i];pre = s[i-1];if ((pre == 25 && cur == 0) || pre + 1 == cur){len++;}else {len = 1;}dp[cur] = Math.max(dp[cur],len);}int ans = 0;for (int i = 0; i < 26; i++) {ans += dp[i];}return ans;
}

8. 940. 不同的子序列 II

8.1 思路

image-20250410164832696


image-20250410164957965

0位置是a,我们的操作是 保留之前的子序列,并将 a 加到之前的子序列后面。

之前的子序列是空集,所以 0 位置的子序列为 {}、{a}

以 ‘a’ 为结尾的子序列数量 = 1。

all = 2( {}、{a}


image-20250410171104127

1位置是b,保留之前的子序列,并将 b 加到之前的子序列后面。

所以 1 位置的子序列为 {}、{a}、{b}、{a,b}

以 ‘b’ 为结尾的子序列数量 = 2。

all = 4


解释一下右上角的规则(看本次修改前的记录):

纯新增的字符 = all - 当前字符(‘b’) 上次的记录 = 2 - 0 = 2。(纯新增的字符是**{b}、{a,b}**)

当前字符的记录(也就是b的记录) = 之前的记录 + 纯新增的字符 = 0 + 2 = 2

all 之前是2,加上新增字符个数,all = 2 + 2 = 4


image-20250410173118755

2位置是’a’,按照之前的操作,我们可以看到{a} 重复了。

按照右上角的规则:

纯新增的字符 = all - 当前字符上次的记录(a) = 4 - 1 = 3

所以新增了3个


image-20250410173347129

更改当前字符记录(a) = 1 + 3 = 4

all = 4 + 3 = 7


image-20250410173725823


image-20250410173829627

8.2 代码

public int distinctSubseqII(String str) {int mod = 1000000007;char[] s = str.toCharArray();// 记录a-z中为结尾字符的子序列数量int[] cnt = new int[26];int all = 1,newAdd = 0;for (char c : s) {newAdd = (all - cnt[c - 'a'] + mod) % mod;cnt[c-'a'] = (cnt[c-'a'] + newAdd) % mod;all = (all + newAdd) % mod;}return (all -1 + mod) % mod;
}

284605611)]

0位置是a,我们的操作是 保留之前的子序列,并将 a 加到之前的子序列后面。

之前的子序列是空集,所以 0 位置的子序列为 {}、{a}

以 ‘a’ 为结尾的子序列数量 = 1。

all = 2( {}、{a}


[外链图片转存中…(img-aqx2hv6r-1744284605611)]

1位置是b,保留之前的子序列,并将 b 加到之前的子序列后面。

所以 1 位置的子序列为 {}、{a}、{b}、{a,b}

以 ‘b’ 为结尾的子序列数量 = 2。

all = 4


解释一下右上角的规则(看本次修改前的记录):

纯新增的字符 = all - 当前字符(‘b’) 上次的记录 = 2 - 0 = 2。(纯新增的字符是**{b}、{a,b}**)

当前字符的记录(也就是b的记录) = 之前的记录 + 纯新增的字符 = 0 + 2 = 2

all 之前是2,加上新增字符个数,all = 2 + 2 = 4


[外链图片转存中…(img-2TL3sQAi-1744284605611)]

2位置是’a’,按照之前的操作,我们可以看到{a} 重复了。

按照右上角的规则:

纯新增的字符 = all - 当前字符上次的记录(a) = 4 - 1 = 3

所以新增了3个


[外链图片转存中…(img-G9wJXr7K-1744284605611)]

更改当前字符记录(a) = 1 + 3 = 4

all = 4 + 3 = 7


[外链图片转存中…(img-y0tSvpOM-1744284605611)]


[外链图片转存中…(img-Ttc4FFnN-1744284605611)]

8.2 代码

public int distinctSubseqII(String str) {int mod = 1000000007;char[] s = str.toCharArray();// 记录a-z中为结尾字符的子序列数量int[] cnt = new int[26];int all = 1,newAdd = 0;for (char c : s) {newAdd = (all - cnt[c - 'a'] + mod) % mod;cnt[c-'a'] = (cnt[c-'a'] + newAdd) % mod;all = (all + newAdd) % mod;}return (all -1 + mod) % mod;
}

相关文章:

从递归入手一维动态规划

从递归入手一维动态规划 1. 509. 斐波那契数 1.1 思路 递归 F(i) F(i-1) F(i-2) 每个点都往下展开两个分支&#xff0c;时间复杂度为 O(2n) 。 在上图中我们可以看到 F(6) F(5) F(4)。 计算 F(6) 的时候已经展开计算过 F(5)了。而在计算 F(7)的时候&#xff0c;还需要…...

【2025年认证杯数学中国数学建模网络挑战赛】A题解题思路与模型代码

【2025年认证杯数学建模挑战赛】A题 该题为典型的空间几何建模轨道动力学建模预测问题。 ⚙ 问题一&#xff1a;利用多个天文台的同步观测&#xff0c;确定小行星与地球的相对距离 问题分析 已知若干地面天文台的观测数据&#xff1a;方位角 (Azimuth) 和 高度角 (Altitude)&…...

蓝桥杯备赛 Day16 单调数据结构

单调栈和单调队列能够动态的维护&#xff0c;还需用1-2两个数组在循环时从单调栈和单调队列中记录答案 单调栈 要点 1.时刻保持内部元素具有单调性质的栈(先进后出),核心是:入栈时逐个删除所有"更差的点",一般可分为单调递减栈、单调递增栈、单调不减栈、单调不增…...

轻量级爬虫框架Feapder入门:快速搭建企业级数据管道

一、目标与前置知识 1. 目标概述 本教程的主要目标是&#xff1a; 介绍轻量级爬虫框架 Feapder 的基本使用方式。快速搭建一个采集豆瓣电影数据的爬虫&#xff0c;通过电影名称查找对应的电影详情页并提取相关信息&#xff08;电影名称、导演、演员、剧情简介、评分&#xf…...

golang gmp模型分析

思维导图&#xff1a; 1. 发展过程 思维导图&#xff1a; 在单机时代是没有多线程、多进程、协程这些概念的。早期的操作系统都是顺序执行 单进程的缺点有&#xff1a; 单一执行流程、计算机只能一个任务一个任务进行处理进程阻塞所带来的CPU时间的浪费 处于对CPU资源的利用&…...

深入理解Java Optional:告别NullPointerException的优雅方式

大家好&#xff01;今天我们来聊聊Java 8引入的一个超实用类 - Optional。不是那个让你重启电脑的CtrlAltDel哦&#xff01;&#x1f604; 这是一个能让我们优雅处理null值的工具类&#xff0c;彻底告别烦人的NullPointerException&#xff01; 一、为什么需要Optional&#x…...

【算法竞赛】树上最长公共路径前缀(蓝桥杯2024真题·团建·超详细解析)

目录 一、题目 二、思路 1. 问题转化&#xff1a;同步DFS走树 2. 优化&#xff1a;同步DFS匹配 3. 状态设计&#xff1a;dfs参数含义 4. 匹配过程&#xff1a;用 map 建立权值索引 5. 终止条件&#xff1a;无法匹配则更新答案 6. 总结 三、完整代码 四、知识点总…...

【windows10】基于SSH反向隧道公网ip端口实现远程桌面

【windows10】基于SSH反向隧道公网ip端口实现远程桌面 1.背景2.SSH反向隧道3.远程连接电脑 1.背景 ‌Windows 10远程桌面协议的简称是RDP&#xff08;Remote Desktop Protocol&#xff09;‌。 RDP是一种网络协议&#xff0c;允许用户远程访问和操作另一台计算机。 远程桌面功…...

Python----概率论与统计(贝叶斯,朴素贝叶斯 )

一、贝叶斯 1.1、贝叶斯定理 贝叶斯定理&#xff08;Bayes Theorem&#xff09;也称贝叶斯公式&#xff0c;是关于随机事件的条件概率的定理 贝叶斯的的作用&#xff1a;根据已知的概率来更新事件的概率。 1.2、定理内容 提示&#xff1a; 贝叶斯定理是“由果溯因”的推断&…...

NO.88十六届蓝桥杯备战|动态规划-多重背包|摆花(C++)

多重背包 多重背包问题有两种解法&#xff1a; 按照背包问题的常规分析⽅式&#xff0c;仿照完全背包&#xff0c;第三维枚举使⽤的个数&#xff1b;利⽤⼆进制可以表⽰⼀定范围内整数的性质&#xff0c;转化成01 背包问题。 ⼩建议&#xff1a;并不是所有的多重背包问题都能…...

vue项目打包里面pubilc里的 tinymce里的js文件问题

以下是解决 Vue 项目打包后 public/tinymce 中 JS 文件路径问题的完整方案&#xff1a; 问题原因 当使用 public 目录存放静态资源时&#xff0c;Vue CLI 默认会将 public 下的文件 直接复制到打包目录的根路径&#xff0c;但以下操作可能导致路径错误&#xff1a; 开发环境使…...

Python星球日记 - 第18天:小游戏开发(猜数字游戏)

🌟引言: 上一篇:Python星球日记 - 第17天:数据可视化 名人说:路漫漫其修远兮,吾将上下而求索。(屈原《离骚》) 创作者:Code_流苏(CSDN)(一个喜欢古诗词和编程的Coder😊) 目录 一、游戏概述与原理1. 游戏基本规则2. 编程知识点3.猜数字游戏流程图二、游戏逻辑设计…...

爬虫抓包工具和PyExeJs模块

我们在处理一些网站的时候, 会遇到一些屏蔽F12, 以及只要按出浏览器的开发者工具就会关闭甚至死机的现象. 在遇到这类网站的时候. 我们可以使用抓包工具把页面上屏蔽开发者工具的代码给干掉. Fiddler和Charles 这两款工具是非常优秀的抓包工具. 他们可以监听到我们计算机上所…...

无人机击落技术难点与要点分析!

一、技术难点 1. 目标探测与识别 小型化和低空飞行&#xff1a;现代无人机体积小、飞行高度低&#xff08;尤其在城市或复杂地形中&#xff09;&#xff0c;雷达和光学传感器难以有效探测。 隐身技术&#xff1a;部分高端无人机采用吸波材料或低可探测设计&#xff0c;进…...

2025年Java无服务器架构实战:AWS Lambda与Spring Cloud Function深度整合

摘要 &#x1f4dd; 本文深入探讨如何在2025年Java生态中实现AWS Lambda与Spring Cloud Function的无缝整合。我们将从基础概念讲起&#xff0c;逐步深入到实际部署、性能优化和最佳实践&#xff0c;通过详实的代码示例展示如何构建高效、可扩展的无服务器Java应用。 目录 &a…...

LeetCode 题目 「二叉树的右视图」 中,如何从「中间存储」到「一步到位」实现代码的优化?

背景简介 在 LeetCode 的经典题目 「二叉树的右视图」 中&#xff0c;我们需要返回从右侧看一棵二叉树时所能看到的节点集合。每一层我们只能看到最右边的那个节点。 最初&#xff0c;我采用了一个常规思路&#xff1a;层序遍历 每层单独保存节点值 最后提取每层最后一个节…...

8.第二阶段x64游戏实战-string类

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 上一个内容&#xff1a;7.第二阶段x64游戏实战-分析人物属性 string类是字符串类&#xff0c;在计算机中…...

Go语言sync.Mutex包源码解读

互斥锁sync.Mutex是在并发程序中对共享资源进行访问控制的主要手段&#xff0c;对此Go语言提供了非常简单易用的机制。sync.Mutex为结构体类型&#xff0c;对外暴露Lock()、Unlock()、TryLock()三种方法&#xff0c;分别用于阻塞加锁、解锁、非阻塞加锁操作&#xff08;加锁失败…...

C++实现文件断点续传:原理剖析与实战指南

文件传输示意图 一、断点续传的核心价值 1.1 大文件传输的痛点分析 网络闪断导致重复传输&#xff1a;平均重试3-5次。 传输进度不可回溯&#xff1a;用户无法查看历史进度。 带宽利用率低下&#xff1a;每次中断需从头开始。 1.2 断点续传技术优势 指标传统传输断点续传…...

MySQL中FIND_IN_SET函数与INSTR函数用法解析

一、功能定义与语法 1、FIND_IN_SET函数 语法&#xff1a;FIND_IN_SET(str, strlist) 功能&#xff1a;在逗号分隔的字符串列表&#xff08;strlist&#xff09;中查找精确匹配的子字符串&#xff08;str&#xff09;&#xff0c;并返回其位置&#xff08;从1开始&#xff09…...

Python贝叶斯回归、强化学习分析医疗健康数据拟合截断删失数据与参数估计3实例

全文链接&#xff1a;https://tecdat.cn/?p41391 在当今数据驱动的时代&#xff0c;数据科学家面临着处理各种复杂数据和构建有效模型的挑战。本专题合集聚焦于有序分类变量处理、截断与删失数据回归分析以及强化学习模型拟合等多个重要且具有挑战性的数据分析场景&#xff0c…...

Git 协同开发的常用操作

1. 单仓库&#xff08;多分支开发&#xff09; 从远程拉取代码 git clone https://gitee.com/...查看当前分支 git branch -- *master创建并切换到你的开发分支&#xff08;my-dev&#xff09; git checkout -b my-dev查看当前分支 git branch -- marster -- *my-dev提交代…...

微信小程序 -- 原生封装table

文章目录 table.wxmltable.wxss注意 table.js注意 结果数据结构 最近菜鸟做微信小程序的一个查询功能&#xff0c;需要展示excel里面的数据&#xff0c;但是菜鸟找了一圈&#xff0c;也没发现什么组件库有table&#xff0c;毕竟手机端好像确实不太适合做table&#xff01; 菜鸟…...

分布式文件存储系统FastDFS

文章目录 1 分布式文件存储1_分布式文件存储的由来2_常见的分布式存储框架 2 FastDFS介绍3 FastDFS安装1_拉取镜像文件2_构建Tracker服务3_构建Storage服务4_测试图片上传 4 客户端操作1_Fastdfs-java-client2_文件上传3_文件下载4_获取文件信息5_问题 5 SpringBoot整合 1 分布…...

ZKmall开源商城服务端验证:Jakarta Validation 详解

ZKmall开源商城基于Spring Boot 3构建&#xff0c;其服务端数据验证采用Jakarta Validation API​&#xff08;原JSR 380规范&#xff09;&#xff0c;通过声明式注解与自定义扩展机制实现高效、灵活的数据校验体系。以下从技术实现、核心能力、场景优化三个维度展开解析&#…...

深度分页及优化建议

深度分页的定义 深度分页是指在分页查询中&#xff0c;当用户请求非常靠后的页面时&#xff0c;数据库需要处理大量数据&#xff0c;导致查询性能显著下降的情况。例如&#xff0c;一个查询结果有 100 万条记录&#xff0c;而用户要查询第 999 页&#xff08;每页 10 条记录&a…...

电网电能质量分析:原理、算法及实际应用

一、引言 在现代社会&#xff0c;电力供应的稳定性和可靠性对工业生产、社会生活的各个方面都至关重要。电能质量作为衡量电力系统供电能力的关键指标&#xff0c;其优劣直接影响到电力设备的运行效率、使用寿命以及生产过程的稳定性。随着电力系统规模的不断扩大&#xff0c;新…...

学透Spring Boot — 017. 魔术师—Http消息转换器

本文是我的专栏《学透Spring Boot》的第17篇文章&#xff0c;了解更多请移步我的专栏&#xff1a; 学透 Spring Boot_postnull咖啡的博客-CSDN博客 目录 HTTP请求和响应 需求—新的Media Type 实现—新的Media Type 定义转换器 注册转换器 编写Controller 测试新的medi…...

BOE(京东方)旗下控股子公司“京东方能源”成功挂牌新三板 以科技赋能零碳未来

2025年4月8日,BOE(京东方)旗下控股子公司京东方能源科技股份有限公司(以下简称“京东方能源”)正式通过全国中小企业股份转让系统审核,成功在新三板挂牌(证券简称:能源科技,证券代码:874526),成为BOE(京东方)自物联网转型以来首个独立孵化并成功挂牌的子公司。此次挂牌是BOE(京…...

Airflow集成Lark机器人

🥭1. 实现目标 🕐 通过自定义函数,实现Lark机器人告警功能 🕐 通过Lark机器人代替邮件数据的发送功能 🥭2.自定义函数实现 from airflow import DAG from airflow.operators.python_operator import PythonOperator from airflow.models import Variable import requ…...