赞
踩
核心思想:
贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。通常用于求解优化问题,如最小生成树、背包问题等。
贪心算法的应用:
常见问题及解答:
// ...
while (!list.isEmpty()) {
// 选取当前【距离最小】的顶点
Vertex curr = chooseMinDistVertex(list);
// 更新当前顶点邻居距离
updateNeighboursDist(curr);
// 移除当前顶点
list.remove(curr);
// 标记当前顶点已经处理过
curr.visited = true;
}
// ...
while (!list.isEmpty()) {
// 选取当前【距离最小】的顶点
Vertex curr = chooseMinDistVertex(list);
// 更新当前顶点邻居距离
updateNeighboursDist(curr);
// 移除当前顶点
list.remove(curr);
// 标记当前顶点已经处理过
curr.visited = true;
}
// ...
while (list.size() < size - 1) {
// 选取当前【距离最短】的边
Edge poll = queue.poll();
// 判断两个集合是否相交
int i = set.find(poll.start);
int j = set.find(poll.end);
if (i != j) { // 未相交
list.add(poll);
set.union(i, j); // 相交
}
}
其它贪心的例子
选择排序、堆排序
拓扑排序
并查集合中的 union by size 和 union by height
哈夫曼编码
钱币找零,英文搜索关键字
任务编排
求复杂问题的近似解
public class Leetcode518 { public int change(int[] coins, int amount) { return rec(0, coins, amount, new LinkedList<>(), true); } /** * 求凑成剩余金额的解的个数 * * @param index 当前硬币索引 * @param coins 硬币面值数组 * @param remainder 剩余金额 * @param stack - * @param first - * @return 解的个数 */ public int rec(int index, int[] coins, int remainder, LinkedList<Integer> stack, boolean first) { if(!first) { stack.push(coins[index]); } // 情况1:剩余金额 < 0 - 无解 int count = 0; if (remainder < 0) { print("无解:", stack); } // 情况2:剩余金额 == 0 - 有解 else if (remainder == 0) { print("有解:", stack); count = 1; } // 情况3:剩余金额 > 0 - 继续递归 else { for (int i = index; i < coins.length; i++) { count += rec(i, coins, remainder - coins[i], stack, false); } } if (!stack.isEmpty()) { stack.pop(); } return count; } private static void print(String prompt, LinkedList<Integer> stack) { ArrayList<Integer> print = new ArrayList<>(); ListIterator<Integer> iterator = stack.listIterator(stack.size()); while (iterator.hasPrevious()) { print.add(iterator.previous()); } System.out.println(prompt + print); } public static void main(String[] args) { Leetcode518 leetcode = new Leetcode518(); // int count = leetcode.coinChange(new int[]{1, 5, 10, 25}, 41); // int count = leetcode.coinChange(new int[]{25, 10, 5, 1}, 41); // int count = leetcode.coinChange(new int[]{5, 2, 1}, 5); // int count = leetcode.coinChange(new int[]{1, 2, 5}, 5); int count = leetcode.change(new int[]{15, 10, 1}, 21); System.out.println(count); } }
public class Leetcode322 { static int min = -1; // 需要的最少硬币数 2 3 public int coinChange(int[] coins, int amount) { rec(0, coins, amount, new AtomicInteger(-1), new LinkedList<>(), true); return min; } // count 代表某一组合 钱币的总数 public void rec(int index, int[] coins, int remainder, AtomicInteger count, LinkedList<Integer> stack, boolean first) { if (!first) { stack.push(coins[index]); } count.incrementAndGet(); // count++ if (remainder == 0) { System.out.println(stack); if (min == -1) { min = count.get(); } else { min = Integer.min(min, count.get()); } } else if (remainder > 0) { for (int i = index; i < coins.length; i++) { rec(i, coins, remainder - coins[i], count, stack, false); } } count.decrementAndGet(); // count-- if (!stack.isEmpty()) { stack.pop(); } } public static void main(String[] args) { Leetcode322 leetcode = new Leetcode322(); // int count = leetcode.coinChange(new int[]{5, 2, 1}, 5); int count = leetcode.coinChange(new int[]{25, 10, 5, 1}, 41); // int count = leetcode.coinChange(new int[]{2}, 3); // int count = leetcode.coinChange(new int[]{15, 10, 1}, 21); System.out.println(count); } }
public class Leetcode322 { public int coinChange(int[] coins, int amount) { int remainder = amount; int count = 0; for (int coin : coins) { while (remainder - coin > 0) { remainder -= coin; count++; } if (remainder - coin == 0) { remainder = 0; count++; break; } } if (remainder > 0) { return -1; } else { return count; } } public static void main(String[] args) { Leetcode322 leetcode = new Leetcode322(); int count = leetcode.coinChange(new int[]{5, 2, 1}, 5); // int count = leetcode.coinChange(new int[]{25, 10, 5, 1}, 41); // int count = leetcode.coinChange(new int[]{2}, 3); // 问题1 没有回头,导致找到更差的解 // int count = leetcode.coinChange(new int[]{15, 10, 1}, 21); // 问题2 没有回头,导致无解 // int count = leetcode.coinChange(new int[]{15, 10}, 20); System.out.println(count); } }
传输时的编码
假设传输的字符中只包含 a,b,c 这 3 个字符,重新设计一张二进制编码表
现在还是传递 abbccccccc 这 10 个字符
必须保证编码后的二进制数字,要能区分它们的前缀(prefix-free)
用满二叉树结构编码,可以确保前缀不重复
编码表
现在还是传递 abbccccccc 这 10 个字符
现在还是传递 abbccccccc 这 10 个字符
public class HuffmanTree { /* Huffman 树的构建过程 1. 将统计了出现频率的字符,放入优先级队列 2. 每次出队两个频次最低的元素,给它俩找个爹 3. 把爹重新放入队列,重复 2~3 4. 当队列只剩一个元素时,Huffman 树构建完成 */ static class Node { Character ch; // 字符 int freq; // 频次 Node left; Node right; String code; // 编码 public Node(Character ch) { this.ch = ch; } public Node(int freq, Node left, Node right) { this.freq = freq; this.left = left; this.right = right; } int freq() { return freq; } boolean isLeaf() { return left == null; } @Override public String toString() { return "Node{" + "ch=" + ch + ", freq=" + freq + '}'; } } String str; Map<Character, Node> map = new HashMap<>(); public HuffmanTree(String str) { this.str = str; // 功能1:统计频率 char[] chars = str.toCharArray(); for (char c : chars) { /*if (!map.containsKey(c)) { map.put(c, new Node(c)); } Node node = map.get(c); node.freq++;*/ Node node = map.computeIfAbsent(c, Node::new); node.freq++; } // 功能2: 构造树 PriorityQueue<Node> queue = new PriorityQueue<>(Comparator.comparingInt(Node::freq)); queue.addAll(map.values()); while (queue.size() >= 2) { Node x = queue.poll(); Node y = queue.poll(); int freq = x.freq + y.freq; queue.offer(new Node(freq, x, y)); } Node root = queue.poll(); // 功能3:计算每个字符的编码, 功能4:字符串编码后占用 bits int sum = dfs(root, new StringBuilder()); for (Node node : map.values()) { System.out.println(node + " " + node.code); } System.out.println("总共会占用 bits:" + sum); } private int dfs(Node node, StringBuilder code) { int sum = 0; if (node.isLeaf()) { node.code = code.toString(); sum = node.freq * code.length(); } else { sum += dfs(node.left, code.append("0")); sum += dfs(node.right, code.append("1")); } if (code.length() > 0) { code.deleteCharAt(code.length() - 1); } return sum; } public static void main(String[] args) { new HuffmanTree("abbccccccc"); } }
注意
- Node::new 是一个 Function,根据 key(即字符)生成 Node 对象
- 对应的是 public Node(Character ch) 有参构造
编解码都用字符串,实际应该按 bits 编解码
public class HuffmanTree { // ... // 编码 public String encode() { char[] chars = str.toCharArray(); StringBuilder sb = new StringBuilder(); for (char c : chars) { sb.append(map.get(c).code); } return sb.toString(); } // 解码 public String decode(String str) { /* 从根节点,寻找数字对应的字符 数字是 0 向左走 数字是 1 向右走 如果没走到头,每走一步数字的索引 i++ 走到头就可以找到解码字符,再将 node 重置为根节点 */ char[] chars = str.toCharArray(); int i = 0; StringBuilder sb = new StringBuilder(); Node node = root; while (i < chars.length) { if (!node.isLeaf()) { // 非叶子 if(chars[i] == '0') { // 向左走 node = node.left; } else if(chars[i] == '1') { // 向右走 node = node.right; } i++; } if (node.isLeaf()) { sb.append(node.ch); node = root; } } return sb.toString(); } @SuppressWarnings("all") public static void main(String[] args) { HuffmanTree tree = new HuffmanTree("abbccccccc"); String encoded = tree.encode(); System.out.println(encoded); System.out.println(tree.decode(encoded)); } }
注意
- 循环中非叶子节点 i 要自增,但叶子节点 i 暂不自增
- 第一个非叶子的 if 判断结束后,仍需要第二个叶子的 if 判断,因为在第一个 if 内 node 发生了变化
/** * <h3>连接棒材的最低费用</h3> * <p>为了装修新房,你需要加工一些长度为正整数的棒材。如果要将长度分别为 X 和 Y 的两根棒材连接在一起,你需要支付 X + Y 的费用。 返回讲所有棒材连成一根所需要的最低费用。</p> */ public class Leetcode1167 { /* 举例 棒材为 [1,8,3,5] 如果以如下顺序连接(非最优) - 1+8=9 - 9+3=12 - 12+5=17 总费用为 9+12+17=38 如果以如下顺序连接(最优) - 1+3=4 - 4+5=9 - 8+9=17 总费用为 4+9+17=30 */ int connectSticks(int[] sticks) { PriorityQueue<Integer> queue = new PriorityQueue<>(); for (int stick : sticks) { queue.offer(stick); } int sum = 0; while (queue.size() >= 2) { Integer x = queue.poll(); Integer y = queue.poll(); int c = x + y; sum += c; queue.offer(c); } return sum; } public static void main(String[] args) { Leetcode1167 leetcode = new Leetcode1167(); System.out.println(leetcode.connectSticks(new int[]{1, 8, 3, 5})); // 30 System.out.println(leetcode.connectSticks(new int[]{2, 4, 3})); // 14 } }
public class ActivitySelectionProblem { /* 要在一个会议室举办 n 个活动 - 每个活动有它们各自的起始和结束时间 - 找出在时间上互不冲突的活动组合,能够最充分利用会议室(举办的活动次数最多) 例1 0 1 2 3 4 5 6 7 8 9 |-------) |-------) |-------) 例2 0 1 2 3 4 5 6 7 8 9 |---) |---) |-----------------------) |-------) |---) |---------------) 几种贪心策略 1. 优先选择持续时间最短的活动 0 1 2 3 4 5 6 7 8 9 |---------------) |-------) |---------------) 2. 优先选择冲突最少的活动 0 1 2 3 4 5 6 7 8 9 |-------) 3 |-------) 4 |-------) 4 |-------) 4 |-------) 4 |-------) 2 |-----------) 4 |-------) 4 |-------) 4 |-------) 4 |-------) 3 3. 优先选择最先开始的活动 0 1 2 3 4 5 6 7 8 9 |-----------------------------------) |---) |---) |---) 4. 优先选择最后结束的活动 */ static class Activity { int index; int start; int finish; public Activity(int index, int start, int finish) { this.index = index; this.start = start; this.finish = finish; } @Override public String toString() { return "Activity(" + index + ")"; } } public static void main(String[] args) { Activity[] activities = new Activity[]{ new Activity(0, 1, 3), new Activity(1, 2, 4), new Activity(2, 3, 5) }; // Activity[] activities = new Activity[]{ // new Activity(0, 1, 2), // new Activity(1, 3, 4), // new Activity(2, 0, 6), // new Activity(3, 5, 7), // new Activity(4, 8, 9), // new Activity(5, 5, 9) // }; select(activities, activities.length); } public static void select(Activity[] activities, int n) { List<Activity> result = new ArrayList<>(); int i, j; i = 0; result.add(activities[i]); for (j = 1; j < n; j++) { if (activities[j].start >= activities[i].finish) { result.add(activities[j]); i = j; } } System.out.println(result); } }
// 下面代码为 Leetcode 435 题解
public int eraseOverlapIntervals(int[][] intervals) {
Arrays.sort(intervals, Comparator.comparingInt(a -> a[1]));
int i, j;
i = 0;
int count = 1;
for (j = 1; j < intervals.length; j++) {
if (intervals[j][0] >= intervals[i][1]) {
i = j;
count++;
}
}
return intervals.length - count;
}
贪心法
public class FractionalKnapsackProblem { /* 1. n个物品都是液体,有重量和价值 2. 现在你要取走 10升 的液体 3. 每次可以不拿,全拿,或拿一部分,问最高价值是多少 编号 重量(升) 价值 0 4 24 水 1 8 160 牛奶 选中 7/8 2 2 4000 五粮液 选中 3 6 108 可乐 4 1 4000 茅台 选中 8140 简化起见,给出的数据都是【价值/重量】能够整除,避免计算结果中出现小数,增加心算难度 */ static class Item { int index; int weight; int value; public Item(int index, int weight, int value) { this.index = index; this.weight = weight; this.value = value; } int unitPrice() { return value / weight; } @Override public String toString() { return "Item(" + index + ")"; } } public static void main(String[] args) { Item[] items = new Item[]{ new Item(0, 4, 24), new Item(1, 8, 160), new Item(2, 2, 4000), new Item(3, 6, 108), new Item(4, 1, 4000), }; select(items, 10); } static void select(Item[] items, int total) { Arrays.sort(items, Comparator.comparingInt(Item::unitPrice).reversed()); int remainder = total; int max = 0; for (Item item : items) { if (remainder - item.weight > 0) { max += item.value; remainder -= item.weight; } else { max += remainder * item.unitPrice(); break; } } System.out.println("最高价值为:" + max); } }
贪心法
可能得不到最优解
public class KnapsackProblem { /* 1. n个物品都是固体,有重量和价值 2. 现在你要取走不超过 10克 的物品 3. 每次可以不拿或全拿,问最高价值是多少 编号 重量(g) 价值(元) 0 1 1_000_000 钻戒一枚 1 4 1600 黄金一块 2 8 2400 红宝石戒指一枚 3 5 30 白银一块 */ static class Item { int index; int weight; int value; public Item(int index, int weight, int value) { this.index = index; this.weight = weight; this.value = value; } public int unitValue() { return value / weight; } @Override public String toString() { return "Item(" + index + ")"; } } public static void main(String[] args) { Item[] items = new Item[]{ new Item(0, 1, 1_000_000), new Item(1, 4, 1600), new Item(2, 8, 2400), new Item(3, 5, 30) }; select(items, 10); } static void select(Item[] items, int total) { Arrays.sort(items, Comparator.comparingInt(Item::unitValue).reversed()); int max = 0; // 最大价值 for (Item item : items) { System.out.println(item); if (total >= item.weight) { // 可以拿完 total -= item.weight; max += item.value; } else { // 拿不完 // max += total * item.unitValue(); // break; } } System.out.println("最大价值是:" + max); } }
问题名称 | 是否能用贪心得到最优解 | 替换解法 |
---|---|---|
Dijkstra(不存在负边) | ✔️ | |
Dijkstra(存在负边) | ❌ | Bellman-Ford |
Prim | ✔️ | |
Kruskal | ✔️ | |
零钱兑换 | ❌ | 动态规划 |
Huffman 树 | ✔️ | |
活动选择问题 | ✔️ | |
分数背包问题 | ✔️ | |
0-1 背包问题 | ❌ | 动态规划 |
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。