背包问题-动态规划算法(附带Python代码解析)

一.背包问题概述:

给定 n 种物品和一个容量为 capacity 的背包,其中每一个物品的重量和价值已知 。

问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?

二.分析过程:

1.思路:

对于每一个物品只有两种选择,第一种情况:装入当前物品;第二种情况:不装入当前物品。

我们从第一个物品开始,将其重量和背包容量进行比较,如果比背包容量小,则选择将这个物品装入背包,记录它的价值(如果比背包容量大,忽略这个物品的重量和价值,直接比较下一个物品);再拿下一个物品的重量与当前背包容量比较(此时背包容量为背包的总容量减去已经装入背包的物品重量),如果比当前背包容量小,则选择将这个物品装入背包,将这个物品的价值与已经装入背包的物品价值相加(如果比背包容量大,忽略这个物品的重量和价值,直接比较下一个物品)......以此类推。

2.具体实例:

题目:

假设一共有 4 个物品,重量分别为 weights = [2, 3, 6, 7],对应的物品价值为values = [1, 4, 5, 6],背包容量为 8,应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?

解析:

初始化一个二维矩阵,i 为当前物品,j 为背包容量,dp[ i ][ j ] 即为在第 i 件物品时,背包容量为 j 时所能获得的最大价值。

看下述列表,第一行 0 ~ 8 为背包的容量,第一列 [0, 0], [1, 2], [4, 3], [5, 6], [6, 7],其中第一个数为物品的价值,第二个数为物品的重量。将物品的重量依次与背包容量相比较。

我们以 [5, 6] 这一排举例,此时一共有 3 种物品,重量分别为2, 3, 6:

(1) 当背包容量为 0 和 1 时,最小重量 2 的物品也装不下,此时价值为 0;

(2) 当背包容量为 2 时,能装下重量为 2 的物品,此时其价值为最大价值,为 1;

(3) 当背包容量为 3 时,能装下重量为 2 或 3 的物品,3 物品的价值为 4,大于 2 物品的价值,此时 3 物品的价值为最大价值,为 4(因为重量 3 + 2 = 5 大于此时背包容量,不能将两个物品都放入背包,因此不能将它们的价值相加)

(4) 当背包容量为 4 时,能装下重量为 2 或 3 的物品,3 物品的价值为 4,大于 2 物品的价值,此时 3 物品的价值为最大价值,为 4(因为重量 3 + 2 = 5 大于此时背包容量,不能将两个物品都放入背包,因此不能将它们的价值相加)

(5) 当背包容量为 5 时,能装下重量为 2 和 3 的物品,此时不能再装入重量为 6 的物品,背包容量不够,因此此时最大价值为重量为 2 和 3 的物品价值相加,1 + 4 = 5,此时最大价值为5;

......

以此类推。

物品价值和重量\背包容量012345678
0,0000000000
1,2001111111
4,3001441+41+41+41+4
5,6001441+4551+5
6,7001441+4566
物品价值和重量\背包容量012345678
0,0000000000
1,2001111111
4,3001445555
5,6001445556
6,7001445566

代码解释:

(1)values[i - 1] + dp[i - 1][j - weights[i - 1]]:

这是背包容量可以放下第 i 件物品的情况,并且考虑拿这个物品是否会获得最大价值,dp[i - 1][j - weights[i - 1]]表示在第 i - 1 个物品,背包容量为 j -  weights[i - 1]时的最大价值,背包容量减去要拿的物品的重量。

(2)dp[i - 1][j]:

这是背包容量放不下第 i 件物品的情况,选择不拿,不计算其价值。

三.Python代码实现:

def bagProblem(weights, values, capacity):
    '''
    :param weights: 物品重量
    :param values: 物品价值
    :param capacity: 总容量
    :return:装入背包中的物品的最大总价值
    '''
    # 获取weights列表的长度
    n = len(weights)
    # 初始化一个所有元素都为0的矩阵,然后逐行填充矩阵的值
    dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]
    # 遍历物品,将每一个物品的重量和当前背包容量相比较
    for i in range(1, n + 1):
        # 遍历背包容量
        for j in range(1, capacity + 1):
            # 如果当前物品的重量小于等于背包容量
            if weights[i - 1] <= j:
                # 选择当前物品和不选择当前物品的最大值(上述分析过程中有对这句代码的具体解释)
                dp[i][j] = max(values[i - 1] + dp[i - 1][j - weights[i - 1]], dp[i - 1][j])
            else:
                # 如果当前物品的重量大于背包容量,则不选择当前物品
                dp[i][j] = dp[i - 1][j]
    return dp[n][capacity]


# 物品的重量,价值和背包容量
values = [1, 4, 5, 6]
weights = [2, 3, 6, 7]
capacity = 8

# 计算最大价值
max_value = bagProblem(weights, values, capacity)
print("最大价值为:", max_value)

### Java ArrayList 底层实现原理 #### 1. 数据结构 `ArrayList` 是基于动态数组的数据结构来实现的。它内部维护了一个 `Object[]` 类型的数组 `elementData`,用于存储实际的元素[^3]。 ```java transient Object[] elementData; ``` 该数组初始状态为空或者具有指定的初始容量,随着元素的增加,当达到容量上限时会自动扩容以适应更多的元素。 --- #### 2. 初始化过程 `ArrayList` 提供了多种构造函数来进行实例化: - **无参构造器** 当使用无参构造器创建 `ArrayList` 对象时,默认不会分配具体的内存空间,而是设置一个特殊的标记值 `DEFAULTCAPACITY_EMPTY_ELEMENTDATA` 表示尚未初始化的实际数组。 ```java public ArrayList() { this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA; } ``` - **带初始容量的构造器** 如果指定了初始容量,则会在创建对象的同时分配相应的内存空间[^2]。 ```java public ArrayList(int initialCapacity) { if (initialCapacity > 0) { this.elementData = new Object[initialCapacity]; } else if (initialCapacity == 0) { this.elementData = EMPTY_ELEMENTDATA; } else { throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); } } ``` --- #### 3. 添加操作 (`add`) 向 `ArrayList` 中添加元素的核心逻辑在于调整底层数组的空间并放置新元素。以下是主要流程: - 调用 `ensureCapacityInternal` 方法检查当前数组是否有足够的空间容纳新增加的元素。如果没有,则触发扩容机制。 - 将新元素放入数组的最后一个可用位置,并更新记录的大小变量 `size`[^4]。 ```java public boolean add(E e) { ensureCapacityInternal(size + 1); // 扩容检查 elementData[size++] = e; // 存储元素 return true; } ``` --- #### 4. 删除操作 (`remove`) 删除某个索引处的元素涉及两个步骤: - 验证目标索引的有效性; - 移动后续所有元素向前覆盖被移除的位置,并减少计数器 `size` 的值[^3]。 ```java public E remove(int index) { rangeCheck(index); // 检查越界 modCount++; // 修改次数统计 E oldValue = elementData(index); int numMoved = size - index - 1; if (numMoved > 0) System.arraycopy(elementData, index+1, elementData, index, numMoved); elementData[--size] = null; // 清理垃圾回收候选者 return oldValue; } ``` --- #### 5. 更新操作 (`set`) 通过索引来替换已有元素的过程相对简单,只需验证索引合法性后直接修改对应位置的内容即可[^3]。 ```java public E set(int index, E element) { rangeCheck(index); // 检查越界 E oldValue = elementData(index); elementData[index] = element; // 替换旧值为新值 return oldValue; } ``` --- #### 6. 查询操作 (`get`) 获取特定索引上的元素仅需访问底层数组相应下标的项。 ```java public E get(int index) { rangeCheck(index); // 检查越界 return elementData(index); // 返回对应的元素 } ``` --- #### 7. 容量扩展机制 每当尝试超出现有容量范围插入新条目时,都会执行扩容动作。具体做法是按照一定比例增大原有尺寸(通常翻倍),然后重新构建更大的临时缓冲区并将之前保存的所有项目迁移过去。 ```java private void grow(int minCapacity) { int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量约为原来的1.5倍 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; ... elementData = Arrays.copyOf(elementData, newCapacity); } ``` 这种设计使得频繁追加成为高效的操作之一,因为大多数情况下无需立即改变容器规模就能继续接纳更多成员加入其中。 --- #### 性能特点总结 由于其依赖连续区块作为载体的缘故,在随机存取方面表现出色;然而对于中间部分的大批量剔除或是前置区域的小幅度变动而言则显得较为低效,因为它可能牵涉到大量位移工作。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值