#4727. DP与背包

DP与背包

当前没有测试数据。

动态规划csp原题(2024) 动态规划csp原题(2023-1) 动态规划csp原题(2023-2) 动态规划csp原题(2022)

一、动态规划基础概念

1. 什么是动态规划

动态规划(Dynamic Programming,简称 DP)是一种通过把原问题分解为相对简单的子问题,利用子问题的解推导原问题解的算法思想。核心是记住已经算过的结果,避免重复计算,从而提升效率,是 CSP-J 入门组复赛的核心高频考点。

2. DP 问题三要素

  • 状态定义:用 dp[i]dp[i][j] 表示某种含义,明确数组每个位置对应的实际意义。
  • 状态转移方程:从已知子问题的结果,推导出当前问题结果的计算公式
  • 初始化与边界:最基础子问题的答案,是递推的起点。

3. 线性 DP 特点

状态转移呈线性顺序推导,通常按数组下标从小到大依次计算,是 DP 中最基础、最常考的类型,背包问题本质上也是“选/不选”决策的线性 DP。


二、经典线性 DP 模型

模型 1:数字三角形

问题描述

给定一个 n 行的数字三角形,从顶部出发,每次可以走到下一行相邻的左或右节点,求从顶部到底部的最大路径和。

状态定义

dp[i][j]:从顶部走到第 i 行第 j 列时,能获得的最大路径和。

状态转移方程

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

含义:当前位置只能从左上方正上方走来,取两者中的较大值,加上当前位置的数字。

边界初始化

dp[1][1] = a[1][1](起点只有一个数字)

模板代码

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1005;
int a[N][N], dp[N][N];

int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i; j++)
            cin >> a[i][j];
    
    dp[1][1] = a[1][1];
    for (int i = 2; i <= n; i++) {
        for (int j = 1; j <= i; j++) {
            dp[i][j] = max(dp[i-1][j-1], dp[i-1][j]) + a[i][j];
        }
    }
    
    int ans = 0;
    for (int j = 1; j <= n; j++)
        ans = max(ans, dp[n][j]);
    cout << ans << endl;
    return 0;
}

📌 真题演练


模型 2:最长上升子序列(LIS)

问题描述

给定一个长度为 n 的序列,求其中最长的严格上升子序列的长度(子序列元素不要求连续)。

状态定义

dp[i]:以第 i 个元素结尾的最长上升子序列的长度。

状态转移方程

dp[i] = 1  (初始值,只选自己)
dp[i] = max(dp[i], dp[j] + 1)  当 j < i 且 a[j] < a[i]

含义:枚举 i 前面所有比 a[i]小的元素 j,尝试接在 j 后面形成更长的子序列。

模板代码(O(n²) 入门版)

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1005;
int a[N], dp[N];

int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        dp[i] = 1;
        for (int j = 1; j < i; j++) {
            if (a[j] < a[i]) {
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
        ans = max(ans, dp[i]);
    }
    cout << ans << endl;
    return 0;
}

📌 真题演练


模型 3:最长公共子序列(LCS)

问题描述

给定两个字符串 A 和 B,求它们最长的公共子序列的长度。

状态定义

dp[i][j]:字符串 A 前 i 个字符、字符串 B 前 j 个字符的最长公共子序列长度。

状态转移方程

若 A[i] == B[j]:dp[i][j] = dp[i-1][j-1] + 1
若 A[i] != B[j]:dp[i][j] = max(dp[i-1][j], dp[i][j-1])

含义:字符相等时,公共长度加 1;不等时,取去掉 A 末尾或去掉 B 末尾中的较大值。

模板代码

  • 字符数组存字符串,设置让实际下标从 1 开始
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;

const int N = 1005;
char a[N], b[N];
int dp[N][N];

int main() {
    cin >> a + 1 >> b + 1;
    int n = strlen(a + 1);
    int m = strlen(b + 1);
    
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            if (a[i] == b[j]) {
                dp[i][j] = dp[i-1][j-1] + 1;
            } else {
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
    }
    cout << dp[n][m] << endl;
    return 0;
}
  • string 存字符串
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;

const int N = 1005;
int dp[N][N];

int main() {
    string a, b;
    cin >> a >> b;
    int n = a.size();
    int m = b.size();
    
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            // string 原生0基,第i个字符对应索引 i-1
            if (a[i-1] == b[j-1]) {
                dp[i][j] = dp[i-1][j-1] + 1;
            } else {
                dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
            }
        }
    }
    cout << dp[n][m] << endl;
    return 0;
}

📌 真题演练


模型 4:编辑距离(进阶线性 DP)

问题描述

给定两个字符串,每次可以选择删除、插入、替换一个字符,求将第一个字符串转为第二个字符串的最少操作次数。

状态定义

dp[i][j]:将第一个字符串前 i 个字符转为第二个字符串前 j 个字符的最少操作数。

状态转移方程

若 str1[i-1] == str2[j-1]:dp[i][j] = dp[i-1][j-1]
否则:dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
在(删掉str1的第i个字符,在第i个后面增加1个字符和str2的第j个字符相等,替换第i个和第j个)

📌 真题演练


三、背包问题(线性 DP 重要分支)

背包问题是入门组 DP 核心考点,本质是“选与不选”的线性决策。

模型 1:01 背包(每件物品只能选 1 次)

问题描述

有 n 件物品,每件物品有重量 w[i] 和价值 v[i],背包容量为 m,求最多能装下的最大总价值。

二维基础解法

状态定义dp[i][j] 前 i 件物品,背包容量为 j 时的最大价值。 转移方程

dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i]] + v[i])

含义:两种选择——不选第 i 件(继承前 i-1 件结果);选第 i 件(占用容量,加上价值)。

一维滚动数组优化(重点掌握)

核心技巧逆序遍历容量,保证每件物品只被选一次。

模板代码

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1005;
int w[N], v[N], dp[N];

int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> w[i] >> v[i];
    
    for (int i = 1; i <= n; i++) {
        // 逆序遍历容量,01背包核心
        for (int j = m; j >= w[i]; j--) {
            dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        }
    }
    cout << dp[m] << endl;
    return 0;
}

📌 真题演练


模型 2:完全背包(每件物品可以选无限次)

问题描述

物品数量无限,其余条件同 01 背包。

核心区别

将 01 背包的逆序遍历容量改为顺序遍历容量,即可实现物品重复选取。

模板代码

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1005;
int w[N], v[N], dp[N];

int main() {
    int n, m;
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> w[i] >> v[i];
    
    for (int i = 1; i <= n; i++) {
        // 顺序遍历容量,完全背包核心
        for (int j = w[i]; j <= m; j++) {
            dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
        }
    }
    cout << dp[m] << endl;
    return 0;
}

📌 真题演练


模型 3:多重背包(每件物品有固定数量)

问题描述

第 i 件物品最多有 c[i] 件,其余条件同 01 背包。

进阶解法:二进制优化

将数量 c 拆分为若干个 2 的幂次,转化为 01 背包求解。 多重背包二进制优化

📌 真题演练


四、DP 解题通用步骤

  1. 读题抓核心:明确问题是求最大值、最小值还是方案数。
  2. 定义状态:用数组下标对应题目中的变量,清晰写出 dp 数组的含义。
  3. 推导转移:思考“最后一步怎么来的”,找到子问题到原问题的关系。
  4. 处理边界:确定初始值和数组下标范围,防止越界。
  5. 确定遍历顺序:线性 DP 通常从小到大;背包注意 01 逆序、完全顺序。
  6. 输出答案:根据状态定义,找到最终结果对应的 dp 数组位置。