LeetCode周赛遇到‘字母收集’别慌,用Java动态规划5分钟搞定最优路径
·
LeetCode周赛高频题解析:用动态规划速解字母收集问题
在算法竞赛的紧张氛围中,遇到二维网格路径优化问题时,动态规划(DP)往往是最高效的解决方案。这类问题在LeetCode周赛和各大厂笔试中频繁出现,比如我们今天要讨论的"字母收集"问题——它本质上是一个变种的"最大路径和"问题。
1. 问题分析与抽象化建模
字母收集问题的核心在于:在一个n*m的字母矩阵中,从左上角出发,每次只能向右或向下移动,收集经过的字母,其中'l'、'o'、'v'、'e'分别对应4、3、2、1分,其他字母0分,求到达右下角时的最大得分。
关键识别点 :
- 移动限制(右/下)暗示了DP的可行性
- 每个格子的得分只与字母类型相关
- 需要累计路径上的最大得分
这类网格DP问题通常具有以下特征:
- 状态定义明确:
dp[i][j]表示到达(i,j)时的最优解 - 转移方程清晰:基于移动限制(通常来自上方或左方)
- 边界条件固定:首行/首列通常只能从一个方向到达
2. 动态规划模板解析
对于网格DP问题,我们可以套用一个通用模板:
// 初始化DP表
int[][] dp = new int[n][m];
// 边界条件处理
dp[0][0] = valueAtStart;
for (int i = 1; i < n; i++) dp[i][0] = dp[i-1][0] + value[i][0];
for (int j = 1; j < m; j++) dp[0][j] = dp[0][j-1] + value[0][j];
// 状态转移
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + value[i][j];
}
}
return dp[n-1][m-1];
针对字母收集问题,我们需要做以下适配:
- 将字母转换为对应分值
- 处理1-based索引(如果输入从(1,1)开始)
- 考虑大矩阵情况下的空间优化
3. 竞赛级Java实现
以下是经过优化的完整实现,考虑了竞赛中的常见输入格式和效率要求:
import java.util.*;
public class LetterCollection {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt(), m = sc.nextInt();
char[][] grid = new char[n][m];
for (int i = 0; i < n; i++) {
grid[i] = sc.next().toCharArray();
}
int[][] dp = new int[n][m];
// 初始化起点
dp[0][0] = getScore(grid[0][0]);
// 初始化第一列
for (int i = 1; i < n; i++) {
dp[i][0] = dp[i-1][0] + getScore(grid[i][0]);
}
// 初始化第一行
for (int j = 1; j < m; j++) {
dp[0][j] = dp[0][j-1] + getScore(grid[0][j]);
}
// 填充DP表
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + getScore(grid[i][j]);
}
}
System.out.println(dp[n-1][m-1]);
}
private static int getScore(char c) {
switch (c) {
case 'l': return 4;
case 'o': return 3;
case 'v': return 2;
case 'e': return 1;
default: return 0;
}
}
}
关键优化点 :
- 使用switch语句处理字母评分,比if-else更清晰
- 分离分数计算逻辑,提高代码可读性
- 显式处理边界条件,避免在双重循环中判断
4. 空间复杂度优化技巧
当处理大矩阵时(如500x500),我们可以进一步优化空间使用:
public static int maxScoreOptimized(char[][] grid) {
int m = grid[0].length;
int[] dp = new int[m];
// 初始化第一行
dp[0] = getScore(grid[0][0]);
for (int j = 1; j < m; j++) {
dp[j] = dp[j-1] + getScore(grid[0][j]);
}
// 逐行处理
for (int i = 1; i < grid.length; i++) {
dp[0] += getScore(grid[i][0]); // 第一列
for (int j = 1; j < m; j++) {
dp[j] = Math.max(dp[j], dp[j-1]) + getScore(grid[i][j]);
}
}
return dp[m-1];
}
这种优化将空间复杂度从O(n*m)降到O(m),在处理大矩阵时能有效减少内存使用。
5. 常见变种与应对策略
在竞赛中,这类问题常有多种变体,我们需要快速识别并调整解决方案:
| 变种类型 | 关键特征 | 解法调整 |
|---|---|---|
| 最小路径和 | 求最小值而非最大值 | 将Math.max改为Math.min |
| 障碍物网格 | 某些格子不可通过 | 在DP转移时跳过障碍物格子 |
| 多路径收集 | 可以重复经过某些格子 | 可能需要三维DP或记忆化搜索 |
| 多目标字母 | 需要收集特定字母组合 | 增加DP状态维度记录收集情况 |
例如,如果题目改为"必须收集至少一个'l'和一个'o'",我们就需要将状态扩展为 dp[i][j][k] ,其中k用位掩码表示已收集的字母情况。
6. 竞赛实战技巧
在时间紧迫的编程竞赛中,以下几点可以帮助你更快解决此类问题:
- 快速识别模式 :看到网格+路径优化,立即想到DP
- 模板化编码 :准备好网格DP的代码模板
- 边界处理 :先处理第一行和第一列,再填充内部
- 空间优化 :根据问题规模决定是否使用滚动数组
- 调试技巧 :对于小样例,手动计算几个关键点的DP值验证
提示:在比赛中,建议先写出基础DP解法,确保正确性后再考虑优化。过早优化可能引入错误并浪费时间。
7. 复杂度分析与适用场景
让我们分析不同解法的性能特点:
基础DP解法 :
- 时间复杂度:O(n*m) —— 必须遍历整个网格
- 空间复杂度:O(n*m) —— 完整的DP表
优化空间解法 :
- 时间复杂度:O(n*m) —— 相同
- 空间复杂度:O(m) —— 仅保留一行
对于LeetCode等编程竞赛中的约束条件:
- 当n,m ≤ 200时,基础解法完全足够
- 当n,m ≤ 1000时,建议使用空间优化版本
- 对于更大的网格,可能需要考虑并行算法或其他优化
在实际笔试中,这类问题通常出现在中等难度环节,建议分配10-15分钟完成,包括问题分析、编码和测试。
更多推荐
所有评论(0)