本人已参与「新人创作礼」活动,一起开启掘金创作之路。
何为贪婪算法
对于一个问题,根据问题要求的目标,每步都选择局部最优解,最终得到的就是全局最优解。 在某些特定的情况下,贪婪算法能够得到最优解,但通常情况下,只能够得到一个接近最优解的解。
练习一
8.1 你在一家家具公司工作,需要将家具发往全国各地,为此你需要将箱子装上卡车。每个箱子的尺寸各不相同,你需要尽可能利用每辆卡车的空间,为此你将如何选择要装上卡车的箱子呢?请设计一种贪婪算法。使用这种算法能得到最优解吗?
运用贪婪策略,依次将所有剩余箱子中体积最大的装上卡车,直到不能再装入箱子为止。
不能得到最优解。
8.2 你要去欧洲旅行,总行程为7天。对于每个旅游胜地,你都给它分配一个价值——表示你有多想去那里看看,并估算出需要多长时间。你如何将这次旅行的价值最大化?请设计一种贪婪算法。使用这种算法能得到最优解吗?
依次挑选可在剩余时间内完成的价值最大的活动,直到余下的时间不足于完成任何活动为止。
不能得到最优解。
集合覆盖问题
假设你办了个广播节目,要让全美50个州的听众都收听得到。为此,你需要决定在哪些广播台播出。 在每个广播台播出都需要支付费用,因此你力图在尽可能少的广播台播出。 现有广播台名单如下:
每个广播台都覆盖特定的区域,不同广播台的覆盖区域可能重叠。 如何找出覆盖全美50个州的最小广播台集合呢?
可以使用近似算法(approximation algorithm):
- 选出这样的一个广播台,即它覆盖了最多未覆盖的州,即便这个广播台覆盖了一些已覆盖的州。
- 重复第一步,直到覆盖了所有的州。
在获得精确解需要的时间太长时,可以使用近似算法。 判断近似算法优劣的标准是:
- 速度有多快
- 得到的近似解与最优解的接近程度。
贪婪算法是不错的选择,它们不仅简单,而且通常运行速度很快。 在本例中,贪婪算法的运行时间为O(n^2),其中n为广播台数量。
var statesNeeded: Set<String> = ["mt", "wa", "or", "id", "nv", "ut", "ca", "az"]
var stations: [String: Set<String>] = [:]
stations["kone"] = ["id", "nv", "ut"]
stations["ktwo"] = ["wa", "id", "mt"]
stations["kthree"] = ["or", "nv", "ca"]
stations["kfour"] = ["nv", "ut"]
stations["kfive"] = ["ca", "az"]
func greedy(_ statesNeeded: Set<String>) -> Set<String> {
var varStates = statesNeeded
var res: Set<String> = []
while varStates.count > 0 {
var bestState = ""
var stateCovered: Set<String> = []
for (station, states) in stations {
let covered = varStates.intersection(states)
if covered.count > stateCovered.count {
bestState = station
stateCovered = covered
}
}
for sc in stateCovered {
varStates.remove(sc)
}
res.insert(bestState)
}
return res
}
greedy(statesNeeded)
使用上述贪婪算法的运行时间仅为O(n^2),比起遍历所有子集来获得精确解的精确算法O(2^n)来说非常快。
集合类似于列表,只是不能包含重复的元素。 集合的特性:
- 并集意味着将集合合并。
- 交集意味着找出两个集合中都有的元素。
- 差集意味着将从一个集合中剔除出现在另一个集合中的元素。
练习二
下面各种算法,是否是贪婪算法? 8.3 快速排序
不是
8.4 广度优先搜索
是
8.5 狄克斯特拉算法
是
NP完全问题
回到之前讨论过的旅行商问题。旅行商需要前往5个不同的城市,如何找出前往这5个城市的最短路径? 为此必须计算每条可能的路径,随着城市个数的个数增加,可能存在路径为城市个数的阶乘。
旅行商问题和集合覆盖问题有一些共同之处:你需要计算所有的解,并从中选出最小/最短的那个。这两个问题都属于NP完全问题。
NP完全问题的简单定义是,以难解著称的问题,如旅行商问题和集合覆盖问题。很多非常聪明的人都认为,根本不可能编写出可快速解决这些问题的算法。
对于NP完全问题,我们可能找不到最优解,但是可以近似求解。
如何识别NP完全问题
- 元素较少时算法的运行速度非常快,但随着元素数量的增加,速度会变得非常慢。
- 涉及“所有组合”的问题通常是NP完全问题。
- 不能将问题分成小问题,必须考虑各种可能的情况。这可能是NP完全问题。
- 如果问题涉及序列(如旅行商问题中的城市序列)且难以解决,它可能就是NP完全问题。
- 如果问题涉及集合(如广播台集合)且难以解决,它可能就是NP完全问题。
- 如果问题可转换为集合覆盖问题或旅行商问题,那它肯定是NP完全问题。
练习三
8.6 有个邮递员负责给20个家庭送信,需要找出经过这20个家庭的最短路径。请问这是一个NP完全问题吗?
是
8.7 在一堆人中找出最大的朋友圈(即其中任何两个人都相识)是NP完全问题吗?
是
8.8 你要制作美国地图,需要用不同的颜色标出相邻的州。为此,你需要确定最少需要使用多少种颜色,才能确保任何两个相邻州的颜色都不同。请问这是NP完全问题吗?
是
小结
- 贪婪算法寻找局部最优解,企图以这种方式获得全局最优解。
- 对于NP完全问题,还没有找到快速解决方案。
- 面临NP完全问题时,最佳的做法是使用近似算法。
- 贪婪算法易于实现、运行速度快,是不错的近似算法。
拓展与应用
给定一个包含大写字母和小写字母的字符串 s
,返回 通过这些字母构造成的 最长的回文串 。
在构造过程中,请注意 区分大小写 。比如 "Aa"
不能当做一个回文字符串。
示例 1:
输入: s = "abccccdd"
输出: 7
解释:
我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。
示例 2:
输入: s = "a"
输入: 1
提示:
1 <= s.length <= 2000
s
只由小写 和/或 大写英文字母组成
解题思路
- 先使用一个数组统计每个字符出现的次数。
- 再循环这个数组,判断每个字符出现的次数的奇偶。如果为偶数,则回文串的长度就是该字符出现的次数;如果为奇数,则回文串的长度就是该字符出现的次数再减一。
- 判断是否有剩余的字符串还剩下一次,则可将这个字符串插入到回文串的中间位置,这样总的结果需再加一。
func longestPalindrome(_ s: String) -> Int {
if s.count == 0 { return 0 }
// 如果题目给出的只是英文字母的话,最好开一个数组而不是哈希表。
// 因为数组比哈希表更加单纯、简单。
// 这一小部分代码就是先将字符串 s 转换成字符,然后统计每个字符所出现的次数
var freqs: [Int] = Array(repeating: 0, count: 128)
for c in s.utf8CString {
let i = Int(c)
if i != 0 {
freqs[i] += 1
}
}
var res = 0 // 用于保存最后的结果,进行返回
var odd = 0 // 用于统计是否有最后落单的那个字符
for freq in freqs {
// 如果某个字符出现的次数为偶数次,则回文串的长度就是该字符出现的次数。
// 如果某个字符出现的次数为奇数次,则回文串的长度就是该字符出现的次数再减一。
res += (freq % 2 == 0) ? freq : (freq - 1)
if freq % 2 == 1 {
// 如果某个字符最后还剩下 1 次,则将这个字符插入到回文串的中间位置
odd = 1
}
}
res += odd
return res
}
let str = "aahajkldah"
longestPalindrome(str)
Output: 7
这里运用了贪婪算法的思想。
时间复杂度:O(N),其中N为字符串s的长度。我们需要遍历每个字符一次。
空间复杂度:O(S),其中S为字符集大小。