目录

题目描述

1301. 最大得分的路径数目

题意分析

给定一个 n × n 的字符方阵 board,左上角 board[0][0]'E'(终点),右下角 board[n-1][n-1]'S'(起点),其余格子要么是 '1'..'9' 的数字,要么是障碍 'X'。从 S 出发,每步只能往上、左、左上三个方向之一移动,走到 E 为止,途中不能踩障碍。路径得分是沿途所有数字格之和(SE 本身不计分)。要返回两个数:最大得分,以及取到最大得分的路径条数(对 $10^9+7$ 取模);如果 S 根本走不到 E,返回 [0, 0]

这道题要求的是两个耦合的量:最优值和最优值的方案数。方案数只在最优值上统计,不是所有路径都算,这决定了不能把两个量拆成两次独立求解——必须在同一次推进里同时维护。

约束给出 n ≤ 100,格子总数最多一万,而路径条数却要取模,说明路径数量是指数级的。这两条合在一起是很强的信号:不能枚举路径,只能按格子递推,让每个格子的结果被后面的格子复用。

移动方向只有三个且全部朝着行、列下标减小的方向,意味着「从哪来」的关系是一张有向无环图,格子之间没有循环依赖,可以按下标顺序一次遍历算完。

边界要留意四点:ES 不贡献分数;X 既不能作为落脚点也不能作为中转;出发格 S 的初始得分是 0 而不是 -1;无路可走时答案是 [0, 0] 而不是 [-1, 0],也不是抛异常。

还要区分「得分为 0」和「不可达」:某条路径可能恰好只经过 ES 相邻的格子而得分很小,但绝不会因此变成不可达。所以需要一个和任何合法得分都不冲突的哨兵值来表示不可达。

解法:DP 求最大分数与路径数

核心思路

把棋盘看成一张有向无环图,同时维护最优得分和达到该得分的方案数:

  • score[i][j]:从 S(i,j) 的合法路径最大得分,包含当前数字格;-1 表示不可达。
  • ways[i][j]:达到 score[i][j] 的路径数量,按 $10^9+7$ 取模。

S(i,j) 的最后一步只能来自 (i+1,j)(i,j+1)(i+1,j+1)。因此从右下向左上遍历,在三个已计算的前驱中找最大 score:遇到更大值时替换最大值并重置方案数;遇到并列最大值时累加方案数;不可达前驱不参与转移。

基例为 score[S]=0, ways[S]=1S 上的空路径是所有计数的来源;ES 都不加分。若某格没有可达前驱,就继续保持 score=-1, ways=0

不变量:处理 (i,j) 时,它的三个前驱状态均已最终确定;score/ways 精确描述从 S 到这些格子的最大得分及对应数量。

正确性:任意到达 (i,j) 的路径都有唯一的最后一个前驱。归纳假设保证三个前驱状态正确,取最大值会排除次优路径,只对并列最优前驱求和则不重不漏地统计所有最优路径。按依赖顺序推进到 E 后即得到全局答案。

解题步骤

  1. score 全部填为 -1ways 默认为 0;设置右下角 S 的状态为 (0,1)
  2. 行、列都从大到小遍历,跳过障碍和已经初始化的 S
  3. 检查下、右、右下三个前驱。更优时重置计数,并列时累加后取模。
  4. 若没有可达前驱,当前格保持不可达;否则加上当前数字,写入两个状态。
  5. 左上角仍不可达时返回 [0,0],否则返回其最大得分和方案数。

样例 ['E23','2X2','12S'] 最优路径得分为 2+3+2=7,且只有一条,返回 [7,1]['E12','1X1','21S'] 有两条最优路径,返回 [4,2]

不可达反例 ['E11','XXX','11S'] 必须返回 [0,0]。它能检验 score=-1 哨兵、障碍跳过和最终包装是否一致。

代码实现

import java.util.Arrays;
import java.util.List;

class Solution {
    private static final int MOD = 1_000_000_007;

    public int[] pathsWithMaxScore(List<String> board) {
        int n = board.size();
        int[][] score = new int[n][n];
        int[][] ways = new int[n][n];
        for (int[] row : score) {
            Arrays.fill(row, -1);
        }
        score[n - 1][n - 1] = 0;
        ways[n - 1][n - 1] = 1;

        int[][] dirs = {{1, 0}, {0, 1}, {1, 1}};
        for (int i = n - 1; i >= 0; i--) {
            for (int j = n - 1; j >= 0; j--) {
                char c = board.get(i).charAt(j);
                if (c == 'X' || (i == n - 1 && j == n - 1)) {
                    continue;
                }
                int best = -1;
                int cnt = 0;
                for (int[] d : dirs) {
                    int ni = i + d[0];
                    int nj = j + d[1];
                    if (ni >= n || nj >= n || score[ni][nj] == -1) {
                        continue;
                    }
                    int val = score[ni][nj];
                    if (val > best) {
                        best = val;
                        cnt = ways[ni][nj];
                    } else if (val == best) {
                        cnt = (cnt + ways[ni][nj]) % MOD;
                    }
                }
                if (best == -1) {
                    continue;
                }
                int add = 0;
                if (c != 'E') {
                    add = c - '0';
                }
                score[i][j] = best + add;
                ways[i][j] = cnt;
            }
        }

        return score[0][0] == -1
                ? new int[]{0, 0}
                : new int[]{score[0][0], ways[0][0]};
    }
}
func pathsWithMaxScore(board []string) []int {
	const mod = 1000000007
	n := len(board)
	score := make([][]int, n)
	ways := make([][]int, n)
	for i := 0; i < n; i++ {
		score[i] = make([]int, n)
		ways[i] = make([]int, n)
		for j := 0; j < n; j++ {
			score[i][j] = -1
		}
	}
	score[n-1][n-1] = 0
	ways[n-1][n-1] = 1

	dirs := [][]int{{1, 0}, {0, 1}, {1, 1}}
	for i := n - 1; i >= 0; i-- {
		for j := n - 1; j >= 0; j-- {
			c := board[i][j]
			if c == 'X' || (i == n-1 && j == n-1) {
				continue
			}
			best := -1
			cnt := 0
			for _, d := range dirs {
				ni := i + d[0]
				nj := j + d[1]
				if ni >= n || nj >= n || score[ni][nj] == -1 {
					continue
				}
				val := score[ni][nj]
				if val > best {
					best = val
					cnt = ways[ni][nj]
				} else if val == best {
					cnt += ways[ni][nj]
					if cnt >= mod {
						cnt -= mod
					}
				}
			}
			if best == -1 {
				continue
			}
			add := 0
			if c != 'E' {
				add = int(c - '0')
			}
			score[i][j] = best + add
			ways[i][j] = cnt
		}
	}

	if score[0][0] == -1 {
		return []int{0, 0}
	}
	return []int{score[0][0], ways[0][0]}
}

复杂度分析

  • 时间复杂度:$O(n^2)$,每个格子只检查三个前驱。
  • 空间复杂度:$O(n^2)$,用于最大得分和方案数两个状态表。

关键点总结

  • 状态必须明确为“从 S 到当前格”,这样三个依赖格就是它在原移动方向上的前驱。
  • 最优值与最优方案数同步转移:更优就重置,相等才累加。
  • -1 区分不可达和合法的 0 分;不可达状态不能参与比较或计数。
  • ways[S]=1 是计数基例,方案数每次相加后取模,得分不取模。
  • 遍历方向必须保证下、右、右下三个状态已计算完成。

易错点总结

  • 把状态误定义为“当前格到 E”,却仍读取下、右、右下:状态含义与依赖方向矛盾。
  • score 默认填 0:不可达反例会沿障碍两侧生成正分、零方案的伪状态;必须使用 -1
  • 忘记 ways[S]=1:所有后续方案数都会保持 0。
  • 遇到更优前驱仍累加旧计数:次优路径会混进答案;更优时必须重置。
  • 并列最优不累加,或累加后不取模:分别会漏计或整数溢出。
  • E 当数字计算 c-'0':样例会凭空增加 21 分。
  • 不可达时直接返回内部状态:会返回 [-1,0],而题目要求 [0,0]

相似题目

题目 难度 考察点
62. 不同路径 中等 只数方案数、没有最优值,转移退化成单纯的加法
63. 不同路径 II 中等 在纯计数基础上加障碍,障碍格方案数置 0 即可,无需哨兵区分不可达
64. 最小路径和 中等 只求最优值不计数,ways 那一半可以整个删掉
120. 三角形最小路径和 中等 网格换成三角形,每层下标范围随行变化,转移只有两个后继
174. 地下城游戏 困难 状态必须定义成「所需初始血量」而非「累计血量」,否则贪心方向错误
688. 骑士在棋盘上的概率 中等 多一维步数,转移是概率求和而非取最大,八个方向且允许走出棋盘
931. 下降路径最小和 中等 逐行推进、每格看上一行三个邻居,只求最优值,可原地滚动
980. 不同路径 III 困难 要求走遍所有空格,无法用 DP 坍缩,必须回溯 + 状态压缩
1289. 下降路径最小和 II 困难 转移是「上一行除同列外的最小值」,需要用最小与次小值把 $O(n)$ 转移降到 $O(1)$
1594. 矩阵的最大非负积 中等 负数乘法会翻转大小关系,必须同时维护最大与最小两个状态
LCR 098. 不同路径 中等 与 62 同题,可用组合数 $C_{m+n-2}^{m-1}$ 直接秒杀
LCR 099. 最小路径和 中等 与 64 同题,适合练一维滚动数组的写法
LCR 100. 三角形最小路径和 中等 与 120 同题,自底向上推可以省掉边界特判
剑指 Offer 47. 礼物的最大价值 中等 只能右和下两个方向求最大和,是本题去掉计数与斜向后的最简版本