数据结构与算法之深入解析“贪心算法“的原理解析和算法实现

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法之深入解析“贪心算法“的原理解析和算法实现相关的知识,希望对你有一定的参考价值。

一、简介

① 贪心算法的基本概念
  • 贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的仅仅是在某种意义上的局部最优解,它是最自然智慧的算法。
  • 贪心算法用一种局部最功利的标准,总是能做出在当前看来是最好的选择,难点在于证明局部最优解最功利的标准可以得到全局最优解。
  • 贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。需要注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性(即某个状态以后的过程不会影响以前的状态,只与当前状态有关)。因此,对所采用的贪心策略一定要仔细分析其是否满足无后效性。
② 贪心算法的算法解释
  • 正例:通过一个例子来解释,假设一个数组中 N 个正数,第一个挑选出来的数乘以 1,第二个挑选出来的数乘以 2,同理,第 N 次挑选出来的数乘以 N,总的加起来是我们的分数,那么怎么挑选数字使我们达到最大分数?
  • 数组按从小到大的顺序排序,按顺序依次挑选,最终结果就是最大的。本质思想是因子随着挑选次数的增加会增大,尽量让大数去结合大的因子。
③ 贪心算法的证明问题
  • 如何证明贪心算法的有效性?一般来说,贪心算法不推荐证明,很多时候证明是非常复杂的。
  • 例如:给定一个由字符串组成的数组 strs,必须把所有的字符串拼接起来,返回所有可能的拼接结果中,字典序最小的结果。
    • 字典序严格定义,把字符串当成 k 进制的数,a-z 当成 26 进制的正数,字符长度一样,abk>abc,那么说 abk 的字典序更大,字符长度不一样 ac 和 b,那么要把短的用 0 补齐,0 小于 a 的 accil,那么 ac<b0,高位 b>a 即可比较出来大小。
    • Java 中字符串的 ComparTo 方法,就是比较字典序。
    • 思路一:按照单个元素字典序贪心,例如在 [ac,bk,sc,ket] 字符串数组中,拼接出来最终的字符串字典序最小,那么依次挑选字典序最小的进行拼接的贪心策略得到 acbkketsc。但是这样的贪心不一定是正确的,例如 [ba,b] 按照上述思路的贪心结果是 bba,但是 bab 明显是最小的结果。
    • 思路二:两个元素 x 和 y,x 拼接 y 小于等于 x 拼接 y,那么 x 放前,否则 y 放前面。例如 x=b,y=ba,bba 大于 bab 的字典,那么 ba 放前面。
    • 证明:
      • 我们把拼接当成 k 进制数的数学运算,把 a-z 的数当成 26 进制的数,ks 拼接 ts 实质是 ks * 26^2 + te;
      • 目标先证明我们比较的传递性:证明 a 拼接 b 小于 b 拼接 a,b 拼接 c 小于等于 c 拼接 b,推出 a 拼接 c 小于等于 c 拼接 a;
      • a 拼接 b 等于 a 乘以 k 的 b 长度次方 + b,我们把 k 的 x 长度次方这个操作当成 m(x) 函数,所以:
	a * m(b) + b <= b * m(a) + a  
	b * m(c) + c <= c * m(b) + b 
	
	=> 
	
	a * m(b) * c <= b * m(a) * c + ac - bc
	b * m(c) * a + ca - ba <= c * m(b) * a 
	
	=>
	
	b * m(c) * a + ca - ba <= b * m(a) * c + ac - bc
	
	=> 
	
	m(c) * a + c <= m(a) * c + a
      • 至此,我们证明出排序具有传递性质。根据排序策略得到的一组序列,证明任意交换两个字符的位置,都会得到更大的字典序。例如,按照思路二得到的 amnb 序列,交换 a 和 b,先把 a 和 m 交换,由于按照思路二得到的序列,满足 a.m <= m.a ,那么 manb > amnb,同理得到 amnb < bmna。
      • 再证明任意三个交换都会变为更大的字典序,那么最终数学归纳法,得到思路二的正确性;
      • 因此,贪心算法的证明实质是比较复杂的,大可不必每次去证明贪心的正确性:
	package class09;
	
	import java.util.ArrayList;
	import java.util.Arrays;
	import java.util.Comparator;
	import java.util.HashSet;
	
	public class Code01_LowestLexicography {
	
	        // 暴力法穷举,排列组合
		public static String lowestString1(String[] strs) {
			if (strs == null || strs.length == 0) {
				return "";
			}
			ArrayList<String> all = new ArrayList<>();
			HashSet<Integer> use = new HashSet<>();
			process(strs, use, "", all);
			String lowest = all.get(0);
			for (int i = 1; i < all.size(); i++) {
				if (all.get(i).compareTo(lowest) < 0) {
					lowest = all.get(i);
				}
			}
			return lowest;
		}
	
		// strs里放着所有的字符串
		// 已经使用过的字符串的下标,在use里登记了,不要再使用了
		// 之前使用过的字符串,拼接成了-> path
		// 用all收集所有可能的拼接结果
		public static void process(String[] strs, HashSet<Integer> use, String path, ArrayList<String> all) {
		        // 所有字符串都是用过了
			if (use.size() == strs.length) {
				all.add(path);
			} else {
				for (int i = 0; i < strs.length; i++) {
					if (!use.contains(i)) {
						use.add(i);
						process(strs, use, path + strs[i], all);
						use.remove(i);
					}
				}
			}
		}
	
		public static class MyComparator implements Comparator<String> {
			@Override
			public int compare(String a, String b) {
				return (a + b).compareTo(b + a);
			}
		}
	
	        // 思路二,贪心解法
		public static String lowestString2(String[] strs) {
			if (strs == null || strs.length == 0) {
				return "";
			}
			Arrays.sort(strs, new MyComparator());
			String res = "";
			for (int i = 0; i < strs.length; i++) {
				res += strs[i];
			}
			return res;
		}
	
		// for test
		public static String generateRandomString(int strLen) {
			char[] ans = new char[(int) (Math.random() * strLen) + 1];
			for (int i = 0; i < ans.length; i++) {
				int value = (int) (Math.random() * 5);
				ans[i] = (Math.random() <= 0.5) ? (char) (65 + value) : (char) (97 + value);
			}
			return String.valueOf(ans);
		}
	
		// for test
		public static String[] generateRandomStringArray(int arrLen, int strLen) {
			String[] ans = new String[(int) (Math.random() * arrLen) + 1];
			for (int i = 0; i < ans.length; i++) {
				ans[i] = generateRandomString(strLen);
			}
			return ans;
		}
	
		// for test
		public static String[] copyStringArray(String[] arr) {
			String[] ans = new String[arr.length];
			for (int i = 0; i < ans.length; i++) {
				ans[i] = String.valueOf(arr[i]);
			}
			return ans;
		}
	
		public static void main(String[] args) {
			int arrLen = 6;
			int strLen = 5;
			int testTimes = 100000;
			String[] arr = generateRandomStringArray(arrLen, strLen);
			System.out.println("先打印一个生成的字符串");
			for (String str : arr) {
				System.out.print(str + ",");
			}
			System.out.println();
			System.out.println("test begin");
			for (int i = 0; i < testTimes; i++) {
				String[] arr1 = generateRandomStringArray(arrLen, strLen);
				String[] arr2 = copyStringArray(arr1);
				if (!lowestString1(arr1).equals(lowestString2(arr2))) {
					for (String str : arr1) {
						System.out.print(str + ",");
					}
					System.out.println();
					System.out.println("Oops!");
				}
			}
			System.out.println("finish!");
		}
	}
      • 全排列的时间复杂度为:O(N!),每一种贪心算法有可能都有属于他自身的特有证明,例如哈夫曼树算法,证明千变万化。

二、求解思路

① 标准求解过程
  • 分析业务;
  • 根据业务逻辑找到不同的贪心策略;
  • 对于能举出反例的策略,直接跳过,不能举出反例的策略要证明有效性,这往往是比较困难的,要求数学能力很高且不具有统一的技巧性。
② 贪心算法的解题套路
  • 实现一个不依靠贪心策略的解法X,可以用暴力尝试;
  • 脑补出贪心策略A,贪心策略B,贪心策略C……;
  • 用解法 X 和对数器,用实验的方式得知哪个贪心策略正确;
  • 不要去纠结贪心策略的证明。

三、贪心算法套路解题实战

① 会议日程安排问题
  • 题目:一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目宣讲。给你每个项目的开始时间和结束时间,你来安排宣讲的日程,要求会议室进行宣讲的场数最多,返回最多的宣讲场次。
  • 思路:本题常见的几种贪心策略,一种是按照谁先开始安排谁,第二种按照持续时间短的先安排,第三种按照谁先结束安排谁。通过验证,无需证明得出第三种贪心策略是正确的。
	package class09;
	
	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;
			}
		}
	
	        // 暴力穷举法,用来做对数器
		public static int bestArrange1(Program[] programs) {
			if (programs == null || programs.length == 0) {
				return 0;
			}
			return process(programs, 0, 0);
		}
	
		// 还剩什么会议都放在programs里
		// done 之前已经安排了多少会议的数量
		// timeLine表示目前来到的时间点是多少
		
		// 目前来到timeLine的时间点,已经安排了done多的会议,剩下的会议programs可以自由安排
		// 返回能安排的最多会议数量
		public static int process(Program[] programs, int done, int timeLine) {
		        // 没有会议可以安排,返回安排了多少会议的数量
			if (programs.length == 0) {
				return done;
			}
			// 还有会议可以选择
			int max = done;
			// 当前安排的会议是什么会,每一个都枚举
			for (int i = 0; i < programs.length; i++) {
				if (programs[i].start >= timeLine) {
					Program[] next = copyButExcept(programs, i);
					max = Math.max(max, process(next, done + 1, programs[i].end));
				}
			}
			return max;
		}
	
		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;
		}
	
	        // 解法2:贪心算法
		public static int bestArrange2(Program[] programs) {
			Arrays.sort(programs, new ProgramComparator());
			// timeline表示来到的时间点
			int timeLine = 0;
			// result表示安排了多少个会议
			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!");
		}
	}
② 居民楼路灯问题
  • 题目:给定一个字符串 str,只由“X”和“.”两中国字符构成,“X”表示墙,不能放灯,也不需要点亮,“.”表示居民点,可以放灯,需要点亮。如果灯放在 i 位置,可以让 i-1,i 和 i+1 三个位置被点亮,返回如果点亮 str 中所需要点亮的位置,至少需要几盏灯。例如: X…X……X…X. 需要至少5盏灯。
  • Java 的算法示例如下:
	package class09;
	
	import java.util.HashSet;
	
	public class Code02_Light {
	
	        // 纯暴力,用来做对数器。点的位置放灯和不放灯全排列
		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 <

以上是关于数据结构与算法之深入解析“贪心算法“的原理解析和算法实现的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法之深入解析KMP算法的核心原理和实战演练

数据结构与算法之深入解析RSA加密算法的实现原理

数据结构与算法之深入解析Base64编码的实现原理

数据结构与算法之深入解析“完美数”的求解思路与算法示例

数据结构与算法之深入解析“股票的最大利润”的求解思路与算法示例

数据结构与算法之深入解析“最长连续序列”的求解思路与算法示例