贪心算法

从活动选择问题引出贪心算法,然后说明贪心算法和动态规划的异同点,并说明贪心算法的求解步骤。

 

活动选择问题

【问题描述】假设有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种选择,这也是动态规划的特点,在发现最优子问题以后,将原问题分解为子问题以后,在诸多的子问题当中选择最优的那一个。 那么我们可不可以直接选择一个活动,那一个的结果就是最优的,这就引出了贪心算法。 在这个问题当中,我们每次选择一个结束时间最早的活动,并且证明一直做这样的选择能够让我们得到最优解,证明如下:

image

所以,当我们用贪心算法来求解的时候,只需要进行一个选择即可,

递归的贪心算法如下:

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章

posted @ 2018-07-16 17:39  小舔哥  阅读(474)  评论(0编辑  收藏  举报