一、什么是贪心算法
1、最自然智慧的算法
2、用一种局部最功利的标准,总是做出在当前看来是最好的选择
3、难点在于证明局部最功利的标准可以得到全局最优解
4、对于贪心算法的学习主要以增加阅历和经验为主
二、贪心算法求解的标准过程
1、分析业务
2、根据业务逻辑找到不同的贪心策略
3、对于能举出反例的策略直接跳过,不能举出反例的策略要证明有效性
这往往是特别困难的,要求数学能力很高且不具有统一的技巧性
三、贪心算法的解题套路
1、实现一个不依靠贪心策略的解法X,可以用最暴力的尝试
2、脑补出贪心策略A、贪心策略B、贪心策略C...
3、用解法X和对数器,用实验的方式得知哪个贪心策略正确
4、不要去纠结贪心策略的证明
四、例子1
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。给你每一个项目开始的时间和结束的时间,你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。返回最多的宣讲场次。
会议:
6:00-8:00
8:00-12:00
8:00-9:00
11:00-12:00
按照会议的开始时间安排(有反例)
按照会议的持续时间安排(有反例)
暴力方法,把安排会议所有可能性都列出来,看哪个安排最多
按照会议结束时间早,先安排,能得到最优解
package class10;
import java.util.Arrays;
import java.util.Comparator;
public class Code04_BestArrange {
public static class Program {
public int start; //开始时间
public int end; //结束时间
public Program(int start, int end) {
this.start = start;
this.end = end;
}
}
/**
* 暴力穷举
* @param programs
* @return
*/
public static int bestArrange1(Program[] programs) {
if (programs == null || programs.length == 0) {
return 0;
}
return process(programs, 0, 0);
}
/**
* 目前来到timeLine的时间点,已经安排了done多的会议,剩下的会议在programs可以自由安排
* @param programs 还剩什么会议
* @param done 之前已经安排了多少会议数量
* @param timeLine 现在时间点来到了什么
* @return 返回能安排的最多会议数量
*/
public static int process(Program[] programs, int done, int timeLine) {
if (programs.length == 0) { //剩余会议数量是0
return done;
}
//还有会议可以选择
int max = done;
//当前安排的会议是什么会,每一个都枚举
//for循环是遍历了每一个会议并产生选择该会议下产生的最大值,并与之前最大值比较
for (int i = 0; i < programs.length; i++) {
if (programs[i].start >= timeLine) { //会议的开始时间比当前时间晚,可以安排
Program[] next = copyButExcept(programs, i);
//剩下的会议,已经安排的会议数量+1,i号会议的结束时间
max = Math.max(max, process(next, done + 1, programs[i].end));
}
}
return max;
}
/**
* 在programs里把i号会议删掉,返回一个新的剩下的会议
* @param programs
* @param i
* @return
*/
public static Program[] copyButExcept(Program[] programs, int i) {
Program[] ans = new Program[programs.length - 1];
int index = 0;
for (int k = 0; k < programs.length; k++) {
if (k != i) {
ans[index++] = programs[k];
}
}
return ans;
}
public static int bestArrange2(Program[] programs) {
Arrays.sort(programs, new ProgramComparator());
int timeLine = 0;
int result = 0;
for (int i = 0; i < programs.length; i++) {
if (timeLine <= programs[i].start) {
result++;
timeLine = programs[i].end;
}
}
return result;
}
/**
* 比较器,根据谁的结束时间早排序
*/
public static class ProgramComparator implements Comparator<Program> {
@Override
public int compare(Program o1, Program o2) {
return o1.end - o2.end;
}
}
// for test
public static Program[] generatePrograms(int programSize, int timeMax) {
Program[] ans = new Program[(int)(Math.random() * (programSize + 1))];
for (int i = 0; i < ans.length; i++) {
int r1 = (int) (Math.random() * (timeMax + 1));
int r2 = (int) (Math.random() * (timeMax + 1));
if (r1 == r2) {
ans[i] = new Program(r1, r1 + 1);
} else {
ans[i] = new Program(Math.min(r1, r2), Math.max(r1, r2));
}
}
return ans;
}
public static void main(String[] args) {
int programSize = 12;
int timeMax = 20;
int timeTimes = 1000000;
for (int i = 0; i < timeTimes; i++) {
Program[] programs = generatePrograms(programSize, timeMax);
if (bestArrange1(programs) != bestArrange2(programs)) {
System.out.println("Oops!");
}
}
System.out.println("finish!");
}
}
五、例子2
给定一个字符串str,只由'X'和'.'两种字符构成。
'X'表示墙,不能放灯,也不需要点亮
'.'表示居民点,可以放灯,需要点亮
如果灯放在i位置,可以让i-1、i和i+1三个位置被点亮
返回如果点亮str中所有需要点亮的位置,至少需要几盏灯
X..XXX....XX.X
暴力方法,把所有放灯的可能性都罗列了,然后检查哪些是所有位置都照亮的,在所有都照亮的可能性里找放灯最少的
贪心策略:
(1)i位置是X的话,跳到下一个位置做决定
(2)i位置是点,i+1位置是X,i位置一定要放灯
(3)i位置是点,i+1位置也是点,灯必然放i+1位置
package class10;
import java.util.HashSet;
/**
* 点亮灯安排
*/
public class Code02_Light {
/**
* 暴力方法
* @param road
* @return
*/
public static int minLight1(String road) {
if (road == null || road.length() == 0) {
return 0;
}
return process(road.toCharArray(), 0, new HashSet<>());
}
// str[index]以后的位置,自由选择放灯还是不放灯
// str[0..index-1]的位置,已经做完决定了,哪些位置放了灯的,存在lights里
// 返回:要求选出能照亮所有.的方案,并且在这些有效的方案中,返回最少需要几个灯
public static int process(char[] str, int index, HashSet<Integer> lights) {
if (index == str.length) { // 当index来到最后一个位置的时候
// 收集到的方案,能否把所有居民点都照亮
for (int i = 0; i < str.length; i++) {
if (str[i] != 'X') {
// i这个点是否被照亮?i-1位置有灯,或者i位置有灯,或者i+1位置有灯
if (!lights.contains(i - 1) && !lights.contains(i) && !lights.contains(i + 1)) {
return Integer.MAX_VALUE; // 返回最大值作为无效解
}
}
}
return lights.size(); // 所有点被照亮,返回这个有效解的大小
} else { // str还没结束
// 当前i位置不放灯,返回后续的最小值
int no = process(str, index + 1, lights);
int yes = Integer.MAX_VALUE;
if (str[index] == '.') { // 如果i位置是点,做出放灯的决定,yes才有效
lights.add(index);
yes = process(str, index + 1, lights);
lights.remove(index); // 恢复现场,因为始终用一个lights来搞
}
return Math.min(no, yes);
}
}
/**
* 贪心策略
* @param road
* @return
*/
public static int minLight2(String road) {
char[] str = road.toCharArray();
int index = 0;
int light = 0;
while (index < str.length) {
if (str[index] == 'X') { // index位置是X
index++;
} else { // index位置是点
light++; // 这个位置一定会放灯,可能放i位置,也可能放i+1位置
if (index + 1 == str.length) {
break;
} else {
if (str[index + 1] == 'X') {
index = index + 2;
} else {
index = index + 3;
}
}
}
}
return light;
}
public static String randomString(int len) {
char[] res = new char[(int) (Math.random() * len) + 1];
for (int i = 0; i < res.length; i++) {
res[i] = Math.random() < 0.5 ? 'X' : '.';
}
return String.valueOf(res);
}
public static void main(String[] args) {
String road = randomString(100);
System.out.println(minLight1(road));
System.out.println("==========");
System.out.println(minLight2(road));
}
}
六、例子3
一块金条切成两半,是需要花费和长度数值一样的铜板的。
比如长度为20的金条,不管怎么切,都要花费20个铜板。一群人想整分整块金条,怎么分最省铜板?
例如:给定数组[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铜板。
输入一个数组,返回分割的最小代价。
[3,9,6,4,1],金条的总长度是所有数的累加和
堆和排序是完成贪心策略最常用的技巧:
(1)将数字放入一个小根堆,排序
(2)弹出2个,合并,然后放回去
(3)周而复始
(4)所有非叶节点加起来就是花费铜板数量
package class10;
import java.util.PriorityQueue;
/**
* 分割最小代价问题
*/
public class Code03_LessMoneySplitGold {
/**
* 暴力方法
* @param arr
* @return
*/
public static int lessMoney1(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
return process(arr, 0);
}
public static int process(int[] arr, int pre) {
if (arr.length == 1) {
return pre;
}
int ans = Integer.MAX_VALUE;
for (int i = 0; i < arr.length; i++) {
for (int j = i + 1; j < arr.length; j++) {
ans = Math.min(ans, process(copyAndMergeTwo(arr, i, j), pre + arr[i] + arr[j]));
}
}
return ans;
}
public static int[] copyAndMergeTwo(int[] arr, int i, int j) {
int[] ans = new int[arr.length - 1];
int ansi = 0;
for (int arri = 0; arri < arr.length; arri++) {
if (arri != i && arri != j) {
ans[ansi++] = arr[arri];
}
}
ans[ansi] = arr[i] + arr[j];
return ans;
}
/**
* 贪心策略
* @param arr
* @return
*/
public static int lessMoney2(int[] arr) {
PriorityQueue<Integer> pq = new PriorityQueue<>();
for (int i = 0; i < arr.length; i++) {
pq.add(arr[i]);
}
int sum = 0;
int cur = 0;
while (pq.size() > 1) {
cur = pq.poll() + pq.poll(); // 每次弹出2个数,合成1个数
sum += cur; // 累加
pq.add(cur); // 把合成的数塞回小根堆
}
return sum;
}
public static void main(String[] args) {
int[] arr = {3,9,6,4,1,2,89,56,4,3};
System.out.println(lessMoney1(arr));
System.out.println("==========");
System.out.println(lessMoney2(arr));
}
}
七、例子4
输入:正数数组costs、正数数组profits、正数K、正数M
cost[i]表示i号项目的花费
profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润)
K表示你只能串行的最多做K个项目
M表示你初始的资金
说明:每做完一个项目,马上获得的收益,可以支持你去做下一个项目。不能并行的做项目。
输出:你最后获得的最大钱数。
解法:
(1)先建立一个小根堆(按花费组织)
锁住的项目
(2)再准备一个大根堆(按利润组织)
解锁的项目
(3)在小根堆中,只要初始资金足够的项目都弹出来,进大根堆
(4)消化大根堆中的项目
(5)再从小根堆中找初始资金足够的项目出来进大根堆,周而复始
package class10;
import java.util.Comparator;
import java.util.PriorityQueue;
/**
* 做项目花费和利润问题
*/
public class Code05_IPO {
/**
*
* @param K 你只能串行的最多做K个项目
* @param W 你初始的资金
* @param Profits 表示i号项目在扣除花费之后还能挣到的钱(利润)
* @param Capitals 表示i号项目的花费
* @return
*/
public static int findMaximizedCapital(int K, int W, int[] Profits, int[] Capitals) {
//根据花费的小根堆
PriorityQueue<Program> minCostQ = new PriorityQueue<>(new MinCostComparator());
//根据利润的大根堆
PriorityQueue<Program> maxProfitQ = new PriorityQueue<>(new MaxProfitComparator());
for (int i = 0; i < Profits.length; i++) {
minCostQ.add(new Program(Profits[i], Capitals[i]));
}
//控制做多少轮
for (int i = 0; i < K; i++) {
while (!minCostQ.isEmpty() && minCostQ.peek().c <= W) {
maxProfitQ.add(minCostQ.poll());
}
if (maxProfitQ.isEmpty()) {
return W;
}
W += maxProfitQ.poll().p;
}
return W;
}
public static class Program {
public int p;
public int c;
public Program(int p, int c) {
this.p = p;
this.c = c;
}
}
/**
* 根据花费组织的小根堆的比较器
*/
public static class MinCostComparator implements Comparator<Program> {
@Override
public int compare(Program o1, Program o2) {
return o1.c - o2.c;
}
}
/**
* 根据利润组织的大根堆的比较器
*/
public static class MaxProfitComparator implements Comparator<Program> {
@Override
public int compare(Program o1, Program o2) {
return o2.p - o1.p;
}
}
public static void main(String[] args) {
int K = 10;
int W = 5;
int[] Capitals = {10, 1, 4, 5, 3, 6, 3, 2, 1};
int[] Profits = {1, 1, 1, 1, 1, 1, 1, 1, 1};
int result = findMaximizedCapital(K, W, Profits, Capitals);
System.out.println(result);
}
}
八、小结
感觉贪心策略就是分析问题的逻辑规则,根据规则来解,每个问题都不一样,没有相同的套路
2390

被折叠的 条评论
为什么被折叠?



