MCP(最小成本路径)问题超详细入门指南

一、问题定义与核心概念

最小成本路径(Minimum Cost Path,MCP)是在网格图中寻找从起点到终点的最优路径问题,核心是最小化路径总成本。数学表示为:

$$ \min \sum_{(i,j)\in \text{Path}} C[i][j] $$

其中:

  • $C$ 是 $m \times n$ 成本矩阵
  • $C[i][j] \geq 0$ 表示通过单元格 $(i,j)$ 的成本
  • 路径只能向右(→)、向下(↓)或对角线(↘)移动
二、动态规划解法原理

采用自底向上的动态规划策略,核心思想:

  1. 最优子结构:整体最优解包含子问题最优解
  2. 重叠子问题:重复计算相同子问题
  3. 状态定义:$dp[i][j]$ 表示到达 $(i,j)$ 的最小成本
  4. 状态转移:根据移动方向定义递推关系
三、算法实现详细步骤(含边界处理)
步骤1:初始化DP表
def init_dp(grid):
    m, n = len(grid), len(grid[0])
    dp = [[0] * n for _ in range(m)]
    
    # 起点初始化
    dp[0][0] = grid[0][0]
    
    # 第一列初始化(只能向下)
    for i in range(1, m):
        dp[i][0] = dp[i-1][0] + grid[i][0]
    
    # 第一行初始化(只能向右)
    for j in range(1, n):
        dp[0][j] = dp[0][j-1] + grid[0][j]
    
    return dp

步骤2:状态转移(三方向移动)
def fill_dp(grid, dp):
    m, n = len(grid), len(grid[0])
    for i in range(1, m):
        for j in range(1, n):
            # 核心递推公式
            dp[i][j] = grid[i][j] + min(
                dp[i-1][j],    # 从上方来
                dp[i][j-1],    # 从左方来
                dp[i-1][j-1]   # 从对角线来
            )
    return dp

步骤3:路径回溯(重建最优路径)
def trace_path(dp):
    m, n = len(dp), len(dp[0])
    path = []
    i, j = m-1, n-1  # 从终点开始
    
    while i > 0 or j > 0:
        path.append((i, j))
        # 确定移动方向
        if i == 0:
            j -= 1  # 只能向左
        elif j == 0:
            i -= 1  # 只能向上
        else:
            # 选择最小成本的来源方向
            min_dir = min(
                (dp[i-1][j], (i-1, j)),  # 上
                (dp[i][j-1], (i, j-1)),  # 左
                (dp[i-1][j-1], (i-1, j-1))  # 对角
            )
            i, j = min_dir[1]  # 移动到前驱位置
    
    path.append((0, 0))  # 添加起点
    return path[::-1]  # 反转路径

四、完整算法实现(带详细注释)
def min_cost_path(grid):
    """计算最小成本路径
    Args:
        grid: m x n 成本矩阵, grid[i][j] >= 0
    Returns:
        min_cost: 最小成本值
        path: 最优路径坐标列表
    """
    # 验证输入有效性
    if not grid or not grid[0]:
        return 0, []
    
    m, n = len(grid), len(grid[0])
    
    # === 步骤1:初始化DP表 ===
    dp = [[0]*n for _ in range(m)]
    dp[0][0] = grid[0][0]  # 起点成本
    
    # 初始化第一列(只能向下移动)
    for i in range(1, m):
        dp[i][0] = dp[i-1][0] + grid[i][0]
    
    # 初始化第一行(只能向右移动)
    for j in range(1, n):
        dp[0][j] = dp[0][j-1] + grid[0][j]
    
    # === 步骤2:填充DP表 ===
    for i in range(1, m):
        for j in range(1, n):
            # 状态转移方程:取三个方向最小值
            dp[i][j] = grid[i][j] + min(
                dp[i-1][j],    # 上方单元格
                dp[i][j-1],    # 左方单元格
                dp[i-1][j-1]   # 对角线单元格
            )
    
    # === 步骤3:路径回溯 ===
    path = []
    i, j = m-1, n-1  # 从终点开始
    
    while i > 0 or j > 0:
        path.append((i, j))
        # 边界处理
        if i == 0:       # 第一行,只能向左
            j -= 1
        elif j == 0:     # 第一列,只能向上
            i -= 1
        else:
            # 确定最小成本来源方向
            candidates = {
                'up': dp[i-1][j],
                'left': dp[i][j-1],
                'diag': dp[i-1][j-1]
            }
            direction = min(candidates, key=candidates.get)
            
            # 更新坐标
            if direction == 'up': 
                i -= 1
            elif direction == 'left': 
                j -= 1
            else:  # diag
                i -= 1
                j -= 1
    
    path.append((0, 0))  # 添加起点
    path.reverse()       # 反转路径
    
    return dp[m-1][n-1], path  # 返回最小成本和路径

五、详细案例解析(3×3网格)
成本矩阵定义:

$$ C = \begin{bmatrix} 1 & 2 & 3 \ 4 & 8 & 2 \ 1 & 5 & 3 \end{bmatrix} $$

步骤分解:
  1. 初始化DP表

    • 起点 (0,0):$dp[0][0] = 1$
    • 第一列:$dp[1][0] = 1+4=5$,$dp[2][0] = 5+1=6$
    • 第一行:$dp[0][1] = 1+2=3$,$dp[0][2] = 3+3=6$

    $$ \text{初始化后:} dp = \begin{bmatrix} 1 & 3 & 6 \ 5 & ? & ? \ 6 & ? & ? \end{bmatrix} $$

  2. 填充DP表

    • (1,1):$C[1][1] + \min(dp[0][1], dp[1][0], dp[0][0]) = 8 + \min(3,5,1) = 8+1=9$
    • (1,2):$2 + \min(6,9,3) = 2+3=5$
    • (2,1):$5 + \min(9,6,5) = 5+5=10$
    • (2,2):$3 + \min(5,10,9) = 3+5=8$

    $$ \text{完整DP表:} dp = \begin{bmatrix} 1 & 3 & 6 \ 5 & 9 & 5 \ 6 & 10 & \boxed{8} \end{bmatrix} $$

  3. 路径回溯

    (2,2) → 来源min(5,10,9)=5 → (1,2)
    (1,2) → 来源min(6,9,3)=3 → (0,2) 或 (0,1)? 
    实际检查:dp[0][2]=6, dp[1][1]=9, dp[0][1]=3 → 选择(0,1)
    (0,1) → 来源min(1,3,不存在)=1 → (0,0)
    

    最终路径:[(0,0), (0,1), (1,2), (2,2)]
    总成本:$1+2+2+3=8$ ✓

六、算法复杂度分析
  1. 时间复杂度

    • 填充DP表:$O(mn)$
    • 路径回溯:$O(m+n)$
    • 总体:$O(mn)$
  2. 空间复杂度

    • DP表存储:$O(mn)$
    • 路径存储:$O(m+n)$
    • 可优化为$O(\min(m,n))$(只存储当前行和上一行)
七、边界条件处理大全
  1. 特殊起点

    # 如果起点有障碍
    if grid[0][0] == float('inf'):
        return float('inf'), []
    

  2. 不可达单元格

    # 在DP填充时跳过障碍
    for i in range(1, m):
        for j in range(1, n):
            if grid[i][j] == float('inf'):
                dp[i][j] = float('inf')
            else:
                # 正常状态转移...
    

  3. 单行/单列网格

    # 单行网格处理
    if m == 1:
        return sum(grid[0]), [(0, j) for j in range(n)]
    
    # 单列网格处理
    if n == 1:
        return sum(grid[i][0] for i in range(m)), [(i, 0) for i in range(m)]
    

八、扩展应用场景
  1. 带障碍物的路径规划
    设置障碍物单元格成本为$\infty$:

    grid = [
        [1, 2, 3],
        [4, float('inf'), 2],
        [1, 5, 3]
    ]
    

  2. 多目标点路径优化
    使用Floyd-Warshall算法预处理:

    def preprocess_all_pairs(grid):
        # 创建距离矩阵
        dist = [[float('inf')]*n for _ in range(m)]
        for i in range(m):
            for j in range(n):
                if grid[i][j] != float('inf'):
                    dist[i][j] = grid[i][j]
        
        # Floyd核心
        for k in range(m*n):
            ki, kj = k//n, k%n
            for i in range(m):
                for j in range(n):
                    new_dist = dist[i][ki] + dist[ki][kj] + dist[kj][j]
                    if new_dist < dist[i][j]:
                        dist[i][j] = new_dist
        return dist
    

  3. 随机成本场路径规划
    引入期望值计算: $$ E[dp[i][j]] = \mu_{ij} + \min \begin{cases} E[dp[i-1][j]] \ E[dp[i][j-1]] \ E[dp[i-1][j-1]] \end{cases} $$ 其中 $\mu_{ij}$ 是位置$(i,j)$的成本期望值

九、实际应用案例

物流仓库拣货路径优化

  • 网格:仓库货架布局(10×15)
  • 成本:通过每个货架区的时间
  • 障碍:禁止通行区域
  • 起点:仓库入口 (0,0)
  • 终点:包装区 (9,14)
# 仓库布局定义
warehouse = [
    [1, 2, 1, 3, 2, 1, ...],  # 第0行
    [2, 0, 0, 2, 0, 1, ...],  # 0表示障碍物
    [...],
    ...
]

# 计算最优路径
cost, path = min_cost_path(warehouse)

# 输出结果
print(f"最短时间: {cost}分钟")
print("最优路径序列:")
for (i, j) in path:
    print(f"→ 货架区 [{i},{j}]")

十、常见错误及调试方法
  1. 错误1:负成本导致失效
    现象:路径成本异常
    解决:添加成本验证

    for row in grid:
        if any(x < 0 for x in row):
            raise ValueError("成本值不能为负数")
    

  2. 错误2:路径不连续
    调试:打印DP表验证

    def print_dp(dp):
        for i in range(len(dp)):
            print(" ".join(f"{x:3d}" for x in dp[i]))
    

  3. 错误3:忽略对角线移动
    验证:检查状态转移代码

    # 正确应包含三个方向
    min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
    

十一、进阶优化技巧
  1. 空间压缩优化
    只需存储两行数据:

    def optimized_mcp(grid):
        m, n = len(grid), len(grid[0])
        prev = [0] * n
        curr = [0] * n
        
        # 初始化第一行
        prev[0] = grid[0][0]
        for j in range(1, n):
            prev[j] = prev[j-1] + grid[0][j]
        
        for i in range(1, m):
            curr[0] = prev[0] + grid[i][0]  # 第一列
            for j in range(1, n):
                curr[j] = grid[i][j] + min(
                    prev[j],      # 上方
                    curr[j-1],    # 左方
                    prev[j-1]     # 对角线
                )
            prev, curr = curr, [0]*n  # 行交换
        
        return prev[-1]
    

  2. 并行计算优化
    对角线填充法:

    # 按对角线顺序处理
    for d in range(1, m+n-1):  # d为对角线索引
        start_row = max(0, d - n + 1)
        end_row = min(d, m-1)
        for i in range(start_row, end_row+1):
            j = d - i
            # 正常状态转移...
    

  3. GPU加速实现
    使用PyTorch并行计算:

    import torch
    
    def gpu_mcp(grid):
        grid_t = torch.tensor(grid, device='cuda')
        dp = torch.zeros_like(grid_t)
        # 初始化第一行/列...
        for i in range(1, m):
            for j in range(1, n):
                # 向量化计算
                dp[i,j] = grid_t[i,j] + torch.min(
                    torch.stack([dp[i-1,j], dp[i,j-1], dp[i-1,j-1]])
                )
        return dp[-1,-1].item()
    

十二、数学证明

定理:DP解法给出的路径是最优解
证明(反证法):
假设存在更优路径 $P^$ 满足 $\sum_{(i,j)\in P^} C[i][j] < dp[m-1][n-1]$

  1. 设 $P^*$ 首次偏离DP路径的位置为 $(k,l)$
  2. 根据DP定义:$dp[k][l] \leq C[k][l] + \min(dp[k-1][l], dp[k][l-1], dp[k-1][l-1])$
  3. $P^*$ 在 $(k,l)$ 后的成本 $V \geq dp[m-1][n-1] - dp[k][l]$
  4. 总成本:$ \sum P^* \geq dp[k][l] + V \geq dp[m-1][n-1] $
    与假设矛盾,故DP解最优。□
十三、练习题与答案
  1. 基础题
    网格:$\begin{bmatrix}1&5\3&2\end{bmatrix}$
    答案
    路径:(0,0)→(1,0)→(1,1)
    成本:1+3+2=6

  2. 障碍物题
    网格:$\begin{bmatrix}1 & \infty \ 3 & 2\end{bmatrix}$
    答案
    路径:(0,0)→(1,0)→(1,1)
    成本:1+3+2=6(避开$\infty$)

  3. 大型网格验证

    grid = [
        [2, 1, 4, 9],
        [3, 5, 1, 2],
        [7, 2, 6, 1],
        [1, 4, 3, 5]
    ]
    

    答案:最小成本=13
    路径:(0,0)→(0,1)→(1,2)→(2,3)→(3,3)

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐