1.什么是贪心算法。
贪心算法是在一种功利的标准下所产生的解,是一种绝对标准下的解,他总是做出对于某一个局部标准来说最好的解,看看以此能否得到全局最优解。如果它以局部想法贪出一个全局最优解,我们就说他的贪心策略有效,如果没有搞错全局最优解,我们就说它的贪心策略无效。
对于我们程序员来讲,他是最没有道理的算法,也是最具自然智慧的算法,它的难点在于证明局部最功利的标准可以得到全局最优解。
我们在学习贪心算法时,主要以增加阅历和经验为主。
举个贪心没有贪对的栗子
有一个小人,他想从左上角走到右下角然后再回去,它再往右下角走时,只能往下或者往有走,而它在回左上角时,只能往左或者往上走。而他的目标是收集最多的 1 ,1 再被收集一次后,就会变成 0 。
根据贪心算法对该栗子进行拆分,我们可以把该过程分为两部分,去的时候一部分,回来的时候一部分,根据贪心策略思想,我们只需要在去的时候获取到一个局部的最优解,回来的时候再获得一个局部的最优解,就能获取到全局最优解,当然贪心算法能在大部分情况下获取到最优解,但还是存在个别反例的情况,例如上述例子,
按照贪心算法,去的时候走绿色这条路,会获取到去的这个局部的最大值,而回去的时候,因为只能向左或者向上,所以黄色的就只能走一条,从而有一个 1 没有获取到,那他的最优解应该是什么呢?
如上图,去的时候我不贪图局部最优,走蓝色路线,回去的时候走红色路线,这样就能收集到全部的 1
这是一个贪心失败的典型栗子。
2.贪心算法的一些题目
2.1给定一个由字符串组成的数组strs,必须把所有的字符串拼接起来,返回所有可能的拼接结果中,字典序最小的结果。
在写该题之前,我们需要知道什么是字典序。
字典序,简单来说就是在编程语言中字符串比较大小的顺序。字符串比较有两种情况,在两个字符串长度相同时,总左到右,依次比较各个字符的ASCII码,譬如 两个字符串 "abc" 和 "bcd" ,因为a的ASCII码小于b的ASCII码,所以 "abc"的字典序小于"bcd"的字典序,如果两个字符长度不相等,则较短的字符后面补最小的ASCII码,补至和另一个长度一样后再比。
接下来看这题,我们可以根据字典序进行排序,然后我们再把每个字符拼接起来即可。
代码如下:
public static String minDictResult(String[] strs){
if (strs == null || strs.length == 0){
return "";
}
Arrays.sort(strs,(a,b)-> (a+b).compareTo(b+a));
StringBuilder sb = new StringBuilder();
for (String str : strs) {
sb.append(str);
}
return sb.toString();
}
2.2 用贪心算法返回最多会议室宣讲场次问题
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。给以每一个项目开始的时间和结束的时间你来安排宣讲的日程,要求会议室进行的宣讲的场次最多,返回最多的宣讲场次。
看到题之后,我们能想到很多种贪心方案,但是,我们想的贪心方案都是有效的吗?
栗如:
1.所有时间中,谁开会时间早,我先安排谁,通过对开始时间的贪心,看看最多能安排几场。
明显该贪心策略不对 例如 [1,100][2,3][3,5][7,10]如果我们按照开始时间进行贪心,那么就会选中[1,100]这场会议,明显不对,贪心策略失效
2.那按照会议持续时间最短进行贪心呢? 明显也不对 ,反例 [1,50][45,55][50,100]在该反例中,如果选最小持续时间的话,就会选[45,55],明显[1,50][50,100]才是正确答案。
3.按照会议结束时间进行贪心,首先对结束时间按小到大进行排序,结束时间一样,就按照开始时间进行开会,该贪心策略有效
代码如下:
public class Metting{
int start;
int end;
Metting(int start,int end){
this.start = start;
this.end = end;
}
}
public static int getMaxMettingCount(Metting[] metting){
if (metting == null || metting.length == 0){
return 0;
}
int count = 0;
int offSet = 0;
Arrays.sort(metting,(a,b)-> a.end - b.end);
//依次遍历每一个会议,结束时间早的先遍历
for (int i = 0; i < metting.length; i++) {
if ( offSet <= metting[i].start){
count ++;
offSet = metting[i].end;
}
}
return count;
}
2.3 黄金分割问题
加入有一块黄金,它的长度是一个数组内元素的总和,每一次分割都需要付出等长的代价,问最小的代价是多少?
举个例子 假如说一个数组[10,20,30] 那么该黄金的长度为 10 + 20 + 30 = 60,我需要把这个长度为60的黄金分割为 长度为 10 20 30 的三块,假如说我要先分割成 长度为10 和 50 的,我需要付出 60 的代价,然后再把50的分割成 20 的和30的,我需要付出50的代价,总共需要付出110的代价。而这道题还有其他更好的解法吗?答案是肯定的,我先把这个60的黄金分割成30 30 的,我需要付出60 的代价 ,我再把一个30的分割成 10 和 20 的,需要付出 30 的代价 ,同样能分割成 10 20 30 ,我这次需要付出的代价是60 + 30 = 90,那我们就可以想了,我们只要从大到小分割不就可以啦,答案是否定的,举个反例 [10,9,8,7] 黄金总长10 +9 + 8 + 7 = 34 ,从大到小分割代价 34 +24 + 15 + 8 = 81 这是最优答案吗?如果先把 长度为34 的黄金分为 15 和 19 ,然后再分为 10 9 8 7 所需要付出的代价 34 + 15 + 19 = 68 显然比刚才的代价小,那么我们该如何进行贪心呢?
答案:就是哈夫曼编码
我们新建一个小根堆,把数组中的值放在小根堆中,然后我们依次弹出两个值,求和后放入小根堆,再弹出两个值,求和后再放入小根堆中,依次往复,直到小根堆中只剩一个值,该过程中所有的求和累加在一起,就是所需的最小代价。
代码如下:
public static int goldenSelection(int[] arr){
if (arr == null || arr.length == 0){
return 0;
}
PriorityQueue<Integer> queue = new PriorityQueue();
for (int i = 0; i < arr.length; i++) {
queue.add(arr[i]);
}
int sum = 0;
int cur = 0;
while (queue.size() > 1){
cur = queue.poll() + queue.poll();
sum += cur;
queue.add(cur);
}
return sum;
}
2.4 输入正整数组costs、正整数组profits、正数k、正数M、输出你最后获得的最大钱数
costs[i] 表示i号项目的花费,profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润)
k表示你只能串行的最多左k个项目,M表示你初始的资金。
说明,没做完一个项目,马上获得的受益,可以支持你去做下一个项目。不能并行的做项目
输出:你最后获得的最大钱数
这一道题我们可以建一个根据花费排序的小根堆,再建一个根据利润排序的大根堆,开始时,我们把所有的项目放在花费的小根堆里面,大根堆是空的。我们把花费小于K的项目全弹出,放在根据利润排序的大根堆里面,然后我们把利润的大根堆弹出一个,把利润加到获得的最大钱数中,直到做了K个项目结束。
代码如下:
public class Profit {
public static class Program{
int cost;
int profit;
Program(int cost,int profit){
this.cost = cost;
this.profit = profit;
}
}
public static int getProfit(int[] costs,int[] profits,int K,int M){
if (costs == null || costs.length == 0){
return 0;
}
Program[] programs = new Program[costs.length];
//建出每一个项目
for (int i = 0; i < costs.length; i++) {
programs[i] = new Program(costs[i],profits[i]);
}
PriorityQueue<Program> programQueue = new PriorityQueue<>((a,b)->a.cost - b.cost);
PriorityQueue<Program> profitQueue = new PriorityQueue<>((a,b)->b.profit - a.profit);
//把项目丢到小根堆中去
for (int i = 0; i < programs.length; i++) {
programQueue.add(programs[i]);
}
for (int i = 0; i < K; i++) {
while (!programQueue.isEmpty()&&programQueue.peek().cost <= M){
profitQueue.add(programQueue.poll());
}
if (profitQueue.isEmpty()){
return M;
}
Program poll = profitQueue.poll();
M += poll.profit;
}
return M;
}
}
2.5 点灯问题
给定一个字符串str,只由 'X' 和 '.' 两种字符构成。
'X'表示墙,不能放灯,也不需要点亮
'.'表示居民点,可以放灯,需要点亮
如果灯灯放在 i 位置, 可以让 i - 1, i 和 i + 1 三个位置被点亮
如果点亮str中所有需要点亮的位置 至少需要几盏灯。
分析:
情况1:
如果 i 位置是 'X' ---> i+1 位置讨论
情况2:
如果 i 位置是 '.' i+1位置是'X' i 位置点灯 --->i位置点灯 灯+1 i+2 位置讨论
情况3:如果 i + 1 也是 '.' 就再往下讨论 i + 2 位置
如果 i 位置是 '.' i+1位置是'.' i+2 位置是 'X' --->i+1位置点灯 灯+1 i+3 位置讨论
情况4:同2,只不过 i+2 位置是 '.'
如果 i 位置是 '.' i+1位置是'.' i+2 位置是 '.' --->i+1位置点灯 灯+1 i+3 位置讨论
代码如下:
public class MinLight {
public static int minLight(String str){
if (str == null || str == ""){
return 0;
}
int light = 0;
int i = 0;
char[] chars = str.toCharArray();
while (i < chars.length) {
if ('X'== chars[i]){
i = i + 1;
}else {
light++;
if (i+1 == chars.length){
break;
}else {
if ('X' == chars[i+1]){
i = i + 2;
}else {
i = i + 3;
}
}
}
}
return light;
}
}
总之,学习贪心算法没有什么捷径可走,需要我们不断刷题来提高我们的经验和阅历。