动态规划

· · 算法·理论

动态规划(简称“DP”)是 1957 年 理查德·贝尔曼(Richard Bellman)在 Dynamic Programming 一书中提出的一种表格处理方法。它将原问题分解为若干子问题,自底向上先求解最小的子问题,并把结果存储在表格中,在求解大的子问题时直接从表格中查询小的子问题的解,以避免重复计算,从而提高效率。

具备的要素

动态规划算法常用来求解最优化问题,尤其是带有多步决策的最优化问题。

能用动态规划解决的问题具备以下 3 个要素:

解题步骤

解DP题目的一般模式如下:

例子实操

01背包问题

题目地址--P1048 [NOIP 2005 普及组] 采药

题目摘自洛谷 P1048。

1.题目大意

山上有 M 株草药,辰辰有 T 的时间去采药。

每株草药有 Wi 的价值,但采某株草药就会花费 Vi 的时间。

求在规定时间内,最多可以采到的草药最大价值。

2.状态表示(初步)

我们先将题目中的值进行表示。

题目中提到两个重要的值:即 VW 。我们该如何对其进行表示?首先我们先定义一个二维数组来存储状态(即上文中的“表格”)。

设数组 dp[i][j] 为,考虑前 i 株草药,时间不超过 j 时能获得的最大价值。

由此,显而易见地,最终答案为 dp[M][T] ,也就是“考虑前 M 株草药,时间不超过 T ,能获得的最大价值”。

3.决策与动态转移方程

对于数组 dp ,我们不难发现有两种选择方式:

其中,“不选第 i 个物品”时,用 dp 数组的第一维表示为 dp[i-1][j] 。也就是根据上一个状态(未选择第 i 个物品)决定,这样的情况便能形成最优解。

先对 dp 数组进行初始化。根据上文定义 dp[i][j] 为,考虑前 i 株草药,时间不超过 j 时能获得的最大价值),我们就可以把 dp[0][j]dp[i][0] 初始为0,接着进行状态转移。

已知“不选第 i 个物品”的状态转移方程为dp[i][j]=dp[i-1][j],不妨推出“选第 i 个物品”的转移方程:

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

直接枚举 ij 即可。

4.参考代码

代码仅供参考,AC记录

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

int main() {
    int T, M;
    cin >> T >> M;

    int v[105], w[105];
    for (int i = 1; i <= M; ++i) {
        cin >> v[i] >> w[i];
    }

    int dp[105][1005] = {0};

    for (int i = 1; i <= M; ++i) {
        for (int j = 0; j <= T; ++j) {
            if (j >= v[i]) {
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]);
            } else {
                dp[i][j] = dp[i-1][j];
            }
        }
    }

    cout << dp[M][T] << endl;
    return 0;
}

5.考虑优化

考虑将二维的 dp 数组优化成一维。

由于 dp[i][j] 只依赖于 dp[i-1][...] ,我们不妨把第一维优化掉。

j的遍历顺序要是反向,以避免内容的覆盖。

6.优化后代码

优化后AC记录

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

int main() {
    int T, M;
    cin >> T >> M;

    int v[105], w[105];
    for (int i = 1; i <= M; ++i) {
        cin >> v[i] >> w[i];
    }

    int dp[1005] = {0};

    for (int i = 1; i <= M; ++i) {
        for (int j = T; j >= v[i]; --j) { //逆序遍历避免覆盖
            dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
        }
    }

    cout << dp[T] << endl;
    return 0;
}

最长上升子序列

题目地址--B3637 最长上升子序列

最长上升子序列(LIS)适用一维的动态规划来解决。

“最长上升子序列”是对于一个给定的长度为 n 的序列,求其单调递增的最长自序列的长度。

最长上升子序列是指,从原序列中按顺序取出一些数字排在一起,这些数字是逐渐增大的。

注:“子序列”可以是非连续的,但是顺序要与原序相同

1.状态设计

状态:dp[i] 表示以 a[i] 结尾的最长上升子序列的长度。

2.状态转移

dp[i] = \max_{\substack{1 \leq j < i \\ a[j] < a[i]}} (dp[j]) + 1

条件:a[j] < a[i](保证上升)。

操作:在所有满足条件的 j 中,找到最大的 dp[j],然后加 1(因为 a[i] 可以接在 a[j] 后面)。

边界:如果没有任何 j 满足 a[j] < a[i],则 dp[i] = 1a[i] 自身构成一个长度为 1 的上升子序列)。

3.参考核心代码

这里只展示核心代码。

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

4.说明

说明:时间复杂度为 O(n^2) ,遇到大的数据可能是会爆掉的。

你可以试着用 贪心、二分查找 等办法进行优化。由于本篇讲述的是 动态规划,所以不展示优化的代码。

或许你可以优化到 O(n log n) 的级别。

推荐题目

不保证按照难度排序。

P1048 [NOIP 2005 普及组] 采药

B3637 最长上升子序列

P1216 [IOI 1994] 数字三角形 Number Triangles

P1002 [NOIP 2002 普及组] 过河卒

P1020 [NOIP 1999 提高组] 导弹拦截

P1091 [NOIP 2004 提高组] 合唱队形

P1880 [NOI1995] 石子合并

P1063 [NOIP 2006 提高组] 能量项链

P1060 [NOIP 2006 普及组] 开心的金明

P1064 [NOIP 2006 提高组] 金明的预算方案

P7074 [CSP-J2020] 方格取数

P8816 [CSP-J 2022] 上升点列

推荐学习资料

oi-wiki--《动态规划基础》

oi-wiki--《背包DP》

oi-wiki--《树形DP》

本篇字数:约 5000 字

完成时间:2025/8/16 10:07