关于动态规划的一些理解
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背包每类物品只能取一次,而完全背包每类物品能取无数次。
实际上,由于“背包容量”的限制,实际能取到的数量是有限的,这也是需要决策取舍的地方。
例题:
题目大意:
在总共 \(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优化。
例题:
题目大意:
在一个圆形操场的四周摆放 \(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)\) 。
如果你看到这里想要试一试,不妨尝试这道简化版无环:
如何处理环?
我们将原数组 \(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]); // 状态转移方程
}
}
}
标签:01,max,理解,背包,物品,动态,规划,DP
From: https://www.cnblogs.com/liserver/p/18352522