题目描述
小明每周上班都会拿到自己的工作清单,工作清单内包含n项工作,每项工作都有对应的耗时时间(单位h)和报酬。工作的总报酬为所有已完成工作的报酬之和。请帮小明安排工作,保证小明在指定的工作时间内工作收入最大化。
输入:
- 第一行为两个正整数T, n。T代表工作时长(单位h, 0 < T < 1000000),n代表工作数量(1 < n ≤ 3000)。
- 接下来是n行,每行包含两个整数t, w。t代表该工作消耗的时长(单位h, t > 0),w代表该项工作的报酬。
输出:
- 小明在指定工作时长内工作可获得的最大报酬。
解题思路
这是一个典型的01背包问题,可以使用动态规划来解决。我们定义一个数组dp,其中dp[i]表示在工作时长为i时可以获得的最大报酬。通过遍历每个工作,并更新dp数组的值,最终dp[T]即为在最大工作时长T内可以获得的最大报酬。
解题步骤
-
初始化:
- 创建一个大小为T+1的数组dp,所有元素初始化为0。dp[0]表示在工作时间为0时,最大报酬为0。
-
遍历工作:
- 对于每个工作(t, w),其中t为耗时,w为报酬,从T开始向下遍历到t(即从大到小遍历,保证每个工作只被考虑一次)。
- 更新dp[i]的值:dp[i] = max(dp[i], dp[i-t] + w)。这表示如果当前工作被选择,则当前的最大报酬为不选择该工作时的最大报酬dp[i]与选择该工作后的报酬dp[i-t] + w中的较大值。
-
输出结果:
- 遍历结束后,dp[T]即为在最大工作时长T内可以获得的最大报酬,输出dp[T]。
示例代码(Java)
package cn.gov.test.gt4.swjggl.leetcode;
import java.util.Scanner;
public class MaxReward {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取输入
System.out.println("请输入工作时长T和工作数量n(用空格分隔):");
int T = scanner.nextInt();
int n = scanner.nextInt();
// 初始化工作数组
int[][] works = new int[n][2];
System.out.println("请输入每项工作的耗时和报酬(每行两个数,用空格分隔):");
for (int i = 0; i < n; i++) {
works[i][0] = scanner.nextInt(); // 耗时
works[i][1] = scanner.nextInt(); // 报酬
}
// 调用方法计算最大报酬
int maxReward = calculateMaxReward(T, works);
// 输出结果
System.out.println("在指定工作时长内工作可获得的最大报酬为: " + maxReward);
scanner.close();
}
/**
* 使用动态规划计算最大报酬
* @param T 工作时长
* @param works 工作数组,works[i][0]为耗时,works[i][1]为报酬
* @return 最大报酬
*/
public static int calculateMaxReward(int T, int[][] works) {
// 初始化动态规划数组,dp[i]表示在工作时长为i的情况下可以获得的最大报酬
int[] dp = new int[T + 1];
// 遍历每个工作
for (int[] work : works) {
int t = work[0]; // 耗时
int w = work[1]; // 报酬
// 从T开始向下遍历到t,更新dp数组
for (int j = T; j >= t; j--) {
// 当前工作时长j下的最大报酬为当前工作的报酬与剩余工作时长(j-t)下的最大报酬之和,与之前的最大报酬进行比较并取较大值
dp[j] = Math.max(dp[j], dp[j - t] + w);
}
}
// dp[T]即为在最大工作时长T内可以获得的最大报酬
return dp[T];
}
}
注意事项
- 在实现时,需要注意遍历顺序,即从大到小遍历工作时长,以避免重复选择同一工作。
- 题目中给出的工作数量和工作时长范围较大,因此需要考虑算法的效率,动态规划是解决此类问题的有效方法。
- 在实际机试中,输入数据会由测试系统提供,考生需要根据题目要求读取输入并输出结果。
运行解析
假如当T=10
(工作时长为10小时)且n=2
(有2项工作时),我们可以详细跟踪calculateMaxReward
方法的运行步骤。假设给定的两项工作分别是(2, 5)
(耗时2小时,报酬5)和(3, 6)
(耗时3小时,报酬6)。
以下是calculateMaxReward
方法的运行步骤:
-
初始化dp数组:
dp = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] # 长度为T+1=11的数组,初始值都为0
-
处理第一项工作(2, 5):
- 从
T=10
开始向下遍历到t=2
(工作的耗时)。 - 当
j=10
时,dp[10] = max(dp[10], dp[10-2] + 5) = max(0, 0 + 5) = 5
。 - 当
j=9
时,dp[9] = max(dp[9], dp[9-2] + 5) = max(0, 0 + 5) = 5
(注意:虽然这里dp[7]是0,但因为还没有更新dp[7]以下的值,所以仍然使用初始值0)。 - 当
j<9
时,由于j-2
会小于0,所以这些迭代实际上不会改变dp数组的值(但在这个特定例子中,我们不需要考虑它们,因为t=2
)。
此时,dp数组变为:
dp = [0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5]
- 从
-
处理第二项工作(3, 6):
-
再次从
T=10
开始向下遍历到t=3
(工作的耗时)。 -
当
j=10
时,dp[10] = max(dp[10], dp[10-3] + 6) = max(5, 5 + 6) = 11
(因为dp[7]在第一步后仍然是0,但这里我们使用的是更新后的dp[7]的值,即dp[7]的“未来值”,在实际代码中,这是通过从T向下遍历来保证的)。 -
当
j=9
时,dp[9] = max(dp[9], dp[9-3] + 6) = max(5, 2 + 6) = 8
(注意:这里dp[6]实际上是上一轮迭代中未更新的值,但在实际执行时,由于我们从T开始向下遍历,所以dp[6]在到达这里之前已经被更新为dp[6]的“未来最大值”)。然而,在这个特定步骤中,我们实际上不需要考虑dp[6]的值,因为dp[9]的更新将基于dp[6]的“未来值”(如果有的话),但在这个例子中,dp[9]的更新仅依赖于dp[6]的当前值(即0,因为还没有被更新),所以实际上dp[9]将更新为max(5, 0 + 6) = 6
,但由于dp[9]在第一步后已经是5,所以这里它会被更新为更大的值6(尽管在这个特定情况下,它实际上会被后续迭代覆盖为更大的值)。然而,为了清晰起见,我们保持上面的解释,但请注意实际执行中的差异。 -
当
j=8
时,dp[8] = max(dp[8], dp[8-3] + 6) = max(0, 5 + 6) = 11
(但这里dp[8]实际上在后续迭代中会被覆盖为更小的值,因为dp[5]在后续没有工作可以更新它,所以保持为0)。然而,重要的是要注意,在动态规划中,我们总是取到当前为止的最大值,所以即使dp[8]后来被覆盖,dp[10]的值已经基于当前信息被正确计算。 -
类似地,我们可以继续计算
j=7
和j=6
(尽管在这个例子中,它们不会影响最终结果,因为dp[10]的值已经确定)。
最终,dp数组(在考虑到所有迭代后)将类似于:
dp = [0, 0, 0, 0, 0, 0, 6, 6, 6, 11, 11] # 注意:这里的dp[7], dp[8], dp[9]的值是示例性的,实际值可能因实现而异
但重要的是,我们关注的是
dp[10]
的值,它是11,这表示在10小时的工作时长内可以获得的最大报酬是11。 -
请注意,上面的dp数组在j=8
和j<8
时的更新是示例性的,并且可能因实际代码实现中的遍历顺序和更新逻辑而略有不同。然而,关键点是dp[10]
的值将正确地反映在给定时长和给定工作下的最大报酬。