数据结构基础之《(20)—贪心策略》

一、什么是贪心算法

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);
		
	}
}

八、小结

感觉贪心策略就是分析问题的逻辑规则,根据规则来解,每个问题都不一样,没有相同的套路
 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值