当前位置:   article > 正文

数据结构与算法 - 贪心算法_贪心算法求最短距离核心思想

贪心算法求最短距离核心思想

一、贪心例子

贪心算法或贪婪算法的核心思想是:

1. 将寻找最优解的问题分为若干个步骤

2. 每一步骤都采用贪心原则,选取当前最优解

3. 因为没有考虑所有可能,局部最优的堆叠不一定让最终解最优

贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。这种算法通常用于求解优化问题,如最小生成树、背包问题等。

贪心算法的应用:

1. 背包问题:给定一组物品和一个背包,每个物品有一定的重量和价值,要求在不超过背包容量的情况下,尽可能多地装入物品。

2. 活动选择问题:在一个活动集合中,每次只能参加一个活动,问如何安排时间以最大化所有活动的收益。

3. 编辑距离问题:给定两个字符串,求它们之间的最小编辑距离(即将一个字符串转换为另一个字符串所需的最少操作次数)。

4. 网络流问题:给定一张有向图和一些起点和终点,求最大流量。

5. 找零问题:给定一定数量的硬币和需要找零的金额,求使用最少的硬币数。

常见问题及解答:

1. 贪心算法一定会找到最优解吗?

答:不一定。贪心算法只保证在每一步选择中都是最优的,但并不能保证整个问题的最优解。例如,背包问题中的贪心算法可能会导致最后一个物品没有被装入背包。

2. 如何判断一个问题是否适合用贪心算法解决?

答:一个问题如果可以用递归的方式分解为若干个子问题,且每个子问题都有明确的最优解(即局部最优),那么这个问题就可以用贪心算法解决。

3. 贪心算法的时间复杂度是多少?

答:贪心算法的时间复杂度取决于问题的规模和具体实现。一般来说,对于规模较小的问题,贪心算法的时间复杂度可以达到O(nlogn)或O(n^2);对于规模较大的问题,可能需要(O^3)或更高。

几个贪心的例子

Dijkstra

  1. // ...
  2. while (!list.isEmpty()) {
  3. // 选取当前【距离最小】的顶点
  4. Vertex curr = chooseMinDistVertex(list);
  5. // 更新当前顶点邻居距离
  6. updateNeighboursDist(curr);
  7. // 移除当前顶点
  8. list.remove(curr);
  9. // 标记当前顶点已经处理过
  10. curr.visited = true;
  11. }
  • 没找到最短路径的例子:负边存在时,可能得不到正确解
  • 问题出在贪心的原则会认为本次已经找到了该顶点的最短路径,下次不会再处理它(curr.visited = true)
  • 与之对比,Bellman-Ford并没有考虑局部距离最小的顶点,而是每次都处理所有边,所以不会出错,当然效率不如Dijkstra
Prim
  1. // ...
  2. while (!list.isEmpty()) {
  3.    // 选取当前【距离最小】的顶点
  4.    Vertex curr = chooseMinDistVertex(list);
  5.    // 更新当前顶点邻居距离
  6.    updateNeighboursDist(curr);
  7.    // 移除当前顶点
  8.    list.remove(curr);
  9.    // 标记当前顶点已经处理过
  10.    curr.visited = true;
  11. }
Kruskal
  1. // ...
  2. while (list.size() < size - 1) {
  3.    // 选取当前【距离最短】的边
  4.    Edge poll = queue.poll();
  5.    // 判断两个集合是否相交
  6.    int i = set.find(poll.start);
  7.    int j = set.find(poll.end);
  8.    if (i != j) { // 未相交
  9.        list.add(poll);
  10.        set.union(i, j); // 相交
  11.   }
  12. }

其它贪心的例子

  • 选择排序、堆排序

  • 拓扑排序(入度最小)

  • 并查集合中的 union by size 和 union by height

  • 哈夫曼编码

  • 钱币找零,英文搜索关键字

    • change-making problem

    • find Minimum number of Coins

  • 任务编排

  • 求复杂问题的近似解

二、零钱兑换问题

1. 零钱兑换

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11

输出:3
 
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3

输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4

解法一:穷举法。超出时间限制

  1. private int min = -1;
  2. public int coinChange(int[] coins, int amount) {
  3. rec(0, coins, amount, new AtomicInteger(-1));
  4. return min;
  5. }
  6. // count代表某一组合,钱币的总数
  7. private void rec(int index, int[] coins, int remainder, AtomicInteger count) {
  8. count.incrementAndGet();
  9. if (remainder == 0) {
  10. if (min == -1) {
  11. min = count.get();
  12. } else {
  13. min = Integer.min(min, count.get());
  14. }
  15. } else if (remainder > 0) {
  16. for (int i = index; i < coins.length; i++) {
  17. rec(i, coins, remainder - coins[i], count);
  18. }
  19. }
  20. count.decrementAndGet();
  21. }

解法二:动态规划

  1. class Solution {
  2. public int coinChange(int[] coins, int amount) {
  3. int[] dp = new int[amount + 1];
  4. Arrays.fill(dp, amount + 1);
  5. // 0元所需的硬币数为0
  6. dp[0] = 0;
  7. // 遍历每一个硬币
  8. for (int coin : coins) {
  9. // 从coin到amount更新dp数组
  10. for (int i = coin; i <= amount; i++) {
  11. // dp[i]为凑成金额i所需的最少硬币个数
  12. dp[i] = Math.min(dp[i], dp[i - coin] + 1);
  13. }
  14. }
  15. return dp[amount] == amount + 1 ? -1 : dp[amount];
  16. }
  17. }

解法三:贪心算法。只能通过部分测试用例

(贪心算法得到的解不一定是全局最优解)

  1. // 贪心算法 可能得到错误的解
  2. public int coinChange(int[] coins, int amount) {
  3. Arrays.sort(coins);
  4. reverseArray(coins);
  5. int remainder = amount;
  6. int count = 0;
  7. for (int coin : coins) {
  8. // 从大面额的金币开始凑
  9. while (remainder > coin) {
  10. remainder -= coin;
  11. count++;
  12. }
  13. if (remainder == coin) {
  14. remainder = 0;
  15. count++;
  16. break;
  17. }
  18. }
  19. if (remainder > 0) {
  20. return -1;
  21. } else {
  22. return count;
  23. }
  24. }
  25. private void reverseArray(int[] coins) {
  26. int left = 0;
  27. int right = coins.length - 1;
  28. while (left < right) {
  29. // 交换元素
  30. int temp = coins[left];
  31. coins[left] = coins[right];
  32. coins[right] = temp;
  33. left++;
  34. right--;
  35. }
  36. }

2. 零钱兑换Ⅱ

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。

假设每一种面额的硬币有无限个。 

题目数据保证结果符合 32 位带符号整数。

示例 1:

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

示例 2:

输入:amount = 3, coins = [2]
输出:0
解释:只用面额 2 的硬币不能凑成总金额 3 。

示例 3:

输入:amount = 10, coins = [10] 
输出:1

提示:

  • 1 <= coins.length <= 300
  • 1 <= coins[i] <= 5000
  • coins 中的所有值 互不相同
  • 0 <= amount <= 5000

解法一:每次都会重复计算相同的子问题,导致时间复杂度较高。超出时间限制

  1. class Solution {
  2. public int change(int amount, int[] coins) {
  3. return rec(0, coins, amount, new LinkedList<>(), true);
  4. }
  5. /**
  6. * 求凑成剩余金额的解的个数
  7. * @param index 当前硬币索引
  8. * @param coins 硬币面值数组
  9. * @param remainder 剩余金额
  10. * @param stack
  11. * @param first
  12. * @return
  13. */
  14. private int rec(int index, int[] coins, int remainder, LinkedList<Integer> stack, boolean first) {
  15. if(!first) {
  16. stack.push(coins[index]);
  17. }
  18. int count = 0;
  19. // 情况1:剩余金额 < 0 -> 无解
  20. if(remainder < 0) {
  21. print("无解:", stack);
  22. }
  23. // 情况2:剩余金额 == 0 -> 找到解
  24. else if(remainder == 0) {
  25. print("有解:", stack);
  26. count = 1;
  27. }
  28. // 情况3:剩余金额 > 0 -> 继续递归
  29. else {
  30. for (int i = index; i < coins.length; i++) {
  31. count += rec(i, coins, remainder - coins[i], stack, false);
  32. }
  33. }
  34. // 回溯backtrack
  35. if(!stack.isEmpty()) {
  36. stack.pop();
  37. }
  38. return count;
  39. }
  40. private static void print(String prompt, LinkedList<Integer> stack) {
  41. ArrayList<Integer> print = new ArrayList<>();
  42. ListIterator<Integer> iterator = stack.listIterator(stack.size());
  43. while(iterator.hasPrevious()) {
  44. print.add(iterator.previous());
  45. }
  46. System.out.println(prompt + print);
  47. }
  48. }

解法二:动态规划。执行耗时2ms

使用一个一维数组dp来保存到达每个金额所需的不同硬币组合的数量

  1. public int change(int amount, int[] coins) {
  2. int[] dp = new int[amount + 1];
  3. dp[0] = 1; // 只有一种方式凑成0元,即不使用任何硬币
  4. // 动态规划:通过循环每一个硬币来计算不同金额的组合数
  5. for (int coin : coins) {
  6. for (int i = coin; i <= amount; i++) {
  7. dp[i] += dp[i - coin];
  8. }
  9. }
  10. return dp[amount];
  11. }

三、Huffman编码问题

1. 问题引入

(1)什么是编码?

答:简单来说就是建立【字符】到【数字】的对应关系,如下面大家熟知的ASCⅡ编码表,例如,可以查表得知字符【a】对应的数字是十六进制数【0x61】

\000102030405060708090a0b0c0d0e0f
0000000102030405060708090a0b0c0d0e0f
0010101112131415161718191a1b1c1d1e1f
002020!"#$%&'()*+,-./
00300123456789:;<=>?
0040@ABCDEFGHIJKLMNO
0050PQRSTUVWXYZ[\]^_
0060`abcdefghijklmno
0070pqrstuvwxyz{|}~7f

注:一些直接以十六进制数字标识的是那些不可打印字符

(2)传输时的编码

java中每个char对应的数字会占用固定长度2个字节

如果在传输中仍采用上述规则,传递abbccccccc这10个字符

  • 实际的字节为 0061006200620063006300630063006300630063(16进制表示)
  • 总共20个字节,不经济

现在希望找到一种最节省字节的传输方式,怎么办?

假设传输的字符中只包含a,b,c这3个字符,有同学重新设计一张二进制编码表,见下图

  • 0表示a
  • 1表示b
  • 10表示c

现在还是传递abbccccccc这10个字符

  • 实际的字节为 01110101010101010 (二进制表示)
  • 总共需要17bits,也就是2字节多一点,行不行?

不行,因为解码会出现问题,因为10会被错误的解码成为ba,而不是c

  • 解码后的结果为 abbbababababababa,是错误的。

怎么解决?必须保证编码后的二进制数字,要能区分它们的前缀(prefix-free),用满二叉树结构编码,可以确保前缀不重复

  • 向左走为0,向右走为1
  • 走到叶子字符,累计起来的0和1就是该字符的二进制编码
  • a的编码为0;b的编码为10;c的编码为11

现在还是传递 abbccccccc 这 10 个字符

  • 实际的字节为 0101011111111111111(二进制表示)
  • 总共需要 19 bits,也是 2 个字节多一点,并且解码没有问题了,行不行?

这次解码没有问题了,但是并非最少字节,因为c的出现频率高(7次),a的出现频率低(1次),因此出现频率高的字符编码成短数字更经济。

考察下面的树

00表示a;01表示b;1表示c

现在还是传递image-20230616095129461

  • 实际的字节为 000101 1111111 (二进制表示)

  • 总共需要 13 bits,这棵树就称之为 Huffman 树

  • 根据 Huffman 树对字符和数字进行编解码,就是 Huffman 编解码

2. Huffman树及Huffman编解码

注:为了简单,期间编码解码都用字符串演示,实际应该按bits编解码

  1. package com.itheima.algorithms.greedy;
  2. import java.util.Comparator;
  3. import java.util.HashMap;
  4. import java.util.Map;
  5. import java.util.PriorityQueue;
  6. /**
  7. * Huffman树及编解码
  8. */
  9. public class HuffmanTree {
  10. /**
  11. * Huffman树的构建过程
  12. * 1. 将统计了出现频率的字符,放入到优先级队列
  13. * 2. 每次出队两个频次最低的元素,给他俩找个爹
  14. * 3. 把爹重新放入队列,重复 2~3
  15. * 4. 当队列中只剩一个元素时,Huffman树构建完成
  16. */
  17. static class Node {
  18. Character ch; // 字符
  19. int freq; // 频次
  20. Node left; // 左孩子
  21. Node right; // 右孩子
  22. String code; // 编码
  23. public Node(Character ch) {
  24. this.ch = ch;
  25. }
  26. public Node(int freq, Node left, Node right) {
  27. this.freq = freq;
  28. this.left = left;
  29. this.right = right;
  30. }
  31. public int getFreq() {
  32. return freq;
  33. }
  34. public boolean isLeaf() {
  35. return left == null;
  36. }
  37. @Override
  38. public String toString() {
  39. return "Node{" + "ch = " + ch + ", freq = " + freq + "}";
  40. }
  41. }
  42. String str;
  43. Node root;
  44. Map<Character, Node> map = new HashMap<>();
  45. public HuffmanTree(String str) {
  46. this.str = str;
  47. // 1. 统计频率
  48. char[] chars = str.toCharArray();
  49. for (char ch : chars) {
  50. /*if(!map.containsKey(ch)) {
  51. map.put(ch, new Node(ch));
  52. }
  53. Node node = map.get(ch);
  54. node.freq++;*/
  55. Node node = map.computeIfAbsent(ch, Node::new);
  56. node.freq++;
  57. }
  58. // 2. 构造树
  59. PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingInt(Node::getFreq));
  60. queue.addAll(map.values());
  61. while (queue.size() >= 2) {
  62. // 每次出队两个频次最低的元素,给他俩找个爹
  63. Node x = queue.poll();
  64. Node y = queue.poll();
  65. int freq = x.freq + y.freq;
  66. // 把爹重新放入队列
  67. queue.offer(new Node(freq, x, y));
  68. }
  69. root = queue.poll();
  70. // 3. 计算每个字符的编码
  71. int bit = dfs(root, new StringBuilder());
  72. for (Node node : map.values()) {
  73. System.out.println(node + ", " + "code:" + node.code);
  74. }
  75. System.out.println("字符串 " + str + " 编码总共占用了 " + bit + " bit");
  76. }
  77. private int dfs(Node node, StringBuilder code) {
  78. int sum = 0;
  79. if (node.isLeaf()) {
  80. // 叶子节点
  81. node.code = code.toString();
  82. // 4. 统计字符串编码后占用多少bit
  83. sum += node.freq * node.code.length();
  84. } else {
  85. sum += dfs(node.left, code.append("0"));
  86. sum += dfs(node.right, code.append("1"));
  87. }
  88. // 回溯
  89. if (code.length() > 0) {
  90. code.deleteCharAt(code.length() - 1);
  91. }
  92. return sum;
  93. }
  94. // 编码
  95. public String encode() {
  96. char[] chars = str.toCharArray();
  97. StringBuilder sb = new StringBuilder();
  98. for (char ch : chars) {
  99. sb.append(map.get(ch).code);
  100. }
  101. return sb.toString();
  102. }
  103. /**
  104. * 解码
  105. * 从根节点开始,寻找数字对应的字符
  106. * 数字是0,向左走
  107. * 数字是1,向右走
  108. * 如果没走到头,每走一步数字的索引 i++
  109. * 走到头就可以找到解码字符,再将node重置为根节点
  110. * @param str -> code
  111. * @return
  112. */
  113. public String decode(String str) {
  114. char[] chars = str.toCharArray();
  115. int i = 0;
  116. StringBuilder sb = new StringBuilder();
  117. Node node = root;
  118. while(i < chars.length) {
  119. if(!node.isLeaf()) { // 非叶子
  120. if(chars[i] == '0') {
  121. // 向左走
  122. node = node.left;
  123. } else if(chars[i] == '1'){
  124. // 向右走
  125. node = node.right;
  126. }
  127. i++;
  128. }
  129. if(node.isLeaf()) {
  130. sb.append(node.ch);
  131. node = root;
  132. }
  133. }
  134. return sb.toString();
  135. }
  136. public static void main(String[] args) {
  137. HuffmanTree tree = new HuffmanTree("abbccccccc");
  138. String encode = tree.encode();
  139. System.out.println(encode);
  140. System.out.println(tree.decode(encode));
  141. }
  142. }

3. 连接木棍的最低费用

为了装修新房,你需要加工一些长度为正整数的棒材 sticks。

如果要将长度分别为 X 和 Y 的两根棒材连接在一起,你需要支付 X + Y 的费用。 由于施工需要,你必须将所有棒材连接成一根。

返回你把所有棒材 sticks 连成一根所需要的最低费用。注意你可以任意选择棒材连接的顺序。

示例 1:
输入:sticks = [2,4,3]
输出:14
解释:先将 2 和 3 连接成 5,花费 5;再将 5 和 4 连接成 9;总花费为 14。

示例 2:
输入:sticks = [1,8,3,5]
输出:30

提示:
1 <= sticks.length <= 10^4
1 <= sticks[i] <= 10^4

解法一:哈夫曼树(建树)

  1. class Solution {
  2. public int connectSticks(int[] sticks) {
  3. // 1. 将元素放入优先队列
  4. PriorityQueue<Integer> queue = new PriorityQueue<>();
  5. for (int stick : sticks) {
  6. queue.offer(stick);
  7. }
  8. int sum = 0;
  9. while(queue.size() >= 2) {
  10. // 每次取最小的两个
  11. Integer x = queue.poll();
  12. Integer y = queue.poll();
  13. int c = x + y;
  14. sum += c;
  15. // 将父节点入队
  16. queue.offer(c);
  17. }
  18. return sum;
  19. }
  20. }

四、活动选择问题

1. 举办最多的活动

要在应该会议室举办n个活动

  •  - 每个活动有它们各自的起始和结束时间
  •  - 找出时间上互不冲突的活动组合,能够充分利用会议室(举办的活动次数最多)
  1. package com.itheima.algorithms.greedy;
  2. import java.util.ArrayList;
  3. import java.util.Arrays;
  4. import java.util.Comparator;
  5. import java.util.List;
  6. /**
  7. * 活动区间选择 - 贪心算法
  8. * 无重叠区间本质就是活动选择问题
  9. */
  10. public class ActivitySelectionProblem {
  11. /**
  12. * 要在应该会议室举办n个活动
  13. * - 每个活动有它们各自的起始和结束时间
  14. * - 找出时间上互不冲突的活动组合,能够充分利用会议室(举办的活动次数最多)
  15. *
  16. * 例1
  17. * 0 1 2 3 4 5 6 7 8 9
  18. * |-------)
  19. * |-------)
  20. * |-------)
  21. * 例2
  22. * 0 1 2 3 4 5 6 7 8 9
  23. * |---)
  24. * |---)
  25. * |-----------------------)
  26. * |-------)
  27. * |---)
  28. * |---------------)
  29. *
  30. * 几种贪心策略
  31. * 1. 优先选择持续时间最短的活动 (×)
  32. * 0 1 2 3 4 5 6 7 8 9
  33. * 1 |---------------) 选中
  34. * 2 |-------)
  35. * 3 |---------------) 选中
  36. *
  37. * 2. 优先选择冲突最少的活动 (×)
  38. * 编号 0 1 2 3 4 5 6 7 8 9 冲突次数 实际解
  39. * 1 |-------) 3 选中
  40. * 2 |-------) 4
  41. * 3 |-------) 4
  42. * 4 |-------) 4
  43. * 5 |-------) 4 选中
  44. * 6 |-------) 2
  45. * 7 |-----------) 4 选中
  46. * 8 |-------) 4
  47. * 9 |-------) 4
  48. * 10 |-------) 4
  49. * 11 |-------) 3 选中
  50. *
  51. * 3. 优先选择最先开始的活动 (×)
  52. * 0 1 2 3 4 5 6 7 8 9
  53. * 2 |---) 选中
  54. * 3 |---) 选中
  55. * 4 |---) 选中
  56. * 1 |-----------------------------------)
  57. *
  58. * 4. 优先选择最先结束的活动 (√)
  59. */
  60. // 活动类
  61. static class Activity {
  62. int index;
  63. int start;
  64. int finish;
  65. public Activity(int index, int start, int finish) {
  66. this.index = index;
  67. this.start = start;
  68. this.finish = finish;
  69. }
  70. public int getFinish() {
  71. return finish;
  72. }
  73. @Override
  74. public String toString() {
  75. return "Activity(" + index + ")";
  76. }
  77. }
  78. public static void main(String[] args) {
  79. Activity[] activities = new Activity[] {
  80. new Activity(1, 2, 4),
  81. new Activity(0, 1, 3),
  82. new Activity(2, 3, 5)
  83. };
  84. Arrays.sort(activities, Comparator.comparingInt(Activity::getFinish));
  85. System.out.println(Arrays.toString(activities));
  86. select(activities, activities.length);
  87. }
  88. private static void select(Activity[] activities, int n) {
  89. List<Activity> result = new ArrayList<>();
  90. Activity prev = activities[0]; // 上次被选中的活动
  91. result.add(prev);
  92. for (int i = 1; i < n; i++) {
  93. Activity curr = activities[i]; // 正在处理的活动
  94. if(curr.start >= prev.finish) {
  95. result.add(curr);
  96. prev = curr;
  97. }
  98. }
  99. for (Activity activity : result) {
  100. System.out.println(activity);
  101. }
  102. }
  103. }

2. 无重叠区间

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 

示例 1:

输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:

输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

示例 3:

输入: intervals = [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

提示:

  • 1 <= intervals.length <= 10^5
  • intervals[i].length == 2
  • -5 * 10^4 <= starti < endi <= 5 * 10^4

解法一:贪心算法

  1. class Solution {
  2. /**
  3. * 找到不重叠的最多的活动数(count),即活动选择问题原始需求
  4. * 在此基础之上,活动总数 - count,就是题目要的排除数量
  5. *
  6. * @param intervals
  7. * @return
  8. */
  9. public int eraseOverlapIntervals(int[][] intervals) {
  10. // 1. 将集合元素按照结束时间升序排序
  11. Arrays.sort(intervals, Comparator.comparingInt(a -> a[1]));
  12. int count = 1; // 默认选择第一个
  13. int i = 0; // 前一个
  14. for (int j = 0; j < intervals.length; j++) {
  15. // 后一个的开始时间 >= 前一个的结束时间
  16. if (intervals[j][0] >= intervals[i][1]) {
  17. i = j;
  18. count++;
  19. }
  20. }
  21. return intervals.length - count;
  22. }
  23. }

解法二:动态规划

  1. class Solution {
  2. public int eraseOverlapIntervals(int[][] intervals) {
  3. if (intervals.length == 0)
  4. return 0;
  5. // 按结束时间排序
  6. Arrays.sort(intervals, (a, b) -> Integer.compare(a[1], b[1]));
  7. // 创建一个数组保存结束时间
  8. int n = intervals.length;
  9. int[] ends = new int[n];
  10. ends[0] = intervals[0][1];
  11. int count = 1; // 至少包括第一个区间
  12. for (int i = 1; i < n; i++) {
  13. // 检查重叠
  14. if (intervals[i][0] >= ends[count - 1]) {
  15. ends[count++] = intervals[i][1]; // 添加到不重叠区间
  16. }
  17. }
  18. return n - count; // 总数减去不重叠的数量
  19. }
  20. }

五、分数背包问题

  1. package com.itheima.algorithms.greedy;
  2. import java.util.Arrays;
  3. import java.util.Comparator;
  4. public class FractionalKnapsackProblem {
  5. /*
  6. 1. n个物品都是液体,有重量和价值
  7. 2. 现在你要取走 10升 的液体
  8. 3. 每次可以不拿,全拿,或拿一部分,问最高价值是多少
  9. 编号 重量(升) 价值
  10. 0 4 24 水
  11. 1 8 160 牛奶 选中 7/8
  12. 2 2 4000 五粮液 选中
  13. 3 6 108 可乐
  14. 4 1 4000 茅台 选中
  15. 8140
  16. 简化起见,给出的数据都是【价值/重量】能够整除,避免计算结果中出现小数,增加心算难度
  17. */
  18. static class Item {
  19. int index;
  20. int weight;
  21. int value;
  22. public Item(int index, int weight, int value) {
  23. this.index = index;
  24. this.weight = weight;
  25. this.value = value;
  26. }
  27. // 单位价值
  28. public int unitValue() {
  29. return value / weight;
  30. }
  31. @Override
  32. public String toString() {
  33. return "Item{" + index + "}";
  34. }
  35. }
  36. public static void main(String[] args) {
  37. Item[] items = new Item[] {
  38. new Item(0, 4, 24),
  39. new Item(1, 8, 160),
  40. new Item(2, 2, 4000),
  41. new Item(3, 6, 108),
  42. new Item(4, 1, 4000)
  43. };
  44. System.out.println(select(items, 10));
  45. }
  46. private static int select(Item[] items, int total) {
  47. // 1. 按单位价值降序排序
  48. Arrays.sort(items, Comparator.comparingInt(Item::unitValue).reversed());
  49. int max = 0;
  50. for (Item item : items) {
  51. if(total >= item.weight) {
  52. // 可以拿完
  53. total -= item.weight;
  54. max += item.value;
  55. } else {
  56. // 拿不完
  57. max += item.unitValue() * total;
  58. break;
  59. }
  60. }
  61. return max;
  62. }
  63. }

六、0-1背包问题

对于此问题,贪心算法可能无法得到最优解

  1. package com.itheima.algorithms.greedy;
  2. import java.util.Arrays;
  3. import java.util.Comparator;
  4. public class KnapsackProblem {
  5. /*
  6. 1. n个物品都是固体,有重量和价值
  7. 2. 现在你要取走不超过 10克 的物品
  8. 3. 每次可以 不拿 或 全拿,问最高价值是多少
  9. 编号 重量(g) 价值(元)
  10. 0 1 1_000_000 钻戒一枚
  11. 1 4 1600 黄金一块
  12. 2 8 2400 红宝石戒指一枚
  13. 3 5 30 白银一块
  14. */
  15. static class Item {
  16. int index;
  17. int weight;
  18. int value;
  19. public Item(int index, int weight, int value) {
  20. this.index = index;
  21. this.weight = weight;
  22. this.value = value;
  23. }
  24. public int unitValue() {
  25. return value / weight;
  26. }
  27. @Override
  28. public String toString() {
  29. return "Item(" + index + ")";
  30. }
  31. }
  32. public static void main(String[] args) {
  33. Item[] items = new Item[]{
  34. new Item(0, 1, 1000000),
  35. new Item(1, 4, 1600),
  36. new Item(2, 8, 2400),
  37. new Item(3, 5, 30)
  38. };
  39. select(items, 10);
  40. }
  41. private static void select(Item[] items, int total) {
  42. Arrays.sort(items, Comparator.comparingInt(Item::unitValue).reversed());
  43. int max = 0; // 最大价值
  44. for (Item item : items) {
  45. System.out.println(item);
  46. if (total >= item.weight) { // 可以拿完
  47. total -= item.weight;
  48. max += item.value;
  49. }
  50. }
  51. System.out.println("最大价值是:" + max);
  52. }
  53. }

贪心算法的局限

问题名称是否能用贪心得到最优解替换解法
Dijkstra(不存在负边)✔️
Dijkstra(存在负边)Bellman-Ford
Prim✔️
Kruskal✔️
零钱兑换动态规划
Huffman 树✔️
活动选择问题✔️
分数背包问题✔️
0-1 背包问题动态规划

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/1003591
推荐阅读
相关标签
  

闽ICP备14008679号