动态规划 ——DP 的介绍
动态规划 (DP) 是一种算法技术,它将大问题分解为更简单的子问题,对整体问题的最优解决方案取决于子问题的最优解决方案。
有的问题有 2个特征:重叠子问题、最优子结构。用 DP可以高效率地处理具有这 2个特征的问题。
处理 DP的大问题和小问题,有两种实现方式 ——自顶向下与记忆化递归 / 自下而上与制表递推。
以斐波那契为例,两种实现方式的代码分别如下:
// 自顶向下与记忆化递归
int memoize[maxn]; //保存结果
int fib (int n){
if (n==1 || n==2)
return 1;
if(memoize[n] != 0) //直接返回保存的结果,不再递归
return memoize[n];
memoize[n]= fib(n-1) + fib(n-2); //递归计算结果,并记忆
return memoize[n];
}
// 自下而上与制表递推
const int maxn = 255;
int dp[maxn];
int fib (int n){
dp[1] = dp[2] =1;
for (int i=3; i<=n; i++)
dp[i] = dp[i-1] + dp[i-2];
return dp[n];
}
两种实现方式的复杂度相同,但是第二种更为常用,超过四维(dp[ ][ ][ ][ ])的表格也是常用的。
核心要点
01背包
01背包问题最暴力的一个解法:二维动态规划
准备工作:
设第 i个物品的体积是 v[i],价值是 w[i],f[i][j]表示只看前 i个物品,“可用(剩余)的 ”总体积是j的情况下,总价值最大是多少。
result = max{f[n][0~v]}
状态的转移:
每个 i对应的 f[i][j],可以看作一个集合的划分,也就是当前拿与不拿对应两种状态:
当前拿了的话,状态就是:dp[i-1][j-v[i]]+w[i] (+之前的是前i-1个物品对应的最大价值) ; 没拿的话,状态就是:dp[i-1][j]。
f[i][j] = max{1,2} (1和 2分别表示两种状态)
合法的初始化: f[0][0] = 0 一个物品都不选的情况下,总体积和价值都为 0
集合如何划分
一般原则:
不重不漏,不重不一定都要满足 (一般求个数时要满足)。
如何将现有的集合划分为更小的子集,使得所有子集都可以计算出来。
例题
AC代码
#include <bits/stdc++.h>
#pragma GCC optimize(3 , "Ofast" , "inline")
using namespace std;
const int N = 1010;
int n,m;
int f[N][N];
int v[N],w[N];
int main()
{
cin>>n>>m;
for(int i=1; i<=n; i++) cin>>v[i]>>w[i];
for(int i=1; i<=n; i++)
{
for(int j=0; j<=m; j++)
{
f[i][j] = f[i-1][j];
if(j>=v[i])
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
int res = 0;
for(int i=0; i<=m; i++) res = max(res,f[n][i]);
cout<<res<<endl;
return 0 ;
}
01背包的优化
每一个状态只与它的前一个状态有关,不需要把所有的状态记录下来,所以可以用一个 通用的优化方式——滚动数组 进行优化 (针对这道题,可以用一维数组去优化) 。尽量对代码进行 等价代换 。
优化过的转移方程:f[j] = max(f[j], f[j-w[i]] + v[i])
1. f[i] 仅用到了f[i-1]层,
2. j与j-v[i] 均小于j
3. 若用到上一层的状态时,从大到小枚举, 反之从小到大
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
cin >> n >> m;
for(int i=1; i<=n; i++) cin>>v[i]>>w[i];
for(int i=1; i<=n; i++)
for(int j=m; j>=v[i]; j--)
f[j] = max(f[j], f[j-v[i]]+w[i]);
cout<<f[m]<<endl;
return 0;
}