动态规划——背包问题

背包问题主要为0-1背包、完全背包、多重背包这三类。

1.0-1背包

有n件物品,每件物品的重量为w[i],其价值为v[i],现在有个有容量为m的背包,如何选择物品使得装入背包物品的价值最大。

先设置一个二维数组dp[ ][ ],令dp[i][j]表示前i个物品装进容量为j的背包能获得的最大价值。则dp[n][m]的值就是0-1背包问题的解。

只考虑第i件物品时,可将情况分为是否放入第i件物品两种:

①对于容量为j的背包,如果不放入第i件物品,那么这个问题就转换成前i-1个物品放入容量为j的背包的问题,即dp[i][j]=dp[i-1][j]。

②对于容量为j的背包,如果放入第i件物品,那么当前背包的容量就变成了j-w[i],并得到这个物品的价值v[i]。之后这个问题就转化成将前i-1个物品放入容量为j-w[i]的背包的问题,即dp[i][j]=dp[i-1][j-w[i]]+v[i]。(注意j-w[i]不能为负数)

而从这两种情况可以得到状态转移方程:

dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])

对于边界情况:

dp[i][0]=0 (0<=i<=n)

dp[0][j]=0 (0<=j<=m)

观察状态转移的特点,可以发现:

dp[i][j]的转移仅与本行的上一行有关,所以可以将原本的二维数组优化为一维数组:

dp[j]=max(dp[j],dp[j-w[i]]+v[i])。为了保证状态正确转移,必须保证在每次更新中确定状态dp[j]时,dp[j-w[i]]尚未被本次更新修改。这就需要在每次更新中,倒序地遍历所有j的值,因为只有这样才能保证在确定dp[j]的值时,dp[j-w[i]]的值尚未被修改,从而完成正确的状态转移。

题目1:点菜问题

北大网络实验室经常有活动需要叫外卖,但是每次叫外卖的报销经费的总额最大为C元,有N种菜可以点,经过长时间的点菜,网络实验室对于每种菜i都有一个量化的评价分数(表示这个菜可口程度),为Vi,每种菜的价格为Pi, 问如何选择各种菜,使得在报销额度范围内能使点到的菜的总评价分数最大。     注意:由于需要营养多样化,每种菜只能点一次。

输入描述:

    输入的第一行有两个整数C(1 <= C <= 1000)和N(1 <= N <= 100),C代表总共能够报销的额度,N>代表能点菜的数目。接下来的N行每行包括两个在1到100之间(包括1和100)的的整数,分别表示菜的>价格和菜的评价分数。

输出描述:

    输出只包括一行,这一行只包含一个整数,表示在报销额度范围内,所点的菜得到的最大评价分数。

示例1

输入

90 4
20 25
30 20
40 50
10 18
40 2
25 30
10 8

输出

95
38

代码

/**
0-1背包问题:报销额度=背包容量,每种菜的价格=商品的重量,每种菜的评分=商品的价值。
***/
#include<iostream>
#include<cstdio>
using namespace std;
const int MAXN=1001;
int dp[MAXN];//令dp[j]表示物品装进容量为j的背包能获得的最大价值
int v[MAXN];//物品价值
int w[MAXN];//物品重量
int main()
{
    int m,n;//m:背包容量,n:物品数量
    while(cin>>m>>n){
    for(int i=0;i<n;i++)
        cin>>w[i]>>v[i];
    for(int i=0;i<=m;i++)
        dp[i]=0;//初始化
    for(int i=0;i<n;i++)
        for(int j=m;j>=w[i];j--)
            dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
    cout<<dp[m]<<endl;
    }
    return 0;
}

题目2:最小邮票数

有若干张邮票,要求从中选取最少的邮票张数凑成一个给定的总值。     如,有1分,3分,3分,3分,4分五张邮票,要求凑成10分,则使用3张邮票:3分、3分、4分即可。

输入描述:

    有多组数据,对于每组数据,首先是要求凑成的邮票总值M,M<100。然后是一个数N,N〈20,表示有N张邮票。接下来是N个正整数,分别表示这N张邮票的面值,且以升序排列。

输出描述:

      对于每组数据,能够凑成总值M的最少邮票张数。若无解,输出0。

示例1

输入

10
5
1 3 3 3 4

输出

3

代码

/**
0-1背包问题:背包容量,每张邮票的面值=商品的重量,每张邮票的张树=商品的价值。

dp变化过程:
    dp[i][j]    10   9   8   7   6   5   4   3   2   1   0
    初始化       ∞    ∞   ∞   ∞   ∞   ∞   ∞   ∞   ∞   ∞   0
    1           ∞    ∞   ∞   ∞   ∞   ∞   ∞   ∞   ∞   1   0
    1 3         ∞    ∞   ∞   ∞   ∞   ∞   2   1   ∞   1   0
    1 3 3       ∞    ∞   ∞   3   2   ∞   2   1   ∞   1   0
    1 3 3 3     4    3   ∞   ∞   2   ∞   2   1   ∞   1   0
    1 3 3 3 4   3    3   3   2   2   2   2   1   ∞   1   0
***/
#include<iostream>
#include<cstdio>
using namespace std;
const int MAXN=101;
int dp[MAXN];
int v[MAXN];//物品价值
int w[MAXN];//物品重量
int main()
{
    int m,n;//m:背包容量,n:物品数量
    while(cin>>m>>n){
    for(int i=0;i<n;i++)
        cin>>w[i];
//物品恰好要将背包装满,只需改变dp[j]的初始值,将dp[0]初始化为0,dp[i](1<=i<=m)初始化为无穷大(不可达到)即可。
    for(int i=0;i<=m;i++)
        dp[i]=9999;//用9999代替∞
    dp[0]=0;
    for(int i=0;i<n;i++)
        for(int j=m;j>=w[i];j--)
                dp[j]=min(dp[j],dp[j-w[i]]+1);
    if(dp[m]==9999)
        cout<<0<<endl;
    else
        cout<<dp[m]<<endl;    
    }
    return 0;
}

 

2.完全背包

如果将0-1背包问题进行扩展,每种物品不知可以取一件,而是可以选择多件,这便得到完全背包问题。

同样,设置一个二维数组dp[ ][ ],令dp[i][j]表示前i个物品装进容量为j的背包能获得的最大价值。则dp[n][m]的值就是完全背包问题的解。

同样,只考虑第i件物品时,可将情况分为是否放入第i件物品两种:

①对于容量为j的背包,如果不放入第i件物品,那么这个问题就转换成前i-1个物品放入容量为j的背包的问题,即dp[i][j]=dp[i-1][j]。

②对于容量为j的背包,如果放入第i件物品,那么当前背包的容量就变成了j-w[i],并得到这个物品的价值v[i]。但由于第i件物品任然可取,所以并不是转移到dp[i-1][j-w[i]],而是转移到dp[i][j-w[i]],即dp[i][j]=dp[i][j-w[i]]+v[i]。

而从这两种情况可以得到状态转移方程:

dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i])

对于边界情况:

dp[i][0]=0 (0<=i<=n)

dp[0][j]=0 (0<=j<=m)

观察状态转移的特点,可以发现:

dp[i][j]的转移仅与本行的上一行有关,所以可以将原本的二维数组优化为一维数组:

dp[j]=max(dp[j],dp[j-w[i]]+v[i])。为了保证状态正确转移,必须保证在每次更新中确定状态dp[j]时,dp[j-w[i]]已经完成了(和0-1背包的不同)本次更新修改。这就需要在每次更新中,正序地遍历所有j的值,因为只有这样才能保证在确定dp[j]的值时,dp[j-w[i]]的值已经被修改,从而完成正确的状态转移。

题目:整数拆分

一个整数总可以拆分为2的幂的和,例如: 7=1+2+4 7=1+2+2+2 7=1+1+1+4 7=1+1+1+2+2 7=1+1+1+1+1+2 7=1+1+1+1+1+1+1 总共有六种不同的拆分方式。 再比如:4可以拆分成:4 = 4,4 = 1 + 1 + 1 + 1,4 = 2 + 2,4=1+1+2。 用f(n)表示n的不同拆分的种数,例如f(7)=6. 要求编写程序,读入n(不超过1000000),输出f(n)%1000000000。

输入描述:

每组输入包括一个整数:N(1<=N<=1000000)。

输出描述:

对于每组数据,输出f(n)%1000000000。

示例1

输入

7

输出

6

代码

/**
其实就是一个完全背包求恰装满背包时的方案总数问题:
物品重量依次为2的幂1,2,4,2^3,...2^19(考虑n最大为10^6),每种物品的个数无限,刚好放进容量为n的背包中,有几种放法。
**/
#include<iostream>
#include<cstdio>
using namespace std;
int dp[1000001]={0};
int w[21];
int main()
{
    int n;
    for(int i=1;i<=20;i++)
        w[i]=(1<<(i-1));
    dp[0]=1;
	while(cin>>n)
    {
	    
        for(int i=1;i<=20;i++)
        {
            for(int j=w[i];j<=n;j++)
            {
                dp[j]=dp[j]+dp[j-w[i]];
                dp[j]%=1000000000;
            }
        }
	    cout<<dp[n]<<endl;
	}
	return 0;
}

 

3.多重背包

如果在0-1背包和完全背包问题之间折中,即每种物品最多只能取k件,这就是多重背包问题。

可以将多重背包问题直接转化为0-1背包问题,即每种物品均被视为k种重量和价值都相同的不同物品,对所有的物品求0-1背包。

还可以采用一种更有技巧性的拆分:将原数量为k的物品拆分为若干组,将每组物品视为一件物品,其价值和重量为该组中所有物品的价值重量总和。每组物品包含的原物品个数分别为2^0,2^1,...,2^(c-1),k-(2^c)+1,其中c是使得k-(2^c)+1>=0的最大整数。这种类似于二进制的拆分,不仅可以使得物品数量大大减少,同时也保证拆分后物品间的组合可以组合出任意数量的物品,如此便转换成了0-1背包问题,之后对所有这些新物品做0-1背包,即可得到多重背包的解。

题目:珍惜现在,感恩生活

代码

#include<iostream>
#include<cstdio>
using namespace std;
const int MAXN=10000;
int dp[MAXN];
int v[MAXN];//物品价值
int w[MAXN];//物品质量
int k[MAXN];//物品数目
int value[MAXN];//分解后物品价值
int weight[MAXN];//分解后物品质量
int main()
{
    int caseNumber;
    cin>>caseNumber;
    while(caseNumber--)
    {
        int n,m;
        cin>>m>>n;//n件物品,m容量的背包
        int number=0;//分解后物品的数量
        for(int i=0;i<n;i++)
        {
            cin>>w[i]>>v[i]>>k[i];
            for(int j=1;j<=k[i];j<<=1){
                value[number]=j*v[i];
                weight[number]=j*w[i];
                number++;
                k[i]-=j;
            }
            if(k[i]>0)
            {
                value[number]=k[i]*v[i];
                weight[number]=k[i]*w[i];
                number++;
            }
        }
        //解下来用0-1背包求解
        for(int i=0;i<=m;i++)
            dp[i]=0;
        for(int i=0;i<number;i++)
            for(int j=m;j>=weight[i];j--)
                dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
        cout<<dp[m]<<endl;
    }
    return 0;
}

 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
动态规划是一种将大问题分解为小问题进解决的方法,背包问题动态规划中最经典的题型之一。背包问题分为三类:01背包、完全背包和多重背包。其中,01背包问题是最经典的背包问题,也是动态规划的入门级必学算法。\[1\] 动态规划解决背包问题的核心思想是将问题分解为若干个小问题,先求解子问题,然后从子问题的解得到原问题的解。与分治法不同的是,动态规划适用于有重叠子问题的情况,即下一阶段的求解是建立在上一阶段的解的基础上进进一步求解。通过填表的方式,逐步推进,最终得到最优解。\[2\] 多重背包问题介于01背包和完全背包之间,可以将其转化为01背包或完全背包问题来求解。对于某种物品,如果其数量乘以单位体积大于背包容量,那么该物品与背包之间是完全背包问题。而对于某种物品,可以将其数量视为不同的物品,然后按照01背包问题处理。这样的转化可以在数据范围较小时适用,但在数量较大时可能会导致超时。因此,可以采用更精炼的划分方案,如二进制拆分,来减少物品分类的组数,从而优化算法的效率。\[3\] 总结来说,动态规划是一种解决背包问题的有效方法,通过将大问题分解为小问题,并利用子问题的解来求解原问题,可以得到背包的最优解。 #### 引用[.reference_title] - *1* *3* [【算法与数据结构】—— 动态规划背包问题](https://blog.csdn.net/the_ZED/article/details/104882665)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* [动态规划算法解决经典背包问题](https://blog.csdn.net/m0_52110974/article/details/120122061)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值