蓝桥杯 Java B 组之区间调度、找零问题(理解贪心局限性)
Day 1:区间调度、找零问题(理解贪心局限性)
📖 一、贪心算法简介
贪心算法(Greedy Algorithm) 是一种通过局部最优解来得到全局最优解的算法策略。它每一步选择当前最优的选择,而不考虑后续的选择。贪心算法适用于某些特定问题,能有效地简化问题的求解过程。
贪心算法的基本特点:
- 局部最优选择:每次选择问题中最优的解。
- 无后效性:每次的选择不会影响之前的选择。
- 全局最优解:当满足某些条件时,贪心策略会导致全局最优解。
贪心算法的核心步骤:
- 贪心选择:从当前状态中选择最优解。
- 可行性检查:检查当前选择是否满足问题的约束。
- 更新状态:做出选择后,更新问题的状态,继续执行下一步。
📖 二、区间调度问题(Activity Selection Problem)
问题描述:
假设有一个会议室,可以接待多个活动(每个活动有开始和结束时间)。我们需要选择尽可能多的活动,使得它们之间不冲突,即活动的开始时间不早于前一个活动的结束时间。
贪心策略:
- 每次选择结束时间最早的活动,因为这样可以腾出更多时间给后续活动,从而最大化能安排的活动数量。
步骤:
- 按照活动的结束时间对所有活动进行排序。
- 从第一个活动开始,选择第一个活动并安排。
- 对于每个后续活动,判断其开始时间是否大于等于已选活动的结束时间,若满足条件,选择该活动。
代码实现(活动选择问题):
import java.util.*;
public class ActivitySelection {
static class Activity {
int start, end;
public Activity(int start, int end) {
this.start = start;
this.end = end;
}
}
public static void activitySelection(List<Activity> activities) {
// 按照结束时间排序
activities.sort(Comparator.comparingInt(a -> a.end));
int count = 1; // 至少选择第一个活动
int lastEndTime = activities.get(0).end; // 第一个活动结束时间
System.out.println("选中的活动是:");
System.out.println("开始时间: " + activities.get(0).start + " 结束时间: " + activities.get(0).end);
// 遍历剩余的活动
for (int i = 1; i < activities.size(); i++) {
// 如果活动的开始时间 >= 上一个活动的结束时间,则选择该活动
if (activities.get(i).start >= lastEndTime) {
System.out.println("开始时间: " + activities.get(i).start + " 结束时间: " + activities.get(i).end);
lastEndTime = activities.get(i).end;
count++;
}
}
System.out.println("最大活动数量为:" + count);
}
public static void main(String[] args) {
List<Activity> activities = new ArrayList<>();
activities.add(new Activity(1, 4));
activities.add(new Activity(2, 6));
activities.add(new Activity(5, 8));
activities.add(new Activity(7, 9));
activities.add(new Activity(8, 10));
activitySelection(activities);
}
}
代码讲解:
- 活动类
Activity
:每个活动有一个开始时间和结束时间。 - 排序:首先按照结束时间对活动进行排序,目的是为了优先选择结束时间早的活动。
- 贪心选择:从第一个活动开始,依次选择那些开始时间不早于前一个选择活动的结束时间的活动。
📖 三、找零问题(Coin Change Problem)
问题描述:
给定一些硬币面额,和一个目标金额 amount
,我们需要用最少的硬币组合成这个目标金额。例如,面额是 [1, 2, 5],目标金额是 11
,用最少的硬币组成 11
。
贪心策略:
- 贪心算法并不适用于所有找零问题。有时贪心选择(选择最大的面额硬币)并不能得到最优解。
贪心策略的局限性:
贪心算法假设选择当前最优解最终能带来全局最优解,但在某些情况下,这种假设是不成立的。例如,面额为 [1, 3, 4]
,目标金额为 6
,贪心策略选择面额 4
,然后剩余 2
,而最优解应该选择 3 + 3
,而非 4 + 1 + 1
。
贪心策略不能保证最优解:
- 在找零问题中,贪心策略(选择最大面额硬币)在某些情况下不能得到最少硬币数。
- 需要考虑其他动态规划策略来确保得到最优解。
代码实现(找零问题):
import java.util.*;
public class CoinChange {
public static int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // 初始化为一个较大的值
dp[0] = 0; // 0元需要0个硬币
for (int i = 1; i <= amount; i++) {
for (int coin : coins) {
if (i - coin >= 0) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount]; // 如果没有找到解,返回-1
}
public static void main(String[] args) {
int[] coins = {1, 2, 5};
int amount = 11;
System.out.println("最少硬币数为: " + coinChange(coins, amount));
}
}
代码讲解:
dp[i]
:表示组成i
元需要的最少硬币数。- 初始化:
dp[0] = 0
(0元需要0个硬币),其他元素初始化为一个很大的数amount + 1
,表示不能通过当前硬币组合组成的金额。 - 动态转移:对于每个金额
i
,遍历所有硬币面额coin
,更新dp[i]
。 - 返回值:如果
dp[amount]
还是初始值,说明无法组成该金额,返回-1
。
贪心与动态规划比较:
- 贪心法:从最大面额硬币开始,每次选择最大可能的硬币。
- 动态规划:计算每个金额的最优解,保证了在每一步做出最优选择,从而找到全局最优解。
📖 四、总结:
常见贪心算法题型
- 区间调度问题:选择不重叠的活动(按结束时间排序)。
- 背包问题:贪心适用于分数背包(物品可以分割),但不适用于0/1背包。
- Huffman 编码:通过贪心算法生成最优编码。
贪心算法的易错点:
- 贪心算法并不总能得到全局最优解,只有在满足“贪心选择性质”和“最优子结构”时,贪心算法才是有效的。
- 找零问题:贪心算法在某些面额下可能无法得到最少硬币数。
贪心算法适用条件:
- 贪心选择性质:局部最优解能推出全局最优解。
- 最优子结构:问题的最优解由子问题的最优解构成。
🎯 练习建议:
- 练习活动选择问题、硬币找零问题等贪心问题。
- 多做贪心和动态规划之间的对比,理解它们的区别和联系。