算法模板:动态规划之01背包

简介: 算法模板:动态规划之01背包

前言


唤我沈七就好啦。


动态规划


核心作用:优化


当数据范围> 30 时 单纯用暴力(DFS BFS)的话指数型枚举必然超时

而动态规划能以某种比较聪明的方式来枚举所有方案。

最后按题目要求(属性)求解。


dp、递归、递推、搜索 的关系


dp 是递推,是搜索的改版,是递归的进化体.


初始化


DP 的细节包括你说的初始化问题,是没有很固定的模板的,一般情况下可以归纳以下三种情形:


求最大值,将所有值初始化为无穷小,找到 DP 的起点(边界),手动赋值;

求最小值,将所有值初始化为无穷大,找到 DP 的起点(边界),手动赋值;

求方案数,将所有值初始化为 0 ,找到 DP 的起点(边界),手动赋值,一般为 1 。

在还没有熟悉的时候,建议认真找到所有边界值,并给它们赋上初值,这样的方式肯定是正确的。

熟悉了以后,针对某些特定的题目,可以根据经验,遵循 不影响最终答案 的原则,省略部分赋值的步骤。

状态转移方程涉及到 i-1 从 i 就从1开始循环,没涉及就从 0 开始。


01背包


二维背包


有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

第 i 件物品的体积是 v[i],价值是 w[i]

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

输出最大价值。


dp[i] [j] 含义:前 i 个物品,背包容量 j下的最优解(最大价值):


(1)当前的状态依赖于之前的状态,可以理解为从初始状态dp[0] [0] = 0 开始决策,


有 N 件物品,则需要 N 次决 策,每一次对第 i 件物品的决策, 状态f[i] [j]不断由之前的状态更新而来。


(2)当前背包容量不够(j < v[i])

没得选,因此前 i个物品最优解即为前 i−1个物品最优解:


对应代码:f[i][j] = f[i - 1][j]。


(3)当前背包容量够,可以选,因此需要决策选与不选第 i个物品:


选: f[i][j] = f[i - 1][j - v[i]] + w[i]。
不选:f[i][j] = f[i - 1][j] 。
我们的决策是如何取到最大价值,因此以上两种情况取 max()
我们的决策是如何取到最大价值,因此以上两种情况取 max() 


为什么选的状态转移 方程 是 f[i] [j] = f[i - 1] [j - v[i]] + w[i] 呢


下面我来说一下我的理解


d p [i-1] [j-v[i]]+w[i ] 代表着 “必选第i个物品” ,怎么样才能做到“必选”呢 ?


其实只要在一开始的时候将第 i 个物品直接放入背包即可 即 +w[i]


然后只在前 i-1 个物品里选剩下的就行了。


#include<bits/stdc++.h>
using namespace std;
const int N=1e4;
int dp[N][N];// dp[i][j], j体积下前i个物品的最大价值
int v[N],w[N];
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 = 1 ; j <= m ; j ++)
        {
            dp[i][j]=dp[i-1][j];
            if(j>=v[i])//j<v[i] dp[i-1][j-v[i]]下标就小于0了
            dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
        }
        cout<<dp[n][m];
    return 0;
}


一维优化


因为 dp[i] 仅用到了dp[i-1]层, 那我们只需要短暂的存一下上一层,就可以去掉这层了


( 1 )dp[j]定义:N件物品,背包容量j下的最优解。


( 2 )注意枚举背包容量j必须从m逆序开始。


( 3 )为什么一维情况下枚举背包容量需要逆序?


在二维情况下,状态f【i】【j】是由上一轮i - 1的状态得来的,f[i] [j]与f[i - 1] [j]是独立的。


而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。


例如:由于j是从大到小循环的。假设j = 5; j >= 2; j -–;


也就是这一层(第i层)会先算出来f[5],


注意这个时候,第i层正在算f[5], 也就是说第i层没有更新f[4] f[3],也就是此时的f[4] f[3] 是i - 1层的)


那么算 f[5] 要用到比他更小的 f[4] 或者f[3]也就是i - 1层的了。


(4)简单来说,一维情况正序更新状态f[j]需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。


#include<bits/stdc++.h>
using namespace std;
const int N=1e4;
int dp[N];
int v[N],w[N];
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 --)
        {
        dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
        }
        cout<<dp[m];
    return 0;
}


经典习题


小A点菜


uim由于买了一些书,口袋里只剩M元(M≤10000)

餐馆虽低端,但是菜品种类不少,有N种(N≤100),

第i种卖a元(ai≤1000) 由于是很低端的餐馆,所以每种菜只有一份。

小A奉行“不把钱吃光不罢休”,所以他点单一定刚好把uim身上所有钱花完。

他想知道有多少种点菜方法。


dp[ j ] 表示达到当前钱数时,已经求出的方案数;


每一种菜都可以选择吃和不吃


#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
int dp[N];
int w[N];
int main()
{
  int n,m;
  cin>>n>>m;
  for (int i = 1 ; i <= n ; i ++)
  cin>>w[i];
  dp[0]=1;//因为当钱数==0时,说明当前方案已经成立,所以方案总数加 1
  for (int i = 1 ; i <= n ; i ++)
  for (int j = m ; j >= w[i] ; j --)
  {
  dp[j]=dp[j]+dp[j-w[i]]; 
        //可以选吃喝和不吃
  } 
  cout<<dp[m];
  return 0;
}


5 倍经验日


现在 A 拿出了 x个迷药,准备开始与那些人打了。

由于迷药每个只能用一次,所以 A要谨慎的使用这些药。悲剧的是,用药量没达到最少打败该人所需的属性药药量,则打这个人必输。例如他用 2 个药去打别人,别人却表明 3个药才能打过,那么相当于你输了并且这两个属性药浪费了。

现在有 n 个好友,给定失败时可获得的经验、胜利时可获得的经验,打败他至少需要的药量。

要求求出最大经验 s,输出 5 * s 。


题解部分:


dp[i]表示用i瓶药获得的最多经验。


1.如果 j 个迷药打不过,只能加 失败的经验: dp[j]=dp[j]+w1[i]


2.如果打的过 ,可以选择打败他获得获胜经验,也可以选择不打败他获得失败经验


然后就是 0 1 背包思想: dp[j]=max(dp[j]+w1[i],dp[j-v[i]]+w2[i]);


注意 如果要打败他的话,需要消耗 迷药 ,所以需要 j - v[i]


#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
long long dp[N],n,m;
long long w1[N],w2[N],v[N];
int main()
{
  cin>>n>>m;
  for(int i = 1 ; i <= n ;i ++)
  cin>>w1[i]>>w2[i]>>v[i];
  for(int i = 1 ; i <= n ; i ++)
  {
        for(int j = m ; j >= 0 ; j --)
        {
            if(v[i]>j)//如果 j 个迷药打不过,只能加 失败的经验
            dp[j]+=w1[i];
            else
            dp[j]=max(dp[j]+w1[i],dp[j-v[i]]+w2[i]);
            //如果打的过 ,可以选择打败他获得获胜经验,也可以选择不打败他获得失败经验。
            //这样问题就转化成了 0 1 背包问题 选和不选
        }
  } 
  cout<<5*dp[m];
  return 0;
 }


买干草


约翰遭受了重大的损失:蟑螂吃掉了他所有的干草,留下一群饥饿的牛.他乘着容量为C(1≤C≤50000)个单位的马车,去顿因家买一些干草. 顿因有H(1≤H≤5000)包干草,每一包都有它的体积Vi(l≤Vi≤C).约翰只能整包购买,


他最多可以运回多少体积的干草呢?


虽然问的是体积,但可以将这个问题看成价值和体积相等


这样就和背包问题 一 模 一 样 了!


#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
long long dp[N];
int v[N];
int main()
{
  int m,n;
  cin>>m>>n;
  for(int i = 1; i <= n ; i ++)
  cin>>v[i];
  for(int i = 1; i <= n ; i ++)
  for(int j = m; j >= v[i] ; j --)
  {
  dp[j]=max(dp[j],dp[j-v[i]]+v[i]);
  }
  cout<<dp[m];
  return 0;
}


完结散花


ok以上就是对 动态规划之01背包 的全部讲解啦,很感谢你能看到这儿。如果有遗漏、错误或者有更加通俗易懂的讲解,欢迎小伙伴私信我,我后期再补充完善。


参考文献


https://www.acwing.com/activity/content/19/


相关文章
|
1月前
|
算法
【算法】前缀和——二维前缀和模板题
【算法】前缀和——二维前缀和模板题
|
2月前
|
算法 开发者 Python
惊呆了!Python算法设计与分析,分治法、贪心、动态规划...这些你都会了吗?不会?那还不快来学!
【7月更文挑战第10天】探索编程巅峰,算法至关重要。Python以其易读性成为学习算法的首选。分治法,如归并排序,将大问题拆解;贪心算法,如找零问题,每步求局部最优;动态规划,如斐波那契数列,利用子问题解。通过示例代码,理解并掌握这些算法,提升编程技能,面对挑战更加从容。动手实践,体验算法的神奇力量吧!
60 8
|
1月前
|
机器学习/深度学习 算法 Java
算法设计(动态规划应用实验报告)实现基于贪婪技术思想的Prim算法、Dijkstra算法
这篇文章介绍了基于贪婪技术思想的Prim算法和Dijkstra算法,包括它们的伪代码描述、Java源代码实现、时间效率分析,并展示了算法的测试用例结果,使读者对贪婪技术及其应用有了更深入的理解。
算法设计(动态规划应用实验报告)实现基于贪婪技术思想的Prim算法、Dijkstra算法
|
1月前
|
算法 Java 测试技术
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
这篇文章介绍了基于动态规划法的三种算法:解决背包问题的递归和自底向上实现、Warshall算法和Floyd算法,并提供了它们的伪代码、Java源代码实现以及时间效率分析。
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
|
2月前
|
算法 Python
Python算法高手进阶指南:分治法、贪心算法、动态规划,掌握它们,算法难题迎刃而解!
【7月更文挑战第10天】探索Python算法的精华:分治法(如归并排序)、贪心策略(如找零钱问题)和动态规划(解复杂问题)。通过示例代码揭示它们如何优化问题解决,提升编程技能。掌握这些策略,攀登技术巅峰。
62 2
|
2月前
|
算法 程序员 Python
算法小白到大神的蜕变之路:Python分治法、贪心、动态规划,一步步带你走向算法巅峰!
【7月更文挑战第9天】探索算法之旅,以Python解锁编程高手之路。分治法如二分查找,将复杂问题拆解;贪心算法解决活动选择,每次选取局部最优;动态规划求斐波那契数列,避免重复计算,实现全局最优。每一步学习,都是编程能力的升华,助你应对复杂挑战,迈向算法大师!
34 1
|
2月前
|
存储 算法 Python
Python算法界的秘密武器:分治法巧解难题,贪心算法快速决策,动态规划优化未来!
【7月更文挑战第9天】Python中的分治、贪心和动态规划是三大关键算法。分治法将大问题分解为小问题求解,如归并排序;贪心算法每步选局部最优解,不保证全局最优,如找零钱;动态规划存储子问题解求全局最优,如斐波那契数列。选择合适算法能提升编程效率。
49 1
|
2月前
|
存储 算法 Python
震撼!Python算法设计与分析,分治法、贪心、动态规划...这些经典算法如何改变你的编程世界!
【7月更文挑战第9天】在Python的算法天地,分治、贪心、动态规划三巨头揭示了解题的智慧。分治如归并排序,将大问题拆解为小部分解决;贪心算法以局部最优求全局,如Prim的最小生成树;动态规划通过存储子问题解避免重复计算,如斐波那契数列。掌握这些,将重塑你的编程思维,点亮技术之路。
51 1
|
15天前
|
算法 BI Serverless
基于鱼群算法的散热片形状优化matlab仿真
本研究利用浴盆曲线模拟空隙外形,并通过鱼群算法(FSA)优化浴盆曲线参数,以获得最佳孔隙度值及对应的R值。FSA通过模拟鱼群的聚群、避障和觅食行为,实现高效全局搜索。具体步骤包括初始化鱼群、计算适应度值、更新位置及判断终止条件。最终确定散热片的最佳形状参数。仿真结果显示该方法能显著提高优化效率。相关代码使用MATLAB 2022a实现。
|
15天前
|
算法 数据可视化
基于SSA奇异谱分析算法的时间序列趋势线提取matlab仿真
奇异谱分析(SSA)是一种基于奇异值分解(SVD)和轨迹矩阵的非线性、非参数时间序列分析方法,适用于提取趋势、周期性和噪声成分。本项目使用MATLAB 2022a版本实现从强干扰序列中提取趋势线,并通过可视化展示了原时间序列与提取的趋势分量。代码实现了滑动窗口下的奇异值分解和分组重构,适用于非线性和非平稳时间序列分析。此方法在气候变化、金融市场和生物医学信号处理等领域有广泛应用。