关于动态规划的一些理解

关于动态规划的一些理解

1. 什么是动态规划

动态规划(DP,Dynamic Programming)是一种解决问题的方法。它通过将难以实现的整体的大问题划分成简单的局部的小问题。最后将小问题一一求解以完成问题。

对于动态规划能否使用有一些限制,这些限制我推荐参看动态规划基础 - OI Wiki中的描述。(实际上是不是用DP看一眼题就知道了)。

动态规划只是一种解决问题的方法,它并不是类似于DFS(深度优先搜索)有特定实现的算法,具体问题具体分析。

2. 常见的动态规划类型以及理解

2.1 背包DP

背包问题是动态规划的一个分支。对于大部分人而言,背包DP更容易理解、入门动态规划。

背包问题本质是在多个种类物品之间做选择。由于[重量]等原因对于选择做了限制,通常求选择的最优解。

2.1.1 01背包

01背包是背包问题的一种。该类问题需要在多个种类的物品之间做出选择,每个种类只能选择一个,由于“选与不选”的特性,类似于二进制中的01[真假],所以称为01背包。

下面对于01背包一个例题:

P2871 [USACO07DEC] Charm Bracelet S - 洛谷

题目大意:

N 件物品和一个容量为 M 的背包。第 i 件物品的重量是 Wi,价值是 Di。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。

输入样例:

4 6
1 4
2 6
3 12
2 7

输出样例:

23

解题思路:

这是一道01背包问题的模板题。对于容量 M 的背包,装 x 个物品使其总价值 D 最大。最先想到的可能是贪心。但是由于重量的存在,使得价值最高可能不是收益最高。高价值高重量的物品可能不及多个低价值低重量物品之和。所以应该使用动态规划

假设我们有 \(f(u, v)\) 。它表示拿前 \(u\) 件物品且剩余容量是 \(v\) 时能拿到的最大值,不难看出,\(f(N, M)\) 是这道题的答案。

那么我们就将求这道题的答案转化为了求 \(f(N, M)\)。我们首先需要把 \(f(u, v)\) 表示出来。

我们只关注于第 u 件物品,它有取与不取两种可能。若是取,则剩余 \(v-W_i\) 的空间,获得了 \(D_i\) 价值的物品。若是不取,则空间没有变化,得到的价值也没有变化。

不管取了还是不取,我们都对第u件物品做出了决策。我们需要在两个决策中选择一个最好的。所以我们需要知道两个决策的价值并比较。

对于取了的情况,我们得到的价值是剩余的空间取前 u-1 个物品的最大价值加上这件物品的价值。即 \(f(u-1, v-D_i)+W_i\) .

对于没有取的情况,我们得到的价值是目前的空间前 u-1 个物品的最大价值。即 \(f(u-1, v)\) .

最后,我们对两个决策之间选择一个最大值,就是 f(u, v) 的最优解

\[f(u,v) = \max\{f(u-1, v-D_i) + W_i, f(u-1, v)\} \]

这就是这道题的状态转移方程。

由于这道题是模板题,实际上所有的01背包方程都是基于这个方程推导出来的。

核心代码:

// C++
for(int i = 1; i <= N; i++) // 循环从1开始,可以避免处理越界行为,DP默认初始化0
{
    for(int j = 1; i <= M; j++) // 列举背包空间的情况
    {
        if (j < D[i]) // 在实际代码中,会有无法取的情况,这种情况就是不取
        {
            DP[i][j] = DP[i-1][j]; // 不取
        }
        else
        {
            DP[i][j] = std::max(DP[i-1][j-D[i]] + W[i], DP[i-1][j]); // 状态转移方程
        }
    }
}

优化:

仔细观察状态转移方程。发现对于 \(f(u,v)\) 有影响的只有 \(u-1\) ,所以我们就可以将状态转移方程压缩为一维,即一个参数。

假设有状态 \(f(v)\) 表示剩余空间为 \(v\) 时的最优解,写出状态转移方程:

\[f(v)=\max\{f(v), f(v-D_i)+W_i\} \]

优化后的核心代码:

for(int i = 1; i <= N; i++)
{
    for(int j = M; j >= W[i]; j--)
    {
        DP[j] = std::max(DP[j], DP[j - D[i]] + W[i]); // 状态转移方程
    }
}

一些练习题:

[P1049 NOIP2001 普及组] 装箱问题 - 洛谷

[P1048 NOIP2005 普及组] 采药 - 洛谷

2.1.2 完全背包

完全背包是背包问题的一种。它与01背包不同,01背包每类物品只能取一次,而完全背包每类物品能取无数次。

实际上,由于“背包容量”的限制,实际能取到的数量是有限的,这也是需要决策取舍的地方。

例题:

P1616 疯狂的采药 - 洛谷

题目大意:

在总共 \(t\) 的时间内,对 \(m\) 种草药中进行无数量限制的采摘。对于每类草药,有 \(A_i\)\(B_i\) 分别表示草药的采集时间和价值。

解题思路:

这是一个完全背包问题。我们将决策从取与不取转换到取多少。我们用 \(k\) 来表示取 \(k\) 个草药。

有状态 \(f(u,v)\) 。表示在剩余背包空间为 \(v\) 时对前 \(u\) 种草药做决策后的最优解。不难发现,\(f(m, t)\) 是本题答案。

如果对第 \(u\) 种草药取了 \(k\) 个,那么获得的收益为:

\[f(u-1, v-k \times A_i)+k \times B_i \]

特别地,如果没有取,则 \(k=0\) ,所以 \(k\) 的取值范围是 \([0, +\infty)\) .

状态转移方程:

\[f(u, v)=\max_{k=0}^{\infty}\{f(u-1, v-k \times A_i)+k \times B_i\} \]

核心代码:

for(int i = 1; i <= m; i++)
{
    for(int j = 1; j <= t; j++)
    {
        for(int k = 0; k * A[i] <= j; k++) // 如果不能取就没有必要增加取的数量
        {
            DP[i][j] = std::max(DP[i][j], DP[i-1][j-k*A[i]]+k*B[i]); // 状态转移方程
        }
    }
}

这是一个 \(O(n^3)\) 的算法。是否能像01背包一样优化?

实际上,对于 \(f(u,v)\) 可以不枚举 \(k\) ,即:

\[f(u,v)=\max\{f(u-1, v), f(u-1, v-Ai) + Bi\} \]

对于核心代码,与01背包的枚举顺序是相反的:

for(int i = 1; i <= m; i++)
{
    for(int j = 0; j <= t - A[i]; j++) // 与01背包相反的枚举顺序
    {
        DP[j + A[i]] = std::max(DP[j] + B[i], DP[j + A[i]]); // 状态转移方程
    }
}

时间复杂度 \(O(n^2)\) .

2.2 区间DP

区间DP是对一个区间进行合并,求合并操作后的最优解。

区间DP最经典的例题就是石子合并。实际上这道题还可以进行四边形不等式优化,在这里不展开,参见后文DP优化。

例题:

P1880 [NOI1995] 石子合并 - 洛谷

题目大意:

在一个圆形操场的四周摆放 \(N\) 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 2 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。求得分的最大值和最小值。

样例输入:

4
4 5 9 4

样例输出:

43
54

解题思路:

这是一道区间DP模板题。非常经典。我们先不考虑环形的问题。假设他是直线的。

对于这道题,有状态转移方程 \(f(i,j)\) 表示在区间 \(i\)\(j\) 之内合并后的最大值。显然,对于这道题的无环情况,\(f(0, N-1)\) 是答案。

仍然是表示状态转移方程。对于区间 \(i\)\(j\) ,我们假设由一个下标 \(k\) 。它将区间 \(i\)\(j\)\(k\) 分隔开成两份。左右两边份分别合并成一堆,最后整体合并成一堆。那么求 \(k\) 的最优解,就是求 \(f(i, j)\) 的最优解。

我们使用枚举法:

\[f(i, j)=\max\{f(i, k) + f(k + 1, j) + cost\} \]

其中:\(k\)\(i\)\(j-1\) 进行枚举。cost是合并 \(i\)\(j\) 后获得的收益。对于此题,\(cost=\sum_{n=i}^jA_n\)\(A_n\) 表示第 \(n\) 堆小石子的数量。

然后设 \(sum\)\(A\) 的前缀和。状态转移方程转化为:

\[f(i, j)=\max\{f(i, k) + f(k + 1, j) + sum(j) - sum(i-1)\} \]

随后我们设 \(length=j-i-1\) 。那么枚举所有的 \(length\) 。随后枚举所有的 \(i\) ,求出 \(j\) 。列举所有的 \(k\) 进行状态转移。时间复杂度 \(O(n^3)\)

如果你看到这里想要试一试,不妨尝试这道简化版无环:

P1775 石子合并(弱化版) - 洛谷

如何处理环?

我们将原数组 \(A\) 扩展到原来的二倍。\(A_n = A_{2n}\) 。随后我们使用动态规划求解。最终的答案为:

\[answer=\max\{f(a + 1, a + n)\} (a\in[0, N-2]) \]

核心代码:

for(int l = 2; l <= N; l ++) // 枚举所有可能的长度
{
    for(int i = 1; i <= 2 * N + 1 - l; i++) // 枚举所有的i
    {
        int j = l + i - 1; // 通过长度与i计算j
        for(int k = i; k < j; k++) // 枚举所有可能的k
        {
            DP[i][j] = std::max(DP[i][j], DP[i][k] + DP[k + 1][j] + sum[j] - sum[i - 1]); // 状态转移方程
        }
    }
}
posted @ 2024-08-10 17:11  LiserverYang  阅读(94)  评论(0)    收藏  举报