用最少的箭引爆气球与柯柯吃香蕉的最小速度:算法优化生活中的资源与效率

#新星杯·14天创作挑战营·第11期#

博客引言:

在我们的日常生活中,优化资源的使用和提高效率是永恒的主题。今天,我们将通过两个有趣的问题,探索如何用算法来优化这些场景。

首先,我们将探讨用最少数量的箭引爆气球问题,看看如何通过算法找到引爆所有气球所需的最少箭数。接着,我们将分析柯柯吃香蕉的最小速度问题,探讨如何通过算法找到柯柯在规定时间内吃完所有香蕉的最小速度。通过这两个案例,你将看到算法如何在实际生活中发挥作用,帮助我们更高效地使用资源、优化效率。

让我们一起进入算法的世界,探索这些优化问题背后的奥秘!


博客正文:

一、用最少数量的箭引爆气球:贪心算法的巧妙应用

场景描述:
一堵墙上贴有很多球形气球,每个气球用水平直径的起点和终点表示。一支弓箭可以沿着x轴射出,只要箭射出的x坐标在某个气球的水平直径范围内,就会引爆那个气球。目标是用最少的箭引爆所有气球。

算法核心:贪心算法
这个问题可以通过贪心算法来解决。贪心算法通过在每一步选择局部最优解,从而达到全局最优解。具体来说,按气球的结束坐标排序,依次选择结束最早的气球,将其结束坐标作为箭的位置,这样可以尽可能多地覆盖后续气球。

详细分析:

  1. 初始化:将所有气球的区间按结束坐标从小到大排序。
  2. 贪心选择:选择第一个气球的结束坐标作为箭的位置,然后跳过所有包含这个点的气球。
  3. 重复步骤:继续选择下一个未处理气球的结束坐标作为箭的位置,直到所有气球都被处理。

题目验证示例:

  • 示例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。

详细分析:

  1. 确定k的范围:最小的k是1,最大的k是max(piles)。
  2. 二分查找:对于每一个k,计算吃掉所有香蕉所需的总时间。如果总时间小于等于h,则尝试寻找更小的k;否则,需要增大k。
  3. 计算总时间:对于每一堆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)))
应用场景资源优化、覆盖问题时间优化、速度规划
优化目标最小化资源使用(箭的数量)最小化速度,满足时间约束

博客总结:

通过今天的分析,我们看到算法不仅仅是冰冷的代码,它还能帮助我们在实际生活中实现资源与效率的优化。无论是用最少的箭引爆气球,还是柯柯吃香蕉的最小速度,背后的算法都在默默发挥作用,帮助我们更高效地使用资源、优化效率。

希望这篇文章能让你对这些优化问题有更深入的了解,也期待你在生活中发现更多有趣的场景,用算法的视角去探索它们!


博客谢言:

感谢你的耐心阅读!如果你觉得这篇文章有趣,不妨在评论区分享你生活中遇到的优化问题,或者你认为可以用算法优化的地方。让我们一起用算法的视角去探索生活的奥秘!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

司铭鸿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值