【动态规划】动态规划基础、背包 dp 学习笔记

动态规划基础概念

动态规划(Dynamic Programming,dp)是一类用来解决最优化问题(和部分计数问题)的算法。动态规划的学习和题目从普及组到 IOI 都会出现。

动态规划可解问题的特点

如果一个问题可以通过动态规划求解,则这个问题一定(充分不必要)满足这两个特点:

最优子结构

动态规划可以解决的问题通常是求问题最优解的问题。且这种问题可被分割为多个子问题,子问题的解也是最优的。通过各个子问题的最优解可以逐步计算出全局最优解,得出答案。

无后效性

动态规划划分出的子问题有以下性质:某个子问题的结果被求解之后,其值不会受如何求解影响。后面的计算如果可以用到这个子问题的结果,则和这个子问题通过怎样的方法怎样的顺序求解无关,即“未来和过去无关”。

动态规划的基本步骤

  1. 找子问题,把问题划分为各个阶段。
  2. 根据阶段划分确定动态规划的状态。
  3. 找到初始状态。
  4. 通过阶段之间的决策找出状态转移方程。
  5. 通过状态转移方程,通过递推或者记忆化搜索写出代码、优化,求出问题的解。

这些步骤看上去比较抽象,下面通过几个例题来熟悉一下这些步骤。

动态规划的时间复杂度分析

状态复杂度 乘以 转移复杂度。

例题

洛谷 P1216 [IOI1994] 数字三角形 Number Triangles

这题是动态规划最典型的入门题,完美满足动态规划可解问题的两个性质。而且 IOI(国际信息学奥林匹克竞赛)作为 OI 最高峰,考动态规划基本题是因为那时候动态规划刚被发明,所以这题极适合入门学习。

先分析这个问题:

  • 最优子结构:显然这个问题可以划分成多个子问题,每个子问题求解出从起点出发走到某个数字的最优解,问题最终的答案是最后一行每个数字对应的子问题的解的最大值,可以通过子问题的最优解计算而来。
  • 无后效性:到达每个数字的最大值只和到达其上一层两个点得到的最大值有关,和上一层两个点的最大值是从哪转移来的无关。

因此,这个问题可以通过动态规划求解,解决步骤:

  1. 子问题:dp[i][j] 表示走到第 i 行第 j 个数时子问题的答案,将其定义为状态。
  2. 初始状态:显然 dp[1][1]=a[1][1],将其作为初始状态。
  3. 决策:每个数可以从他上面两个数走来,所以状态转移方程: dp[i][j]=max(dp[i-1][j],dp[i-1][j-1])+a[i][j]
  4. 根据状态转移方程写出代码,最终答案是 max(dp[n][1...n])

下面我们看如何写出代码:

在转移过程中,用每个已经计算出的状态计算当前的新状态,代码实现如下:

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

int n;
int a[1005][1005], dp[1005][1005];

signed main() {
    ios::sync_with_stdio(0);
    #ifdef DEBUG
    clock_t t0 = clock();
    freopen("data.in", "r", stdin);
    freopen("data.out", "w", stdout);
    #endif

    // Don't stop. Don't hide. Follow the light, and you'll find tomorrow. 

    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++) {
            if (j == 1) dp[i][j] = dp[i - 1][j] + a[i][j];
            else if (j == i) dp[i][j] = dp[i - 1][j - 1] + a[i][j];
            else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - 1]) + a[i][j];
            // 状态转移,根据状态转移方程,注意边界条件
        }
    }

    int ans = 0;
    for (int i = 1;i <= n;i++) ans = max(ans, dp[n][i]);
    cout << ans << endl;

    #ifdef DEBUG
    cerr << "Time used:" << clock() - t0 << "ms" << endl;
    #endif
    return 0;
}

背包问题

背包问题是一类经典的可以使用动态规划解决的问题。先看以下几个动态规划基本模型:

0 - 1 背包问题

例题:P1048 [NOIP2005 普及组] 采药

0 - 1 背包问题基本模型是:给定 \(n\) 个物品,每个物品有一个价值和一个体积,分别记作 \(w_i\)\(v_i\),给定一个容量为 \(m\) 的背包,把每个物品装入背包需要占用 \(w_i\) 的体积,总占用体积不能超过背包容量,获得的价值是 \(v_i\)。求如何装入物品的所有方案中,价值和的最大值。

状态表示

dp[i][j] 表示前 \(i\) 个物品,占用了 \(j\) 的空间的所有方案中,价值的最大值。
最优子结构性质:dp[i][j] 可以从 dp[i-1][j] (不选第 i 个物品) 和 dp[i-1][j-w[i]] 的最优解计算。
无后效性:把状态的第一位看作阶段,每个阶段的答案只和上一个阶段有关,不和后面的阶段有关,所以,具有无后效性。

状态计算

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

优化

时间复杂度:\(O(nm) \times O(1) = O(nm)\),空间复杂度:\(O(mn)\),空间超限。
通过去掉第一维,倒序计算,可以把空间复杂度优化到 \(O(m)\)

代码实现

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

int N, V, v[1005], w[1005];
int dp[1005];

int main()
{
    cin >> V >> N;
    for (int i = 1; i <= N; i++)
        cin >> v[i] >> w[i];
    for (int i = 1; i <= N; i++)
        for (int j = V; j >= 0; j--)
            if (j >= v[i])
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
    cout << dp[V] << endl;
    return 0;
}

习题:

  • 洛谷 P2871 [USACO07DEC] Charm Bracelet S
  • 洛谷 P1049 [NOIP2001 普及组] 装箱问题

完全背包问题

例题:P1616 疯狂的采药

状态表示

状态计算

优化

二维 0 - 1 背包

多重背包问题

例题:P1776 宝物筛选

朴素解法

二进制拆分优化

分组背包问题

混合背包问题

例题:P1833 樱花

背包模型的灵活运用

posted @ 2023-07-17 21:31  蒟蒻OIer-zaochen  阅读(39)  评论(0编辑  收藏  举报