当前位置: 首页 > news >正文

算法笔记17 - 贪心算法介绍与思路 | 路灯摆放问题 | 活动安排问题 | 最低字典序拼接 | 金条分割问题 | 项目投资问题

文章目录

    • 1. 引言:什么是贪心算法?
    • 2. 贪心算法的实战思路:策略与验证
    • 3. 贪心算法题目解析
      • 3.1 题目一:会议室宣讲(活动安排问题)
        • 代码实现:
        • 对数器:
      • 3.2 题目二:最低字典序拼接
        • 代码实现:
        • 对数器:
      • 3.3 题目三:金条分割(哈夫曼编码应用)
        • 代码实现:
        • 对数器:
      • 3.4 题目案例四:项目投资(IPO问题)
      • 3.5 题目五:放置路灯问题

1. 引言:什么是贪心算法?

贪心算法(Greedy Algorithm)是一种在每一步决策中都采取当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法策略。

贪心算法的核心思想是“目光短浅”,它不从整体最优上加以考虑,而是只做出在当前看来是最好的选择。这种策略并不总是能得到全局最优解,但对于许多问题,它却能产生全局最优解,或者至少是一个可接受的近似解。

2. 贪心算法的实战思路:策略与验证

在解决算法问题时,贪心策略因其高效和直观而备受青睐。但其最大的难点在于证明其正确性。在实际工程或竞赛中,我们往往采用一种更务实的方法:

  • 贪心策略的选择

首先,我们需要根据问题情境,构思出一个局部的最优解。例如:

  • 在安排会议时,是优先选择开始早的,还是结束早的?
  • 在拼接字符串时,是按字典序排,还是按其他规则?

选定一个策略后,我们先假设它是正确的,并着手实现。

  • “对数器”:验证贪心策略

与其陷入复杂的数学归纳法来证明贪心策略,不如实现一个“对数器”来验证其正确性(或者用一个绝对正确但效率低的方法)。

“对数器”本质上是一个暴力解法。这个解法必须保证绝对正确,但可以不考虑时间复杂度(例如,使用全排列或递归搜索)。

如在接下来的代码中所示,验证步骤如下:

  1. 实现我们的贪心算法(例如 greedy 方法)。
  2. 实现一个绝对正确但低效的暴力解法(例如 brute 方法)。
  3. 编写一个随机样本生成器(例如 randomPrograms)。
  4. 进行成千上万次测试:用同一个随机样本,分别调用贪心解法和暴力解法,对比它们的结果。
  5. 如果所有测试结果都一致,我们就可以在工程上高度确信这个贪心策略是正确的。

接下来,我们将解析贪心策略的应用。

3. 贪心算法题目解析

3.1 题目一:会议室宣讲(活动安排问题)

问题描述:

给定一系列项目的开始时间和结束时间,一个会议室同一时间只能安排一个项目。求如何安排才能使得宣讲的场次最多。

贪心策略:

这个问题的核心贪心策略是:优先安排结束时间最早的项目。

直观理解是,一个项目结束得越早,会议室就能越早被释放出来,从而为后续安排更多项目提供可能性。

实现细节:

我们使用一个“小根堆”(PriorityQueue)来存储所有项目,并自定义比较器,使其按照项目的结束时间 end 从小到大排序。

我们维护一个变量 curTime,表示当前会议室可用的最早时间点(初始为0)。

  1. 不断从小根堆中弹出结束时间最早的项目 poll
  2. 如果该项目的开始时间 poll.start 大于等于 curTime,说明这个项目可以被安排。
  3. 安排该项目后,更新 curTime 为该项目的结束时间 poll.end,并将场次 sum 加一。
  4. 如果项目开始时间早于 curTime,则跳过该项目(因为它与已安排的项目冲突)。
代码实现:
public class ProgramArrange {public static class Program {public int start;public int end;public Program(int start, int end) {this.start = start;this.end = end;}}// ============= 贪心策略 ==============public static int greedy(Program[] arr) {if (arr == null || arr.length == 0) {return 0;}PriorityQueue<Program> pq = new PriorityQueue<>(new ProgramComparator());pq.addAll(Arrays.asList(arr));int sum = 0;int curTime = 0;while(!pq.isEmpty()) {Program poll = pq.poll();if (poll.start >= curTime) {sum++;curTime = poll.end;}}return sum;}public static class ProgramComparator implements Comparator<Program> {@Overridepublic int compare(Program o1, Program o2) {return o1.end - o2.end;}}}
对数器:

​ 暴力解法思路: 为了找到字典序最小的拼接结果,暴力解法会尝试所有可能的字符串排列组合。它使用递归(process 方法)来生成全排列。getArrWithoutI 辅助函数用于在递归中获取“剩余”的字符串数组。最后,它使用 TreeSet 数据结构,该结构会自动按字典序排序所有结果,我们只需取出第一个(最小的)即可。

/*** 暴力方法解决*/
public static String brute(String[] arr) {if (arr == null || arr.length == 0) {return "";}TreeSet<String> rst = process(arr);return rst.first();
}// 方法作用:返回arr字符串数组中所有可能的排列方式
public static TreeSet<String> process(String[] arr) {TreeSet<String> rst = new TreeSet<>();if (arr == null || arr.length == 0) {rst.add("");return rst;}for (int i = 0; i < arr.length; i++) {String first = arr[i];TreeSet<String> rest = process(getArrWithoutI(arr, i));for (String str : rest) {rst.add(first + str);}}return rst;
}public static String[] getArrWithoutI(String[] arr, int idx) {if (arr == null || arr.length == 0) return null;String[] rst = new String[arr.length - 1];int rstIdx = 0;for (int i = 0; i < arr.length; i++) {if (i == idx) {continue;}rst[rstIdx++] = arr[i];}return rst;
}

3.2 题目二:最低字典序拼接

问题描述:

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

贪心策略:

此问题的贪心策略体现在排序规则上。我们不能简单地按单个字符串的字典序排序。

正确的贪心策略是:对于任意两个字符串 o1o2,如果 (o1 + o2) 的字典序小于 (o2 + o1),那么 o1 就应该排在 o2 的前面。

实现细节:

  1. 自定义一个比较器 MyComparator
  2. compare 方法中,返回 (o1 + o2).compareTo(o2 + o1) 的结果。
  3. 使用 Arrays.sort 和这个自定义比较器对原数组进行排序。
  4. 最后,将排序后的字符串依次拼接起来即可。
代码实现:
public class LowestLexicography {// 比较器public static class MyComparator implements Comparator<String> {@Overridepublic int compare(String o1, String o2) {return (o1 + o2).compareTo(o2 + o1);}}// 贪心方法public static String lowestLexicography(String[] arr) {if (arr == null || arr.length == 0) return "";Arrays.sort(arr, new MyComparator());StringBuilder rst = new StringBuilder();for (String curr : arr) {rst.append(curr);}return rst.toString();}}
对数器:

暴力解法思路: 同样使用递归(process 方法)。process 方法的含义是:在 programs 数组中,从 curTime 这个时间点开始,最多还能安排 done 场会议。 它遍历所有剩余的会议,尝试两种选择:

  1. 不安排当前会议(通过循环到下一个 i 实现)。
  2. 如果当前会议 programs[i] 的开始时间晚于 curTime,则安排它 (done + 1),并从这个会议的结束时间 (programs[i].end) 开始,递归地去安排剩下的会议。 最后返回所有可能性中的最大值 max
public static int brute(Program[] arr) {if (arr == null || arr.length == 0) {return 0;}return process(arr, 0, 0);
}// 方法作用:返回能安排的最多的会议
// 还剩下的会议都放在programs里
// done之前已经安排了多少会议的数量
// curTime目前来到的时间点是什么
public static int process(Program[] programs, int done, int curTime) {if (programs.length == 0) {return done;}int max = done;for (int i = 0; i < programs.length; i++) {if (programs[i].start >= curTime) {max = Math.max(max, process(restPrograms(programs, i), done + 1, programs[i].end));}}return max;
}public static Program[] restPrograms(Program[] arr, int idx) {Program[] rst = new Program[arr.length - 1];int rstIdx = 0;for (int i = 0; i < arr.length; i++) {if (i == idx) {continue;}rst[rstIdx++] = arr[i];}return rst;
}

3.3 题目三:金条分割(哈夫曼编码应用)

问题描述:

一块金条切成两半,需要花费和长度数值一样的铜板。给定一个数组,代表最终要分成的几部分。求如何分割能使总花费(铜板)最少。

贪心策略:

这个问题可以逆向思考:看作是将几块小金条合并成一块大金条,每次合并的代价是两块金条的长度之和。

这完全符合哈夫曼编码的思想。贪心策略是:永远优先合并当前最小的两块金条

实现细节:

  1. 使用一个小根堆(PriorityQueue)。
  2. 将所有目标长度(数组元素)加入小根堆。
  3. 当堆中元素多于一个时,循环执行:
    • 弹出堆顶两个最小元素(pq.poll() 两次)。
    • 将这两个元素相加得到 cur,这就是本次合并的代价。
    • cur 累加到总代价 cost 中。
    • cur(合并后的新长度)重新放回小根堆。
  4. 循环结束时,cost 即为最小总代价。
代码实现:
public class LessMoneySplitGold {// =========== 贪心 ===========public static int huffmanSolution(int[] gold) {if (gold == null || gold.length == 0) {return 0;}PriorityQueue<Integer> pq = new PriorityQueue<>();int cost = 0;for (int i = 0; i < gold.length; i++) {pq.add(gold[i]);}while (pq.size() > 1) {int cur = pq.poll() + pq.poll();cost += cur;pq.add(cur);}return cost;}}
对数器:

暴力解法思路: 这个问题(哈夫曼编码问题)的暴力解法是:尝试所有可能的合并顺序。 process 方法递归地尝试合并 arr 数组中的任意两个元素(ij)。每次合并,都会产生 arr[i] + arr[j] 的代价,这个代价被累加到 pre 中。然后,它将合并后的新数组(通过 mergeResult 获得)传入下一层递归。 ans 变量会取所有不同合并路径下的最小值,最终返回全局最小代价。

public static int brute(int[] gold) {if (gold == null || gold.length == 0) {return 0;}return process(gold, 0);
}// 作用:返回合并arr最小的总代价
// 等待合并的数都在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(mergeResult(arr, i, j), pre + arr[i] + arr[j]));}}return ans;
}public static int[] mergeResult(int[] arr, int a, int b) {int[] rst = new int[arr.length - 1];int sum = arr[a] + arr[b];for (int i = 0, idx = 0; i < arr.length; i++) {if (i == a || i == b) {continue;}rst[idx++] = arr[i];}rst[rst.length - 1] = sum;return rst;
}

3.4 题目案例四:项目投资(IPO问题)

问题描述:

给定项目的花费数组 costs、利润数组 profits、最多可做项目数 K 和初始资金 M。串行做项目,收益可以用于下一个项目。求最后获得的最大钱数。

贪心策略:

这是一个复合贪心策略。我们最多只能做 K 个项目,在每一步(选择第1个、第2个…第K个项目)时,我们都应该做出最优选择。

最优选择是:在所有当前资金 M 允许(cost <= M)的项目中,选择利润 profit 最高的那个项目来做

实现细节:

为了高效实现这个策略,我们使用两个堆:

  1. 成本小根堆 (small):按 cost 排序。存放所有“目前还买不起”或“还未解锁”的项目。
  2. 利润大根堆 (big):按 profit 排序。存放所有“已经买得起”(cost <= M)的项目。

流程如下:

  1. 将所有项目按成本加入成本小根堆 small

  2. 循环 K 次(或直到无法再做项目):

    a. [解锁] 将 small 堆中所有 cost <= M 的项目,全部“解锁”并移入 big 堆(利润大根堆)。

    b. [选择] 如果此时 big 堆为空,说明当前资金买不起任何新项目,提前结束。

    c. [执行] 从 big 堆弹出一个项目(即利润最高的项目),将其利润加到 M 上。

  3. 返回最终的 M

代码实现:

public class IPO {// ============== 贪心 =============public static class Program {public int cost;public int profit;public Program(int cost, int profit) {this.cost = cost;this.profit = profit;}}public static int greedy(int[] costs, int[] profits, int K, int M) {PriorityQueue<Program> small = new PriorityQueue<>(new MinCostComparator());PriorityQueue<Program> big = new PriorityQueue<>(new MaxProfitComparator());int length = costs.length;for (int i = 0; i < length; i++) {small.add(new Program(costs[i], profits[i]));}int count =  0;while (count < K || !small.isEmpty() || !big.isEmpty()) {while (!small.isEmpty() && small.peek().cost <= M) {Program poll = small.poll();big.add(poll);}if (!big.isEmpty()) {Program curProgram = big.poll();M += curProgram.profit;count++;} else {break;}}return M;}public static class MinCostComparator implements Comparator<Program> {@Overridepublic int compare(Program o1, Program o2) {return o1.cost - o2.cost;}}public static class MaxProfitComparator implements Comparator<Program> {@Overridepublic int compare(Program o1, Program o2) {return o2.profit - o1.profit;}}
}

3.5 题目五:放置路灯问题

问题描述:

给定一个由 X(墙)和 .(居民点)组成的字符串。X 不需点亮,. 需点亮。灯放在 i 位置,可以点亮 i-1, i, i+1。求点亮所有 . 所需的最少灯数。

贪心策略:

我们从左到右遍历字符串。当遇到一个 .(居民点)时,这个点必须被照亮。我们总希望一盏灯能发挥最大效率。

  1. i 位置是 . 时,我们必须放一盏灯。
  2. 为了让这盏灯覆盖尽可能多的新区域(即右侧),我们优先考虑放在 i+1 位置。
    • 情况A: 如果 i+1 也是 .(且未越界),我们就把灯放在 i+1。这盏灯能覆盖 i, i+1, i+2。因此,我们下一个需要检查的位置是 i+3
    • 情况B: 如果 i+1X,或者 i 已经是最后一个字符,我们别无选择,灯只能放在 i 位置。这盏灯覆盖 i-1, i, i+1。因此,我们下一个需要检查的位置是 i+2
  3. 如果 i 位置是 X,则跳过。

代码实现:

public class LightProblem {// ======== 贪心 =========public static int light(String str) {if (str == null) {return 0;}char[] arr = str.toCharArray();int light = 0;int i = 0;while (i < arr.length) {if (arr[i] == 'X') {i++;} else {light++;if (i + 1 == arr.length) {break;} else {if (arr[i + 1] == 'X') {i = i + 2;} else {i = i + 3;}}}}return light;}// ... [对数器和其他方法省略] ...
}
http://www.dtcms.com/a/601211.html

相关文章:

  • CentOS系统一键安装Memcached教程
  • 构建本质安全:现代智能消防的物联网架构深度解析
  • 筑牢API安全防线
  • openssl自动升级(下载git相关)~~坑点
  • 用源代码做网站注册网站代码
  • 个人博客网站logo网络营销推广形式
  • 【计网】基于三层交换机的多 VLAN 局域网组建
  • Python键盘鼠标自动化库详解:从入门到精通
  • Prompt-R1:重新定义AI交互的「精准沟通」范式
  • 郑州国外网站建设克拉玛依市建设局网站
  • 国产化中间件东方通TongWeb环境安装部署(图文详解)
  • 防爆六维力传感器的本质安全,破解高危环境自动化难题
  • 达内网站开发做网站费用会计分录
  • 深圳营销型网站建设公司网络服务php网站开发打不开
  • GIT版本管理工具轻松入门 | TortoiseGit,Git 介绍软件安装配置,笔记01
  • Flutter中Column中使用ListView时溢出问题的解决方法
  • Linux 传输层协议
  • 攻防世界-Misc-适合作为桌面
  • STM32F103VET6开发板例程(一)-LED
  • 上海网站优化推广公司阿里云网站建设方案书
  • 基于Water Physics在Unreal Engine中实现水系统模拟与物体漂浮状态模拟
  • Qt-自定义按钮动画
  • llm course 5.6 学习笔记 同样的文本 模型输出的固定向量和计算出来的哈希值为什么携带的信息不同
  • 轻量化笔记推荐:Docker安装部署FlatNotes
  • 永康市住房建设局网站淮南网站建设
  • Facebook矩阵引流:从防封机制拆解
  • 新时代旅游职业教育系列教材编写研讨会成功举办
  • vue学习第一天
  • 各大编码编辑器的缓存目录迁移到D盘【未完待续】
  • 【XR开发系列】Unity第一印象:编辑器界面功能布局介绍(六大功能区域介绍)