一、题目

题目描述:

宝宝和妈妈参加亲子游戏,在一个二维矩阵(N*N)的格子地图上,宝宝和妈妈抽签决定各自的位置,地图上每个格子有不同的糖果数量,部分格子有障碍物。

游戏规则是妈妈必须在最短的时间(每个单位时间只能走一步)到达宝宝的位置,路上的所有糖果都可以拿走,不能走障碍物的格子,只能上下左右走。

请问妈妈在最短到达宝宝位置的时间内最多拿到多少糖果(优先考虑最短时间到达的情况下尽可能多拿糖果)。

输入描述:

第一行输入为 N,N 标识二维矩阵的大小
之后 N 行,每行有 N 个值,表格矩阵每个位置的值

其中:

  • -3:妈妈
  • -2:宝宝
  • -1:障碍0:糖果数(0 表示没有糖果,但是可以走)
  • ≥0:糖果数(0表示没有糖果,但是可以走)

输出描述:
输出妈妈在最短到达宝宝位置的时间内最多拿到多少糖果,行末无多余空格

备注:
地图最大 50*50

示例1:
输入:
4
3 2 1 -3
1 -1 1 1
1 1 -1 2
-2 1 2 3

输出:
9

说明:
此地图有两条最短路径可到宝宝位置,都是最短路径6步,但先向下再向左可以拿到9个糖果

示例2:
输入:

4
3 2 1 -3
-1 -1 1 1
1 1 -1 2
-2 1 -1 3
输出:
-1
说明:
此地图妈妈无法到达宝宝位置

二、题目深度解析🧠

1. 核心矛盾

通常的 BFS 只关心“是否到达”或“最短步数”。但本题增加了第二维度的约束:在步数最短的前提下,最大化路径权值和

  • 第一优先级:时间(步数)最短。
  • 第二优先级:糖果数量最多。

2. 为什么不能直接用 Dijkstra?

虽然 Dijkstra 可以处理带权最短路,但本题的“权值”有两个维度:

  1. 边的权重恒为 1(步数)。
  2. 点的权重不同(糖果数)。
    由于边权为 1,BFS 天然保证第一次到达某点时的步数是最小的。因此,我们不需要复杂的优先队列,只需在 BFS 过程中维护一个 maxCandy[x][y] 数组即可。

3. 解题策略:BFS + 贪心更新

  • 状态定义dist[x][y] 记录到达 (x, y) 的最短步数,candy[x][y] 记录在最短步数下能拿到的最大糖果数。
  • 初始化dist 全为无穷大,candy 全为 -1。起点 candy 设为起点的糖果数(注意起点是 -3,需特殊处理,视为 0 糖果或根据题意,通常起点不计入糖果,只计路上的,但本题示例暗示起点和终点的数值可能只是标记,实际糖果看 >=0 的值。修正:根据示例1,起点 -3 和终点 -2 所在的格子如果不含正数糖果,通常不计分,或者题目隐含 -3/-2 位置本身没有额外糖果,只有 >=0 的格子有。观察示例1:
    • 路径:(0,3)[-3] -> (1,3)[1] -> (1,2)[1] -> (2,2)[-1 障碍, 不通] ...
    • 让我们重新推导示例1的逻辑:
      • 起点 (0,3),终点 (3,0)。
      • 路径1:(0,3)→(0,2)[1]→(0,1)[2]→(0,0)[3]→(1,0)[1]→(2,0)[1]→(3,0)[-2]。步数6。糖果:1+2+3+1+1 = 8? 不对,答案是9。
      • 路径2(说明中提到“先向下再向左”):(0,3)→(1,3)[1]→(2,3)[2]→(3,3)[3]→(3,2)[2]→(3,1)[1]→(3,0)[-2]。
      • 糖果计算:1(1,3) + 2(2,3) + 3(3,3) + 2(3,2) + 1(3,1) = 9。
      • 结论:起点(-3)和终点(-2)本身不计入糖果数,只计算路径上 >=0 的格子。
  • 更新规则
    当从 curr 扩展到 next 时:
    1. 若 next 未访问过(dist[next] == INF):
      • 更新 dist[next] = dist[curr] + 1
      • 更新 candy[next] = candy[curr] + getVal(next)
      • next 入队。
    2. 若 next 已访问过,且 dist[next] == dist[curr] + 1(说明找到了另一条同样短的路):
      • 如果 candy[curr] + getVal(next) > candy[next]
        • 更新 candy[next] 为更大值。
        • 关键点:此时 next 需要再次入队吗
          • 不需要再次入队去更新它的子节点吗?
          • 需要! 因为 next 的糖果数变大了,它传递给后续节点的糖果基数也变大了。所以必须将 next 再次加入队列(或者在同类层处理中传播),以便更新其后续路径的最大值。
          • 优化策略:标准的 BFS 每个点只出队一次。但在“最短路最大权值”问题中,如果在同一层发现更优解,通常需要允许该点再次入队,或者使用类似 SPFA 的思想,但因为边权为 1,我们可以限制只在 dist 相等且 candy 变大时入队。考虑到 N≤50N≤50 ,数据量小,即使稍微冗余的入队也能轻松通过。

三、Java 实现 (面向对象风格)💻

Java 版本利用内部类 Point 封装坐标,代码结构清晰,适合工程化场景。

import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;

public class Main {
    
    // 方向数组:上下左右
    private static final int[] DX = {-1, 1, 0, 0};
    private static final int[] DY = {0, 0, -1, 1};

    static class Point {
        int x, y;
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        if (!scanner.hasNextInt()) return;
        
        int n = scanner.nextInt();
        int[][] grid = new int[n][n];
        
        int startX = -1, startY = -1;
        int endX = -1, endY = -1;

        // 读取矩阵并定位起点终点
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                grid[i][j] = scanner.nextInt();
                if (grid[i][j] == -3) {
                    startX = i;
                    startY = j;
                } else if (grid[i][j] == -2) {
                    endX = i;
                    endY = j;
                }
            }
        }

        System.out.println(solve(n, grid, startX, startY, endX, endY));
    }

    private static int solve(int n, int[][] grid, int sx, int sy, int ex, int ey) {
        // dist[i][j]: 到达 (i,j) 的最短步数
        int[][] dist = new int[n][n];
        // maxCandy[i][j]: 在最短步数下,到达 (i,j) 的最大糖果数
        int[][] maxCandy = new int[n][n];

        // 初始化
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                dist[i][j] = Integer.MAX_VALUE;
                maxCandy[i][j] = -1;
            }
        }

        Queue<Point> queue = new LinkedList<>();
        
        // 起点初始化
        dist[sx][sy] = 0;
        maxCandy[sx][sy] = 0; // 起点本身不计糖果
        queue.offer(new Point(sx, sy));

        while (!queue.isEmpty()) {
            Point curr = queue.poll();
            int cx = curr.x;
            int cy = curr.y;

            // 如果已经到达终点,我们不能直接break,因为可能有同步数的其他路径糖果更多
            // 但因为是BFS,第一次遇到终点时的步数一定是最小的。
            // 我们需要继续处理完当前层所有可能更新终点的情况,或者允许终点多次入队更新最大值。
            // 这里采用通用策略:只要找到更优解就入队。

            for (int i = 0; i < 4; i++) {
                int nx = cx + DX[i];
                int ny = cy + DY[i];

                // 边界检查
                if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
                // 障碍物检查
                if (grid[nx][ny] == -1) continue;

                int step = dist[cx][cy] + 1;
                int candyGain = (grid[nx][ny] >= 0) ? grid[nx][ny] : 0;
                int newCandy = maxCandy[cx][cy] + candyGain;

                // 情况1: 第一次到达该点 (找到更短路径)
                if (step < dist[nx][ny]) {
                    dist[nx][ny] = step;
                    maxCandy[nx][ny] = newCandy;
                    queue.offer(new Point(nx, ny));
                } 
                // 情况2: 以相同步数到达,但糖果更多
                else if (step == dist[nx][ny] && newCandy > maxCandy[nx][ny]) {
                    maxCandy[nx][ny] = newCandy;
                    // 重要:糖果数更新了,需要重新入队,以便将更大的糖果数传递给后续节点
                    queue.offer(new Point(nx, ny));
                }
            }
        }

        if (dist[ex][ey] == Integer.MAX_VALUE) {
            return -1;
        }
        
        return maxCandy[ex][ey];
    }
}

✅ Java 版亮点

  • 封装性:使用 Point 类管理坐标,代码可读性强。
  • 逻辑严密:处理了 step == dist 且 candy 更大的情况,并重新入队,确保最优解能传递到终点。
  • 健壮性:完善的边界和障碍物判断。

四、Go 语言实现 (原生高性能风格)🚀

Go 版本利用 struct 和 slice 模拟队列,无依赖,运行效率极高,非常适合对性能敏感的机考环境。

package main

import (
	"fmt"
	"math"
)

type Point struct {
	x, y int
}

func main() {
	var n int
	if _, err := fmt.Scan(&n); err != nil {
		return
	}

	grid := make([][]int, n)
	startX, startY, endX, endY := -1, -1, -1, -1

	for i := 0; i < n; i++ {
		grid[i] = make([]int, n)
		for j := 0; j < n; j++ {
			fmt.Scan(&grid[i][j])
			if grid[i][j] == -3 {
				startX, startY = i, j
			} else if grid[i][j] == -2 {
				endX, endY = i, j
			}
		}
	}

	result := solve(n, grid, startX, startY, endX, endY)
	fmt.Println(result)
}

func solve(n int, grid [][]int, sx, sy, ex, ey int) int {
	// 初始化距离和糖果数组
	dist := make([][]int, n)
	maxCandy := make([][]int, n)
	for i := 0; i < n; i++ {
		dist[i] = make([]int, n)
		maxCandy[i] = make([]int, n)
		for j := 0; j < n; j++ {
			dist[i][j] = math.MaxInt32
			maxCandy[i][j] = -1
		}
	}

	// 手动实现队列
	queue := make([]Point, 0)
	queue = append(queue, Point{x: sx, y: sy})

	dist[sx][sy] = 0
	maxCandy[sx][sy] = 0

	dx := []int{-1, 1, 0, 0}
	dy := []int{0, 0, -1, 1}

	head := 0
	for head < len(queue) {
		curr := queue[head]
		head++ // 出队

		cx, cy := curr.x, curr.y

		for i := 0; i < 4; i++ {
			nx := cx + dx[i]
			ny := cy + dy[i]

			// 边界检查
			if nx < 0 || nx >= n || ny < 0 || ny >= n {
				continue
			}
			// 障碍物
			if grid[nx][ny] == -1 {
				continue
			}

			step := dist[cx][cy] + 1
			gain := 0
			if grid[nx][ny] >= 0 {
				gain = grid[nx][ny]
			}
			newCandy := maxCandy[cx][cy] + gain

			// 发现更短路径
			if step < dist[nx][ny] {
				dist[nx][ny] = step
				maxCandy[nx][ny] = newCandy
				queue = append(queue, Point{x: nx, y: ny})
			} else if step == dist[nx][ny] && newCandy > maxCandy[nx][ny] {
				// 同步数但糖果更多,更新并入队
				maxCandy[nx][ny] = newCandy
				queue = append(queue, Point{x: nx, y: ny})
			}
		}
	}

	if dist[ex][ey] == math.MaxInt32 {
		return -1
	}

	return maxCandy[ex][ey]
}

✅ Go 版亮点

  • 零开销抽象:直接使用 slice 作为队列,配合 head 指针,避免了 container/list 的指针跳转开销。
  • 内存紧凑dist 和 maxCandy 使用二维切片,内存布局连续,缓存友好。
  • 逻辑一致:完美复刻了核心算法,处理了重复入队逻辑。

五、关键逻辑推演 (以示例1为例)🔍

输入地图

3  2  1 -3(起)
1 -1  1  1
1  1 -1  2
-2(终) 1  2  3
  1. Start: (0,3), Step=0, Candy=0. 入队。
  2. Layer 1:
    • (0,2): Step=1, Candy=1.
    • (1,3): Step=1, Candy=1.
  3. Layer 2:
    • 从 (0,2) -> (0,1)[2]: Step=2, Candy=1+2=3.
    • 从 (1,3) -> (2,3)[2]: Step=2, Candy=1+2=3.
    • ...
  4. 关键分歧点:
    • 假设某点 P 可以通过路径 A (Step=3, Candy=5) 到达,也可以通过路径 B (Step=3, Candy=8) 到达。
    • BFS 先处理路径 A,dist[P]=3candy[P]=5,P 入队。
    • 随后处理路径 B,发现 step==3 但 8 > 5,更新 candy[P]=8P 再次入队
    • 当 P 第二次出队时,它会带着 8 个糖果去更新它的邻居,从而保证最终结果是最优的。

结果验证
示例1中,向下走的路径累积糖果更多,算法会正确捕捉到这一差异,输出 9
示例2中,障碍物阻断所有路径,dist 保持无穷大,输出 -1


六、避坑指南⚠️

  1. 起点/终点数值处理
    • 题目中 -3 和 -2 只是标记,不要把它们加到糖果总数里!只有 >=0 的数字才代表糖果。代码中已通过 if grid[nx][ny] >= 0 严格过滤。
  2. 重复入队死循环
    • 有人会担心 step == dist 且 candy 更新会导致死循环。
    • 不会。因为糖果数是单调递增的,且网格有限,每个点的 maxCandy 更新次数是有限的(最多等于到达该点的路径数,实际上远小于此)。对于 50×5050×50 的地图,完全不会超时。
  3. 不可达判断
    • 最后一定要检查 dist[ex][ey] 是否仍为初始化的最大值,若是则输出 -1。

七、复杂度分析📊

  • 时间复杂度: O(K⋅N2) 。
    • 在最坏情况下,每个点可能会因为糖果数更新而多次入队。但在网格图中,这个系数 KK 很小。对于 N=50 ,运算量在万级别,毫秒级完成。
  • 空间复杂度: O(N2) 。
    • 用于存储 distmaxCandy 数组和队列。

八、结语

这道题是 BFS 最短路模型 的经典变种,考察了候选人对 状态多维更新 的理解。

  • Java 开发者可以学习如何在对象模型下优雅地处理状态转移。
  • Go 开发者可以体验如何用极简的原生语法实现高效算法。

掌握这种 "最短路 + 最大权值" 的模板,无论是应对华为 OD、大厂笔试,还是实际工作中的路径规划问题,都能游刃有余。

觉得有帮助请 点赞👍、收藏⭐、关注🙋!后续将带来更多 机考真题全语言解析 系列!

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐