本文针对活动选择问题,对不同情况分别使用了贪心算法和动态规划。文中两个问题基本上全部涵盖的活动选择问题,所以说起到了团灭这类问题,看完本文就再也不怕活动选择问题啦。
(图片来源:一个博主文章里的)我也想用一下,哈哈。
附上Leetcode上和活动选择类似的题,希望大家能举一反三,活学活用。
Leetcode 1235. 规划兼职工作
一 活动选择问题:选择活动的最大个数
1.1 问题背景
1.2 贪心策略
下面有三种贪心策略,只有第三种是正确的。这是因为策略3:最早结束活动优先
选择最早结束的活动,可以给后面的活动留更大的选择空间。
1.3 代码实现
贪心算法的时间复杂度为O(nlogn),也就是排序的时间复杂度
class Solution1:
def activitiesSelect(self, activities):
# 把活动集合按照每个活动的结束时间升序排序
activities.sort(key=lambda x :x[1])
# print(activities)
res = [activities[0]] # 初始化
n = len(activities)
for i in range(1, n): # 检查每个活动
if activities[i][0] >= res[-1][1]: # 下一个开始活动的时间大于等于,前一个活动结束的时间
res.append(activities[i])
return res
activities = [[3,5], [0,6],[1,4], [8,11], [8,12], [5,7], [3,9], [5,9], [6,10], [2,14], [12,16]]
print(Solution1().activitiesSelect(activities))
输出结果
[(1, 4), (5, 7), (8, 11), (12, 16)]
说明:如果活动的开始时间与结束时间是分开的,首先将两者对应合并成如上述activities列表。然后再进行接下来的过程即可。
方法也十分简单,如下:
start = [1,3,0,4,3,5,6,8,8,2,12] # 活动开始时间
end = [4,5,6,7,9,9,10,11,12,14,16] # 活动结束时间
activities = []
for i in range(len(start)):
activities.append([start[i], end[i]])
print(activities)
二 带权活动选择问题
扩展问题:会场出租,选择出租的活动时间不能冲突,活动出租收益各不相同。怎样选让收益总和最大?
2.1 问题定义
这个问题就是在第一个问题的基础上增加了一个活动权重,因此可称为带权活动选择问题。
2.2 问题比较
第一个问题可看作权重都为1的带权活动选择问题。那么是否还能继续使用贪心算法呢?从下面的图可知,使用贪心算法是不正确的。
2.3 从贪心策略到动态规划
存在重叠子问题,使用动态规划求解。但是在这之前首先要进行的是数据预处理。
排序:按活动结束时间升序
求𝒑[𝒊]:在𝒂𝒊开始前最后结束的活动
排序
求p[i]
排序后使用用两个循环遍历求p[i]
2.4 递推关系建立:构造递推公式
计算实例结果
2.5 代码实现
时间复杂度是O(n^2), 就是在求p[i]是使用了两层循环。其余具体过程详见代码,注释比较完整。
class Solution2: # 带权活动选择问题
def rentActiv(self, act):
# 预处理
n =len(act)
act.sort(key=lambda x: x[1]) # 排序
# print(act)
p = [0] * (n) # 在a_i开始前最后结束的活动
for i in range(1, n):
for j in range(i-1, -1, -1):
if act[j][1] <= act[i][0]: # act[i]的开始时间大于等于act[j]的结束时间,并且要act[j]是最后结束的活动
p[i] = j + 1 # 这里为了让活动与下标一样,+1
break # 当第一个满足判断条件的就停止,此时的j就是在act[i]开始前最后结束的活动
# return p
# 动态规划
dp = [0] * (n+1)
rec = [0] * n
for j in range(0, n): # D[j] = max(D[p[j]] + w[j], D[j-1])
if dp[p[j]] + act[j][2] > dp[j]:
dp[j+1] = dp[p[j]] + act[j][2]
rec[j] = 1
else:
dp[j+1] = dp[j]
rec[j] = 0
return dp, p, rec
if __name__ == "__main__":
activities = [[1,4,1],[3,5,6], [0,6,4], [4,7,7], [3,9,3], [5,9,12], [6,10,2], [8,11,9], [8,12,11], [2,14,8]]
dp, p, rec = Solution2().rentActiv(activities)
print("dp:", dp)
print("rec:", rec)
print("revenue:",dp[-1])
selectActivities = []
k = len(activities)-1
while k >= 0:
if rec[k] == 1:
selectActivities.append(activities[k])
# 回溯子问题
k = p[k] - 1 # 这里要减去1,是因为在记录p数组的时候加了1,母的是为了与activities[k]对应
else:
k -= 1 # 不选择活动a[k]
selectActivities.reverse()
print("选择方案:", selectActivities)
输出结果
dp: [0, 1, 6, 6, 8, 8, 18, 18, 18, 19, 19]
rec: [1, 1, 0, 1, 0, 1, 0, 0, 1, 0]
revenue: 19
选择方案: [[1, 4, 1], [4, 7, 7], [8, 12, 11]]
由输出结果这与计算结果的一样,说明方法是可行的。
附上伪代码
参考资源
算法设计与分析—童咏昕 https://www.icourse163.org/learn/BUAA-1449777166?tid=1463474515#/learn/announce