DP入门探讨
动态规划是什么
动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题[1]和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
———-摘自wiki
适用情况
- 最优子结构性质。
- 无后效性。
- 子问题重叠性质。
最优子结构
如果一个问题的最优解包含其子问题的最优解,我们就称此问题具有最优子结构。
如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
也就是说,当我们求解一个问题时候, 可以先求解一个子问题的结果,将最终的结果递推出来,自底向上的求解。
无后效性
即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
重叠子问题
子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
求解问题
- 要求的状态是什么啊,怎么确定状态?
- 状态转移方程怎么列出来的? 相似的子结构?
- 状态自底向上,开始的条件???
- 递推结束的条件,结果?
背包问题
扯概念根本听不懂啊啊啊,
0-1背包
问题:
有 N N 件物品和一个容量为的背包。第 i i 件物品的体积是,其价值是 Wi W i 。求解,在不超过背包容量情况下,将哪些物品装入背包可使价值总和最大。
解题思路:
状态 F[i,v] F [ i , v ] 表示前i件物品中选择若干件放在容量为 v v 的背包中,可以取得的最大价值。
转移方程
对于第 i i 件物品,有放与不放两种选择。若选择不放,若选择放, v−Ci v − C i 确保有足够的空间, F[i,v]=F[i−1,v−Ci]+Wi。 F [ i , v ] = F [ i − 1 , v − C i ] + W i 。
空间降一维?
咋降啊? 我们知道第
i
i
个物品的更新,只依赖于第个的解最优子结构, 所以我们可以直接滚动数组,每次状态只存
i
i
与 时候的值
第 i−1 i − 1 个物体在容积为 j j 状态的更新,只依赖与物体容量里 j−w[i] j − w [ i ] 里面的状态的结果
所以我们要从背包的容量后向前开始更新,在求 j j 位置的时候,的值依旧为 i−1 i − 1 时候的所含值,这个也感觉就是最优子结构。 \
对 dp d p 数组进行初始化
- 恰好装满背包的最优解: dp[0]=0 d p [ 0 ] = 0 其余为 −INF − I N F ,可以保证最终得到的 dp[v] d p [ v ] 时是装满背包的最优解,背包问题就是一个选择 or o r 不选择的问题,当要求背包要被恰好装满的时候,只有容量为0的背包在什么也不装的情况下价值为0,代表着被装满,其余情况,并没有合法的解,状态不确定,不能赋值其价值为0。
- 只求最优解:全部赋值为0,任何容量的背包的价值都有一个合法的解,什么都不装,价值为0。
C++代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2e5+10;
int w[MAXN], v[MAXN];
int dp[MAXN];
int main(int argc, char const *argv[])
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
for(int i = 1; i <= n; i++) {
for(int j = m; j >= w[i]; j--) {
dp[j] = max(dp[j], dp[j-w[i]]+v[i]);
}
}
cout << dp[n] << endl;
return 0;
}
完全背包
问题:
有 N N 件物品和一个容量为的背包。每个物品的个数可以选取无限个,第 i i 件物品的体积是,其价值是 Wi W i 。求解,在不超过背包容量情况下,将哪些物品装入背包可使价值总和最大。
解题思路:
状态
F[i,v]
F
[
i
,
v
]
表示前i件物品中选择若干件放在容量为
v
v
的背包中,可以取得的最大价值。 和01背包一样啊
状态转移方程
dp[i][v]=max(dp[i−k∗w[i]]+k∗v[i]|0<=k∗c[i]<=v) d p [ i ] [ v ] = m a x ( d p [ i − k ∗ w [ i ] ] + k ∗ v [ i ] | 0 <= k ∗ c [ i ] <= v )
我们来对这个方程进行变形优化,就会发现 结果与 k k 其实关系并不大
我们知道 在的计算中 选择 k k 个的情况,与在的计算中选择 k−1 k − 1 的情况是完全相同的 ,所以在 dp[i][j] d p [ i ] [ j ] 的的递推中 k>=1 k >= 1 的部分计算已经在 dp[i][j−w[i]] d p [ i ] [ j − w [ i ] ] 的计算中完成了
考虑对转移方程进行变形
max(dp[i−1][j−k∗w[i]]+k∗v[i]|0<=k) m a x ( d p [ i − 1 ] [ j − k ∗ w [ i ] ] + k ∗ v [ i ] | 0 <= k )
=max(dp[i−1][j],max(dp[i−1][j−k∗w[i]]+k∗v[i])|1<=k) = m a x ( d p [ i − 1 ] [ j ] , m a x ( d p [ i − 1 ] [ j − k ∗ w [ i ] ] + k ∗ v [ i ] ) | 1 <= k )
=max(dp[i−1][j],max(dp[i−1][(j−w[i])−k∗w[i]]+k∗v[i]|0<=k)+v[i]) = m a x ( d p [ i − 1 ] [ j ] , m a x ( d p [ i − 1 ] [ ( j − w [ i ] ) − k ∗ w [ i ] ] + k ∗ v [ i ] | 0 <= k ) + v [ i ] )
=max(dp[i−1][j],d[i][j−w[i]]+v[i]) = m a x ( d p [ i − 1 ] [ j ] , d [ i ] [ j − w [ i ] ] + v [ i ] )
对比01背包的状态转移方程
dp[i][j]=max(dp[i−1][j],dp[i−1][j−w[i]]+v[i]) d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w [ i ] ] + v [ i ] )
观察一下考虑进行滚动数组,只要把01背包的内循环进行逆序就可以了
为什么逆序? 在01背包中,求解的子状态都是下一个状态,但是在完全背包当中 max m a x 函数里面有当前状态的值,必须顺序的求解。
当我们把 i i 从到 N N 循环时,表示容量为 v v 在前i种背包时所得的价值,这里我们要添加的不是前一个背包,而是当前背包。所以我们要考虑的当然是当前状态。
C++代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2e5+10;
int w[MAXN], v[MAXN];
int dp[MAXN];
int main(int argc, char const *argv[])
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++) {
cin >> w[i] >> v[i];
}
for(int i = 1; i <= n; i++) {
for(int j = w[i]; j <= m; j++) {
dp[j] = max(dp[j], dp[j-w[i]]+v[i]);
}
}
cout << dp[n] << endl;
return 0;
}
多重背包
问题:
有件物品和一个容量为 V V 的背包。每个物品的个数可以选取个,第 i i 件物品的体积是,其价值是 Wi W i 。求解,在不超过背包容量情况下,将哪些物品装入背包可使价值总和最大。
解题思路:
状态
F[i,v]
F
[
i
,
v
]
表示前i件物品中选择若干件放在容量为
v
v
的背包中,可以取得的最大价值。 和完全背包一样啊
状态转移方程
这样的话 如果不优化的话,就是跑 Ci C i 次0-1背包了,复杂度较高
考虑优化方式
- 进制拆分
- 单调队列
二进制拆分的原理很简单
考虑对每个物品的个数 Ci C i 进行拆分,拆出来的数 的和 可以表示 [1,Ci] [ 1 , C i ] 区间内的每个数
例如 17 : 考虑拆分 1, 2, 4, 8, 2 这样就可以表示出来了
拆分原理(怎么拆分) :
x=1+2+4+8+...+(x−sum) x = 1 + 2 + 4 + 8 + . . . + ( x − s u m )
令 2m 2 m 这个数字恰好大于 x x , 那么上面的式子就是加到第(m-2)个数
c++代码
#pragma comment(linker, "/STACK:1024000000,1024000000")
#include <iostream>
#include <algorithm>
using namespace std;
int n, w;
int wi, pi, ci;
int c[MAXN], v[MAXN];
int dp[MAXN];
int main(int argc, char const *argv[])
{
cin >> n >> w;
int cnt = 1;
for(int i = 0; i < n; i++) {
cin >> wi >> pi >> ci;
int k = 1;
while(ci) {
if(ci >= k) {
c[cnt] = k*wi;
v[cnt++] = k*pi;
ci -= k;
k *= 2;
} else {
c[cnt] = ci*wi;
v[cnt++] = ci*pi;
ci = 0;
}
}
}
for(int i = 0; i < cnt; i++) {
for(int j = w; j >= c[i]; j--) {
dp[j] = max(dp[j], dp[j-c[i]]+v[i]);
}
}
cout << dp[w] << endl;
return 0;
}
例题
几个比较有趣的dp题
背包题
——未完待续