贪心算法——最低字典序,从头到尾利用贪心算法求解

173 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情

贪心算法

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.0x的字典序 <= y的字典序,x放前;否则,y放前(单独拿单个字符串的字典序拼大小,然后决定谁放前),大多数情况正确,但是本题不行,比如:[ba,b]。按照单个字符从字典序来拼大小的话,结果是"bba",但字典序最小的拼接结果是"bab" 在这里插入图片描述 排序策略2.0x拼接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!");
	}
}

本文出自:贪心算法——最低字典序,从头到尾利用贪心算法求解