01背包问题(动态规划)

动态规划有关的理论知识

一、最优化原理 

最优化原理 指的最优策略具有这样的性质:不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简单来说就是一个最优策略的子策略也是必须是最优的,而所有子问题的局部最优解将导致整个问题的全局最优。如果一个问题能满足最优化原理,就称其具有最优子结构性质

这是判断问题能否使用动态规划解决的先决条件,如果一个问题不能满足最优化原理,那么这个问题就不适合用动态规划来求解。

这样说可能比较模糊,来举个栗子吧:

 

如上图,求从A点到E点的最短距离,那么子问题就是求从A点到E点之间的中间点到E点的最短距离,比如这里的B点

那么这个问题里,怎么证明最优化原理呢?

我们假设从A点到E点的最短距离为d,其最优策略的子策略假设经过B点,记该策略中B点到E点的距离为d1,A点到B点的距离为d2。我们可以使用反证法,假设存在B点到E点的最短距离d3,并且d3 < d1,那么 d3 + d2 < d1 + d2 = d,这与d是最短距离相矛盾,所以,d1是B点到E点的最短距离。

为了增加理解,这里再举一个反例:

图中有四个点,A、B、C、D,相邻两点有两条连线,代表两条通道,d1,d2,d3,d4,d5,d6代表的是道路的长度,求A到D的所有通道中,总长度除以4得到的余数最小的路径为最优路径,求一条最优路径

这里如果还是按照上面的思路去求解,就会误入歧途了。按照之前的思路,A的最优取值应该可以由B的最优取值来确定,而B的最优取值为(3+5)mod 4 = 0。所以应该选d2d6这两条道路,而实际上,全局最优解是d4+d5+d6或者d1+d5+d3。所以这里子问题的最优解并不是原问题的最优解,即不满足最优化原理。所以就不适合使用动态规划来求解了。

 

二、无后效性 

无后效性指的是某状态下决策的收益,只与状态和决策相关,与到达该状态的方式无关。某个阶段的状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响。换句话说,未来与过去无关,当前状态是此前历史状态的完整总结,此前历史决策只能通过影响当前的状态来影响未来的演变。再换句话说,过去做的选择不会影响现在能做的最优选择,现在能做的最优选择只与当前的状态有关,与经过如何复杂的决策到达该状态的方式无关。

这也是用来验证问题是否可以使用动态规划来解答的重要方法。

我们再回头看看上面的最短路径问题,如果在原来的基础上加上一个限制条件:同一个格子只能通过一次。那么, 这个题就不符合无后效性了,因为前一个子问题的解会对后面子问题的选择策略有影响,比如说,如果从A到B选择了一条如下图中绿色表示的路线,那么从B点出发到达E点的路线就只有一条了。也就是说从A点到B点的路径选择会影响B点到E点的路径选择。

 

 

01背包问题

题目 :
N 件物品和一个容量为 V 的背包。第 i 件物品的费用是 c[i] ,价值是 w[i] 。求解将哪些物 品装入背包可使价值总和最大。

 

一、验证动态规划求解的可行性

先来看看最优化原理。我们使用反证法:

假设(x1,x2,…,xn)是01背包问题的最优解,则有(x2,x3,…,xn)是其子问题的最优解,假设(y2,y3,…,yn)是上述问题的子问题最优解,则有(v2y2+v3y3+…+vnyn)+v1x1 > (v2x2+v3x3+…+vnxn)+v1x1。说明(X1,Y2,Y3,…,Yn)才是该01背包问题的最优解,这与最开始的假设(X1,X2,…,Xn)是01背包问题的最优解相矛盾,故01背包问题满足最优性原理

至于无后效性,其实比较好理解。对于任意一个阶段,只要背包剩余容量和可选物品是一样的,那么我们能做出的现阶段的最优选择必定是一样的,是不受之前选择了什么物品所影响的。即满足无后效性

 

二、解题思路

这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。

用子问题定义状态:即 f[i][j] 表示前i 件物品恰放入一个容量为j 的背包可以获得的最大价值。

则其状态转移方程便是:

这个方程表示的意思:

“ 将前 i 件物品放入容量为 v 的背包中 ” 这个子问题,若只考虑第 i 件物品的策略(放或者不放),那么就可以转化为一个只牵扯前 i-1 件物品的问题。

如果不放第 i 件物品,那么问题就转化为 “ 前 i-1 件物品放入容器为 v 的背包中 ” ,价值为 f[i-1][v] ;

如果放第 i 件物品,那么问题就转化为 “ 前 i-1 件物品放入剩下的容量为 v-c[i] 的背包中 ”,的背包中此时能获得的最大价值就是 f[i-1][v-c[i]] 价值再加上通过放入第 i 件物品获得的价值 w[i]。

 

三、过程分析

简单起见,我们来将上面的问题具体化:假设有5个物品,它们的价值(v)和重量(w)如下图:

背包总容量为10,现在要从中选择物品装入背包中,要求物品的重量不能超过背包的容量,并且最后放在背包中物品的总价值最大。

 

详细过程: 

a) 把背包问题抽象化(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 个物品选或不选),Vi表示第 i 个物品的价值,Wi表示第 i 个物品的体积(重量);

b) 建立模型,即求max(V1X1+V2X2+…+VnXn);

c) 约束条件,W1X1+W2X2+…+WnXn<capacity;

d) 定义 f(i,j):当前背包容量 j,前 i 个物品最佳组合对应的价值;

e) 寻找递推关系式,面对当前商品有两种可能性:

    第一,包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的,即 f(i,j) = f(i-1,j);

    第二,还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即f(i,j)=max{ f(i-1,j),f(i-1,j-w(i))+v(i) }

       其中f(i-1,j)表示不装,f(i-1,j-w(i))+v(i) 表示装了第i个商品,背包容量减少w(i)但价值增加了v(i);

    由此可以得出递推关系式:

    1) j<w(i)      f (i,j)=f (i-1,j)

    2) j>=w(i)     f (i,j)=max{ f (i-1,j),f (i-1,j-w(i))+v(i) 

  g) 填表,首先初始化边界条件,f (0,j)=f (i,0)=0;

 

  h) 然后一行一行的填表,

    1) 如,i=1,j=1,w(1)=2,v(1)=3,有j<w(1),故 f (1,1)=f (1-1,1)=0;

    2) 又如i=1,j=2,w(1)=2,v(1)=3,有j=w(1),故 f (1,2)=max{ f (1-1,2),f (1-1,2-w(1))+v(1) }=max{0,0+3}=3;

    3) 如此下去,填到最后一个,i=4,j=8,w(4)=5,v(4)=6,有j>w(4),故 f (4,8)=max{ f (4-1,8),f (4-1,8-w(4))+v(4) }=max{9,4+6}=10;所以填完表如下图:

 

 

四、伪代码+代码

​for (int i = 1; i <= N; i++)
    for (int j = 1; j <= V; j++)
        f[i][j] = max(f[i-1][j], f[i][j-w[i]] + v[i]);

 

#include <iostream>
#include <algorithm>
using namespace std;
#define N 1001
#define W 1002

int dp[N][W];
int w[N];
int v[N];

int main() {
    int n,ww; cin >> n >> ww;
    for(int i=1;i<=n;i++) {
        cin >> w[i] >> v[i];
    }
    for(int i=1;i<=n;i++) {
        for(int j=1;j<=ww;j++) {
            if(j < w[i]) {
                dp[i][j] = dp[i-1][j];
            }
            else {
                dp[i][j] = max( dp[i-1][j], dp[i-1][ j - w[i] ] + v[i] );
            }
        }
    }
    cout << dp[n][ww] << endl;
    return 0;
}

 

 

五、优化空间复杂度

 
以上方法的时间和空间复杂度均为 Θ( V N ) ,其中时间复杂度应该已经不能 再优化了,但空间复杂度却可以优化到Θ( N ) 1
 
 

每一次 f (i)(j)改变的值只与 f (i-1)(x) {x:1...j}有关,f (i-1)(x)是前一次i循环保存下来的值;

因此,可以将 f 缩减成一维数组,从而达到优化空间的目的,状态转移方程转换为 B(j)= max{B(j), B(j-w(i))+v(i)}

并且,状态转移方程,每一次推导 f(i)(j)是通过 f(i-1)(j-w(i))来推导的,所以一维数组中j的扫描顺序应该从大到小(capacity到0),否者前一次循环保存下来的值将会被修改,从而造成错误。

 

 

如果j不逆序而采用正序j=0...capacity,则在计算 f(i)(j) 时,变成了通过 f(i)(j-w(i))来推导, 而不是由 f(i-1)(j-w(i)) 来推导的。

所以该一维数组后面的值需要前面的值进行运算再改动,如果正序便利,则前面的值将有可能被修改掉从而造成后面数据的错误;相反如果逆序遍历,先修改后面的数据再修改前面的数据,此种情况就不会出错了。

 

优化后代码如下:

#include <iostream>
#include <algorithm>
using namespace std;
#define N 1001
#define W 1002

int dp[W];
int w[N];
int v[N];

int main() {
    int n,ww; cin >> n >> ww;
    for(int i=1;i<=n;i++) {
        cin >> w[i] >> v[i];
    }
    for(int i=1;i<=n;i++) {
        for(int j=ww;j>=w[i];j--) {
            if(j < w[i]) {
                dp[j] = dp[j];
            }
            else {
                dp[j] = max( dp[j], dp[ j - w[i] ] + v[i] );
            }
        }
    }
    cout << dp[ww] << endl;
    return 0;
}

 

 

六、初始化的细节问题

在求最优解的背包问题题目中,有两种不太相同的问法。

1. 有的题目要求  “ 恰好装满背包 ” 时的最优解。

2. 有的题目则并没有要求把背包装满。

这两种问法的区别是在初始化的时候有所不同。

如果是第一种问法,要求恰好装满背包,那么在初始化时除了 f[0]  为 0 ,其他  f[1...V]  均设为  −∞  ,这样就可以保证最终得到的  是一种恰好装满背包的最优解。

如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将  f[0...V] 全部设为 0 。 

因为:初始化的 f 数组事实上就是没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么只有容量为 0 的背包可能被价值为 0 的 nothing  恰好装满,其他容量的背包均没有合法的解,属于未定义的状态,他们的值就都应该是−∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解 “ 什么都不装 ” ,这个解的价值为0,所以初始状态的值也就全部为 0 了。

这个小技巧完全可以推广到其它类型的背包问题。

 

 

七、一个常数优化
 

前面的代码中有:

可以将这个循环的下限进行改进。

由于只需要最后f[j] f[j]f[j]的值,倒推前一个物品,其实只要知道 f[j−w[n]]  即可。以此类推,对以第j 个背包,其实只需要知道到  f [ j-sum{w[j...n]} ] 即可,即代码可以改成

for (int i = 1; i <= n; i++) {
    int bound = max(V - sum{w[i]...w[n]}, w[i]);
    for (int j = V; j >= bound, j--)
        f[j] = max(f[j], f[j - w[i]] + v[i]);
}


对于求sum 可以用前缀和,这对于V 比较大时是有用的。
 

 

 

  • 13
    点赞
  • 68
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值