动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。
动态规划常常适用于有重叠子问题[1]和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。
———-摘自wiki
如果一个问题的最优解包含其子问题的最优解,我们就称此问题具有最优子结构。
如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
也就是说,当我们求解一个问题时候, 可以先求解一个子问题的结果,将最终的结果递推出来,自底向上的求解。
即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。
扯概念根本听不懂啊啊啊,
有 N N 件物品和一个容量为 V V 的背包。第 i i 件物品的体积是 Ci C i ,其价值是 Wi W i 。求解,在不超过背包容量情况下,将哪些物品装入背包可使价值总和最大。
状态 F[i,v] F [ i , v ] 表示前i件物品中选择若干件放在容量为 v v 的背包中,可以取得的最大价值。
转移方程
F[i,v]=max(F[i−1,v],F[i−1,v−Ci]+Wi) F [ i , v ] = m a x ( F [ i − 1 , v ] , F [ i − 1 , v − C i ] + W i )
对于第 i i 件物品,有放与不放两种选择。若选择不放, F[i,v]=F[i−1,v]; F [ i , v ] = F [ i − 1 , v ] ; 若选择放, 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−1 i − 1 个的解最优子结构, 所以我们可以直接滚动数组,每次状态只存 i i 与 i−1 i − 1 时候的值
第 i−1 i − 1 个物体在容积为 j j 状态的更新,只依赖与 i−1 i − 1 物体容量里 j−w[i] j − w [ i ] 里面的状态的结果
所以我们要从背包的容量后向前开始更新,在求 j j 位置的时候, j−w[i] j − w [ i ] 的值依旧为 i−1 i − 1 时候的所含值,这个也感觉就是最优子结构。 \
对 dp d p 数组进行初始化
C++代码
#include
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 件物品和一个容量为 V V 的背包。每个物品的个数可以选取无限个,第 i i 件物品的体积是 Ci C i ,其价值是 Wi W i 。求解,在不超过背包容量情况下,将哪些物品装入背包可使价值总和最大。
状态 F[i,v] F [ i , v ] 表示前i件物品中选择若干件放在容量为 v v 的背包中,可以取得的最大价值。 和01背包一样啊
状态转移方程
dp[i][v]=max(dp[i−1][v−k∗c[i]]+k∗w[i]|0<=k∗c[i]<=v) d p [ i ] [ v ] = m a x ( d p [ i − 1 ] [ v − k ∗ c [ i ] ] + k ∗ w [ i ] | 0 <= k ∗ c [ i ] <= v )
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 其实关系并不大
我们知道 在 dp[i][j] d p [ i ] [ j ] 的计算中 选择 k k 个的情况,与在 dp[i][j−w[i]] d p [ i ] [ j − w [ i ] ] 的计算中选择 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 从 1 1 到 N N 循环时, dp[v] d p [ v ] 表示容量为 v v 在前i种背包时所得的价值,这里我们要添加的不是前一个背包,而是当前背包。所以我们要考虑的当然是当前状态。
C++代码
#include
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;
}
有 N N 件物品和一个容量为 V V 的背包。每个物品的个数可以选取 Ci C i 个,第 i i 件物品的体积是 Ci C i ,其价值是 Wi W i 。求解,在不超过背包容量情况下,将哪些物品装入背包可使价值总和最大。
状态 F[i,v] F [ i , v ] 表示前i件物品中选择若干件放在容量为 v v 的背包中,可以取得的最大价值。 和完全背包一样啊
状态转移方程
dp[i][v]=max(dp[i−1][v−k∗c[i]]+k∗w[i]|0<=k∗c[i]<=v) d p [ i ] [ v ] = m a x ( d p [ i − 1 ] [ v − k ∗ c [ i ] ] + k ∗ w [ i ] | 0 <= k ∗ c [ i ] <= 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)个数
sum=1+2+4+8+...+2m−2 s u m = 1 + 2 + 4 + 8 + . . . + 2 m − 2
c++代码
#pragma comment(linker, "/STACK:1024000000,1024000000")
#include
#include
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;
}
——未完待续