从递归入手一维动态规划
从递归入手一维动态规划
1. 509. 斐波那契数
1.1 思路
递归
F(i) = F(i-1) + F(i-2)
每个点都往下展开两个分支,时间复杂度为 O(2n) 。
在上图中我们可以看到 F(6) = F(5) + F(4)。
计算 F(6) 的时候已经展开计算过 F(5)了。而在计算 F(7)的时候,还需要再一次展开计算 F(5)。
记忆化搜索
我们可以使用一张缓存表记录已经展开计算的结果。
上图右侧就是缓存表。
仔细看,我们主要是沿着这棵树的左边一直计算,计算好后将结果记录缓存表中。轮到计算右边的时候就可以直接返回。
例如我们一直沿着左边计算 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) 表示递归函数。
一共有三种情况:
- 索引i 的元素是0,没有办法转换,直接返回0。
- 将索引i 上的数字转换为字母,再调用 f(i+1)
以上面的字符串为例,1 -> A,f(i+1)
-
将索引 i 与 i + 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 思路
递归
- 如果 i 位置是0,没办法转,直接返回0。
- i 位置 不是0 (i 位置的字符单独转,f(i+1)后续能有多少种转换情况)
- i 位置不是 ‘*’ ,那么 i 上的数字就是 1 ~ 9 ,继续递归调用 f(i+1)
- i 位置是 ‘*’ ,而 ‘*’ 可以转换 1 ~ 9,那结果就是 9 * f(i+1)
- i 和 i +1 一起转 (f(i + 2) 后续有多少种转换情况)
- i 和 i +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种情况)
- i 和 i +1 都是数字
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。
在此基础上我们可以改进。
准备三个指针,* 2、* 3、 * 5。它们最初都指向1。
1* 2 = 2、1 * 3 = 3、1 * 5 = 5。
2 最小,因此下一个丑数是2。然后 * 2指针向后移动。
上面三个指针所计算出来的数字,3最小,下一个丑数是3。* 3指针向后移动。
下一个丑数是4,* 2指针向后移动。
下一个丑数是5,* 5指针向后移动。
下一个丑数是6,* 2、* 3指针都向后移动。
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 思路
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,这个就是结果。
我们求 dp[i] 的时候,证明 i 之前的位置,都得出结果了。
如果 i 位置是左括号,直接返回0即可。dp[i] 之前的数值有跟没有都无所。
i 位置是右括号的话,看 dp[i-1] = 4。证明 i - 4 到 i - 1 都是有效括号。
看 i - 5 位置,如果 i - 5 上是右括号,说明不能配对,dp[i] = 0。
如果 i - 5 上是右括号,说明可以配对,dp[i] 至少是 6。即dp[i-1] + 2 = 4 + 2 = 6。
为什么说至少是6?因为我们目前并不清楚 dp[i-6] 是多少。
如果dp[i - 6] = 4,那么dp[i] = 10。
这个时候不用再往i - 9前看了,因为dp[i-6] 就是 i - 6 往左最多推多远能整体有效的数值。
过程详解
索引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。
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。
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。
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
最大长度是用来去重的。每个长度累加的结果就是返回值。
具体操作
s 的 0位置 是 ‘z’, z 向左不能延伸,len = 1 。dp[‘z’] = 1。
len代表当前字符能向左延伸的最长长度,dp记录的是每个字符向左延伸的最长长度。
1位置是 ‘a’,a 前面是 z ,以 a 结尾子串向左延伸的最长长度为2,len = 2,dp[‘a’] = 2。
2位置是 ‘b’,b 前面是 a ,以 b 结尾子串向左延伸的最长长度为3,len = 3,dp[‘b’] = 3。
3位置是 ‘p’,p 前面是 b ,而在base串中p 前面不应该是 b。以 p 结尾子串向左延伸的最长长度为1,len = 1,dp[‘p’] = 1。
4 位置是 ‘x’,len = 1,dp[‘p’] = 1。
5 位置是 ‘y’,len = 2,dp[‘y’] = 2。
6 位置是 ‘z’, ‘z’ 前面是**‘y’**,我们可以复用dp[‘y’]的结论。len =dp[‘y’] + 1 = 2 + 1 = 3,dp[‘z’] = 3。
7 位置是 ‘a’, ‘a’ 前面是**‘z’**,我们可以复用dp[‘z’]的结论。len =dp[‘z’] + 1 = 3 + 1 = 4,dp[‘a’] = 4。
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 思路
0位置是a,我们的操作是 保留之前的子序列,并将 a 加到之前的子序列后面。
之前的子序列是空集,所以 0 位置的子序列为 {}、{a}
以 ‘a’ 为结尾的子序列数量 = 1。
all = 2( {}、{a})
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
2位置是’a’,按照之前的操作,我们可以看到{a} 重复了。
按照右上角的规则:
纯新增的字符 = all - 当前字符上次的记录(a) = 4 - 1 = 3
所以新增了3个
更改当前字符记录(a) = 1 + 3 = 4
all = 4 + 3 = 7
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) 每个点都往下展开两个分支,时间复杂度为 O(2n) 。 在上图中我们可以看到 F(6) F(5) F(4)。 计算 F(6) 的时候已经展开计算过 F(5)了。而在计算 F(7)的时候,还需要…...
鸿蒙HarmonyOS埋点SDK,ClkLog适配鸿蒙埋点分析
ClkLog埋点分析系统,是一种全新的、开源的洞察方案,它能够帮助您捕捉每一个关键数据点,确保您的决策基于最准确的用户行为分析。技术人员可快速搭建私有的分析系统。 ClkLog鸿蒙埋点SDK通过手动埋点的方式实现HarmonyOS 原生应用的前端数据采…...
HarmonyOS:HMPermission权限请求框架
前段时间利用空余时间写了一个权限请求库:HMPermission。 一,简介 HMPermission 是鸿蒙系统上的一款权限请求框架,封装了权限请求逻辑,采用链式调用的方式请求权限,简化了权限请求的代码。 二,使用方法 …...
【书籍】DeepSeek谈《持续交付2.0》
目录 一、深入理解1. 核心理念升级:从"自动化"到"双环模型"2. 数字化转型的五大核心能力3. 关键实践与案例4. 组织与文化变革5. 与其它框架的关系6. 实际应用建议 二、对于开发实习生的帮助1. 立刻提升你的代码交付质量(技术验证环实…...
Spring AOP 扫盲
🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,…...
银河麒麟v10(arm架构)部署Embedding模型bge-m3【简单版本】
硬件 服务器配置:鲲鹏2 * 920(32c) 4 * Atlas300I duo卡 参考文章 https://www.hiascend.com/developer/ascendhub/detail/07a016975cc341f3a5ae131f2b52399d 鲲鹏昇腾Atlas300Iduo部署Embedding模型和Rerank模型并连接Dify(自…...
如何通过流程管理优化企业运营?
流程管理的本质是“用确定性的规则应对不确定性的业务”。 那么,具体该如何通过流程管理来优化企业的运作呢?以下是一些关键步骤和思路,或许能给到一些启发。 1. 从流程梳理开始:摸清现状,找准问题 想要管理好企业的…...
ZYNQ笔记(四):AXI GPIO
版本:Vivado2020.2(Vitis) 任务:使用 AXI GPIO IP 核实现按键 KEY 控制 LED 亮灭(两个都在PL端) 一、介绍 AXI GPIO (Advanced eXtensible Interface General Purpose Input/Output) 是 Xilinx 提供的一个可…...
Java学习手册:JVM、JRE和JDK的关系
在Java生态系统中,JVM(Java虚拟机)、JRE(Java运行时环境)和JDK(Java开发工具包)是三个核心概念。它们共同构成了Java语言运行和开发的基础。理解它们之间的关系对于Java开发者来说至关重要。本文…...
Java 并发-newFixedThreadPool
前言 为什么选择使用多线程?一种场景是在数据和业务处理能力出现瓶颈时,而服务器性能又有空闲,通常是cpu空闲,这时使用多线程就能很好的解决问题,而又无需加硬件,实际使用中,线程池又是最为常用…...
C# task任务异步编程提高UI的响应性
方式1:async/await模式 private async void button1_Click(object sender, EventArgs e){try{var result await Task.Run(() > CalculateResult());label1.Text result.ToString();}catch (Exception ex){label1.Text $"Error: {ex.Message}";}}pri…...
Spring Bean生命周期执行流程详解
文章目录 一、什么是Spring Bean生命周期?工作流程图:二、Bean生命周期执行流程验证1.编写测试代码验证结果2.源码追溯Bean初始化回调过程 一、什么是Spring Bean生命周期? Spring Bean生命周期是指从Bean的创建到销毁的整个过程,…...
windows 安装 pygame( pycharm)
一、安装流程 1.查看python版本 2.检查是否安装pip 3.下载pygame安装文件 下载地址:https://pypi.org/project/pygame/#files 选择合适的版本(我选择的是 python3.7 windows 64bit): 4.使用pip安装pygame 将下载好的whl文件移动到…...
Envoy网关实例异常重启排查总结
一、事件背景 于10月24日凌晨业务租户有业务应用发版上线,中午收到pod连续5分钟重启严重告警,登录管理节点查看异常重启的应用网关pod日志,存在内核段错误报错信息导致进程终止并触发监控检查异常并重启; 该报错主要是访问的内存超出了系统…...
WinForm真入门(13)——ListBox控件详解
WinForm ListBox 详解与案例 一、核心概念 ListBox 是 Windows 窗体中用于展示可滚动列表项的控件,支持单选或多选操作,适用于需要用户从固定数据集中选择一项或多项的场景。 二、核心属性 属性说明Items管理列表项的集合,支持动…...
【Linux网络编程】UDP Echo Server的实现
本文专栏:Linux网络编程 目录 一,Socket编程基础 1,IP地址和端口号 端口号划分范围 理解端口号和进程ID 源端口号和目的端口号 理解Socket 2,传输层的典型代表 3,网络字节序 4,Socket编程接口 s…...
8.3.5 ToolStripContainer(工具栏容器)控件
版权声明:本文为博主原创文章,转载请在显著位置标明本文出处以及作者网名,未经作者允许不得用于商业目的 ToolStripContainer控件是一个容器,可以包含菜单和工具条、状态栏。 在设计窗体中放入一个ToolStripContainer࿱…...
代码随想录-06-二叉树-05.05 N叉树的层序遍历
N叉树的层序遍历 #模板题 题目描述 给定一个 N 叉树,返回其节点值的_层序遍历_。(即从左到右,逐层遍历)。 树的序列化输入是用层序遍历,每组子节点都由 null 值分隔(参见示例)。 具体思路 …...
【NEPVR】《A Lightweight Palm Vein Recognition Algorithm NEPVR》
[1]马莉,刘子良,谭振林,等.一种轻量级掌静脉识别算法NEPVR[J].计算机技术与发展,2024,34(12):213-220.DOI:10.20165/j.cnki.ISSN1673-629X.2024.0248. 文章目录 1、背景2、相关工作3、创新点4、NEPVR 手掌静脉识别算法5、实验结果及分析6、总结 / 未来工作 1、背景 手掌静脉独…...
牟乃夏《ArcGIS Engine地理信息系统开发教程》学习笔记1
(适合GIS开发入门者,通俗解析核心知识点) 目录 一、ArcGIS Engine是什么? 二、ArcGIS Engine能做什么? 三、ArcGIS Engine与ArcObjects的区别 四、开发资源与学习路径 五、对象模型图(OMD)…...
架构师论文《论模型驱动软件开发方法在智能制造转型实践中的应用》
摘要: 本人现任某大型装备制造企业智能制造研究院首席架构师,主导集团级数字化工厂平台建设。面对多品种小批量生产模式下普遍存在的交付周期超预期(平均延期21天)、设备综合效率OEE不足65%的痛点,我司于2021年启动基…...
探索MCP.so:AI生态的创新枢纽
今天在研究MCP时发现了一个还不错的网站,分享给大家。后续会基于这些mcp servers做一些有趣的应用。 在人工智能飞速发展的当下,AI与各类工具、数据源的协同合作变得愈发关键。MCP.so这个平台,正悄然成为AI领域的重要枢纽,为众多开发者和AI爱好者打开了新的大门。 MCP,即…...
JVM底层详解
JVM底层详解 目录 JVM概述JVM内存模型垃圾回收机制类加载过程JIT编译JVM调优JVM监控与故障排查JVM与多线程JVM与性能优化JVM发展历程与未来JVM实战案例分析JVM高级特性JVM安全机制JVM与容器化 一、JVM概述 1.1 什么是JVM Java虚拟机(Java Virtual Machine&…...
多点:分布式升级助力新零售转型,成本节省超80% | OceanBase 案例
本文作者:多点数据库DBA团队 编者按:多点是零售行业数字(智)化的先行者,为全球企业提供创新的数字化解决方案。然而,在数字化转型的过程中,多点原有的数据库架构逐渐暴露出架构复杂、成本上升等…...
Java权限修饰符深度解析
Java权限修饰符深度解析与最佳实践 一、权限修饰符总览 Java提供四种访问控制修饰符,按访问范围从宽到窄排序如下: 修饰符类内部同包类不同包子类全局范围public✔️✔️✔️✔️protected✔️✔️✔️❌默认(无)✔️✔️❌❌pr…...
RocketMQ和kafka 的区别
一、数据可靠性与容错机制 数据可靠性 RocketMQ支持同步刷盘和同步复制,确保消息写入磁盘后才返回确认,单机可靠性高达10个9,即使操作系统崩溃也不会丢失数据159。而Kafka默认采用异步刷盘和异步复制,虽然吞吐量高,但极…...
分布式限流器框架 eval-rate-limiter
分布式限流器框架 eval-rate-limiter 文章目录 分布式限流器框架 eval-rate-limiter前言设计流程图 核心方法tryAcquire 获取通信证增加访问次数 incrementRequestCount生成分布式 key generateRateLimiterKey 测试测试代码结果Redis 客户端 前言 基于 redis 实现的分布式限流…...
使用Docker部署Java项目的完整指南
前言 Docker是一个轻量级的容器化平台,可将应用及其依赖打包成标准化单元,实现快速部署和环境隔离。本文以Spring Boot项目为例,演示如何通过Dockerfile部署Java应用。 准备工作 本地环境 安装Docker Desktop(官网下载࿰…...
机器学习数据需求与应用分析
监督学习、无监督学习和强化学习作为机器学习的核心范式,对数据条件的需求存在显著差异。以下是具体分析: 一、监督学习的数据条件 数据要求 监督学习需要带标签(labeled)的数据集,即每个输入样本都有对应的目标输出&a…...
【机器学习算法】基于python商品销量数据分析大屏可视化预测系统(完整系统源码+数据库+开发笔记+详细启动教程)✅
目录 一、项目背景 二、技术思路 三、算法介绍 四、项目创新点 五、开发技术介绍 六、项目展示 一、项目背景 本项目基于Python技术栈构建了"商品销量数据分析与预测系统",通过自动化爬取淘宝商品多维数据(价格、销量、评价、品类等&a…...
springboot集成springcloud vault读值示例
接上三篇 Vault---机密信息管理工具安装及常用示例 Vault机密管理工具集群配置示例 vault签发根证书、中间证书、ca证书流程记录 项目里打算把所有密码都放到vault里管理,vault提供了springcloud vault用来在springboot里连接vault,启动加载vault里的值放…...
BERT 模型是什么
BERT 模型是什么? BERT(Bidirectional Encoder Representations from Transformers)是一种基于Transformer架构的深度学习模型,由Google于2018年提出。它在自然语言处理领域取得了显著成就,成为众多NLP任务的基础。 …...
三元电池正极材料除杂工艺介绍
三元电池正极材料的除杂工艺对于提高电池性能、安全性和稳定性至关重要。以下是对三元电池正极材料除杂工艺的详细介绍: 物理除杂工艺 磁选 原理:利用磁场对磁性杂质的吸引作用实现分离。在三元电池正极材料生产中,常混入铁、钴、镍等磁性金…...
wx212基于ssm+vue+uniapp的科创微应用平台小程序
开发语言:Java框架:ssmuniappJDK版本:JDK1.8服务器:tomcat7数据库:mysql 5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包:M…...
Multi Agents Collaboration OS:数据与知识协同构建数据工作流自动化
1-背景 传统数据系统与业务数字化的开发与维护面临诸多挑战:行业知识获取壁垒高、需求变化快导致开发周期长、系统复杂度高以及人力与资源投入成本巨大。同时,用户在使用过程中也常遇到痛点:手动录入数据繁琐低效、数据分散于各模块难以整合…...
elemenPlus中,如何去掉el-input中 文本域 textarea自带的边框和角标
1、去掉角标 :deep(.el-textarea__inner) {resize: none !important; // 去除右下角图标 }2、去除边框,并自定义背景色 <el-inputref"textareaRef"v-model"tempContent":style"{--el-border-color: rgba(255,255,255,0.0),--el-input-…...
Excel 动态比较两列数据:实现灵活的数据验证
目录 动态比较两列数据的需求动态公式的实现使用INDIRECT和ROW函数公式解释应用 动态公式的优点 快速添加一列公式的技巧使用快捷键Ctrl D使用VBA宏自动化使用“表格”功能自动填充 实际应用场景数据验证动态报告数据清洗 注意事项总结 在数据处理和分析中,Excel 是…...
谷歌推出可免费使用的Firebase Studio:Gemini全栈AI开发利器
谷歌刚刚发布了Firebase Studio,这是其打造的一款沉浸式代码开发平台,旨在与Cursor、Lovable、Bolt和V0等工具竞争。如果你是一名网页开发者,可能只知道Firebase是谷歌的数据库工具。 但现在,它已远不止于此。 Firebase已发展成一个完整的生态系统,如今能帮助你从头到尾…...
spark(二)
本节课接上节课继续对于RDD进行学习,首先是对于创建RDD的不同方式,接着学习了RDD的三种转换算子:Value类型、双Value类型、Key-Value类型,以及各个转换算子的不同使用方式。 学习到如下的区别: map 与 mapPartitions…...
Fay 数字人部署环境需求
D:\ai\Fay>python main.py pygame 2.6.1 (SDL 2.28.4, Python 3.11.9) Hello from the pygame community. https://www.pygame.org/contribute.html [2025-04-11 00:10:16.7][系统] 注册命令... [2025-04-11 00:10:16.8][系统] restart 重启服务 [2025-04-11 00:10:16.8][…...
【Harmony】端云一体化(云函数)
一、云函数的概述 1、什么是云函数 官方解释 云函数是一项Serverless计算服务,提供FaaS(Function as a Service)能力,一方面云函数将开发测试的对象聚焦到函数级别,可以帮助您大幅简化应用开发与运维相关的事务&…...
利用大模型和聚类算法找出 Excel 文件中重复或相似度高的数据,并使用 FastAPI 进行封装的详细方案
以下是一个利用大模型和聚类算法找出 Excel 文件中重复或相似度高的数据,并使用 FastAPI 进行封装的详细方案: 方案流程 数据读取:从 Excel 文件中读取数据。文本向量化:使用大模型将文本数据转换为向量表示。聚类分析:运用聚类算法对向量进行分组,将相似度高的数据归为…...
通过远程桌面连接wsl2中安装的ubuntu24.04
要介绍的这种方式其实跟直接用wsl来执行命令差不多,是在终端去操作ubuntu。WSL2 默认只提供命令行界面,本文安装xrdp后通过windows远程桌面连接过去。 1、更新软件包列表 sudo apt update 确保你的软件包列表是最新的,否则可能找不到某些包…...
对接和使用国内稳定无水印的 Suno API
随着 AI 的应用日益广泛,各种 AI 程序已经融入我们的日常生活。从最早的写作,到医疗、教育,如今甚至扩展到了音乐领域。 Suno 是一个专注于高质量 AI 歌曲和音乐创作的平台。用户只需输入简单的文本提示词,便可以按照流派风格和歌…...
LeetCode算法题(Go语言实现)_38
我将按照您提供的文档结构为您整理二叉树最近公共祖先(LCA)问题的解决方案: 一、代码实现 type TreeNode struct {Val intLeft *TreeNodeRight *TreeNode }func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode {if root nil ||…...
Linux学习笔记 1
1.发展史 略...... 2.xshell的使用方法 2.1登录 ssh root公网地址 输入密码,用 uname -r 指令来鉴定是否登录成功。之后就可以进行命令行操作了。 alt enter 全屏、退出 设置多用户指令,新建用户 adduser 名字 passwd 密码 销毁用户…...
微信小程序跳4
formatMillisecondsTime: function(milliseconds, formatStr) { // 创建一个新的Date对象,传入毫秒值 const date new Date(milliseconds); // 获取年月日时分秒,并确保它们都是两位数 const year date.getFullYear(); const month (date.getMonth() …...
STM32单片机入门学习——第31节: [10-1] I2C通信协议
写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难,但我还是想去做! 本文写于:2025.04.10 STM32开发板学习——第31节: [10-1] I2C通信协议 前言开发板说明引用解答和科普一…...
OpenCV 图形API(24)图像滤波-----双边滤波函数bilateralFilter()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 应用双边滤波到图像。 该函数对输入图像应用双边滤波,如 http://www.dai.ed.ac.uk/CVonline/LOCAL_COPIES/MANDUCHI1/Bilateral_Fil…...
【图像处理基石】什么是影调?并用python实现一个哈苏色彩影调
影调是摄影语言的核心,通过控制明暗、虚实与色彩,可精准传达创作意图。实际选择需结合主题情感、光线条件及画面结构,灵活运用高调、低调或冷暖色调,以强化视觉表现力。 一、影调的定义 影调指画面中明暗、虚实、色彩的层次与对比…...