贪心算法
- 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;
}
}