1:
思路:
- 首先判断数组长度,如果长度小于 3,直接返回 0,因为等差数列至少需要三个元素。
- 初始化一个 dp 数组,
dp[i]
表示以nums[i]
结尾的等差子数组的个数。 - 遍历数组,从索引 2 开始,检查当前元素和前两个元素是否构成等差数列。如果
nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]
,说明以nums[i]
结尾的等差子数组个数是dp[i - 1] + 1
。 - 最后遍历 dp 数组,累加所有元素的值,即为所有等差子数组的个数。
代码:
public class Solution {
public int numberOfArithmeticSlices(int[] nums) {
int n = nums.length;
if (n < 3) {
return 0;
}
int[] dp = new int[n];
for (int i = 2; i < n; i++) {
if (nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]) {
dp[i] = dp[i - 1] + 1;
}
}
int sum = 0;
for (int count : dp) {
sum += count;
}
return sum;
}
}
结果:
为什么是dp[i - 1] + 1。
在动态规划的解法中,dp[i] = dp[i - 1] + 1
的核心逻辑在于:当以位置 i
结尾的子数组满足等差数列条件时,它会继承之前所有的等差数列,并新增一个长度为 3 的等差数列。以下是详细解释:
关键理解点
-
等差数列的连续性
题目要求子数组必须是连续的。因此,当位置i
能与前两个位置i-1
和i-2
构成等差数列时,所有以i-1
结尾的等差数列都可以延伸到i
。 -
继承性
如果nums[i-2], nums[i-1], nums[i]
构成等差数列,那么:- 所有以
i-1
结尾的等差数列(共有dp[i-1]
个),都可以加上nums[i]
形成更长的等差数列。 - 新增一个长度为 3 的等差数列:即
nums[i-2], nums[i-1], nums[i]
本身。
- 所有以
-
数学归纳
例如:- 当
dp[i-1] = 2
时(表示以i-1
结尾的等差数列有 2 个),若i
加入后仍满足等差数列条件,则以i
结尾的等差数列数量为:- 原有的 2 个延伸到
i
,新增 1 个长度为 3 的数列,因此dp[i] = 2 + 1 = 3
。
- 原有的 2 个延伸到
- 当
示例说明
假设数组为 nums = [1, 2, 3, 4, 5]
:
-
i=2
(元素 3):nums[0]=1
,nums[1]=2
,nums[2]=3
构成等差数列,长度为 3,因此dp[2] = 1
。
-
i=3
(元素 4):nums[1]=2
,nums[2]=3
,nums[3]=4
构成等差数列,继承dp[2]=1
,新增 1 个,因此dp[3] = 1 + 1 = 2
。- 此时以
i=3
结尾的等差数列为[1,2,3,4]
和[2,3,4]
。
-
i=4
(元素 5):nums[2]=3
,nums[3]=4
,nums[4]=5
构成等差数列,继承dp[3]=2
,新增 1 个,因此dp[4] = 2 + 1 = 3
。- 此时以
i=4
结尾的等差数列为[1,2,3,4,5]
,[2,3,4,5]
,[3,4,5]
。
总结
dp[i-1] + 1
的本质是:
dp[i-1]
:继承所有以i-1
结尾的等差数列。+1
:新增一个由i-2, i-1, i
构成的长度为 3 的等差数列。
这种递推关系确保了动态规划能够高效统计所有可能的等差数列子数组。
2:
思路:
- 初始化:首先判断输入矩阵是否有效,然后获取矩阵的行数
m
和列数n
,并初始化两个布尔型二维数组pacific
和atlantic
,用于记录可以流向太平洋和大西洋的单元格。 - DFS 函数:递归函数
dfs
用于从当前单元格出发,探索所有可以到达的更高或等高的单元格,并将这些单元格在对应的布尔数组中标记为true
。 - 太平洋边界处理:从左边界和上边界的每个单元格出发,调用 DFS 函数,将可以到达的单元格在
pacific
数组中标记为true
。 - 大西洋边界处理:从右边界和下边界的每个单元格出发,调用 DFS 函数,将可以到达的单元格在
atlantic
数组中标记为true
。 - 求交集:遍历所有单元格,将同时在
pacific
和atlantic
数组中被标记为true
的单元格添加到结果列表中。
代码:
import java.util.ArrayList;
import java.util.List;
class Solution {
private int[][] heights;
private int m, n;
public List<List<Integer>> pacificAtlantic(int[][] heights) {
List<List<Integer>> result = new ArrayList<>();
if (heights == null || heights.length == 0 || heights[0].length == 0) {
return result;
}
this.heights = heights;
m = heights.length;
n = heights[0].length;
boolean[][] pacific = new boolean[m][n];
boolean[][] atlantic = new boolean[m][n];
// 从太平洋边界开始DFS
for (int r = 0; r < m; r++) {
dfs(r, 0, pacific, Integer.MIN_VALUE);
}
for (int c = 0; c < n; c++) {
dfs(0, c, pacific, Integer.MIN_VALUE);
}
// 从大西洋边界开始DFS
for (int r = 0; r < m; r++) {
dfs(r, n - 1, atlantic, Integer.MIN_VALUE);
}
for (int c = 0; c < n; c++) {
dfs(m - 1, c, atlantic, Integer.MIN_VALUE);
}
// 找出同时满足两个条件的坐标
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (pacific[r][c] && atlantic[r][c]) {
result.add(List.of(r, c));
}
}
}
return result;
}
private void dfs(int r, int c, boolean[][] visited, int prevHeight) {
if (r < 0 || c < 0 || r >= m || c >= n || visited[r][c] || heights[r][c] < prevHeight) {
return;
}
visited[r][c] = true;
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
for (int[] dir : directions) {
dfs(r + dir[0], c + dir[1], visited, heights[r][c]);
}
}
}
结果:
核心思路
水从高处流向低处,但我们可以反向思考:从海洋边界(太平洋和大西洋)出发,找到所有能从海洋逆流而上到达的单元格。如果某个单元格既可以从太平洋到达,也可以从大西洋到达,那么它就是符合条件的解。
代码结构与关键步骤
1. 初始化与边界检查
if (heights == null || heights.length == 0 || heights[0].length == 0) {
return result;
}
- 处理空输入的情况,直接返回空结果。
2. 全局变量与矩阵尺寸
private int[][] heights;
private int m, n;
heights
:存储输入矩阵。m
和n
:矩阵的行数和列数。
3. 标记数组初始化
boolean[][] pacific = new boolean[m][n];
boolean[][] atlantic = new boolean[m][n];
pacific[r][c]
:表示坐标(r, c)
是否能流向太平洋。atlantic[r][c]
:表示坐标(r, c)
是否能流向大西洋。
4. 从边界开始 DFS 探索
太平洋边界(左 + 上)
// 左边界(所有行的第0列)
for (int r = 0; r < m; r++) {
dfs(r, 0, pacific, Integer.MIN_VALUE);
}
// 上边界(第0行的所有列)
for (int c = 0; c < n; c++) {
dfs(0, c, pacific, Integer.MIN_VALUE);
}
- 从左边界(
c=0
)和上边界(r=0
)的所有单元格出发,标记能逆流到达的单元格。
大西洋边界(右 + 下)
// 右边界(所有行的最后一列)
for (int r = 0; r < m; r++) {
dfs(r, n - 1, atlantic, Integer.MIN_VALUE);
}
// 下边界(最后一行的所有列)
for (int c = 0; c < n; c++) {
dfs(m - 1, c, atlantic, Integer.MIN_VALUE);
}
从右边界(c=n-1
)和下边界(r=m-1
)的所有单元格出发,标记能逆流到达的单元格
5. DFS 深度优先搜索函数
private void dfs(int r, int c, boolean[][] visited, int prevHeight) {
if (r < 0 || c < 0 || r >= m || c >= n || visited[r][c] || heights[r][c] < prevHeight) {
return;
}
visited[r][c] = true;
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
for (int[] dir : directions) {
dfs(r + dir[0], c + dir[1], visited, heights[r][c]);
}
}
- 终止条件:
- 越界(
r < 0
或c < 0
或r >= m
或c >= n
)。 - 已访问(
visited[r][c]
为true
)。 - 当前高度小于上一个单元格的高度(
heights[r][c] < prevHeight
),说明无法从当前单元格流向之前的单元格。
- 越界(
- 标记当前单元格:
visited[r][c] = true
。 - 递归探索四个方向:上、下、左、右。
6. 结果收集
for (int r = 0; r < m; r++) {
for (int c = 0; c < n; c++) {
if (pacific[r][c] && atlantic[r][c]) {
result.add(List.of(r, c));
}
}
}
复杂度分析
- 时间复杂度:O(m*n),每个单元格最多被访问两次(一次从太平洋边界,一次从大西洋边界)。
- 空间复杂度:O(m*n),主要用于存储两个标记数组。