首页 > 编程语言 >算法学习笔记(8.3)-(0-1背包问题)

算法学习笔记(8.3)-(0-1背包问题)

时间:2024-07-13 13:28:04浏览次数:26  
标签:8.3 背包 return val mem cap wgt 算法 dp

目录

最常见的0-1背包问题:

第一步:思考每轮的决策,定义状态,从而得到dp表

第二步:找出最优子结构,进而推导出状态转移方程

第三步:确定边界条件和状态转移顺序

方法一:暴力搜素

代码示例:

方法二:记忆化搜索

时间复杂度取决于子问题数量,也就是O(n*cap)。

实现代码如下:

方法三:动态规划

代码如下所示:

方法四:空间优化

代码示例

最常见的0-1背包问题:

Question:给定n个物品,第i个物品的重量为wgt[i]、价值为val[i],和一个容量为cap的背包。每个物品只能选择一次,问在限定背包的容量下能放入物品的最大价值。

观察图下所示,由于物品编号i从1开始计数,数组索引从0开始计数,因此物品i对应重量wgt[i]和价值val[i]。

我们可以将0-1背包问题看作一个由n轮决策组成的过程,对于每个物体都有不放入和放入两种决策,因此该问题满足决策树模型。

该问题的目标是求解“在限定背包容量下能放入物品的最大价值”,因此较大概率是一个动态规划的问题。

第一步:思考每轮的决策,定义状态,从而得到dp表

对于每个物品来说,不放入背包,背包容量不变;放入背包,背包容量减小。由此可得状态定义:当前物品编号i和剩余背包容量c,记作[i,c]。

状态[i,c]对应的子问题为:前i个物品在剩余容量为c的背包中的最大价值,记为dp[i,c]。

待求解的是dp[n,cap],因此需要一个尺寸为(n+1) * (cap+1)的二维dp表。

第二步:找出最优子结构,进而推导出状态转移方程

当我们做出物品i的决策后,剩余的是前i-1个物品的决策,可分为以下两种情况。

  1. 不放入物品i:背包容量不变,状态变化为[i-1,c].
  2. 放入物品i:背包容量减少wgt[i-1],价值增加val[i-1],状态变化为[i-1,c-wgt[i-1]]。

上述分析揭示了本题的最优子结构:最大价值dp[I,c]等于不放入物品i和放入物品i两种方案中价值更大的那一个。由此可以推导出状态转移方程为:

dp[i,c] = max(dp[i-1,c],dp[i-1,c-wgt[i-1] ] + val[i-1])

需要注意是,当前物品重量wgt[i-1]超过剩余背包容量c,则只能选择不放入背包。

第三步:确定边界条件和状态转移顺序

当无物品时或无背包剩余容量时最大价值为0,即首列dp[i,0]和首列dp[0,c]都等于0.

当前状态[i,c]从上方的状态[i-1,c]和左上方的状态[i-1,c-wgt[i-1]]转移而来,因此通过两层循环正序遍历整个dp表即可。

根据以上的分析,采取三种方法顺序进行实现暴力搜索,记忆化搜索,动态规划解法。

方法一:暴力搜素

搜索代码需要包含的要素:

  1. 递归参数:状态[i,c]
  2. 返回值:子问题的解dp[i,c]
  3. 终止条件:当物品编号越界I = 0 或背包容量为0时,终止递归并返回价值0.
  4. 剪枝:当前物品重量超出背包剩余容量时,则只能选择不放入背包。
代码示例:
# python 代码示例

def knapsack_dfs(wgt, val, i, c) :
    if i == 0 or c == 0 :
        return 0
    if wgt[i - 1] > c :
        return knapsack(wgt, val, i - 1, c)
    nohave = knapsack_dfs(wgt, val, i - 1, c)
    have = knapsack_dfs(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1]
    return max(nohave, have)
// C++代码示例

int knapsackDFS(vector<int> &wgt, vector<int> &val, int i, int c) :
    if (i == 0 || c == 0)
    {
        return 0 ;
    }
    if (wgt[i - 1] > c)
    {
        return knapsackDFS(wgt, val, i - 1, c) ;
    }
    int nohave = knapsackDFS(wgt, val, i - 1, c) ;
    int have = knapsackDFS(wgt, val, i - 1, c - wgt[i - 1]) + val[i - 1] ;
    return max(nohave, have) ;

如图所示:由于每个物品都会产生选或者不选两条搜索分支,因此时间复杂度为O(2^n)。

观察递归树,容易发现其中存在重叠子问题,例如dp[1,10]。而当物品较多,背包容量较大,尤其是相同重量的物品较多时,重叠子问题的数量将会大幅度增多。

方法二:记忆化搜索

为了保证重叠子问题只被计算一次,我们借助记忆列表mem来记录子问题的解,其中menm[i][c]对应dp[i][c]。

时间复杂度取决于子问题数量,也就是O(n*cap)。
实现代码如下:
# python 代码示例
def knapsack_dfs_mem(wgt, val, i, c, mem) :
    if i == 0 or c == 0 :
        return 0
    if mem[i][c] != -1 :
        return mem[i][c]
    if wgt[i - 1] > c :
        return knapsack_dfs_mem(wgt, val, i - 1, c, mem)
    noput = knapsack_dfs_mem(wgt, val, i - 1, c, mem)
    yesput = knapsack_dfs_mem(wgt, val, i - 1, c - wgt[i - 1], mem) + val[i - 1]
    mem[i][c] = max(noput, yesput)
    return mem[i][c]
// c++ 代码示例
int knapSackDFSMem(vector<int> &wgt, vector<int> &val, int i, int c, vector<int> &mem)
{
    if (i == 0 || c == 0)
    {
        return 0 ;
    }
    if (mem[i][c] != 0)
    {
        return mem[i][c] ;
    }
    if (wgt[i - 1] > c)
    {
        return knapSackDFSMem(wgt, val, i - 1, c, mem) ;
    }
    int noput = knapSackDFSMem(wgt, val, i - 1, c, mem) ;
    int yesput = knapSackDFSMem(wgt, val, i - 1, c - wgt[i - 1], mem) + val[i - 1];
    mem[i][c] = max(noput, yesput) ;
    return mem[i][c] ;
}

方法三:动态规划

动态规划实质是就是在状态转移的过程中填充dp表的过程,

代码如下所示:
# python 代码示例
def knapsack_dp(wgt, val, cap) :
    n = len(wgt)
    dp = [ [0] * (cap + 1) for _ in range(n + 1)]
    for i in range(1, n + 1) :
        for c in range(1, cap + 1) :
            if wgt[i - 1] > c :
                dp[i][c] = dp[i - 1][c]
            else :
                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1]) 
    return dp[n][cap] 
// c++ 代码示例
int knapSackDP(vector<int> &wgt, vector<int> &val, int cap)
{ 
    int n = wgt.size() ;
    vector<vector<int>> dp(n + 1, vector<int>(cap + 1, 0)) ;
    for (int i = 1 ; i <= n ; i++)
    {
        for (int c = 1 ; c <= cap ; c++)
        {
            if (wgt[i - 1] > c)
            {
                dp[i][c] = dp[i - 1][c] ;
            }
            else
            {
                dp[i][c] = max(dp[i - 1][c], dp[i - 1][c - wgt[i - 1]] + val[i - 1])  ;
            }
        }
    }
    return dp[n][cap] ;
}

时间复杂度和空间复杂度都是由数组dp所决定的,即O(n*cap)。

如图所示:

方法四:空间优化

由于每个状态都至于其上一行有关系,因此我们可以使用两个数组进行滚动前进,将复杂度从O(n^2)降低为O(n)。

进一步思考,我们能否仅用一个数组实现空间优化?观察可知,每个状态都是有正上方或左上方的格子的状态转移而来。假设只有一个数组,当开始遍历第i行时,该数组存储仍然是第i-1行的状态。

  1. 如果采取正序遍历,那么遍历到dp[i,j]时,左上方的dp[i-1,1]~dp[i-1,j-1]值可能已经覆盖了,因此无法得到状态转移的正确结果。
  2. 如果采取倒序遍历,则不会发生覆盖问题,状态转移可以正确的进行。

代码示例:
def knap_sack_dp_comp(wgt, val, cap) :
    n = len(wgt)
    dp = [0] * (cap + 1)
    for i in range(1, n + 1) :
        for c in range(cap, 0 ,-1) :
            if wgt[i - 1] > c :
                dp[c] = dp[c]
            else :
                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1])
    return dp[cap]    
// c++ 代码示例
int kanpSackDPComp(vector<int> &wgt, vector<int> &val, int cap)
{
    int n = wgt.size() ;
    vector<int> dp(cap + 1, 0) ;
    for (int i = 1 ; i <= n ; i++)
    {
        for (int c = cap; c > 0 ; c--)
        {
            if (wgt[i - 1] > c)
            {
                dp[c] = dp[c] ;
            }
            else
            {
                dp[c] = max(dp[c], dp[c - wgt[i - 1]] + val[i - 1]) ;
            }
        }
    }
    return dp[cap] ;
}

标签:8.3,背包,return,val,mem,cap,wgt,算法,dp
From: https://blog.csdn.net/2301_76874968/article/details/140341798

相关文章

  • 昇思25天学习打卡营第14天|K近邻算法实现红酒聚类
    红酒Wine数据集类别(13类属性):Alcohol,酒精;Malicacid,苹果酸Ash,灰;Alcalinityofash,灰的碱度;Magnesium,镁;Totalphenols,总酚;Flavanoids,类黄酮;Nonflavanoidphenols,非黄酮酚;Proanthocyanins,原花青素;Colorintensity,色彩强度;Hue,色调;OD280/OD315ofdilutedwines,稀释酒的......
  • 排序算法——选择排序法
    选择排序算法概述选择排序(SelectionSort)是一种简单直观的排序算法。它的基本思想是:在要排序的一组数中,选出最小(或最大)的一个数与第一个位置的数交换;然后在剩下的数当中再找最小(或最大)的与第二个位置的数交换,依次类推,直到第n-1个元素(倒数第二个数)和第n个元素(最后一个数)比较......
  • 「代码随想录算法训练营」第十天 | 栈与队列 part2
    150.逆波兰表达式求值题目链接:https://leetcode.cn/problems/evaluate-reverse-polish-notation/题目难度:中等文章讲解:https://programmercarl.com/0150.逆波兰表达式求值.html视频讲解:https://www.bilibili.com/video/BV1kd4y1o7on题目状态:多次修改bug后通过个人思路:......
  • 机器学习算法-决策树
    一、决策树简介    决策树是一种分类与回归的方法,它以树形结构的形式进行呈现,可以认为是if-then规则的集合,也可以认为是特征空间与类空间上的条件概率分布。二、如何理解决策树?    我们大部分人都有过租房子的经历,那你是怎么决定要租一个房子的呢?我们一般判......
  • 【算法】求 x 的 n 次方
    1.概述题目链接牛客网题目描述给定一个double类型的浮点数x和int类型的整数n,求x的n次方。1.1解题思路最直观的解法是将x重复乘n次,x\*x\*x...\*x,那么时间复杂度为O(N)。因为乘法是可交换的,所以可以将上述操作拆开成两半(x\*x..\*x)\*(x\*x..\*x),两......
  • 基于ACO蚁群优化算法的WSN网络路由优化matlab仿真
    1.程序功能描述      基于ACO蚁群优化算法的WSN网络路由优化,通过蚁群优化迭代,在WSN中搜索一个最短的路由路径。在仿真过程中,实时显示每一次迭代过程中找到的路径,最后输出ACO的优化迭代过程,网络路由路径的搜索结果。 2.测试软件版本以及运行结果展示MATLAB2022a版本运......
  • 沁园春 · 算法
    OI风光,千里DP,万里背包。望深搜内外,唯余莽莽;广搜上下,顿失滔滔。山舞图论,原驰蜡象,欲与AC试比高。惜指针链表,略输文采;滚动数组,稍逊风骚;一代天骄,Vector数组,只识弯弓射大雕。俱往矣,数风流算法,还看今朝。OI风光,千里**DP**,万里**背包**。望**深搜**内外,唯余莽莽;**广搜**上......
  • 代码随想录算法训练营第八天| leetcode 344、541、卡码网54
    反转字符串 leetcode344classSolution{public:voidreverseString(vector<char>&s){intindex1=0,index2=s.size()-1;chartmp;while(index1<index2){tmp=s[index1];s[index1]=s[index2];......
  • Sentinel-1 Level 1数据处理的详细算法定义(二)
    《Sentinel-1Level1数据处理的详细算法定义》文档定义和描述了Sentinel-1实现的Level1处理算法和方程,以便生成Level1产品。这些算法适用于Sentinel-1的Stripmap、InterferometricWide-swath(IW)、Extra-wide-swath(EW)和Wave模式。今天介绍的内容如下:S......
  • KD树空间划分算法碰撞检测
    参考:KD树详解-CSDN博客 KD树(k-dimensionaltree)是一种用于多维空间中点数据的高效存储和检索的数据结构。在游戏开发中,KD树具有多种重要的应用,主要体现在以下几个方面:1.空间分区KD树可以用于将游戏世界划分为多个区域,从而提高碰撞检测、物体查询等操作的效率。通过将空间划......