马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
从递归入手一维动态规划
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)。
滚动数组
设置两个变量,lastLast = 0,last = 1。
cur = lastLast + last = 0 + 1 = 1。
之后分别向后移动lastLast 、 last。
lastLast = last
last = cur
如许就节省了额外空间复杂度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 4
- costs [a b c]
- 0 1 2
复制代码 递归函数 f(days,costs,i) 。该函数返回的是从days数组 索引i 的日期开始观光,所需的最小耗费。(days 和 costs 是稳固的,以下用 f(i) 代指递归函数)
i = 0,days = 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 4
- costs [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 思绪
递归
递归函数 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 = 11
- if (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] = 1
- int 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种情况)
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+1
- if ((s[i] - '0') * 10 + (s[i+1] - '0') <= 26){
- ans += f1(s,i+2);
- }
- }else {
- // num *
- // i i+1
- if (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+1
- if (s[i+1] <= '6'){
- ans += f1(s,i+2) * 2;
- }else {
- ans += f1(s,i+2);
- }
- }else {
- // * *
- // i i+1
- ans += 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+1
- if ((s[i] - '0') * 10 + (s[i+1] - '0') <= 26){
- ans += f2(s,i+2,dp);
- }
- }else {
- // num *
- // i i+1
- if (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+1
- if (s[i+1] <= '6'){
- ans += f2(s,i+2,dp) * 2;
- }else {
- ans += f2(s,i+2,dp);
- }
- }else {
- // * *
- // i i+1
- ans += 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+1
- if ((s[i] - '0') * 10 + (s[i + 1] - '0') <= 26) {
- dp[i] += dp[i + 2];
- }
- } else {
- // num *
- // i i+1
- if (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+1
- if (s[i + 1] <= '6') {
- dp[i] += dp[i + 2] * 2;
- } else {
- dp[i] += dp[i + 2];
- }
- } else {
- // * *
- // i i+1
- dp[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+1
- if ((s[i] - '0') * 10 + (s[i + 1] - '0') <= 26) {
- cur += nextNext;
- }
- } else {
- // num *
- // i i+1
- if (s[i] == '1') {
- cur += nextNext * 9;
- }
- if (s[i] == '2') {
- cur += nextNext * 6;
- }
- }
- } else {
- if (s[i + 1] != '*') {
- // * num
- // i i+1
- if (s[i + 1] <= '6') {
- cur += nextNext * 2;
- } else {
- cur += nextNext;
- }
- } else {
- // * *
- // i i+1
- cur += 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 位置末了,往左最多推多远能团体有效。
我们按照上面的流程求出dp表中每个位置的答案,然后再求dp表中的max,这个就是效果。

我们求 dp 的时间,证实 i 之前的位置,都得出效果了。
如果 i 位置是左括号,直接返回0即可。dp 之前的数值有跟没有都无所。
i 位置是右括号的话,看 dp[i-1] = 4。证实 i - 4 到 i - 1 都是有效括号。
看 i - 5 位置,如果 i - 5 上是右括号,阐明不能配对,dp = 0。
如果 i - 5 上是右括号,阐明可以配对,dp 至少是 6。即dp[i-1] + 2 = 4 + 2 = 6。
为什么说至少是6?由于我们现在并不清楚 dp[i-6] 是多少。

如果dp[i - 6] = 4,那么dp = 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 = 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];
- // 第一个字符的长度初始值一定为1
- dp[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;
- }
复制代码 |