AcWing 2 01背包问题

题目描述:

有 N 件物品和一个容量是 V的背包。每件物品只能使用一次。第 i件物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N行,每行两个整数 vi,wi,用空格隔开,分别表示第 i件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000,0<vi,wi≤1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8

分析:

从这题开始就进入到动态规划的刷题了,首先回忆下动态规划的一些概念。

动态规划的定义:邓公在MOOC中是这么说的,动态规划就是先用递归的思想找出问题的本质,再将其等效地转化成迭代的写法。更常规的定义是DP是用来解决一类最优化问题的算法思想,将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。一般使用递归或者递推的写法来实现动态规划,递归的写法又称为记忆化搜索。

重叠子问题:如果一个问题可以被分解为若干个子问题,且这些子问题会重复出现,那么称这个问题拥有重叠子问题。需要用动态规划记录重叠子问题的解,避免大量重复计算。举个简单的例子就是斐波那契数问题,每个状态都是由前两个状态转移过来的,fib(n)的计算用到了fib(n-1)和fib(n-2),fib(n-1)的计算用到了fib(n-2)和fib(n-3),如果不去存储中间结果,fib(n-2)就要被重复计算,斐波那契问题里规模为n-1的子问题和规模为n-2的子问题就是重叠的,甚至可以说n-1子问题的求解完全包括了n-2子问题的求解。

最优子结构:如果一个问题的最优解可以由其子问题的最优解有效的构造出来,那么称这个问题拥有最优子结构。

一个问题必须拥有重叠子问题和最优子结构,才能使用动态规划去解决。动态规划的出现一方面是消除了重叠子问题的重复计算,另一方面要从子问题的解推出总问题的解,就要求问题必须要有最优子结构。

有一类动态规划可解的问题,它可以描述成若干个有序的阶段,且每个阶段的状态只和上一个阶段的状态有关,一般把这类问题称为多阶段动态规划问题。

状态无后效性:当前状态记录了历史信息,一旦当前状态确定,就不会再改变,且未来的决策只能在已有的一个或若干个状态的基础上进行,历史信息只能通过已有的状态去影响未来的决策。

动态规划中的状态与搜索中的状态基本类似,我们找到初始的状态,然后要一步步转移到最终的状态。首先要做的就是状态的表示,一般用数组来表示状态,状态的表示取决于问题中状态涉及的属性。表示好状态后,我们易得问题的初始状态,也就是DP的边界,然后通过状态转移方程的设计逐步扩散到所有的状态,最终可以解决问题。所以DP的核心在于状态转移方程的设计,换而言之,状态转移方程就是规定了由子问题的解是如何得到问题的解的。解决问题有分而治之和减而治之两种思想,减而治之一般只需要从规模为n-1的子问题推出规模为n的问题,而分而治之则需要合并若干个规模较小的子问题才能得出问题的解。

本题是最简单的01背包问题,物品有三种属性,数量,体积和价值,需要我们求解的最优解是背包中物品的最大价值。用数组f来表示状态,第一维表示问题的规模,第二维表示体积,f数组的值表示最大价值,即f[i][j]表示前i个物品中体积不超过j状态的最大价值。下面要解决的就是如何从规模为i - 1的问题的解推出规模为i问题的解。背包问题的关键在于物品的选择,01背包问题每个物品要么选要么不选,且数量都只有一个。还记得最初对DP的定义是用递归的思想找出问题的本质,假设我们现在要写个DFS找出本题的最优解,则需要从第一个物品逐个往后遍历,每个物品选了加进背包是一种状态,不选又是一种状态。所以规模为i的问题可能由两种状态转移而来,一种是选择了第i个物品,另一种是没有选择第i个物品。f[i][j]表示在前j个物品中选出总体积不超过j的物品
的最大价值,如果第i个物品没有选择,说明在遍历完前i - 1个物品时,背包中已然装进体积不超过j的物品了,故这种状态表示为f[i-1][j];如果选择了第i个物品,第i个物品体积为v[i],说明在前i-1个物品中已经装进了不超过j - v[i]体积的物品,状态表示为f[i-1][j-v[i]]。总结一下,f[i][j]这种状态可以由f[i-1][j]以及f[i-1][j-v[i]]这两种状态转移而来。题目的要求是价值最大,f数组存储的永远是价值最大的那个,故f[i][j] = max(f[i-1][j],f[i-1][j-v[i]] + w[i])。

确定了状态转移方程是f[i][j] = max(f[i-1][j],f[i-1][j-v[i]] + w[i])后,需要确定DP的边界状态,即背包中什么都不装,总价值自然是0。f[0][0] = f[0][1] = ... = f[0][m] = 0。然后确定状态转移的方向,既然f[i][j]表示的是前i个物品的最大价值,前i个物品的状态是由前i - 1个物品的状态转移过来的,那么我们需要先求出前1个物品的所有状态,然后是前2个物品的所有状态...。设一共n个物品,背包容量为m,那么循环第一层是1到n,第二层是0到m。注意:在进行状态转移时,只有当j > v[i]时才可能装了第i个物品,这是显然的,如果装了第i个物品背包中物品体积肯定超过v[i]。具体代码如下:

#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 1005;
int w[maxn],v[maxn],f[maxn][maxn];
int main(){
    int n,m;
    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]);
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

以前初学DP时,最迷糊的就是挑战程序设计竞赛上面使用滚动数组来节省DP的存储空间了。为此,我们需要先明白状态转移的方向,也就是循环的方向,不妨将f数组打印出来。

以f[4][5]为例,仅由f[3][1]和f[3][5]转移而来。我们计算f数组的顺序是自左而右,自上而下。每个状态仅用到上一行的两个状态,之前行的状态都没用了,所以不妨将f[i][j]去掉第一维,我们即f[j]在循环遍历到第一行时存储的是f[1][j],遍历到第二行时存储的是f[2][j],覆盖掉了第一行的状态值。还有个需要考虑的问题是假如我们还按照之前自左而右的f数组填充顺序的话,在算完f[4][1]时,已经覆盖掉了f[3][1]的值,后面计算f[4][5]时便无法获得之前f[3][1]的值了。每个状态都只会用的上一行的两个状态,分别是上一行与之对齐的状态和上一行相对偏左位置的状态,因此,上一行右边的状态不会影响下一行左边的状态,我们可以自右而左的填充f数组,这样,计算f[4][5]时用到的两个状态都还没被覆盖掉。这时的一维数组f就是滚动数组,在不同的状态转移过程中表示不同的含义,有效的节省了时间和空间。具体代码的变化只需要去掉上面代码f的第一维,并将内层循环的枚举循环改为自右向左即可。

#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 1005;
int w[maxn],v[maxn],f[maxn];
int main(){
    int n,m;
    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;
}
发布了272 篇原创文章 · 获赞 26 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_30277239/article/details/103838342