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

从递归入手一维动态规划

从递归入手一维动态规划

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;还需要…...

鸿蒙HarmonyOS埋点SDK,ClkLog适配鸿蒙埋点分析

ClkLog埋点分析系统&#xff0c;是一种全新的、开源的洞察方案&#xff0c;它能够帮助您捕捉每一个关键数据点&#xff0c;确保您的决策基于最准确的用户行为分析。技术人员可快速搭建私有的分析系统。 ClkLog鸿蒙埋点SDK通过手动埋点的方式实现HarmonyOS 原生应用的前端数据采…...

HarmonyOS:HMPermission权限请求框架

前段时间利用空余时间写了一个权限请求库&#xff1a;HMPermission。 一&#xff0c;简介 HMPermission 是鸿蒙系统上的一款权限请求框架&#xff0c;封装了权限请求逻辑&#xff0c;采用链式调用的方式请求权限&#xff0c;简化了权限请求的代码。 二&#xff0c;使用方法 …...

【书籍】DeepSeek谈《持续交付2.0》

目录 一、深入理解1. 核心理念升级&#xff1a;从"自动化"到"双环模型"2. 数字化转型的五大核心能力3. 关键实践与案例4. 组织与文化变革5. 与其它框架的关系6. 实际应用建议 二、对于开发实习生的帮助1. 立刻提升你的代码交付质量&#xff08;技术验证环实…...

Spring AOP 扫盲

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家&#xff0c;历代文学网&#xff08;PC端可以访问&#xff1a;https://literature.sinhy.com/#/literature?__c1000&#xff0c;移动端可微信小程序搜索“历代文学”&#xff09;总架构师&#xff0c;15年工作经验&#xff0c;…...

银河麒麟v10(arm架构)部署Embedding模型bge-m3【简单版本】

硬件 服务器配置&#xff1a;鲲鹏2 * 920&#xff08;32c&#xff09; 4 * Atlas300I duo卡 参考文章 https://www.hiascend.com/developer/ascendhub/detail/07a016975cc341f3a5ae131f2b52399d 鲲鹏昇腾Atlas300Iduo部署Embedding模型和Rerank模型并连接Dify&#xff08;自…...

如何通过流程管理优化企业运营?

流程管理的本质是“用确定性的规则应对不确定性的业务”。 那么&#xff0c;具体该如何通过流程管理来优化企业的运作呢&#xff1f;以下是一些关键步骤和思路&#xff0c;或许能给到一些启发。 1. 从流程梳理开始&#xff1a;摸清现状&#xff0c;找准问题 想要管理好企业的…...

ZYNQ笔记(四):AXI GPIO

版本&#xff1a;Vivado2020.2&#xff08;Vitis&#xff09; 任务&#xff1a;使用 AXI GPIO IP 核实现按键 KEY 控制 LED 亮灭&#xff08;两个都在PL端&#xff09; 一、介绍 AXI GPIO (Advanced eXtensible Interface General Purpose Input/Output) 是 Xilinx 提供的一个可…...

Java学习手册:JVM、JRE和JDK的关系

在Java生态系统中&#xff0c;JVM&#xff08;Java虚拟机&#xff09;、JRE&#xff08;Java运行时环境&#xff09;和JDK&#xff08;Java开发工具包&#xff09;是三个核心概念。它们共同构成了Java语言运行和开发的基础。理解它们之间的关系对于Java开发者来说至关重要。本文…...

Java 并发-newFixedThreadPool

前言 为什么选择使用多线程&#xff1f;一种场景是在数据和业务处理能力出现瓶颈时&#xff0c;而服务器性能又有空闲&#xff0c;通常是cpu空闲&#xff0c;这时使用多线程就能很好的解决问题&#xff0c;而又无需加硬件&#xff0c;实际使用中&#xff0c;线程池又是最为常用…...

C# task任务异步编程提高UI的响应性

方式1&#xff1a;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生命周期&#xff1f;工作流程图&#xff1a;二、Bean生命周期执行流程验证1.编写测试代码验证结果2.源码追溯Bean初始化回调过程 一、什么是Spring Bean生命周期&#xff1f; Spring Bean生命周期是指从Bean的创建到销毁的整个过程&#xff0c…...

windows 安装 pygame( pycharm)

一、安装流程 1.查看python版本 2.检查是否安装pip 3.下载pygame安装文件 下载地址&#xff1a;https://pypi.org/project/pygame/#files 选择合适的版本&#xff08;我选择的是 python3.7 windows 64bit&#xff09;&#xff1a; 4.使用pip安装pygame 将下载好的whl文件移动到…...

Envoy网关实例异常重启排查总结

一、事件背景 于10月24日凌晨业务租户有业务应用发版上线&#xff0c;中午收到pod连续5分钟重启严重告警&#xff0c;登录管理节点查看异常重启的应用网关pod日志&#xff0c;存在内核段错误报错信息导致进程终止并触发监控检查异常并重启; 该报错主要是访问的内存超出了系统…...

WinForm真入门(13)——ListBox控件详解

WinForm ListBox 详解与案例 一、核心概念 ‌ListBox‌ 是 Windows 窗体中用于展示可滚动列表项的控件&#xff0c;支持单选或多选操作&#xff0c;适用于需要用户从固定数据集中选择一项或多项的场景‌。 二、核心属性 属性说明‌Items‌管理列表项的集合&#xff0c;支持动…...

【Linux网络编程】UDP Echo Server的实现

本文专栏&#xff1a;Linux网络编程 目录 一&#xff0c;Socket编程基础 1&#xff0c;IP地址和端口号 端口号划分范围 理解端口号和进程ID 源端口号和目的端口号 理解Socket 2&#xff0c;传输层的典型代表 3&#xff0c;网络字节序 4&#xff0c;Socket编程接口 s…...

8.3.5 ToolStripContainer(工具栏容器)控件

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的 ToolStripContainer控件是一个容器&#xff0c;可以包含菜单和工具条、状态栏。 在设计窗体中放入一个ToolStripContainer&#xff1…...

代码随想录-06-二叉树-05.05 N叉树的层序遍历

N叉树的层序遍历 #模板题 题目描述 给定一个 N 叉树&#xff0c;返回其节点值的_层序遍历_。&#xff08;即从左到右&#xff0c;逐层遍历&#xff09;。 树的序列化输入是用层序遍历&#xff0c;每组子节点都由 null 值分隔&#xff08;参见示例&#xff09;。 具体思路 …...

【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

&#xff08;适合GIS开发入门者&#xff0c;通俗解析核心知识点&#xff09; 目录 一、ArcGIS Engine是什么&#xff1f; 二、ArcGIS Engine能做什么&#xff1f; 三、ArcGIS Engine与ArcObjects的区别 四、开发资源与学习路径 五、对象模型图&#xff08;OMD&#xff09;…...

架构师论文《论模型驱动软件开发方法在智能制造转型实践中的应用》

摘要&#xff1a; 本人现任某大型装备制造企业智能制造研究院首席架构师&#xff0c;主导集团级数字化工厂平台建设。面对多品种小批量生产模式下普遍存在的交付周期超预期&#xff08;平均延期21天&#xff09;、设备综合效率OEE不足65%的痛点&#xff0c;我司于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虚拟机&#xff08;Java Virtual Machine&…...

多点:分布式升级助力新零售转型,成本节省超80% | OceanBase 案例

本文作者&#xff1a;多点数据库DBA团队 编者按&#xff1a;多点是零售行业数字&#xff08;智&#xff09;化的先行者&#xff0c;为全球企业提供创新的数字化解决方案。然而&#xff0c;在数字化转型的过程中&#xff0c;多点原有的数据库架构逐渐暴露出架构复杂、成本上升等…...

Java权限修饰符深度解析

Java权限修饰符深度解析与最佳实践 一、权限修饰符总览 Java提供四种访问控制修饰符&#xff0c;按访问范围从宽到窄排序如下&#xff1a; 修饰符类内部同包类不同包子类全局范围public✔️✔️✔️✔️protected✔️✔️✔️❌默认&#xff08;无&#xff09;✔️✔️❌❌pr…...

RocketMQ和kafka 的区别

一、数据可靠性与容错机制 数据可靠性 RocketMQ支持同步刷盘和同步复制&#xff0c;确保消息写入磁盘后才返回确认&#xff0c;单机可靠性高达10个9&#xff0c;即使操作系统崩溃也不会丢失数据159。而Kafka默认采用异步刷盘和异步复制&#xff0c;虽然吞吐量高&#xff0c;但极…...

分布式限流器框架 eval-rate-limiter

分布式限流器框架 eval-rate-limiter 文章目录 分布式限流器框架 eval-rate-limiter前言设计流程图 核心方法tryAcquire 获取通信证增加访问次数 incrementRequestCount生成分布式 key generateRateLimiterKey 测试测试代码结果Redis 客户端 前言 基于 redis 实现的分布式限流…...

使用Docker部署Java项目的完整指南

前言 Docker是一个轻量级的容器化平台&#xff0c;可将应用及其依赖打包成标准化单元&#xff0c;实现快速部署和环境隔离。本文以Spring Boot项目为例&#xff0c;演示如何通过Dockerfile部署Java应用。 准备工作 本地环境 安装Docker Desktop&#xff08;官网下载&#xff0…...

机器学习数据需求与应用分析

监督学习、无监督学习和强化学习作为机器学习的核心范式&#xff0c;对数据条件的需求存在显著差异。以下是具体分析&#xff1a; 一、监督学习的数据条件 数据要求 监督学习需要带标签&#xff08;labeled&#xff09;的数据集&#xff0c;即每个输入样本都有对应的目标输出&a…...

【机器学习算法】基于python商品销量数据分析大屏可视化预测系统(完整系统源码+数据库+开发笔记+详细启动教程)✅

目录 一、项目背景 二、技术思路 三、算法介绍 四、项目创新点 五、开发技术介绍 六、项目展示 一、项目背景 本项目基于Python技术栈构建了"商品销量数据分析与预测系统"&#xff0c;通过自动化爬取淘宝商品多维数据&#xff08;价格、销量、评价、品类等&a…...

springboot集成springcloud vault读值示例

接上三篇 Vault---机密信息管理工具安装及常用示例 Vault机密管理工具集群配置示例 vault签发根证书、中间证书、ca证书流程记录 项目里打算把所有密码都放到vault里管理&#xff0c;vault提供了springcloud vault用来在springboot里连接vault&#xff0c;启动加载vault里的值放…...

BERT 模型是什么

BERT 模型是什么&#xff1f; BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;是一种基于Transformer架构的深度学习模型&#xff0c;由Google于2018年提出。它在自然语言处理领域取得了显著成就&#xff0c;成为众多NLP任务的基础。 …...

三元电池正极材料除杂工艺介绍

三元电池正极材料的除杂工艺对于提高电池性能、安全性和稳定性至关重要。以下是对三元电池正极材料除杂工艺的详细介绍&#xff1a; 物理除杂工艺 磁选 原理&#xff1a;利用磁场对磁性杂质的吸引作用实现分离。在三元电池正极材料生产中&#xff0c;常混入铁、钴、镍等磁性金…...

wx212基于ssm+vue+uniapp的科创微应用平台小程序

开发语言&#xff1a;Java框架&#xff1a;ssmuniappJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;M…...

Multi Agents Collaboration OS:数据与知识协同构建数据工作流自动化

1-背景 传统数据系统与业务数字化的开发与维护面临诸多挑战&#xff1a;行业知识获取壁垒高、需求变化快导致开发周期长、系统复杂度高以及人力与资源投入成本巨大。同时&#xff0c;用户在使用过程中也常遇到痛点&#xff1a;手动录入数据繁琐低效、数据分散于各模块难以整合…...

elemenPlus中,如何去掉el-input中 文本域 textarea自带的边框和角标

1、去掉角标 :deep(.el-textarea__inner) {resize: none !important; // 去除右下角图标 }2、去除边框&#xff0c;并自定义背景色 <el-inputref"textareaRef"v-model"tempContent":style"{--el-border-color: rgba(255,255,255,0.0),--el-input-…...

Excel 动态比较两列数据:实现灵活的数据验证

目录 动态比较两列数据的需求动态公式的实现使用INDIRECT和ROW函数公式解释应用 动态公式的优点 快速添加一列公式的技巧使用快捷键Ctrl D使用VBA宏自动化使用“表格”功能自动填充 实际应用场景数据验证动态报告数据清洗 注意事项总结 在数据处理和分析中&#xff0c;Excel 是…...

谷歌推出可免费使用的Firebase Studio:Gemini全栈AI开发利器

谷歌刚刚发布了Firebase Studio,这是其打造的一款沉浸式代码开发平台,旨在与Cursor、Lovable、Bolt和V0等工具竞争。如果你是一名网页开发者,可能只知道Firebase是谷歌的数据库工具。 但现在,它已远不止于此。 Firebase已发展成一个完整的生态系统,如今能帮助你从头到尾…...

spark(二)

本节课接上节课继续对于RDD进行学习&#xff0c;首先是对于创建RDD的不同方式&#xff0c;接着学习了RDD的三种转换算子&#xff1a;Value类型、双Value类型、Key-Value类型&#xff0c;以及各个转换算子的不同使用方式。 学习到如下的区别&#xff1a; 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计算服务&#xff0c;提供FaaS&#xff08;Function as a Service&#xff09;能力&#xff0c;一方面云函数将开发测试的对象聚焦到函数级别&#xff0c;可以帮助您大幅简化应用开发与运维相关的事务&…...

利用大模型和聚类算法找出 Excel 文件中重复或相似度高的数据,并使用 FastAPI 进行封装的详细方案

以下是一个利用大模型和聚类算法找出 Excel 文件中重复或相似度高的数据,并使用 FastAPI 进行封装的详细方案: 方案流程 数据读取:从 Excel 文件中读取数据。文本向量化:使用大模型将文本数据转换为向量表示。聚类分析:运用聚类算法对向量进行分组,将相似度高的数据归为…...

通过远程桌面连接wsl2中安装的ubuntu24.04

要介绍的这种方式其实跟直接用wsl来执行命令差不多&#xff0c;是在终端去操作ubuntu。WSL2 默认只提供命令行界面&#xff0c;本文安装xrdp后通过windows远程桌面连接过去。 1、更新软件包列表 sudo apt update 确保你的软件包列表是最新的&#xff0c;否则可能找不到某些包…...

对接和使用国内稳定无水印的 Suno API

随着 AI 的应用日益广泛&#xff0c;各种 AI 程序已经融入我们的日常生活。从最早的写作&#xff0c;到医疗、教育&#xff0c;如今甚至扩展到了音乐领域。 Suno 是一个专注于高质量 AI 歌曲和音乐创作的平台。用户只需输入简单的文本提示词&#xff0c;便可以按照流派风格和歌…...

LeetCode算法题(Go语言实现)_38

我将按照您提供的文档结构为您整理二叉树最近公共祖先&#xff08;LCA&#xff09;问题的解决方案&#xff1a; 一、代码实现 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公网地址 输入密码&#xff0c;用 uname -r 指令来鉴定是否登录成功。之后就可以进行命令行操作了。 alt enter 全屏、退出 设置多用户指令&#xff0c;新建用户 adduser 名字 passwd 密码 销毁用户&#xf…...

微信小程序跳4

formatMillisecondsTime: function(milliseconds, formatStr) { // 创建一个新的Date对象&#xff0c;传入毫秒值 const date new Date(milliseconds); // 获取年月日时分秒&#xff0c;并确保它们都是两位数 const year date.getFullYear(); const month (date.getMonth() …...

STM32单片机入门学习——第31节: [10-1] I2C通信协议

写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难&#xff0c;但我还是想去做&#xff01; 本文写于&#xff1a;2025.04.10 STM32开发板学习——第31节: [10-1] I2C通信协议 前言开发板说明引用解答和科普一…...

OpenCV 图形API(24)图像滤波-----双边滤波函数bilateralFilter()

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 应用双边滤波到图像。 该函数对输入图像应用双边滤波&#xff0c;如 http://www.dai.ed.ac.uk/CVonline/LOCAL_COPIES/MANDUCHI1/Bilateral_Fil…...

【图像处理基石】什么是影调?并用python实现一个哈苏色彩影调

影调是摄影语言的核心&#xff0c;通过控制明暗、虚实与色彩&#xff0c;可精准传达创作意图。实际选择需结合主题情感、光线条件及画面结构&#xff0c;灵活运用高调、低调或冷暖色调&#xff0c;以强化视觉表现力。 一、影调的定义 影调指画面中明暗、虚实、色彩的层次与对比…...