【码道初阶】国服ad两种殊途同归的贪心算法详解Leetcode452弓箭射气球问题(与Leetcode435十分相似)


用最少箭数引爆气球:贪心策略详解

引言

在解决 LeetCode 的「452. 用最少数量的箭引爆气球」问题时,我们需要在保证射爆所有气球的前提下,找到最少的弓箭数量。本文将结合具体代码,深入解析该问题的贪心解法,用两种不同的循环写法来达成目的并揭示其与经典区间问题(Leetcode 435.区间重叠问题)的异同。


一、问题描述

给定气球区间的数组 points,其中每个区间表示气球的水平直径范围。弓箭可以从任意 x 坐标垂直射出,若该坐标在气球直径范围内,则气球被引爆。求引爆所有气球所需的最小弓箭数。

示例

输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:在 x=6 处射箭引爆 [2,8][1,6],在 x=11 处射箭引爆 [10,16][7,12]

二、算法核心思想

1. 问题分析

  • 目标:找到一组射箭位置,使得每个气球至少被一支箭覆盖。
  • 关键约束:弓箭可以无限前进,但必须垂直射出。
  • 等价转化:问题可转化为寻找一组点,使得每个区间至少包含一个点,求最小点数。

2. 贪心策略

  • 排序策略:排序在贪心算法中属于重中之重,良好的排序方法往往大大节省开销,使代码更加清晰,本题按区间右端点升序排序是最好的办法。
  • 射箭规则:每次选择当前区间的右端点作为射箭位置,尽可能覆盖后续更多区间。

三、代码实现与解析

#include 
#include 
#include 

using namespace std;

class Solution {
public:
    int findMinArrowShots(vector<vector<int>>& points) {
        if (points.empty()) return 0;

        // 按区间右端点升序排序
        sort(points.begin(), points.end(), [](const vector<int>& a, const vector<int>& b) {
            return a[1] < b[1];
        });

        int arrows = 0;
        long last_end = LONG_MIN; // 记录上一支箭的位置

        for (int i = 0; i < points.size(); ++i) {
            // 当前区间左端点 > 上一箭位置,需要新箭
            if (points[i][0] > last_end) {
                arrows++;
                last_end = points[i][1]; // 射箭位置设为当前区间右端点
            }
        }
        return arrows;
    }
};

关键代码解析

  1. 排序预处理

    sort(points.begin(), points.end(), [](const vector<int>& a, const vector<int>& b) {
        return a[1] < b[1];
    });
    
    • 目的:将区间按右端点升序排列,确保每次选择最靠左的右端点作为射箭位置。
    • 效果:例如输入 [[10,16],[2,8],[1,6],[7,12]] 排序后为 [[1,6], [2,8], [7,12], [10,16]]
  2. 贪心遍历

    for (int i = 0; i < points.size(); ++i) {
        if (points[i][0] > last_end) {
            arrows++;
            last_end = points[i][1];
        }
    }
    
    • 条件判断:若当前区间左端点 points[i][0] > 上一箭位置 last_end,说明需要新箭。
    • 更新位置:射箭位置设为当前区间右端点 points[i][1],以覆盖后续可能重叠的区间。

四、正确性证明

1. 贪心选择性质

  • 最优性:每次选择右端点射箭,能覆盖所有与该区间重叠的后续区间。
  • 反证法:假设存在更优解,其第一次射箭位置不在第一个区间的右端点,则可通过替换为右端点位置,得到相同或更优结果。

2. 覆盖性分析

  • 数学归纳:设前 k 个区间已被最优覆盖,第 k+1 个区间若与当前射箭位置无重叠,则必须新增箭,而选择其右端点可最大化覆盖后续区间。

五、与区间问题的对比

相似性

  • 贪心框架:均通过排序和遍历实现。
  • 核心操作:判断区间重叠关系。

差异性

问题 合并区间问题 气球弓箭问题
排序方式 按左端点升序排序 按右端点升序排序
目标 合并重叠区间 寻找覆盖所有区间的最少点
关键条件 当前左端点 <= 上一右端点 当前左端点 > 上一射箭位置

六、边界与测试

1. 边界处理

  • 空输入:直接返回 0
  • 单气球:返回 1
  • 相邻区间:例如 [[1,2],[2,3]] 需要 2 支箭(端点相接不算重叠)。

2. 测试用例

输入:[[1,2],[3,4],[5,6],[7,8]]
输出:4 (无重叠,需4支箭)

输入:[[1,2],[2,3],[3,4],[4,5]]
输出:2 (射在24处)

七、复杂度分析

  • 时间复杂度:O(n log n),排序占主导。
  • 空间复杂度:O(1),仅用常数空间。

八、总结

通过按右端点排序和贪心遍历,我们以 O(n log n) 的时间复杂度高效解决了问题。代码简洁且覆盖所有边界条件,体现了贪心算法“局部最优即全局最优”的核心思想。理解排序策略与射箭位置更新的逻辑,是掌握此类区间覆盖问题的关键。

附:贪心排序策略最优解分析思路

在解决「452. 用最少数量的箭引爆气球」问题时,使用按右端点升序排序的方法展现了独特的优势。不过其实作者自己写的时候也在想到底怎么把这个思路给说通,我们分析问题能分析出应该按右端点排序,大概是因为这样排我们满足从局部最优→到整体最优的贪心思想。So,以下从排序策略的核心逻辑贪心选择的最优性覆盖范围的效率三个方面详细分析这种方法的精妙之处:


一、排序策略的核心逻辑

1. 排序目标

将区间按右端点升序排列,例如:

原始输入:[[10,16], [2,8], [1,6], [7,12]]
排序后:[[1,6], [2,8], [7,12], [10,16]]
  • 排序目的:确保每次处理的区间是当前剩余区间中右端点最小的。
2. 贪心选择

每次选择当前区间的右端点作为射箭位置:

  • 覆盖能力:右端点是最早结束的区间的最右点,选择此处射箭可以覆盖所有左端点小于等于该位置的后续区间。
  • 示例:在排序后的区间 [[1,6], [2,8], [7,12], [10,16]] 中,第一支箭射在 6,覆盖 [1,6][2,8];第二支箭射在 12,覆盖 [7,12][10,16]

二、贪心选择的最优性

1. 局部最优性
  • 选择右端点的意义
    每次射箭覆盖尽可能多的后续区间。由于后续区间的右端点更大,它们的左端点可能更靠近右侧,选择当前右端点可以最大化覆盖潜力。
  • 数学归纳证明
    假设前 k 个区间已被最优覆盖,处理第 k+1 个区间时,若其左端点超出当前箭的位置,必须新增箭。选择其右端点射箭,能覆盖所有可能重叠的后续区间。
2. 全局最优性
  • 反证法
    若存在更优解,其第一次射箭位置不在第一个区间的右端点,则可以通过替换为右端点位置,得到相同或更优的结果。

三、覆盖范围的效率

1. 处理相邻区间

题目规定端点相接不算重叠(如 [1,2][2,3]):

  • 按右端点排序:射在 2 处无法覆盖下一个区间 [2,3](左端点等于当前射箭位置),需新增箭。
  • 按左端点排序:射在 2 处可能覆盖后续区间,但需频繁调整射箭位置。
2. 复杂示例验证

以输入 [[1,5], [2,3], [4,7]] 为例:

  • 按右端点排序[[2,3], [1,5], [4,7]]
    射在 3 处覆盖 [2,3][1,5],射在 7 处覆盖 [4,7]2支箭
  • 按左端点排序[[1,5], [2,3], [4,7]]
    射在 5 处仅覆盖 [1,5][4,7][2,3] 需要额外箭 → 2支箭

两种排序方式结果相同,但按右端点排序的逻辑更统一,无需额外条件判断。


四、对比其他排序策略

1. 按左端点排序的劣势
  • 覆盖不足:射箭位置可能选择较大的右端点,导致后续区间无法被覆盖。
  • 示例:区间 [[1,10], [2,3]]
    按左端点排序后,射在 10 处覆盖所有区间;按右端点排序后,射在 3 处即可覆盖所有区间。
2. 按区间长度排序的问题
  • 无法保证最优:短区间的覆盖范围有限,可能增加箭数。

五、边界处理与代码实现

1. 空输入处理
if (points.empty()) return 0; // 无气球需处理
2. 大整数溢出

使用 LONG_MIN 避免 INT_MIN 的溢出风险:

long last_end = LONG_MIN; // 初始化为极小值
3. 核心遍历逻辑
for (int i = 0; i < points.size(); ++i) {
    if (points[i][0] > last_end) { // 需要新箭
        arrows++;
        last_end = points[i][1]; // 贪心选择右端点
    }
}

六、总结

按右端点升序排序的方法在本题中体现以下优势:

  1. 最大化覆盖:每次射箭覆盖后续最多区间。
  2. 逻辑统一性:无需特殊处理边界和相邻区间。
  3. 高效简洁:时间复杂度为 O(n log n),主要来自排序操作。

这种策略通过贪心思想的局部最优选择,确保了全局最优解的必然性,是处理区间覆盖问题的经典范式。

你可能感兴趣的:(码道初阶,贪心算法,算法,leetcode,c++)