P1216 数字三角形

文章目录

题目信息

字段 内容
题号 P1216
难度 橙题(普及-)
知识点 动态规划、记忆化搜索

题目描述

观察如下所示的数字三角形:

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5

从顶部(第一行)出发,在每一层可以选择向左下向右下走(即 (i, j) 只能走到 (i+1, j)(i+1, j+1))。

找到一条路径,使路径经过的格子中的数字之和最大,输出这个最大值。

输入格式:

第一行一个整数 n,表示数字三角形的行数(1 ≤ n ≤ 1000)。

接下来 n 行,第 i 行有 i 个整数,表示每行的数字(各数字不超过 10000)。

输出格式:

一个整数,表示从顶部到底部的最大路径和。

样例:

输入

5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5

输出

30

样例解释:

最大路径为 7 → 3 → 8 → 7 → 5,和为 7 + 3 + 8 + 7 + 5 = 30

解题思路

核心思想:动态规划

这道题是**动态规划(Dynamic Programming)**的经典入门题。

动态规划的核心思想:把一个大问题拆成若干小问题,先解决小问题,再用小问题的解推出大问题的解。

方法一:记忆化递归(自顶向下)

从顶部开始,递归地计算每个位置能得到的最大和。递归过程中用数组缓存结果,避免重复计算。

dp[i][j] 表示从位置 (i, j) 出发到最底层,能够得到的最大路径和。

递归方程:

dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + triangle[i][j]

终止条件:当 i == n(到达最后一行)时,dp[i][j] = triangle[i][j]

方法二:自底向上递推(更常见)

从最后一行开始,依次向上计算。每一行的每个位置,只需要知道它下方两个位置的最大值即可。

// 从倒数第二行开始,依次向上递推
for (int i = n - 1; i >= 1; i--) {
    for (int j = 1; j <= i; j++) {
        triangle[i][j] += max(triangle[i+1][j], triangle[i+1][j+1]);
    }
}

最后 triangle[1][1] 就是答案。

为什么从下往上递推可行?

因为最后一行没有更深的路径,所以 dp[n][j] = triangle[n][j] 是确定的。

有了最后一行,倒数第二行的每个位置只需要比较它下方两个相邻位置的最大值,再加上自己的值,就得到了从这个位置出发的最大和。

这个过程不断向上重复,直到顶部。

完整代码

C++(记忆化递归)

#include <bits/stdc++.h>
using namespace std;

const int MAX_N = 1001;
int triangle[MAX_N][MAX_N];
int memo[MAX_N][MAX_N];
int n;

int MaxSumFrom(int row, int col) {
    if (memo[row][col] != -1) {
        return memo[row][col];
    }
    if (row == n) {
        memo[row][col] = triangle[row][col];
    } else {
        int left = MaxSumFrom(row + 1, col);
        int right = MaxSumFrom(row + 1, col + 1);
        memo[row][col] = max(left, right) + triangle[row][col];
    }
    return memo[row][col];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= i; j++) {
            cin >> triangle[i][j];
            memo[i][j] = -1;
        }
    }

    cout << MaxSumFrom(1, 1) << "\n";
    return 0;
}

C++(自底向上递推)

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    const int MAX_N = 1001;
    int triangle[MAX_N][MAX_N] = {0};

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= i; j++) {
            cin >> triangle[i][j];
        }
    }

    // 自底向上递推
    for (int i = n - 1; i >= 1; i--) {
        for (int j = 1; j <= i; j++) {
            triangle[i][j] += max(triangle[i + 1][j], triangle[i + 1][j + 1]);
        }
    }

    cout << triangle[1][1] << "\n";
    return 0;
}

关键代码讲解

记忆化递归的核心

int MaxSumFrom(int row, int col) {
    if (memo[row][col] != -1) {
        return memo[row][col];  // 已计算过,直接返回
    }
    // ... 计算并缓存结果
    memo[row][col] = max(left, right) + triangle[row][col];
    return memo[row][col];
}

memo 数组用于记录每个位置已经计算过的最大路径和。当递归走到已经计算过的位置时,直接返回缓存结果,避免了大量重复计算。

自底向上递推的核心

for (int i = n - 1; i >= 1; i--) {
    for (int j = 1; j <= i; j++) {
        triangle[i][j] += max(triangle[i + 1][j], triangle[i + 1][j + 1]);
    }
}

从倒数第二行开始,每个位置的值变成「自身 + 下方两个相邻位置中的较大值」。这样一层层向上,最终顶部就变成了整个三角形的最大路径和。

复杂度分析

方法 时间复杂度 空间复杂度
记忆化递归 O(n²) O(n²)
自底向上递推 O(n²) O(n²)(可优化到 O(n))

两种方法的时间复杂度相同。自底向上递推的空间可以优化到 O(n),只需要保存最后一行,然后逐行向上更新。

易错点

  1. 数组下标:本题采用 1-indexed(下标从 1 开始),递归时注意 (i, j) 的相邻位置是 (i+1, j)(i+1, j+1),不是 (i+1, j-1)

  2. 边界处理:使用自底向上递推时,循环到 j <= i 即可,因为第 i 行恰好有 i 个元素。

  3. 初始化:使用记忆化递归时,memo 数组必须初始化为一个不可能的值(如 -1),用于标记「未计算过」。

其他语言解法

Python(自底向上递推)

import sys

def main():
    data = sys.stdin.read().strip().split()
    if not data:
        return
    it = iter(data)
    n = int(next(it))
    triangle = [[0] * (n + 2) for _ in range(n + 2)]

    for i in range(1, n + 1):
        for j in range(1, i + 1):
            triangle[i][j] = int(next(it))

    for i in range(n - 1, 0, -1):
        for j in range(1, i + 1):
            triangle[i][j] += max(triangle[i + 1][j], triangle[i + 1][j + 1])

    print(triangle[1][1])

if __name__ == "__main__":
    main()

Java(自底向上递推)

import java.io.BufferedInputStream;
import java.util.Arrays;
import java.util.InputMismatchException;
import java.util.NoSuchElementException;

public class Main {
    private static final class FastScanner {
        private final BufferedInputStream in;
        private final byte[] buffer = new byte[1 << 16];
        private int ptr = 0;
        private int len = 0;

        FastScanner() {
            in = new BufferedInputStream(System.in);
        }

        private int readByte() throws java.io.IOException {
            if (ptr >= len) {
                len = in.read(buffer);
                ptr = 0;
                if (len <= 0) {
                    return -1;
                }
            }
            return buffer[ptr++];
        }

        int nextInt() throws java.io.IOException {
            int c, sign = 1, res = 0;
            do {
                c = readByte();
            } while (c <= ' ' && c != -1);
            if (c == '-') {
                sign = -1;
                c = readByte();
            }
            while (c > ' ') {
                res = res * 10 + (c - '0');
                c = readByte();
            }
            return res * sign;
        }
    }

    public static void main(String[] args) throws Exception {
        FastScanner fs = new FastScanner();
        int n = fs.nextInt();
        int[][] triangle = new int[n + 2][n + 2];

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                triangle[i][j] = fs.nextInt();
            }
        }

        for (int i = n - 1; i >= 1; i--) {
            for (int j = 1; j <= i; j++) {
                triangle[i][j] += Math.max(triangle[i + 1][j], triangle[i + 1][j + 1]);
            }
        }

        System.out.println(triangle[1][1]);
    }
}

Go(自底向上递推)

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
)

func main() {
    scanner := bufio.NewScanner(os.Stdin)
    scanner.Split(bufio.ScanWords)

    if !scanner.Scan() {
        return
    }
    n, _ := strconv.Atoi(scanner.Text())

    const maxN = 1001
    triangle := make([][]int, maxN)
    for i := 0; i < maxN; i++ {
        triangle[i] = make([]int, maxN)
    }

    for i := 1; i <= n; i++ {
        for j := 1; j <= i; j++ {
            if scanner.Scan() {
                val, _ := strconv.Atoi(scanner.Text())
                triangle[i][j] = val
            }
        }
    }

    for i := n - 1; i >= 1; i-- {
        for j := 1; j <= i; j++ {
            triangle[i][j] += max(triangle[i+1][j], triangle[i+1][j+1])
        }
    }

    fmt.Println(triangle[1][1])
}

func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}

JavaScript(自底向上递推)

'use strict';

const fs = require('fs');
const data = fs.readFileSync('/dev/stdin', 'utf8');
const tokens = data.trim().split(/\s+/).map(Number);
let idx = 0;

const n = tokens[idx++];
const triangle = Array.from({ length: n + 2 }, () =>
    Array(n + 2).fill(0)
);

for (let i = 1; i <= n; i++) {
    for (let j = 1; j <= i; j++) {
        triangle[i][j] = tokens[idx++];
    }
}

for (let i = n - 1; i >= 1; i--) {
    for (let j = 1; j <= i; j++) {
        triangle[i][j] += Math.max(triangle[i + 1][j], triangle[i + 1][j + 1]);
    }
}

console.log(triangle[1][1]);