任务调度问题:从Top-Down转Bottom-Up动态规划求解
我有一个包含每个任务成本和所需时间的列表,两台服务器分别为付费服务器与免费服务器。使用免费服务器的前提是需先在付费服务器上运行任务:付费服务器执行任务的时长为time数组指定值,免费服务器执行任意任务仅需1单位时间。需找到运行所有任务的最小成本。
我已经实现了Top-Down方式的解法:
@cache def dfs(job_idx, time_spent): if job_idx == n: return 0 if time_spent >= 0 else float('inf') # 选择付费服务器:加上当前任务成本,累计时间增加time[job_idx] option1 = cost[job_idx] + dfs(job_idx + 1, time_spent + time[job_idx]) # 选择免费服务器:无成本,累计时间减少1(因为免费任务消耗1单位付费时长额度) option2 = dfs(job_idx + 1, time_spent - 1) return min(option1, option2) dfs(0, 0)
这是基于DFS的Top-Down方法,遍历所有任务分配给付费/免费服务器的可能方案。但我希望转换为Bottom-Up方法却不知如何着手——我能分别用两种方法解决DP问题,但不会相互转换。我自己总结了转换的步骤:
- 识别递归函数。
- 识别递归函数的基准情况。
- 创建二维数组存储子问题结果。
- 填充数组的基准情况(递归基准返回值)。
- 嵌套循环遍历子问题节点/状态,用已有结果填充剩余值。
- 返回初始子问题状态对应的结果。
请求帮忙解释如何针对这个问题实现两种方法的转换?
按照你总结的6个步骤,结合当前问题逐一拆解:
步骤1:识别递归函数
你的递归函数dfs(job_idx, time_spent)的核心含义是:处理到第job_idx个任务时,当前累计的付费时长余额为time_spent,此时完成剩余任务的最小成本。
- 状态变量:
job_idx:当前处理的任务索引,范围是0到n(n为任务总数)time_spent:累计付费时长余额,代表当前可用的、能支撑免费任务的额度
- 状态转移逻辑:每个状态有两个选择
- 用付费服务器:支付当前任务成本,余额增加
time[job_idx](新增付费时长额度) - 用免费服务器:无成本,余额减少1(消耗1单位付费时长额度)
- 用付费服务器:支付当前任务成本,余额增加
步骤2:识别递归基准情况
对应递归终止条件,当job_idx == n(所有任务处理完毕):
- 若
time_spent >= 0:免费任务的额度足够,无额外成本,返回0 - 若
time_spent < 0:免费任务用超了,不符合规则,返回无穷大(表示该状态不可行)
步骤3:创建二维数组存储子问题结果
首先确定time_spent的取值范围:
- 最大值:所有任务都用付费服务器,余额为
sum(time) - 最小值:所有任务都用免费服务器,余额为
0 - n(初始余额为0,每个免费任务减1)
由于数组索引不能为负,我们给余额加一个偏移量offset = n,将负索引转为非负。最终创建二维DP数组dp[job_idx][balance + offset],其中:
job_idx范围:0到nbalance + offset范围:0到sum(time) + n
步骤4:填充数组的基准情况
对应递归的终止条件,当job_idx = n时:
- 遍历所有可能的
balance值,若balance >= 0,则dp[n][balance + offset] = 0 - 若
balance < 0,则dp[n][balance + offset] = float('inf')
步骤5:嵌套循环遍历子问题节点,填充剩余值
Top-Down是从job_idx=0向job_idx=n递归,Bottom-Up需要反向遍历:从job_idx = n-1到job_idx=0,这样计算当前任务状态时,下一个任务的状态已经完成计算。
遍历逻辑:
对于每个job_idx(从n-1到0): 遍历所有可能的balance值(从-n到sum(time)): # 计算选择付费服务器的成本 新余额1 = balance + time[job_idx] 选项1成本 = cost[job_idx] + dp[job_idx+1][新余额1 + offset] # 计算选择免费服务器的成本 新余额2 = balance - 1 选项2成本 = dp[job_idx+1][新余额2 + offset] # 当前状态的最小成本取两者较小值 dp[job_idx][balance + offset] = min(选项1成本, 选项2成本)
注意:如果新余额超出预设的范围(比如大于sum(time)或小于-n),直接设为无穷大表示该选择不可行。
步骤6:返回初始子问题状态对应的结果
初始状态是job_idx=0、balance=0,对应数组中的位置是dp[0][0 + offset],直接返回该值即可。
完整Bottom-Up代码示例
def min_cost(cost, time): n = len(cost) total_time = sum(time) offset = n # 偏移量,处理负余额的索引问题 max_balance = total_time min_balance = -n # 初始化DP数组,默认值设为无穷大 dp = [[float('inf')] * (max_balance - min_balance + 1) for _ in range(n+1)] # 填充基准情况:所有任务处理完毕时的状态 for balance in range(min_balance, max_balance + 1): if balance >= 0: dp[n][balance + offset] = 0 # 从后往前遍历任务,填充DP数组 for job_idx in range(n-1, -1, -1): for balance in range(min_balance, max_balance + 1): # 选项1:使用付费服务器 new_balance1 = balance + time[job_idx] option1 = float('inf') if max_balance >= new_balance1 >= min_balance: option1 = cost[job_idx] + dp[job_idx+1][new_balance1 + offset] # 选项2:使用免费服务器 new_balance2 = balance - 1 option2 = float('inf') if max_balance >= new_balance2 >= min_balance: option2 = dp[job_idx+1][new_balance2 + offset] dp[job_idx][balance + offset] = min(option1, option2) return dp[0][0 + offset]
内容的提问来源于stack exchange,提问作者AS Scorpio

