9.贪心算法

贪心算法

  • 1)最自然智慧的算法
  • 2)用一种局部最功利的标准,总是做出在当前看来是最好的选择
  • 3)难点在于证明局部最功利的标准可以得到全局最优解
  • 4)对于贪心算法的学习主要以增加阅历和经验为主

从头到尾讲一道利用贪心算法求解的题目

【题目】

给定一个由字符串组成的数组strs,必须把所有的字符串拼接起来,返回所有可能的拼接结果中,字典序最小的结果

字典序:两个字符串如果要放入字典中,谁放在前面谁的字典序就小。

  • 如果两个字符串长度一样,将其视为K进制的正数进行比较(K=26)

  • 如果两个字符串长度不一样,短的要补的和长的一样长(补0,0就是比a的ASCII码还小的一个值)。比如 str1=“ac” 和 str2=“b”,str2补齐后为 “b0”,str2虽然低位比str1的低位小,但是高位比str1的大,所以"ac"的字典序比"b"小(“ac”<“b0”)

  • 排序策略1.0:x的字典序 <= y的字典序,x放前;否则,y放前(单独拿单个字符串的字典序拼大小,然后决定谁放前)

  • 排序策略2.0:x拼接y的字典序 <= y拼接x的字典序,x放前(即x作为前缀比y作为前缀要好);否则,y放前

排序的传递性

虽然生活中常见的列子都天然具有传递性,所以很容易忽视,但自己所定义的排序策略不一定具有传递性

为什么一定要纠结有没有传递性:证明在数组中,任何一个在前的和任何一个在后的,都有前结合后 <= 后结合前。(即**[前…后] => 前+后 <= 后+前**)

如何证明:a.b <= b.a && b.c <= c.b --> a.c <= c.a
  • 先弄懂什么是字符串拼接:“ks”+“te” --> “kste”,字符串就是K进制的正数,K==26。所以就是 “ks” * 26^2 + “te”。(a * K^b长度 + b)

  • K^x长度 记为 m(x)

  • 所以条件1和2分别变为:

    • a * m(b) + b <= b * m(a) + a 不等式1
    • b * m© + c <= c * m(b) + b 不等式2
  • 不等式1两侧都 减b乘c:a * m(b) * c <= b * m(a) * c + a * c - b * c

  • 不等式2两侧都 减b乘a:b * m ( c ) * a + c * a - b * a <= c * m (b) * a

  • 所以 b * m(a) * c + a * c - b * c >= b * m ( c ) * a + c * a - b * a

  • 所以 m ( a ) * c - c >= m ( c ) * a - a

  • 所以 m ( a ) * c + a >= m ( c ) * a + c

  • 所以最终证明了排序策略2.0有传递性,它可以得到一个序列 [前…后] --> 前+后 <= 后+前

接下来还要证明为什么这个序列最后拼起来的字典序是最小呢?

假设经过排序策略2.0得到一个数组 […a…b…],先证明一件事情,任何一个在前的字符串和任何一个在后的字符串,这两个字符串交换位置得到的结果都一定会得到更大的字典序。什么意思?

  • 假设a和b中间有两个字符串 […a…m1…m2…b…],证明所得到的新的序列 […b…m1…m2…a…] 拼起来的字典序一定更大,证明任意两个调换会更大,任意三个调换会更大,四个…数学归纳法

如何证明:

  • 原序列 […a…m1…m2…b…] <= […m1…a…m2…b…],因为 a.m1 <= m1.a(原序列中a在m1前)
  • […m1…a…m2…b…] <= […m1…m2…a…b…],因为 a.m2 <= m2.a (原序列中a在m2前)
  • […m1…m2…a…b…] <= […m1…m2…b…a…],因为 a.b <= b.a
  • […m1…m2…b…a…] <= […m1…b…m2…a…],因为 m2.b <= b.m2
  • […m1…b…m2…a…] <= […b…m1…m2…a…],因为 m1.b <= b.m1
package com.harrison.class09;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.TreeSet;

public class Code01_LowestLexicography {
    
    
//	//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);
//				}
//			}
//		}
//	}

	// {"abc", "cks", "bct"}
	// 0 1 2
	// removeIndexString(arr , 1) -> {"abc", "bct"}
	public static String[] removeIndexString(String[] arr, int index) {
    
    
		int N = arr.length;
		String[] ans = new String[N - 1];
		int ansIndex = 0;
		for (int i = 0; i < N; i++) {
    
    
			if (i != index) {
    
    
				ans[ansIndex++] = arr[i];
			}
		}
		return ans;
	}

	// strs中所有字符串全排列,返回所有可能的结果
	public static TreeSet<String> process(String[] strs) {
    
    
		TreeSet<String> ans = new TreeSet<>();
		if (strs.length == 0) {
    
    
			ans.add("");
			return ans;
		}
		for (int i = 0; i < strs.length; i++) {
    
    
			String first = strs[i];
			String[] nexts = removeIndexString(strs, i);
			TreeSet<String> next = process(nexts);
			for (String cur : next) {
    
    
				ans.add(first + cur);
			}
		}
		return ans;
	}

	public static String lowestString1(String[] strs) {
    
    
		if (strs == null || strs.length == 0) {
    
    
			return "";
		}
		TreeSet<String> ans = process(strs);
		return ans.size() == 0 ? "" : ans.first();
	}

	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 = 10000;
		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!");
	}
}

贪心算法求解的标准过程

  • 1)分析业务
  • 2)根据业务逻辑找到不同的贪心策略
  • 3)对于能举出反例的策略直接跳过,不能举出反例的策略要证明有效性
  • 4)这往往是特别困难的,要求数学能力很高且不具有统一的技巧性

贪心算法的解题套路

  • 1)实现一个不依靠贪心策略的解法X,可以用最暴力的尝试
  • 2)脑补出贪心策略A、贪心策略B、贪心策略C…
  • 3)用解法X和对数器,用实验的方式得知哪个贪心策略正确
  • 4)不要去纠结贪心策略的证明

贪心算法的解题套路实战

【题目1】

一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。给你每一个项目开始的时间和结束的时间。你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。返回最多的宣讲场次。

package com.harrison.class09;

import java.util.Arrays;
import java.util.Comparator;

public class Code02_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 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;
	}
	
	/**
	 * 
	 * @param programs 还剩下的会议
	 * @param done 之前已经安排的会议的数量
	 * @param timeLine 目前来到的时间点是多少
	 * @return
	 */
	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 int bestArrange1(Program[] programs) {
    
    
		if(programs==null || programs.length==0) {
    
    
			return 0;
		}
		return process(programs,0,0);
	}
	
	//按会议结束时间排序
	//结束时间早的会议排前面
	public static class ProgramComparator implements Comparator<Program>{
    
    
		@Override
		public int compare(Program o1,Program o2) {
    
    
			return o1.end-o2.end;
		}
	}
	
	//会议的开始时间和结束时间都是数值,不会<0
	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 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中所有需要点亮的位置,至少需要几盏灯。

package com.harrison.class09;

import java.util.HashSet;

public class Code03_Light {
    
    
	// str[index....]位置,自由选择放灯还是不放灯
	// str[0..index-1]位置呢?已经做完决定了,那些放了灯的位置,存在lights里
	// 要求选出能照亮所有.的方案,并且在这些有效的方案中,返回最少需要几个灯
	public static int process(char[] str, int index, HashSet<Integer> lights) {
    
    
		if (index == str.length) {
    
    // 结束的时候
			for (int i = 0; i < str.length; i++) {
    
    
				if (str[i] != 'X') {
    
    // 如果当前位置是点的话
					if (!lights.contains(i - 1) && !lights.contains(i) && !lights.contains(i + 1)) {
    
    
						return Integer.MAX_VALUE;
					}
				}
			}
			return lights.size();
		} else {
    
    // str还没结束
				// i位置无论是'X'还是'.',都有一个选择,那就是不放灯
				// no 当前i位置没有放灯,返回后续的最好灯数
			int no = process(str, index + 1, lights);
			// yes 只有'.'才会变
			int yes = Integer.MAX_VALUE;
			if (str[index] == '.') {
    
    
				lights.add(index);
				yes = process(str, index + 1, lights);
				lights.remove(index);
			}
			return Math.min(no, yes);
		}
	}

	// 暴力解
	public static int minLight1(String road) {
    
    
		if (road == null || road.length() == 0) {
    
    
			return 0;
		}
		return process(road.toCharArray(), 0, new HashSet<>());
	}

	// 贪心解法 index位不会被之前位影响到!!!(潜台词)
	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++;
			} else {
    
    
				light++;
				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) {
    
    
		int len = 20;
		int testTime = 100000;
		for (int i = 0; i < testTime; i++) {
    
    
			String test = randomString(len);
			int ans1 = minLight1(test);
			int ans2 = minLight2(test);
			if (ans1 != ans2) {
    
    
				System.out.println("oops!");
			}
		}
		System.out.println("finish!");
	}
}
【题目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铜板。
输入一个数组,返回分割的最小代价。

package com.harrison.class09;

import java.util.PriorityQueue;

public class Code04_LessMoneySplitGold {
    
    
	// 等待合并的数都在arr里,pre之前的合并行为产生了多少总代价
	// arr中只剩一个数字的时候,停止合并,返回最小的总代价
	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;
	}
	
	//纯暴力!
	public static int lessMoney1(int[] arr) {
    
    
		if(arr==null || arr.length==0) {
    
    
			return 0;
		}
		return process(arr,0);
	}
	
	//用小根堆
	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();
			sum+=cur;
			pQ.add(cur);
		}
		return sum;
	}
	
	public static int[] generateRandomArray(int maxSize,int maxValue) {
    
    
		int [] arr=new int[(int)(Math.random()*(maxSize+1))];
		for(int i=0; i<arr.length; i++) {
    
    
			arr[i]=(int)(Math.random()*(maxValue+1));
		}
		return arr;
	}
	
	public static void main(String[] args) {
    
    
		int maxSize=6;
		int maxValue=1000;
		int testTime=100000;
		for(int i=0; i<testTime; i++) {
    
    
			int [] arr=generateRandomArray(maxSize,maxValue);
			if(lessMoney1(arr)!=lessMoney2(arr)) {
    
    
				System.out.println("Oops!");
			}
		}
		System.out.println("finish!");
	}
} 
【题目4】

输入:正数数组costs、正数数组profits、正数K、正数M
costs[i]表示i号项目的花费
profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润)
K表示你只能串行的最多做k个项目
M表示你初始的资金
说明:每做完一个项目,马上获得的收益,可以支持你去做下一个项目。不能并行的做项目。
输出:你最后获得的最大钱数。

package com.harrison.class09;

import java.util.Comparator;
import java.util.PriorityQueue;

public class Code05_IPO {
    
    
	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;
		}
	}

	// 最多K个项目
	// W是初始资金
	// Profits[] Capital[] 一定等长
	// 返回最终最大的资金
	public static int findMaximizedCapital(int k,int w,int[] profits,int[] capital) {
    
    
		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],capital[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;
	}
}

猜你喜欢

转载自blog.csdn.net/weixin_44337241/article/details/121201186