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

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

贪心算法

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"

image.png

排序策略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!");
  }
}


相关文章
|
2月前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
71 2
|
3月前
|
算法 Java C++
【贪心算法】算法训练 ALGO-1003 礼物(C/C++)
【贪心算法】算法训练 ALGO-1003 礼物(C/C++)
【贪心算法】算法训练 ALGO-1003 礼物(C/C++)
|
4月前
|
算法 前端开发
一文了解贪心算法和回溯算法在前端中的应用
该文章深入讲解了贪心算法与回溯算法的原理及其在前端开发中的具体应用,并通过分析LeetCode题目来展示这两种算法的解题思路与实现方法。
|
5月前
|
算法
【算法】贪心算法——柠檬水找零
【算法】贪心算法——柠檬水找零
|
5月前
|
算法
【算法】贪心算法简介
【算法】贪心算法简介
130 0
|
6月前
|
算法 Python
Python算法高手进阶指南:分治法、贪心算法、动态规划,掌握它们,算法难题迎刃而解!
【7月更文挑战第10天】探索Python算法的精华:分治法(如归并排序)、贪心策略(如找零钱问题)和动态规划(解复杂问题)。通过示例代码揭示它们如何优化问题解决,提升编程技能。掌握这些策略,攀登技术巅峰。
156 2
|
6月前
|
存储 算法 Python
Python算法界的秘密武器:分治法巧解难题,贪心算法快速决策,动态规划优化未来!
【7月更文挑战第9天】Python中的分治、贪心和动态规划是三大关键算法。分治法将大问题分解为小问题求解,如归并排序;贪心算法每步选局部最优解,不保证全局最优,如找零钱;动态规划存储子问题解求全局最优,如斐波那契数列。选择合适算法能提升编程效率。
80 1
|
6月前
|
存储 算法 大数据
Python算法高手的必修课:深入理解分治法、贪心算法、动态规划,让你的代码更智能!
【7月更文挑战第9天】在Python算法学习中,分治法(如归并排序)将大问题分解为小部分递归解决;贪心算法(如货币找零)在每步选择局部最优解尝试达到全局最优;动态规划(如斐波那契数列)通过存储子问题解避免重复计算,解决重叠子问题。掌握这三种方法能提升代码效率,解决复杂问题。
61 1
|
6月前
|
算法 索引 Python
逆袭算法界!Python分治法、贪心算法、动态规划深度剖析,带你走出算法迷宫!
【7月更文挑战第8天】分治法,如快速排序,将大问题分解并合并解;贪心算法,选择局部最优解,如活动选择;动态规划,利用最优子结构避免重复计算,如斐波那契数列。Python示例展示这些算法如何解决实际问题,助你精通算法,勇闯迷宫。
59 1
|
6月前
|
算法 索引 Python
Python算法设计与分析大揭秘:分治法、贪心算法、动态规划...掌握它们,让你的编程之路更加顺畅!
【7月更文挑战第8天】探索Python中的三大算法:分治(如快速排序)、贪心(活动选择)和动态规划(0-1背包问题)。分治法将问题分解求解再合并;贪心策略逐步求局部最优;动态规划通过记忆子问题解避免重复计算。掌握这些算法,提升编程效率与解决问题能力。
55 1

热门文章

最新文章