对动态规划(dp) 的一些思考与背包问题浅析(背包九讲笔记

    • DP入门探讨
      • 动态规划是什么
      • 适用情况
        • 最优子结构
        • 无后效性
        • 重叠子问题
      • 求解问题
    • 背包问题
      • 0-1背包
        • 问题:
        • 解题思路:
      • 完全背包
        • 问题:
        • 解题思路:
      • 多重背包
        • 问题:
        • 解题思路:
      • 例题
        • 几个比较有趣的dp题
        • 背包题

DP入门探讨

动态规划是什么

动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划常常适用于有重叠子问题[1]和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

​ ———-摘自wiki

适用情况

  • 最优子结构性质。
  • 无后效性。
  • 子问题重叠性质。

最优子结构

如果一个问题的最优解包含其子问题的最优解,我们就称此问题具有最优子结构

如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。

也就是说,当我们求解一个问题时候, 可以先求解一个子问题的结果,将最终的结果递推出来,自底向上的求解。

无后效性

即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。

重叠子问题

子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。

求解问题

  • 要求的状态是什么啊,怎么确定状态?
  • 状态转移方程怎么列出来的? 相似的子结构?
  • 状态自底向上,开始的条件???
  • 递推结束的条件,结果?

背包问题

扯概念根本听不懂啊啊啊,

0-1背包

问题:

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[i1,v],F[i1,vCi]+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[i1,v] F [ i , v ] = F [ i − 1 , v ] ; 若选择放, vCi v − C i 确保有足够的空间, F[i,v]=F[i1,vCi]+Wi F [ i , v ] = F [ i − 1 , v − C i ] + W i 。

空间降一维

咋降啊? 我们知道第 i i 个物品的更新,只依赖于第 i1 i − 1 个的解最优子结构, 所以我们可以直接滚动数组,每次状态只存 i i i1 i − 1 时候的值

i1 i − 1 个物体在容积为 j j 状态的更新,只依赖与 i1 i − 1 物体容量里 jw[i] j − w [ i ] 里面的状态的结果

所以我们要从背包的容量后向前开始更新,在求 j j 位置的时候, jw[i] j − w [ i ] 的值依旧为 i1 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 

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[i1][vkc[i]]+kw[i]|0<=kc[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[ikw[i]]+kv[i]|0<=kc[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][jw[i]] d p [ i ] [ j − w [ i ] ] 的计算中选择 k1 k − 1 的情况是完全相同的 ,所以在 dp[i][j] d p [ i ] [ j ] 的的递推中 k>=1 k >= 1 的部分计算已经在 dp[i][jw[i]] d p [ i ] [ j − w [ i ] ] 的计算中完成了

考虑对转移方程进行变形

max(dp[i1][jkw[i]]+kv[i]|0<=k) m a x ( d p [ i − 1 ] [ j − k ∗ w [ i ] ] + k ∗ v [ i ] | 0 <= k )

=max(dp[i1][j],max(dp[i1][jkw[i]]+kv[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[i1][j],max(dp[i1][(jw[i])kw[i]]+kv[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[i1][j],d[i][jw[i]]+v[i]) = m a x ( d p [ i − 1 ] [ j ] , d [ i ] [ j − w [ i ] ] + v [ i ] )

对比01背包的状态转移方程

dp[i][j]=max(dp[i1][j],dp[i1][jw[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[i1][vkc[i]]+kw[i]|0<=kc[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+...+(xsum) x = 1 + 2 + 4 + 8 + . . . + ( x − s u m )

2m 2 m 这个数字恰好大于 x x , 那么上面的式子就是加到第(m-2)个数

sum=1+2+4+8+...+2m2 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;
}

例题

几个比较有趣的dp题

背包题

——未完待续

你可能感兴趣的:(浅谈系列)