算法学习07: 贪心算法
贪心策略
贪心策略的基本套路就是: 定一个指标,根据这个指标确定一个优先级,然后按照优先级顺序执行操作.
实际做题时,贪心策略不好证明,可以选择用对数器来验证算法正确性.
拼接最小字典序
题目: 给定一个字符串数组strs,找到一种拼接方式,使得把所有字符串拼起来形成的字符串具有最低的字典序
分析: 尝试用贪心策略,就要寻找一个给字符串排序的指标,下边两个思路都是找一个排序字符串的指标.
错误思路: 我们选择先把所有字符串按字典序排序,排序完成之后.按此顺序把字符串拼接起来,小的放前边,大的放后边.
这种思路是错误的,反例:["b","ba"]
.按照字典序排列应该得到"bba"
,而实际上字典序最小的排列为"bab"
.
这种思路为什么错误了? 因为所有字符串长度不同(若所有字符串长度相同,这个方法是正确的).
将字符串考虑成为26进制的数,字符串长度不同时,将 一个子字符串
移动到 拼接成的字符串前边
所付出的代价不仅是字符串本身的值,还要考虑将这个字符串左移了多少位.长的子字符串移动位数少,而短的子字符串移动位数大.
正确思路: 正确思路非常的简单粗暴,我们的排序标准应是哪个字符串最适合放在前边
.因此我们给排序时传入的比较器应该做如下判断:
将被比较的字符串(str1
和str2
)拼接,若"str1+str2"<"str2+str1"
,则我们认为str1更适合放在字符串前边.
直观体会: 第一种排序方法的错误在于没有考虑字符串长度对拼接成字典序的影响.因此我们使用str1
和str2
构造了两个长度相等的字符串str1+str2
和str2+str1
,这样抵消了字符串长度对判断的影响.
代码:
public static String getLowestString(String[] strs) {
// 将strs[]中的字符串拼接起来,得到最小的字符串
if (strs == null || strs.length == 0) {
return "";
}
// 将字符串排序
Arrays.sort(strs, new Comparator<String>() {
public int compare(String str1, String str2) {
return (str1 + str2).compareTo(str2 + str1);
}
});
// 拼接字符串
String res = "";
for (String string : strs) {
res += string;
}
return res;
}
证明: 将字符串看成26进制的数,易知该判断大小的方法具有传递性(尽管证明过程很繁琐).然后利用反证法和比较的传递性可知我们排成的长字符串中任意两项互换位置,会导致新字符串的字典序增加.
// 将字符串aa,bb互换位置,字符串字典序增加
... aa,m1,m2,m3,bb...(原字符串)
< ... m1,aa,m2,m3,bb...
< ... m1,m2,aa,m3,bb...
< ... m1,m2,m3,aa,bb...
< ...m1,m2,m3,bb,aa...
< ...m1,m2,bb,m3,aa...
< ...m1,bb,m2,m3,aa...
< ...bb,m1,m2,m3,aa...(互换aa与bb得到的字符串)
切金条问题(哈夫曼编码)
题目:将一块金条切成两半的要花费为原金条长度,将一段长度的金条切成给定长度的几段,怎么切最划算?
例如,给定数组{10,20,30},代表要把长度为60的金条分成10,20,30三个部分.
若先把长度60的金条分成10和50(花费60),再把长度50的金条分成20和30,(花费50),一共花费110铜板.
但如果先把长度60的金条分成30和30,(花费60),再把长度30的金条分成10和20,(花费30),一共花费90铜板.
分析: 这是一个标准的哈夫曼编码问题:子节点合在一起的代价是加起来的和.
问题转化为有10,20,30,三个节点,如何生成一棵树,使加权路径和最低?把所有数搞成一个小根堆,每次找出堆中两个最小的合并扔回堆里.
得到哈夫曼树之后,切割顺序的为树从树根到树叶的关系.
哈夫曼编码的贪心策略适合一类问题: 总代价是由子代价组成的,就适用于哈夫曼编码
ipo问题(两个不同比较准则的堆)leetcode 502
题目:每一个项目都有一个成本和利润(成本利润均大于零),现在给你一笔启动资金,要求你连续做k个项目使得最后资金量最大
解法:
- 准备一个按成本排列的小根堆和一个按利润排列的大根堆
- 不断弹出成本小根堆直到堆顶元素成本大于此元素,得到所有现在能做的项目,将这些项目加入利润大根堆.
- 做利润大根堆顶的项目
- 重复2,3步骤k次