Activity Selection:给定一组具有开始时间和结束时间的活动A,找到相互兼容的活动的最大子集。
我的问题
两种方法似乎相同,但是firstApproach中的numSubproblems是指数的,而secondApproach中的numSubproblems是O(n^2)
。如果我要记住结果,那么我该如何记住firstApproach?
天真的第一方法
let max = 0
for (a: Activities):
let B = {Activities - allIncompatbleWith(a)}
let maxOfSubproblem = ActivitySelection(B)
max = max (max, maxOfSubproblem+1)
return max
1. Assume a particular activity `a` is part of the optimal solution
2. Find the set of activities incompatible with `a: allIncompatibleWith(a)`.
2. Solve Activity for the set of activities: ` {Activities - allImcompatibleWith(a)}`
3. Loop over all activities `a in Activities` and choose maximum.
基于CLRS第16.1节的第二种方法
Solve for S(0, n+1)
let S(i,j) = 0
for (k: 0 to n):
let a = Activities(k)
let S(i,k) = solution for the set of activities that start after activity-i finishes and end before activity-k starts
let S(k,j) = solution for the set of activities that start after activity-k finishes and end before activyty-j starts.
S(i,j) = max (S(i,k) + S(k,j) + 1)
return S(i,j)
1. Assume a particular activity `a` is part of optimal solution
2. Solve the subproblems for:
(1) activities that finish before `a` starts
(2) activities that start after `a` finishes.
Let S(i, j) refer to the activities that lie between activities i and j (start after i and end before j).
Then S(i,j) characterises the subproblems needed to be solved above. ),
S(i,j) = max S(i,k) + S(k,j) + 1, with the variable k looped over j-i indices.
我的分析
firstApproach:
#numSubproblems = #numSubset of the set of all activities = 2^n.
secondApproach:
#numSubproblems = #number of ways to chooose two indicises from n indices, with repetition. = n*n = O(n^2)
两种方法似乎相同,但是firstApproach中的numSubproblems是指数的,而secondApproach中的numSubproblems是O(n^2)
。有什么收获?他们为何不同,甚至认为这两种方法似乎相同?
答案 0 :(得分:1)
这两种方法似乎是相同的
两种解决方案不相同。不同之处在于搜索空间中可能的状态数。两种解决方案都存在重叠的子问题和最佳子结构。无需记忆,这两种解决方案都可以浏览整个搜索空间。
这是一个backtracking解决方案,其中尝试与活动兼容的所有子集,并且每次选择一项活动时,您的候选解决方案都会增加1并与当前存储的最大值进行比较。它不了解活动的开始时间和结束时间。主要区别在于重复发生的状态是活动(兼容活动)的整个子集,需要为其确定解决方案(无论其开始和结束时间如何)。如果要记住解决方案,则必须使用位掩码(或(在C ++中为std::bitset
)来存储解决方案的一部分活动,也可以使用std::set
或其他{{1 }}数据结构。
第二个解决方案中子问题的状态数量大大减少,因为递归关系仅解决了在当前活动开始之前完成的活动以及在当前活动开始之后开始的活动当前活动完成。请注意,在这种解决方案中,状态的数量由元组的可能值(开始时间,结束时间)确定。由于存在 n 个活动,因此状态的数量最多为 n 2 。如果我们记住此解决方案,则只需要为给定的开始时间和结束时间存储解决方案,这将自动为属于该范围的活动子集提供解决方案,无论它们之间是否兼容。
答案 1 :(得分:0)
记忆化总是不会导致多项式时间渐近时间的复杂性。在第一种方法中,您可以应用记忆,但是不会减少多项式时间的时间复杂度。
用简单的话来说,记忆只是递归解决方案(自上而下),该解决方案将计算出的解决方案的结果存储到子问题中。而且,如果要再次计算相同的子问题,则返回原始存储的解决方案,而不是重新计算它。
在您的情况下,每个子问题都在为子集找到最佳的活动选择。因此,备忘录(针对您的情况)将导致为所有子集存储最佳解决方案。
毫无疑问,通过避免对以前“见过”的活动子集重新计算解决方案,记忆化将为您带来性能增强,但由于您最终无法将时间复杂度降低为多项式时间,因此记忆化存储每个子集的子解决方案(在最坏的情况下)。
另一方面,如果看到this,其中对斐波那契数列应用了备忘,则必须存储的子解决方案总数与输入的大小成线性关系。从而将指数复杂度降低为线性。
要在第一种方法中应用记忆,您需要维护子解决方案。您可以使用的数据结构为Map<Set<Activity>, Integer>
,它将为给定的Set<Activity>
存储最大数量的兼容活动。在Java equals()
中,java.util.Set
可以在所有实现中正常工作,因此您可以使用它。
您的第一种方法将被这样修改:
// this structure memoizes the sub-solutions
Map<Set<Activity>, Integer> map;
ActivitySelection(Set<Activity> activities) {
if(map contains activities)
return map.getValueFor(activities);
let max = 0
for (a: activities):
let B = {Activities - allIncompatbleWith(a)}
let maxOfSubproblem = ActivitySelection(B)
max = max (max, maxOfSubproblem+1)
map.put(activities, max)
return max
}
要注意的一点:
第二个解决方案(CLRS 16.1)的时间复杂度将为O(n^3)
,而不是O(n^2)
。您必须为i
,j
和k
设置3个循环。此解决方案的空间复杂度为O(n^2)
。