01背包问题模型讲解和【题解】—— [NOIP2005 普及组] 采药

01背包问题模型讲解

1.概念解析

    01背包是指在N件物品取出若干件放在空间为M的背包里,每件物品的体积为 W 1 W_1 W1 W 2 W_2 W2 W n W_n Wn,与之相对应的价值为 P 1 P_1 P1, P 2 P_2 P2 P n P_n Pn。通常要求使得价值最大的方案。因为每件物品只有放或者不放两种可能,刚好对应01,所以这种问题叫做01背包。

2.举例了解

    01背包是一类很经典的运用了动态规划思想解答的题目。下面我们先来看一个例子:

将下面 5 5 5个物品放入容量为 20 20 20的背包,使得背包内物品的总价值最大

物品属性/物品编号12345
重量10135820
价格11159710

    大多数人第一眼就会使用贪心来做这道题。根据贪心策略,每次选择当前价格最高的。但是我们会得到10,即只选择 5 5 5号物品。可正解是24,选择 2 2 2号物品和 3 3 3号物品。贪心的鼠目寸光在这里体现的淋漓尽致。

    那么在这里,我们就可以使用动态规划的思想来解这道题。先来一个动态规划五步走。

1.抽象问题:在五个物品里面选择,使得重量和不超过 20 20 20的情况下总价值最大。
2.状态:dp[i][j]表示只在前i个物品选择,放进容量为j的背包的最大价值。
3.初始条件:dp[0][i]=0
4.状态转移方程:
d p [ i ] [ j ] = { j < a [ i ] ,   d p [ i − 1 ] [ j ] j ≥ a [ i ] ,   m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − a [ i ] . w e i g h t ] + a [ i ] . v a l u e ) dp[i][j]= \begin{cases} j<a[i],\space dp[i-1][j]\\ j\geq a[i],\space max(dp[i-1][j],dp[i-1][j-a[i].weight]+a[i].value) \end{cases} dp[i][j]={j<a[i], dp[i1][j]ja[i], max(dp[i1][j],dp[i1][ja[i].weight]+a[i].value)
    其中,a[i]weight表示编号为i的物品的重量,a[i].value表示编号为i的物品的价值。
5.答案:dp[n][m]。在此题中, n = 5 n=5 n=5 m = 20 m=20 m=20

    首先是状态,根据动态规划思想的核心,我们将这个大问题拆解成相同的子问题。这就是状态。

    然后是初始状态,容易知道,前i个物品放进容量为 0 0 0的背包里,无论怎么样都放不下,也就不可能创造价值。

    接下来是状态转移方程。首先,对于目前枚举的j,如果j小于a[i].weight,即当前背包一件编号为i的物品都装不下,那么就不能对总的方法数产生影响。这样我们就只能继承上一层的方法数。

    否则,j大于等于a[i].weight,那么我们有两种选择:一种是装下这个物品,一种是不装这个物品。

    装下这个物品。那么背包的容量就只剩下了j-a[i].weight,那么我们就需要取容量为j-a[i].weight的背包能装下的物品的最大价值。可以直接查表解决,即dp[i-1][j-a[i].weight]

    不装这个物品,跟上面一样,直接继承上一层的方法数,即dp[i-1][j]

    取两者最大值,即 m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − a [ i ] . w e i g h t ] + a [ i ] . v a l u e ) max(dp[i-1][j],dp[i-1][j-a[i].weight]+a[i].value) max(dp[i1][j],dp[i1][ja[i].weight]+a[i].value)就行了。

    最后是答案,直接就根据状态的定义理解就行了,即 d p [ 物品数 ] [ 背包容量 ] dp[物品数][背包容量] dp[物品数][背包容量]

    既然是动态规划,那必定少不了填表,以下是针对上述问题的二维表格,请耐心观察,进一步加深对动态转移方程的理解。

i/j1234567891011121314151617181920
10000000001111111111111111111111
200000000111111151515151515151515
30000999991111111515202020242424
40000999991111111616202020242424
50000999991111111616202020242424

    可以发现,答案刚好符合我们的预测。接下来就是编写程序了,具体请看下列代码:

//输入样例复制

5 20
10 11
13 15
5 9
8 7
20 10

3.示例程序

    直接输出答案:

#include<bits/stdc++.h>
using namespace std;
struct object { // 储存物品的重量和价值
    int weight, value;
}a[110];
int n, m, dp[110][1010];
// 用dp[i][j]储存在前i个物品选择放入容量为j的背包时的最大价值
int main() {
    cin >> n >> m;// 输入物品数和背包重量
    for (int i = 1; i <= n; i++)
        cin >> a[i].weight >> a[i].value;
    for (int i = 1; i <= n; i++)// 枚举物品
        for (int j = 1; j <= m; j++)// 枚举重量
            // 状态转移方程,每一个物品都可以选择放或不放
            if (j >= a[i].weight) // 装得下
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - a[i].weight] + a[i].value);
            else // 装不下 
                dp[i][j] = dp[i - 1][j];
    cout << dp[n][m] << endl;
    return 0;
}

    输出表格:

#include<bits/stdc++.h>
using namespace std;
struct object { // 储存物品的重量和价值
    int weight, value;
}a[110];
int n, m, dp[110][1010];
// 用dp[i][j]储存在前i个物品选择放入容量为j的背包时的最大价值
int main() {
    cin >> n >> m;// 输入物品数和背包重量
    for (int i = 1; i <= n; i++)
        cin >> a[i].weight>> a[i].value;
    for (int i = 1; i <= n; i++)// 枚举物品
        for (int j = 1; j <= m; j++)// 枚举重量
            // 状态转移方程,每一个物品都可以选择放或不放
            if (j >= a[i].weight) // 装得下
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - a[i].weight] + a[i].value);
            else // 装不下 
                dp[i][j] = dp[i - 1][j];
    for (int i = 1; i <= m; i++)
    {
        for (int j = 1; j <= t; j++)
            cout << dp[i][j] << " ";
        puts("");
    }
    return 0;
}

    纯净无注释版:

#include<bits/stdc++.h>
using namespace std;
struct object {
    int weight, value;
}a[110];
int n, m, dp[110][1010];
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> a[i].weight >> a[i].value;
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= m; j++)
            if (j >= a[i].weight)
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - a[i].weight] + a[i].value);
            else
                dp[i][j] = dp[i - 1][j];
    cout << dp[n][m] << endl;
    return 0;
}

4.滚动数组优化

    在上面,我们可以发现:每一个dp[i][j]都只会查询上一层,即dp[i-1][j]。所以,为了节省空间,我们可以只用一个一维数组记录此时的状态。

    但是要注意,循环需要改成这样:

for (int j = m; j >= a[i].weight; j--)

    有以下两点:

    1.我们在更新dp[j]的值的时候,只需要用到小于j的状态的值,即 d p [ k ] ( k < j ) dp[k](k<j) dp[k](k<j)。如果从小到大更新,会造成使用前面更新过的值的情况。

    2. d p [ j ] dp[j] dp[j]在更新前本身储存的就是上一层的值,所以我们只需要枚举到需要改变的状态,即a[i].weiight就行了。

    示例代码如下:

#include<bits/stdc++.h>
using namespace std;
struct object { // 储存物品的重量和价值
    int weight, value;
}a[110];
int n, m, dp[1010];
// 用dp[j]储存当前放入容量为j的背包时的最大价值
int main() {
    cin >> n >> m;// 输入物品数和背包重量
    for (int i = 1; i <= n; i++)
        cin >> a[i].weight >> a[i].value;
    for (int i = 1; i <= n; i++)// 枚举物品
        for (int j = m; j >= a[i].weight; j--)// 枚举重量
            // 状态转移方程,每一个物品都可以选择放或不放
            dp[j] = max(dp[j], dp[j - a[i].weight] + a[i].value);
    cout << dp[m] << endl;
    return 0;
}

    纯净无注释版:

#include<bits/stdc++.h>
using namespace std;
struct object {
    int weight, value;
}a[110];
int n, m, dp[1010];
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> a[i].weight >> a[i].value;
    for (int i = 1; i <= n; i++)
        for (int j = m; j >= a[i].weight; j--)
            dp[j] = max(dp[j], dp[j - a[i].weight] + a[i].value);
    cout << dp[m] << endl;
    return 0;
}

    顺便提一句,这里不太建议大家使用一维 d p dp dp,因为有点难以理解。如果不完全理解一维 d p dp dp,还是老老实实使用二维 d p dp dp吧。

[NOIP2005 普及组] 采药

通往洛谷的传送门

题目描述

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是辰辰,你能完成这个任务吗?

输入格式

第一行有 2 2 2 个整数 T T T 1 ≤ T ≤ 1000 1 \le T \le 1000 1T1000)和 M M M 1 ≤ M ≤ 100 1 \le M \le 100 1M100),用一个空格隔开, T T T 代表总共能够用来采药的时间, M M M 代表山洞里的草药的数目。

接下来的 M M M 行每行包括两个在 1 1 1 100 100 100 之间(包括 1 1 1 100 100 100)的整数,分别表示采摘某株草药的时间和这株草药的价值。

输出格式

输出在规定的时间内可以采到的草药的最大总价值。

输入输出样例

输入 #1

70 3
71 100
69 1
1 2

输出 #1

3

提示

【数据范围】

  • 对于 30 % 30\% 30% 的数据, M ≤ 10 M \le 10 M10
  • 对于全部的数据, M ≤ 100 M \le 100 M100

【题目来源】

NOIP 2005 普及组第三题

1.题意解析

    这道题只需要套上面的模版就行了。把规定时间看成背包,采每株草药的时间看成重量。具体请看代码注释。

2.AC代码

2.1.二维dp

#include<bits/stdc++.h>
using namespace std;
struct medicine//储存草药的采摘时间和价值 
{
	int t,value;
}a[110];
//用dp[i][j]储存背包容量为j时在前i个物品选择的最大价值
int t,m,dp[110][1010];
int main()
{
    cin>>t>>m;
    for(int i=1;i<=m;i++)
        cin>>a[i].t>>a[i].value;
    for(int i=1;i<=m;i++)//枚举物品 
    	for(int j=1;j<=t;j++)//枚举重量 
    	//状态转移方程,每一个物品都可以选择放或不放
    	if(j>=a[i].t)//装得下 
        	dp[i][j]=max(dp[i-1][j],dp[i-1][j-a[i].t]+a[i].value);
        else//装不下 
            dp[i][j]=dp[i-1][j];
    cout<<dp[m][t];
	return 0;
}

2.2.一维dp

#include<bits/stdc++.h>
using namespace std;
struct medicine//储存草药的总时间和总价值 
{
	int t,value;
}a[110];
int t,m,dp[1010]={};//用dp[j]储存背包容量为j时的最大价值
int main()
{
    cin>>t>>m;
    for(int i=1;i<=m;i++)
        cin>>a[i].t>>a[i].value;
    for(int i=1;i<=m;i++)
    	for(int j=t;j>=a[i].t;j--)//一定要从后往前遍历
    	//状态转移方程,每一个物品都可以选择放或不放
        	dp[j]=max(dp[j],dp[j-a[i].t]+a[i].value);
    cout<<dp[t];
	return 0;
}

喜欢就订阅此专辑吧!

【蓝胖子编程教育简介】
蓝胖子编程教育,是一家面向青少年的编程教育平台。平台为全国青少年提供最专业的编程教育服务,包括提供最新最详细的编程相关资讯、最专业的竞赛指导、最合理的课程规划等。本平台利用趣味性和互动性强的教学方式,旨在激发孩子们对编程的兴趣,培养他们的逻辑思维能力和创造力,让孩子们在轻松愉快的氛围中掌握编程知识,为未来科技人才的培养奠定坚实基础。

欢迎扫码关注蓝胖子编程教育
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝胖子教编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值