算法基础--贪心策略

image
本文主要作为自己的学习笔记,并不具备过多的指导意义。

概述

贪心算法通常用来求解最优问题

  1. 由局部最优解到整体最优解

    通过不断对局部最优进行操作,最终达到整体最优

  2. 无后效性

    后序操作,不会出现数据状态的回滚

  3. 和DP(动态规划)之间的联系

    很多贪心问题可以通过DP进行求解


最优装载问题

  1. 给出N个物体,第一个物体重量为Mi
  2. 尽量选择最多的物品,总重不超过C

先将物品按照质量排序,然后依次放入每个物品,直到总重量将超过C位置。

这里依次将剩余物品中质量最小的物品放入的过程,就是贪心的过程。


合并果子

一类总过程代价,取决于子过程代价的问题

  1. 有N堆果子,没堆果子的数量为Ai,每次可以将两堆果子合并,每次合并将消耗两堆果子总数的体力。
  2. 求最小消耗的体力
  3. 1<N<10000

首先,如果我们什么都不管直接两两合并:总计消耗48点体力

image

然后,我们尝试排序后两两合并:总计消耗44点体力

image

最后,我们尝试只将当前所有数据中最小的两个进行合并:总计消耗38点体力

image

解法

构建一个小根堆,每次从堆顶推出两个元素合并。并且将合并都的元素追加进小根堆中即可。

具体证明的过程有一定难度,可以参考哈夫曼编码证明的过程。

以上的操作过程,也就是贪心的过程。他只保证单次合并所消耗的体力最优,而不在意其他的数据该如何合并。

堆结构往往用来解决贪心的问题。因为贪心问题往往需要一个明确的指标,最大值或者最小值。


项目利润

输入:

cost[]:每个项目的花费

profits[]:每个项目的利润(纯利润)

k:最多能做k个项目

w:表示初始资金

输出:

最后可以获得的最大钱数

说明:一次只能做一个项目,且做完一个之后马上就能获得收益,可以支持做下一个项目

  1. costprofits中的元素依次合并成一个新的节点node:
public class Node {
    public var c :Int //项目花费
    public var p :Int //项目利润
    
    public init(cost:Int,profit:Int) {
        self.c = cost
        self.p = profit
    }
}
  1. 准备一个以项目花费构建的小根堆

将所有node依次放入

  1. 准备一个以项目利润构建的大根堆

贪心过程:

  1. 从小根堆中依次弹出堆顶元素,直到node.c>w(项目所需资金大于当前资金)

    具体代码上,将小根堆数组removeFirst,然后将arr[0]与arr[arr.count-1]位置交换。让小根堆对arr[0]位置元素向下调整即可。

  2. 将小根堆中弹出的元素放入大根堆中(大根堆中即为当前可执行的项目)

    具体代码上,将元素追加进大根堆数组末尾,并进行调整即可。

  3. 从大根堆中弹出堆顶元素,并将w += node.p(执行收益最大的项目,并且更新当前资金)

    具体代码上与第一步类似

该贪心过程总计执行k次,每一次执行都只需要关心小根堆中最小值,与大根堆中最大值即可。最后的w即为最大总资产。


会议安排

在优先的时间内安排数量最多的会议

做一张图可以直观表示过程:

我们将蓝色表示为待安排红色表示为已安排黑色表示为不可安排

我们可以尝试几种不同的贪心策略

  1. 每次选择持续时间最短的安排
image

显然不可行

  1. 每次选择开始时间最早的
image

显然也不可行

  1. 每次选择开始时间最早的并且持续时间最短的来安排
image
image

由此可见该方案是可以行的

代码也很简单,只需要关心当前有效数据内开始时间晚于当前会议结束时间结束时间最早的一个数据即可。

func bestArrange(programs:[Program]) -> Int {
    program.sort("end")//根据program.end进行排序
    
    var res = 0
    var current = 0
    
    for p in programs {
        if p.current > current {  //开始时间晚于当前时间,否则作废
            res += 1
            current = p.end //开会,当前时间变成会议结束时间
        }
    }
    return res
}

贪心策略的证明

贪心策略的数学证明通常很复杂,有能力可以去翻阅

这里推荐一种很方便的方式,对数器。

通过小样本大样本量的测试,证明贪心策略的正确性。

以排序算法的证明举例

var checkOK = true
for i in 0..<10000 {
    var arr1 = generateRandomArray(size: 5, value: 20) //获取一个长度为10,最大值为20的随机数数组
    var arr2 = arr1 //数组在swift里属于值类型,赋值动作会自动copy
    let originalArr = arr1
    arr1.sort()//一定正确的算法
    radixSort(arr: &arr2, maxDigit: 2)
    if arr1 != arr2 {
        checkOK = false
        print(originalArr)
        print(arr2)
        break
    }
}

print(checkOK ? "比对成功":"比对失败")

对于贪心问题,可能不一定存在一个一定正确的算法。那么我们完全可以不去比对结果是否一致,只要贪心策略的结果永远优于默认顺序得出的结果即可。

关于对数器的介绍可以参阅另一篇


贪心算法

贪心算法3: 会议安排

左神牛课网算法课

推荐阅读更多精彩内容

  • 栈 1. 栈(stack)又名堆栈,它是一种运算受限的线性表。其限制是仅允许在表的一端进行插入和删除运算。这一端被...
    IT程序员阅读 2,998评论 0 8
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 10,797评论 0 33
  • 一些概念 数据结构就是研究数据的逻辑结构和物理结构以及它们之间相互关系,并对这种结构定义相应的运算,而且确保经过这...
    Winterfell_Z阅读 1,246评论 0 4
  • Java继承关系初始化顺序 父类的静态变量-->父类的静态代码块-->子类的静态变量-->子类的静态代码快-->父...
    第六象限阅读 1,042评论 0 10
  • 第六章(一)“喜欢看书吗?光是坐在这里发呆,不如找点什么丰富一下你的大脑。” 从FLLFFL进入花园修剪花花草草到...
    汀雨S26阅读 50评论 0 2