贪心算法
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”)
所以什么是字典序?总的一句话概括就是:两个个字符串一样长的话,直接当作K进制的正数进行比较;如果两个字符串长度不一样,短的那一个在后面用0补齐再进行比较。(Java中字符串的compareTo方法就是在比字典序)
排序策略1.0:x的字典序 <= y的字典序,x放前;否则,y放前(单独拿单个字符串的字典序拼大小,然后决定谁放前),大多数情况正确,但是本题不行,比如:[ba,b]。按照单个字符从字典序来拼大小的话,结果是"bba",但字典序最小的拼接结果是"bab"
排序策略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 <= 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 ( c ) * a + c * a - b * a <= b * m(a) * c + a * c - b * c
所以 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
回到题目
给定一个由字符串组成的数组strs,必须把所有的字符串拼接起来,返回所有可能的拼接结果中,字典序最小的结果
方法一:全排列暴力解决,时间复杂度O(N!)*O(M),因为两个字符串拼接也有代价(跟字符串的平均长度有关)。
方法二:用贪心,每一种贪心都有自己独特的证明,每一个贪心算法的证明都不一样,难道一定要通过证明才能找到贪心策略吗?不用,不通过证明同样可以得到贪心策略。后面文章会继续贪心的练习,敬请关注哈哈哈哈!
附上这题完整代码:
package com.harrison.class09; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; public class Code01_LowestLexicography { // strs 放着所有的字符串 // use 已经使用过的字符串的下标,在use里登记过了,不要再使用了 // path 之前已经使用过的字符串,拼接成了path // all 收集所有可能拼接的结果 public static void process1( String[] strs, HashSet<Integer> use,String path,ArrayList<String> all) { // 所有字符串都已经使用过了,path就是拼接的一种结果 if(use.size()==strs.length) { all.add(path); }else {// strs里的字符串还没有使用完,遍历strs中所有的字符串 for(int i=0; i<strs.length; i++) { if(!use.contains(i)) {// 如果当前的字符串是没有使用过的,将其加入到use里 use.add(i); // 每一个没有使用过的字符串都这么处理 process1(strs, use, path+strs[i], all); // 恢复现场!!!(深度优先遍历的常见做法) use.remove(i); } } } } // 暴力解:全排列 public static String lowestLexicography1(String[] strs) { if(strs==null || strs.length==0) { return ""; } ArrayList<String> all=new ArrayList<>(); HashSet<Integer> use=new HashSet<>(); process1(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; } public static class Comparator implements java.util.Comparator<String>{ @Override public int compare(String a, String b) { return (a+b).compareTo(b+a); } } public static String lowestLexicography2(String[] strs) { if(strs==null || strs.length==0) { return ""; } Arrays.sort(strs, new Comparator()); String ans=""; for(int i=0; i<strs.length; i++) { ans+=strs[i]; } return ans; } 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); } 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; } 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 (!lowestLexicography1(arr1).equals(lowestLexicography2(arr2))) { for (String str : arr1) { System.out.print(str + ","); } System.out.println(); System.out.println("Oops!"); } } System.out.println("finish!"); } }