算法设计与分析笔记4———贪心算法

目录

一,算法的总体思想

二,贪心算法的基本要素

三,贪心算法的一些例子

1,活动安排问题

2,Huffman编码问题

3,最小生成树

一,算法的总体思想

         贪心算法总是选择当前局面的最优解,也就是说贪心算法不从全局最优解来考虑,它做出的选择是在某种意义上的局部最优。在很多情况下贪心算法得出的结果是最优解的一个近似。

下面有一个硬币找零的例子:

        假设有四种硬币:二角五分、一角、五分和一分。现要找给顾客六角三分。 显然,会拿出2个二角五分、1个一角和3个一分的硬币交给顾客。

        贪心方法思路:首先选出一个面值不超过六角三分的最大硬币,即二角五分,然后在剩余数中再选最大面值的硬币,依此类推,得到其解。

若硬币面值改为:

        一角一分、五分和一分,而要找给顾客一角五分钱。 用贪心算法将找给1个一角一分和4个一分的硬币。然而,3个五分硬币是最好的找法。

         此时,贪心算法没有得到整体最优解。但通常可得到最优解的很好近似。

二,贪心算法的基本要素

1,最优子结构性质

2,贪心选择性质

        所求问题的整体最优可以通过一系列的局部最优解来达到

        这是贪心算法与动态规划的主要区别:

                动态规划将问题分解为可以解决的子问题,从最小规模的问题开始求解,逐步解得更大规模的问题,是自下而上的求解;贪心选择是自上向下的,迭代地进行贪心选择,每做一次贪心选择问题的规模就会缩小一部分,从而得到将问题变成规模更小的子问题。

        贪心算法证明时,通常使用替换法。

三,贪心算法的一些例子

1,活动安排问题

        设有n个活动的集合E={1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源。

         每个活动i都有一个要求使用该资源的起始时间si和一个结束时间fi, 且si <fi 。

         如果选择了活动i,则它在[si, fi)内占用资源。

         若[si, fi)与[sj, fj)不相交,则称活动i与j是相容的,即当si≥fj或sj≥fi时,活动i与活动j相容。

         活动安排问题就是求E的最大相容活动子集

问题分析:

        某活动结束时间越早,则留给其他活动的时间就越长,因此贪心选择算法每次选择结束时间最早的活动。

替换法证明 :

        设最优排序 S = a, b, c

        贪心选择出的 a' 是全局结束时间最早的活动,因此 S' = a', b, c 肯定满足时间要求,则 S' 也是最优解。

def greedy_selector(n, s, f) :
    s = [int(ele) for ele in s]
    f = [int(ele) for ele in f]
    items = list(range(1, n + 1))
    sort_sf = sorted(zip(s, f, items), key = lambda x : x[1])
    s_sorted, f_sorted, items_sorted = zip(*sort_sf)
    ans = []
    cnt = 0
    finish_time = 0
    for i in range(n) :
        if s_sorted[i] >= finish_time :
            ans.append(items_sorted[i])
            finish_time = f_sorted[i]
            cnt += 1
    return ans, cnt

n = int(input())
s = input().split()
f = input().split()
ans, cnt = greedy_selector(n, s, f)
print(ans)
print(cnt)
# 输入
11
1   3   0   5   3   5    6   8    8    2   12
4   5   6   7   8   9  10  11  12  13  14

# 输出
[1, 4, 8, 11]
4

2,Huffman编码问题

        在计算机中,所有的数据传输(以字符串为例)都是通过二进制的形式进行的,因此,对出现频率较高的字符赋予长度较短的二进制码,可以提高数据传输的效率,为避免歧义,需要每一个字符的二进制编码都不是其他字符的前缀码,这时便可以通过树来实现,对于二叉树的非根节点而言,若左节点赋值为0,右节点赋值为1,则所有叶子节点所代表的值即符合要求,即Huffman树。

        首先需要统计各个字符在字符串中出现的次数放入优先队列,可以通过字典记录,并根据出现的次数升序排序,将当前队列中频次最少的两项合成为一个二叉树,并将其父节点赋值为二者频次的和,插入到上述队列中,重复上述过程,直到优先队列中只有一个节点,此时Huffman树便构建完成了。压缩时,根据Huffman树中的编码将所给字符串依次转换为二进制字符,解压时将二进制字符依次转换为相应字符,即可完成传输。

关于代码实现:

首先构建Huffman节点:

class HuffmanNode:
    def __init__(self, frequency, value=None):
        self.frequency = frequency
        self.value = value
        self.left = None
        self.right = None

    def __lt__(self, other): # 重载运算符,用于优先队列的构建
        return self.frequency < other.frequency

节点中存储有当前字符value和字符出现的频率frequency,并创建了左右子节点,将其指向None。

有了Huffman节点后,开始构建Huffman树:

def build_huffman_tree(data):
    # 首先统计各字符出现的次数
    frequency_map = defaultdict(int)
    for char in data:
        frequency_map[char] += 1
    # 创建优先队列,并按照出现频率升序排序
    priority_queue = []
    for char, frequency in frequency_map.items():
        node = HuffmanNode(frequency, char)
        heapq.heappush(priority_queue, node)
    # 构造Huffman树并返回树的根节点
    while len(priority_queue) > 1:
        left_node = heapq.heappop(priority_queue)
        right_node = heapq.heappop(priority_queue)
        parent_node = HuffmanNode(left_node.frequency + right_node.frequency)
        parent_node.left = left_node
        parent_node.right = right_node
        heapq.heappush(priority_queue, parent_node)

    return heapq.heappop(priority_queue)

对Huffman树进行编码,并返回得到的映射规则

def encode_huffman_tree(node, prefix='', encoding_map = None):
    if encoding_map is None:
        encoding_map = {}

    if node.value:
        encoding_map[node.value] = prefix
    else:
        encode_huffman_tree(node.left, prefix + '0', encoding_map)
        encode_huffman_tree(node.right, prefix + '1', encoding_map)

return encoding_map

代码中,如果value的值不为None,即此时为叶子节点,则将编码prefix赋予value;

若value的值为None,则还没有达到叶子节点,将prefix + ‘0’,访问左子节点,抑或是将

prefix + ‘1’访问右子节点。

按照得到的编码规则对所给字符串进行compress

def compress(data, encoding_map):
    compressed_data = ''
    for char in data:
        compressed_data += encoding_map[char]
return compressed_data

按照Huffman树对得到的编码进行解码

def decompress(compressed_data, huffman_tree):
    current_node = huffman_tree
    decompressed_data = ''
    for bit in compressed_data:
        if bit == '0':
            current_node = current_node.left
        else:
            current_node = current_node.right

        if current_node.value:
            decompressed_data += current_node.value
            current_node = huffman_tree

return decompressed_data

以下是输入与结果

data = "Hello World!"
huffman_tree = build_huffman_tree(data)
encoding_map = encode_huffman_tree(huffman_tree)
compressed_data = compress(data, encoding_map)
decompressed_data = decompress(compressed_data, huffman_tree)
print("映射规则:",encoding_map)
print("原始数据:", data)
print("压缩后的数据:", compressed_data)
print("解压后的数据:", decompressed_data)

# 输出为:
映射规则: {'r': '000', '!': '001', 'l': '01', 'd': '100', 'o': '101', 'e': '1100', ' ': '1101', 'H': '1110', 'W': '1111'}
原始数据: Hello World!
压缩后的数据: 1110110001011011101111110100001100001
解压后的数据: Hello World!

3,最小生成树

设G = (V, E)是一个无向连通带权图,即一个网络。E的每条边(v, w)的权为c[v][w]。

如果G的一个子图G’是一棵包含G的所有顶点的树,则称G’为G的生成树。

生成树的各边权的总和称为该生成树的耗费。

在G的所有生成树中,耗费最小的生成树称为G的最小生成树MST(minimum spanning tree) 。

最小生成树的贪心选择性质:

        不断从当前已有的选择中,找到符合条件的最优解,直到构造出最小生成树,树是由顶点和边构成的,而最小生成树的边会比顶点少一个,从这两个方面出发可以有不同的贪心选择策略

Prim算法:

        从顶点出发,创建已选顶点集S,未选顶点集K,任选一顶点 i 初始化S,在K中找到顶点 j 使得边(i, j)最小,注意 i 是已选顶点集S中,j 是未选顶点集K中。将 j 加入S,并从K中去除。

边集用c[ i ][ j ]给出,c 是边的权重。若顶点 i 与顶点 j 之间没有边相连,则c[ i ][ j ] = -1

图如下:

抽象为矩阵:

def Prim(c, n) :
    ans = []
    S = [1]
    K = list(range(2, n + 1))
    sum = 0
    se = float('inf')    # select 用于存储当前最小权重
    sec = []    # select 用于存储最小权重对应的边
    while len(K) > 0 :    # K不为空,算法继续
        for i in S :    # 在已选集中选择顶点i作为起点
            for j in K :    # 在未选集中选择顶点j作为终点
                if c[int(i)][int(j)] == -1 :    # 若(i, j)没有边连接,则跳过
                    continue
                else :
                    if se > c[int(i)][int(j)] :
                        se = c[int(i)][int(j)]
                        sec = [i, j]
        S.append(int(sec[1]))
        sum += se
        K.pop(K.index(int(sec[1])))
        ans.append(sec)
        se = float('inf')
        sec = []
    print(sum)
    return ans

下面是输入处理与运行结果

n = int(input())
c = []
for i in range(n + 1) :
    line = list(map(int, input().split()))
    c.append(line)
print(Prim(c, n))


# 输入:
6
0	1	2	3	4	5	6
1	-1	6	1	5	-1	-1
2	6	-1	5	-1	3	-1
3	1	5	-1	5	6	4
4	5	-1	5	-1	-1	2
5	-1	3	6	-1	-1	6
6	-1	-1	4	2	6	-1

# 输出:
15
[[1, 3], [3, 6], [6, 4], [3, 2], [2, 5]]

相同问题的Kruskal算法:

        该算法从边出发,选择当前权值最小的边并判断是否会构成回路(若边的两个端点都在已选顶点集内,则构成回路),直到选择n - 1条边为止

该算法可以使用并查集实现,先将所有边的权重排序,从最小的开始选择,若一个边中两个节点的根节点一致,则代表两个节点之间已经构成回路,若根节点不同,则将其加入并查集中。

实现代码:

def find(parent, i) :
    if parent[i] == i :
        return i
    return find(parent, parent[i])

def union(parent, rank, x, y) :
    xroot = find(parent, x)
    yroot = find(parent, y)

    if rank[xroot] < rank[yroot] :
        parent[xroot] = yroot
    elif rank[xroot] > rank[yroot] :
        parent[yroot] = xroot
    else :
        parent[yroot] = xroot
        rank[xroot] += 1


def Kruskal(c, n) :
    edges = []
    for i in range(1, n + 1) :
        for j in range(i + 1, n + 1) :
            if c[i][j] != -1 :
                edges.append((c[i][j], i, j))
    edges.sort()

    parent = [i for i in range(n + 1)]
    rank = [0] * (n + 1)
    ans = []
    sum = 0

    for edge in edges :
        weight, x, y = edge
        if find(parent, x) != find(parent, y) :
            ans.append((x, y))
            sum += weight
            union(parent, rank, x, y)

    return ans, sum

结果:

n = int(input())
c = []
for i in range(n + 1) :
    line = list(map(int, input().split()))
    c.append(line)
print(Kruskal(c, n))

# 输入:
6
0	1	2	3	4	5	6
1	-1	6	1	5	-1	-1
2	6	-1	5	-1	3	-1
3	1	5	-1	5	6	4
4	5	-1	5	-1	-1	2
5	-1	3	6	-1	-1	6
6	-1	-1	4	2	6	-1

# 输出:
([(1, 3), (4, 6), (2, 5), (3, 6), (2, 3)], 15)
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值