问题重述
小S打算在码头租用货船,那里提供了Q种不同类型的货船。每一种货船都有着明确的相关信息,像数量是m[i]、租赁成本为v[i]以及最大载货量是w[i]。小S手里有V元的预算,她期望能在这个预算范围内,找出一种货船组合,使得所租用的这些货船能够承载的总货物量达到最大。
这里有几个关键的量:
- Q代表货船的种类数量;
- V代表小S能够使用的总预算(单位:元);
- ships是一个列表,列表里的每个元素都是一个元组,像[m[i], v[i], w[i]]这样,它们依次表示第i种货船的数量、租赁价格以及每艘货船的最大载货量。
现在要问的是:在给定的预算V元的限制下,小S所能租用的货船的最大总载货量是多少呢?
思路分析
1. 定义数据结构与输入数据
- 首先,我们已经明确了几个关键变量:
Q(货船种类数量)、V(预算)以及ships(包含每种货船具体信息的列表,元素为形如[m[i], v[i], w[i]]的元组)。可以按照以下方式在Python中表示这些数据,例如通过用户输入或者从外部文件读取等方式获取相应的值来初始化这些变量。
2. 确定解决问题的方法(可以考虑使用动态规划或者暴力枚举等方式,这里以暴力枚举为例进行说明)
- 生成所有可能的租用组合:
- 由于每种货船都有一定的数量,我们可以通过多层嵌套循环(循环层数等于货船种类数量
Q)来生成所有可能的租用情况。例如,如果有3种货船,那就需要三层循环,每层循环分别控制对应种类货船租用的数量(从0到其最大数量m[i]),从而枚举出所有可能的租用组合。 - 在循环过程中,可以使用列表或者元组等数据结构来记录每种租用组合里每种货船租用的具体数量,比如
[num_ship1, num_ship2,..., num_shipQ],其中num_shipi表示第i种货船租用的数量。 - 计算每种组合的成本和载货量:
- 对于每一种生成的租用组合,我们需要计算该组合下租用这些货船的总成本以及总载货量。
- 计算总成本时,遍历租用组合里每种货船租用的数量以及对应的租赁成本
v[i],按照总成本 = ∑(num_shipi * v[i])(i从1到Q)的公式来计算,然后判断总成本是否超过给定预算V,如果超过则该组合不符合要求,可以直接跳过后续载货量计算等操作。 - 若总成本未超过预算,那就计算该组合下的总载货量,同样遍历租用组合里每种货船租用的数量以及对应的每艘货船最大载货量w[i],按照总载货量 = ∑(num_shipi * w[i])(i从1到Q)的公式来算出总载货量。 - 记录最大载货量: - 在计算完每一种符合预算要求的租用组合的总载货量后,我们需要比较并记录下最大的总载货量。可以初始化一个变量(比如
max_load)为0,每当计算出一个符合要求的组合的总载货量后,与max_load进行比较,如果大于max_load,则更新max_load的值为当前组合的总载货量。
3. 输出结果
- 经过上述步骤遍历完所有可能的租用组合后,最终记录的
max_load变量的值就是在给定预算下能够租用的货船的最大总载货量,将其输出展示给用户即可。 如果使用动态规划的方法来解决这个问题,思路如下: - 状态定义:可以定义一个二维数组(或者字典等合适的数据结构),比如
dp[i][j],其中i表示考虑前i种货船(i取值从0到Q),j表示预算为j(j取值从0到V),dp[i][j]的值表示在考虑前i种货船且预算为j时能获得的最大载货量。 - 状态转移方程推导: - 对于
dp[i][j],需要分情况讨论: - 当不租用第
i种货船时,dp[i][j]的值等于dp[i - 1][j],也就是只考虑前i - 1种货船且预算仍为j时的最大载货量。 - 当尝试租用第
i种货船时,要遍历第i种货船可能租用的数量k(从0到其最大数量m[i],并且要保证租用k艘第i种货船的成本k * v[i]不超过当前预算j),然后取dp[i - 1][j - k * v[i]] + k * w[i](表示前i - 1种货船在剩余预算下的最大载货量加上租用k艘第i种货船的载货量)的最大值,与不租用第i种货船的情况比较,取两者中的较大值作为dp[i][j]的值。 - 最终
dp[Q][V]的值就是在给定预算V下考虑所有Q种货船能获得的最大载货量,将其输出即可。 在实际编写Python代码时,需要根据上述思路选择合适的方法,通过定义函数、使用合适的循环和数据结构等来实现具体的逻辑,完成对问题的求解。
代码解析
1. solution 函数整体功能概述
solution 函数的目的是解决在给定预算内租用货船以获取最大载货量的问题,它采用了动态规划的思想来实现。通过构建二维动态规划数组 dp,逐步计算在考虑不同种类货船以及不同预算情况下能达到的最大载货量,最终返回在给定的货船种类数量 Q 和预算 V 下的最大载货量。
2. 函数内具体代码解析
- 初始化动态规划数组
dp:
dp = [[0] * (V + 1) for _ in range(Q + 1)]
这段代码创建了一个二维列表 dp,它的行表示考虑的货船种类数量(从 0 到 Q,共 Q + 1 行),列表示预算金额(从 0 到 V,共 V + 1 列)。初始时,所有元素都被设置为 0,表示在没有考虑任何货船或者预算为 0 等情况下,最大载货量自然为 0。
- 动态规划的状态转移过程(两层嵌套循环部分) :
for i in range(1, Q + 1):
m, v_cost, w_capacity = ships[i - 1]
for j in range(V + 1):
dp[i][j] = dp[i - 1][j]
for k in range(1, min(m, j // v_cost) + 1):
dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v_cost] + k * w_capacity)
- 外层循环(按货船种类遍历) :
for i in range(1, Q + 1):这个循环从 1 开始到Q + 1,表示依次考虑每一种货船(因为索引从 0 开始,所以i对应的是第i - 1种货船)。每次循环都在更新考虑当前这种货船时不同预算下的最大载货量情况。 - 中层循环(按预算金额遍历) :
for j in range(V + 1):这个循环从 0 到V,用于遍历不同的预算金额情况。在每次预算金额的迭代中,要去更新在当前考虑的货船种类和该预算金额下的最大载货量。 - 内层循环(尝试租用不同数量的当前货船) :
for k in range(1, min(m, j // v_cost) + 1):这个循环是在尝试租用当前货船(第i - 1种货船)不同数量的情况。min(m, j // v_cost)这部分表示能租用的最大数量,要取货船本身的最大数量m和当前预算j能承担的该货船最大数量(通过j // v_cost计算,即预算除以单艘货船租赁成本得到能租用的最大艘数)中的较小值。然后,在每次循环中,去更新dp[i][j]的值,取不租用当前货船(dp[i - 1][j])和尝试租用k艘当前货船(dp[i - 1][j - k * v_cost] + k * w_capacity,也就是在前i - 1种货船在剩余预算下的最大载货量基础上加上租用k艘当前货船带来的载货量)这两种情况中的最大值作为dp[i][j]的值。 - 返回最终结果: 、
return dp[Q][V]
在完成所有的动态规划状态转移计算后,返回 dp 数组中对应考虑所有 Q 种货船且预算为 V 时的最大载货量,也就是最终所求的在给定预算下租用货船能达到的最大总载货量。
3. 测试部分代码解析
ships = [[2, 3, 2], [3, 2, 10]]
print(solution(2, 10, ships) == 32)
这里定义了一个简单的 ships 列表,代表两种货船的信息(每种货船信息分别是数量、租赁成本、最大载货量),然后调用 solution 函数传入货船种类数量为 2、预算为 10 以及这个 ships 列表作为参数,计算得到的最大载货量结果并与预期的 32 进行比较,通过 print 输出比较结果(布尔值,表示是否相等)。
ships2 = [[30, 141, 47], [9, 258, 12], [81, 149, 13], [91, 236, 6], [27, 163, 74], [34, 13, 58], [61, 162, 1], [80, 238, 29], [36, 264, 28], [36, 250, 2], [70, 214, 31], [39, 116, 39], [83, 287, 4], [61, 269, 94], [23, 187, 46], [78, 33, 29], [46, 151, 2], [71, 249, 1], [67, 76, 85], [72, 239, 17], [61, 256, 49], [48, 216, 73], [39, 49, 74]]
print(solution(23, 400, ships2) == 1740)
同样的道理,这里定义了一个包含 23 种货船信息的更复杂的 ships2 列表,然后调用 solution 函数传入货船种类数量为 23、预算为 400 以及 ships2 列表作为参数,计算最大载货量并与预期的 1740 进行比较,通过 print 输出比较结果(布尔值),用于验证 solution 函数在更复杂数据场景下计算结果的正确性。
总的来说,这段代码完整地实现了使用动态规划求解在给定预算下租用货船获取最大载货量的功能,并通过简单和复杂的测试用例对函数进行了结果验证。
def solution(Q, V, ships):
dp = [[0] * (V + 1) for _ in range(Q + 1)]
for i in range(1, Q + 1):
m, v_cost, w_capacity = ships[i - 1]
for j in range(V + 1):
dp[i][j] = dp[i - 1][j]
for k in range(1, min(m, j // v_cost) + 1):
dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v_cost] + k * w_capacity)
return dp[Q][V]
ships = [[2, 3, 2], [3, 2, 10]]
print(solution(2, 10, ships) == 32)
ships2 = [[30, 141, 47], [9, 258, 12], [81, 149, 13], [91, 236, 6], [27, 163, 74], [34, 13, 58], [61, 162, 1], [80, 238, 29], [36, 264, 28], [36, 250, 2], [70, 214, 31], [39, 116, 39], [83, 287, 4], [61, 269, 94], [23, 187, 46], [78, 33, 29], [46, 151, 2], [71, 249, 1], [67, 76, 85], [72, 239, 17], [61, 256, 49], [48, 216, 73], [39, 49, 74]]
print(solution(23, 400, ships2) == 1740)
知识点总结
本题主要运用了以下几个知识点:
1. 动态规划
- 概念:动态规划是一种用于解决优化问题的算法策略,它通过将复杂问题分解为一系列相互关联的子问题,并避免重复计算子问题的解,从而高效地找到全局最优解。在本题中,通过定义二维数组
dp来记录不同状态(考虑不同种类货船以及不同预算情况下)下的最大载货量,这就是典型的动态规划思想的体现。把原问题逐步细化到考虑每一种货船、每一种预算金额这样的子问题,再通过状态转移方程(本题中根据是否租用当前货船等情况来更新最大载货量的相关逻辑)去构建子问题之间的联系,最终汇总得到整个问题的最优解(给定预算下的最大载货量)。 - 示例应用场景类比:就好比爬楼梯问题,假如要爬到第
n层楼梯,每次可以走 1 步或者 2 步,求有多少种不同的走法。可以通过定义dp[n]表示爬到第n层的走法数量,然后dp[n] = dp[n - 1] + dp[n - 2](因为到达第n层可以是从第n - 1层走 1 步上来,或者从第n - 2层走 2 步上来),通过这样逐步计算dp[1]、dp[2]等子问题,最终得到dp[n]的值,这和本题中利用动态规划计算最大载货量有着相似的思路,都是通过状态之间的关联递推来求解最优结果。
2. 二维列表(数组)的使用
- 概念:在Python中,使用列表推导式
dp = [[0] * (V + 1) for _ in range(Q + 1)]创建了二维列表dp。二维列表可以看作是一个表格结构,行和列分别代表不同的维度信息,在本题中,行维度对应着考虑的货船种类数量,列维度对应着预算金额。它能够方便地存储和访问在不同状态组合下(不同货船种类与不同预算组合)的数据(也就是最大载货量的值),便于在动态规划过程中进行状态的更新和查询。 - 示例:可以想象成一个班级成绩表,行表示不同的学生,列表示不同的科目,表格中的每个单元格(即二维列表中的每个元素)存储对应的学生在相应科目下的成绩,方便对成绩数据进行管理和分析,类似地在本题中方便对不同状态下的最大载货量进行记录和操作。
3. 循环结构(嵌套循环)
- 概念:代码中使用了多层嵌套循环,外层循环
for i in range(1, Q + 1)用于遍历货船的种类,中层循环for j in range(V + 1)用于遍历不同的预算金额,内层循环for k in range(1, min(m, j // v_cost) + 1)用于遍历当前货船可能租用的数量。通过这种嵌套循环结构,能够全面地覆盖所有可能的情况,对每种可能的租用组合进行分析和状态更新,以实现动态规划的计算过程。 - 示例类比:比如要打印一个九九乘法表,就需要两层嵌套循环,外层循环控制行数(相当于本题中控制货船种类),内层循环控制列数(相当于本题中控制预算或者租用数量等),通过循环的层层嵌套来实现对所有乘法运算情况的遍历输出,本题中也是借助这样的嵌套循环遍历不同状态下的各种租用场景来完成最大载货量的计算。
4. 列表的索引与元素解包
- 概念:在代码
m, v_cost, w_capacity = ships[i - 1]中体现了这两个知识点。ships是一个列表,其中每个元素又是包含三个元素的元组,通过ships[i - 1]这样的索引操作可以获取到对应位置的元组(表示第i种货船的信息),然后利用解包操作将元组中的三个元素分别赋值给m、v_cost和w_capacity,方便后续在计算中直接使用这些具体的货船信息(数量、租赁成本、最大载货量)。 - 示例:假设有一个学生信息列表,每个元素是一个包含学生姓名、年龄、成绩的元组,如
students = [("小明", 10, 90), ("小红", 11, 85)],如果要获取小明的年龄,就可以通过name, age, score = students[0]这样的解包操作,将元组里的元素分别赋值给对应的变量,然后就能直接使用age变量获取到小明的年龄为 10 了,和本题中获取货船信息的操作是类似的逻辑。
5. 函数的定义与调用
- 概念:通过
def solution(Q, V, ships):定义了solution函数,将实现计算最大载货量的逻辑封装在函数内部,使得代码结构更加清晰,便于复用和维护。然后通过print(solution(2, 10, ships) == 32)以及print(solution(23, 400, ships2) == 1740)这样的调用方式,传入相应的参数来执行函数,获取计算结果并与预期结果进行比较验证,展示了函数在代码中的使用流程和作用。 - 示例:就像定义一个计算两个数之和的函数
def add(num1, num2): return num1 + num2,之后可以在不同地方多次调用它,如result = add(3, 5),就能方便地得到计算结果 8,提高了代码编写的效率和灵活性,本题中solution函数的定义和调用也是出于同样的便于操作和验证结果的目的。