博客引言:
在我们的日常生活中,优化资源的使用和提高效率是永恒的主题。今天,我们将通过两个有趣的问题,探索如何用算法来优化这些场景。
首先,我们将探讨用最少数量的箭引爆气球问题,看看如何通过算法找到引爆所有气球所需的最少箭数。接着,我们将分析柯柯吃香蕉的最小速度问题,探讨如何通过算法找到柯柯在规定时间内吃完所有香蕉的最小速度。通过这两个案例,你将看到算法如何在实际生活中发挥作用,帮助我们更高效地使用资源、优化效率。
让我们一起进入算法的世界,探索这些优化问题背后的奥秘!
博客正文:
一、用最少数量的箭引爆气球:贪心算法的巧妙应用
场景描述:
一堵墙上贴有很多球形气球,每个气球用水平直径的起点和终点表示。一支弓箭可以沿着x轴射出,只要箭射出的x坐标在某个气球的水平直径范围内,就会引爆那个气球。目标是用最少的箭引爆所有气球。
算法核心:贪心算法
这个问题可以通过贪心算法来解决。贪心算法通过在每一步选择局部最优解,从而达到全局最优解。具体来说,按气球的结束坐标排序,依次选择结束最早的气球,将其结束坐标作为箭的位置,这样可以尽可能多地覆盖后续气球。
详细分析:
- 初始化:将所有气球的区间按结束坐标从小到大排序。
- 贪心选择:选择第一个气球的结束坐标作为箭的位置,然后跳过所有包含这个点的气球。
- 重复步骤:继续选择下一个未处理气球的结束坐标作为箭的位置,直到所有气球都被处理。
题目验证示例:
- 示例1:points = [[10,16],[2,8],[1,6],[7,12]],输出为2。箭的位置分别在6和11。
- 示例2:points = [[1,2],[3,4],[5,6],[7,8]],输出为4。每个气球都需要一支箭。
- 示例3:points = [[1,2],[2,3],[3,4],[4,5]],输出为2。箭的位置分别在2和4。
#include <stdio.h> // 标准输入输出头文件
#include <stdlib.h> // 标准库头文件(包含qsort函数)
/**
* 比较函数:用于qsort排序,按气球区间的结束坐标升序排列
* @param a 指向第一个元素的指针(这里元素类型是int*)
* @param b 指向第二个元素的指针
* @return 正数/负数/0表示排序关系
*/
int compare(const void *a, const void *b) {
// 将void指针转换为二级指针并解引用,得到两个气球区间的指针
int *intervalA = *(int **)a; // 解析为第一个气球区间(start1, end1)
int *intervalB = *(int **)b; // 解析为第二个气球区间(start2, end2)
return intervalA[1] - intervalB[1]; // 按结束坐标end升序排序
}
/**
* 计算引爆所有气球需要的最少箭数
* @param points 二维指针数组,每个元素指向一个气球区间[start, end]
* @param pointsSize 气球数量(数组行数)
* @param pointsColSize 每个气球的列数(固定为2,实际未使用)
* @return 最少需要的箭数
*/
int findMinArrowShots(int **points, int pointsSize, int *pointsColSize) {
// 边界条件:没有气球时不需要箭
if (pointsSize == 0) return 0;
// 使用标准库qsort对气球区间进行排序
// 参数:数组地址,元素个数,元素大小,比较函数
qsort(points, pointsSize, sizeof(int *), compare);
int arrows = 1; // 初始化箭数(至少需要一支)
int arrowPos = points[0][1]; // 第一箭的位置设为第一个区间的结束坐标
// 遍历排序后的所有气球区间(从第二个开始)
for (int i = 1; i < pointsSize; ++i) {
// 如果当前气球的起点超过当前箭的位置(无法被当前箭覆盖)
if (points[i][0] > arrowPos) {
arrows++; // 需要增加一支箭
arrowPos = points[i][1]; // 更新箭的位置为该区间的结束坐标
}
// 此处隐藏逻辑:当区间重叠时(points[i][0] <= arrowPos),
// 当前箭可以同时覆盖,不需要增加箭数
}
return arrows; // 返回最终计算的箭数
}
int main() {
// ----------------- 示例1测试 -----------------
// 原始数据:[[10,16],[2,8],[1,6],[7,12]],预期输出2
int row1[] = {10, 16}; // 创建第一个气球的区间数组
int row2[] = {2, 8}; // 创建第二个气球的区间数组
int row3[] = {1, 6}; // 创建第三个气球的区间数组
int row4[] = {7, 12}; // 创建第四个气球的区间数组
// 创建指针数组,每个元素指向对应的区间数组
int *points1[] = {row1, row2, row3, row4};
int colSize = 2; // 每行的列数(固定为2)
printf("Example 1 output: %d\n", findMinArrowShots(points1, 4, &colSize));
// ----------------- 示例2测试 -----------------
// 原始数据:[[1,2],[3,4],[5,6],[7,8]],预期输出4
int r1[] = {1, 2}; // 创建测试数据
int r2[] = {3, 4};
int r3[] = {5, 6};
int r4[] = {7, 8};
int *points2[] = {r1, r2, r3, r4};
printf("Example 2 output: %d\n", findMinArrowShots(points2, 4, &colSize));
// ----------------- 示例3测试 -----------------
// 原始数据:[[1,2],[2,3],[3,4],[4,5]],预期输出2
int a1[] = {1, 2}; // 创建测试数据
int a2[] = {2, 3};
int a3[] = {3, 4};
int a4[] = {4, 5};
int *points3[] = {a1, a2, a3, a4};
printf("Example 3 output: %d\n", findMinArrowShots(points3, 4, &colSize));
return 0; // 正常退出程序
}
/* 关键执行流程说明:
1. 程序启动后依次执行三个测试用例
2. 每个测试用例构造对应的气球数据
3. 调用findMinArrowShots计算最小箭数
4. 打印结果与预期值对比验证
*/
输出结果:
二、柯柯吃香蕉的最小速度:二分查找的高效应用
场景描述:
柯柯有n堆香蕉,每堆有piles[i]根。她需要在h小时内吃掉所有香蕉。她可以选择吃香蕉的速度k(单位是根/小时)。每小时她会选择一堆香蕉,吃掉k根或者吃掉这堆剩下的所有香蕉,然后这一小时内不会再吃其他香蕉。目标是找到最小的k,使得柯柯能在h小时内吃掉所有香蕉。
算法核心:二分查找
这个问题可以通过二分查找来解决。k的可能取值范围是有序的,我们可以从最小的k开始尝试,逐步增加,直到找到满足条件的最小k。
详细分析:
- 确定k的范围:最小的k是1,最大的k是max(piles)。
- 二分查找:对于每一个k,计算吃掉所有香蕉所需的总时间。如果总时间小于等于h,则尝试寻找更小的k;否则,需要增大k。
- 计算总时间:对于每一堆piles[i],计算吃掉这堆所需的小时数,即ceil(piles[i] / k),然后将所有堆的小时数相加,得到总时间。
题目验证示例:
- 示例1:piles = [3,6,7,11], h = 8,输出为4。柯柯可以在4小时内吃掉所有香蕉。
- 示例2:piles = [30,11,23,4,20], h = 5,输出为30。柯柯必须以30根/小时的速度吃掉所有香蕉。
- 示例3:piles = [30,11,23,4,20], h = 6,输出为23。柯柯可以在23根/小时的速度下吃掉所有香蕉。
#include <stdio.h> // 标准输入输出头文件(用于printf函数)
/**
* 计算柯柯吃香蕉的最小速度
* @param piles 香蕉堆数组,每个元素表示一堆的香蕉数量
* @param pilesSize 数组长度(香蕉堆的总数)
* @param h 允许吃香蕉的总小时数
* @return 满足时间要求的最小吃香蕉速度k
*/
int minEatingSpeed(int* piles, int pilesSize, int h) {
// 步骤1:找到香蕉堆中的最大值,作为二分查找的右边界
int max_pile = 0; // 初始化最大堆值为0
for (int i = 0; i < pilesSize; i++) { // 遍历所有香蕉堆
if (piles[i] > max_pile) { // 比较当前堆与已知最大值
max_pile = piles[i]; // 更新最大堆值
}
}
// 步骤2:初始化二分查找范围
int left = 1; // 最小可能吃香蕉速度(每小时至少吃1根)
int right = max_pile; // 最大可能速度(最大堆的值)
int result = max_pile; // 初始化结果为最大值(保证首次有效更新)
// 步骤3:执行二分查找算法
while (left <= right) { // 当搜索区间有效时循环
// 计算中间速度(防溢出写法)
int mid = left + (right - left) / 2; // 等效于(left + right)/2,但避免溢出
// 计算以mid速度吃完所有香蕉需要的时间
long long hours = 0; // 使用长整型防止大数溢出
for (int i = 0; i < pilesSize; i++) {
// 向上取整技巧:(a + b - 1)/b 等效于 ceil(a/b)
// 例如:7根香蕉用3速,计算为(7+3-1)/3 = 9/3=3小时
hours += (piles[i] + mid - 1) / mid;
}
// 判断时间是否满足条件
if (hours <= h) { // 如果时间足够
result = mid; // 更新结果为当前速度mid
right = mid - 1; // 尝试寻找更小的速度(缩小右边界)
} else { // 如果时间不足
left = mid + 1; // 需要增大速度(提升左边界)
}
}
return result; // 返回最终找到的最小速度
}
int main() {
// ----------------- 示例1测试 -----------------
// 输入:piles = [3,6,7,11], h=8,预期输出4
int piles1[] = {3, 6, 7, 11}; // 创建测试数据数组
printf("Example 1 output: %d\n", minEatingSpeed(piles1, 4, 8)); // 调用函数并打印
// ----------------- 示例2测试 -----------------
// 输入:piles = [30,11,23,4,20], h=5,预期输出30
int piles2[] = {30, 11, 23, 4, 20}; // 创建测试数据数组
printf("Example 2 output: %d\n", minEatingSpeed(piles2, 5, 5)); // 参数说明:数组、长度5、h=5
// ----------------- 示例3测试 -----------------
// 输入:piles = [30,11,23,4,20], h=6,预期输出23
int piles3[] = {30, 11, 23, 4, 20}; // 注意与示例2相同数据不同h值
printf("Example 3 output: %d\n", minEatingSpeed(piles3, 5, 6)); // 参数:长度5,h=6
return 0; // 程序正常退出
}
/* 代码执行流程说明:
1. main函数依次执行三个预定义的测试用例
2. 每个测试用例构造对应的香蕉堆数组和小时数
3. 调用minEatingSpeed函数计算最小速度
4. 通过printf输出计算结果
5. 最终返回0表示程序正常结束
*/
输出结果:
三、全方位对比:用最少的箭 vs 柯柯吃香蕉
对比维度 | 用最少的箭引爆气球 | 柯柯吃香蕉的最小速度 |
---|---|---|
问题类型 | 区间覆盖问题 | 速度优化问题 |
算法核心 | 贪心算法 | 二分查找 |
复杂度 | 时间O(n log n) | 时间O(n log(max(piles))) |
应用场景 | 资源优化、覆盖问题 | 时间优化、速度规划 |
优化目标 | 最小化资源使用(箭的数量) | 最小化速度,满足时间约束 |
博客总结:
通过今天的分析,我们看到算法不仅仅是冰冷的代码,它还能帮助我们在实际生活中实现资源与效率的优化。无论是用最少的箭引爆气球,还是柯柯吃香蕉的最小速度,背后的算法都在默默发挥作用,帮助我们更高效地使用资源、优化效率。
希望这篇文章能让你对这些优化问题有更深入的了解,也期待你在生活中发现更多有趣的场景,用算法的视角去探索它们!
博客谢言:
感谢你的耐心阅读!如果你觉得这篇文章有趣,不妨在评论区分享你生活中遇到的优化问题,或者你认为可以用算法优化的地方。让我们一起用算法的视角去探索生活的奥秘!