贪心算法
从活动选择问题引出贪心算法,然后说明贪心算法和动态规划的异同点,并说明贪心算法的求解步骤。
活动选择问题
【问题描述】假设有n个活动的组合 $S = \{ a_1,a_2, ...,a_n \}$,这些活动使用同一个教室,同一时刻,教室只能举行一个活动,每个活动$a_i$都有一个开始时间$s_i$和一个结束时间$f_i$, 其中 $0 \leq s_i < f_i $,若活动被选中,则发生在区间$[s_i, f_i)$内。 假如两个活动$a_i, a_j$,满足$[s_i, f_i)$和$[s_j, f_j)$不重叠,则它们是兼容的。 活动选择问题是,在这些活动当中选择一个最大的兼容活动集,它是活动的数量最多的集合。
动态规划思想求解
寻找这个问题的最优子结构,原问题是寻找事件S中的最多的活动,令$s_{min}$表示S中最早开始的活动, $f_{max}$是最晚结束的活动,原问题等价于选择时间$[s_{min}, f_{max}]$内的活动。 我们假设活动$a_i$是最优解当中的一个值,它的时间为$[s_i, f_i)$,那么当我们选择$a_i$以后,会出现两个子问题,选择时间$[s_{min}, s_i]$ 和时间$[f_i, f_{max}]$的活动。这样就可以建立递归方程,使用动态规划的思想来进行求解。而这样的$a_i$有n个,在这n个当中选择最大值。
递归的方法如下:
def activity_selection(s, f, start_time, end_time): """首先是做一个选择,并且假设该选择有最大值,在给选择下产生子问题""" if start_time == end_time: return 0 max_value = 0 for i in range(len(s)): current_start = s[i] current_end = f[i] if current_start >= start_time and current_end <= end_time: max_value = max(max_value, activity_selection(s, f, start_time, current_start)+activity_selection(s, f, current_end, end_time) + 1) return max_value if __name__ == '__main__': # s是活动开始的时间,f是活动结束的时间,按照活动结束的时间进行了排序。 s = [1, 3, 0, 5, 3, 5, 6, 8, 8, 2, 12, 14] f = [4, 5, 6, 7, 9, 9, 10, 11, 12, 14, 16, 20] start_time = min(s) end_time = max(f) print(activity_selection(s, f, start_time, end_time))
动态规划方法如下:
def activity_selection(s, f, start_time, end_time): total_time = end_time - start_time + 1 memorized = [[0 for i in range(total_time+1)] for i in range(total_time+1)] for i in range(1, total_time+1): for j in range(1, total_time+1): max_value = 0 for index in range(len(s)): current_start = s[index] - start_time current_end = f[index] - start_time if current_start-start_time >= i and current_start-start_time <=j and current_end >=i and current_end <= j: max_value = max(max_value, memorized[i][current_start] + memorized[current_end][j] + 1) memorized[i][j] = max_value return memorized[1][-1]
贪心算法求解
在这个问题当中,我们在选择一个最优的活动的时候,有n种选择,这也是动态规划的特点,在发现最优子问题以后,将原问题分解为子问题以后,在诸多的子问题当中选择最优的那一个。 那么我们可不可以直接选择一个活动,那一个的结果就是最优的,这就引出了贪心算法。 在这个问题当中,我们每次选择一个结束时间最早的活动,并且证明一直做这样的选择能够让我们得到最优解,证明如下:
所以,当我们用贪心算法来求解的时候,只需要进行一个选择即可,
递归的贪心算法如下:
def recursive_activity_selector(s, f, k, n): activity = [] # 活动m的开始时间应该小于活动k的结束时间 m = k + 1 while m <= n and s[m] < f[k]: m += 1 if m <= n: return [[s[m],f[m]]]+ recursive_activity_selector(s, f, m, n) else: return activity if __name__ == '__main__': s = [1, 3, 0, 5, 3, 5, 6, 8, 8, 2, 12, 14] f = [4, 5, 6, 7, 9, 9, 10, 11, 12, 14, 16, 20] # 插入0,表示前面有一个活动是从0开始,方便后续运算 s.insert(0, 0) f.insert(0, 0) m = 0 n = len(s) print(recursive_activity_selector(s, f, 0, n-1)) # result [[1, 4], [5, 7], [8, 11], [12, 16]]
非递归版的贪心算法如下:
def greedy_activity_selector(s, f): """选择结束时间最早的活动""" activity = [[s[0], f[0]]] pre_activity = 0 for index in range(1, len(s)): if s[index] >= f[pre_activity]: pre_activity = index activity.append([s[index], f[index]]) return activity if __name__ == '__main__': s = [1, 3, 0, 5, 3, 5, 6, 8, 8, 2, 12, 14] f = [4, 5, 6, 7, 9, 9, 10, 11, 12, 14, 16, 20] print(greedy_activity_selector(s, f))
贪心算法
贪心算法和动态规划问题一样,也需要最优子结构,原问题的最优解能够由子问题的最优解来构造。和动态规划不同的是,动态规划将原问题划分为子问题的时候涉及到诸多的选择,我们在这些选择当中选择一个最优的解,而贪心算法只涉及到一个选择:选择当前的最优解,不过贪心算法需要对这个选择进行证明,证明一直按照局部最优解进行选择,结果是全局最优解,这个性质叫做贪心选择性质。能用贪心算法解决的问题,都是可以用动态规划来解决。
贪心算法分为以下步骤:
1、将最优化问题转化为这样的形式:对其作出一次选择后,只剩下一个子问题需要求解。
2、证明作出贪心选择后,原问题总是存在最优解,即贪心算法是安全的。
3、证明作出贪心算法后,剩下的子问题满足性质:其最优解与贪心选择组合即可得到原问题的最优解,这样就得到了最优子结构。
0-1背包和分数背包问题
0-1背包问题的求解可以参考我的博客:背包问题 。 分数背包问题和0-1背包问题很像,但是物品的重量是可以分的,在选择拿物品的时候可以拿走一部分,而不必是整个物品。 分数背包问题可以用贪心算法来求解,但是0-1背包问题却不能用贪心算法来求解。分数背包问题的求解如下:
def fraction_package(w, v, m): """ 分数背包问题, 使用贪心算法来求解 Args: w: weight v: value m: max weight of blg """ current_weight = 0 max_value = 0 veight_per_weight = [] for i in range(len(w)): veight_per_weight.append(v[i]/w[i]) # 按照物品的单位价值,从大到小对index进行排序。 value_per_weight_index = sorted(range(len(veight_per_weight)), key = lambda x:veight_per_weight[x], reverse=True) # 每次选择,总是选择单位价值最高的物品 for index in value_per_weight_index: if current_weight < m: remainder_weight = m - current_weight if remainder_weight > w[index]: current_weight += w[index] max_value += v[index] else: current_weight += remainder_weight max_value += veight_per_weight[index] * remainder_weight return max_value if __name__ == '__main__': w = [10, 20, 30] v = [60, 100, 120] m = 50 print(fraction_package(w, v, m))
参考:
算法导论16章