【贪心算法】LC 763.划分字母区间

文章目录

前言

本专栏文章为《LeetCode 热题 100》的刷题题解,相关内容如有侵权,立即删除。

一、题目

1、原题链接

763.划分字母区间

2、题目描述

二、个人思路整理

1、思路分析

核心思路:题目要求同一字母最多出现在一个片段中,这意味着:同一个字母的所有出现位置,必须被圈在同一个片段里 。因此,一个片段至少要延伸到该片段内所有字符最后一次出现的位置 。为了让划分出的片段尽可能多,我们需要让每个片段在满足条件的前提下尽可能短。

具体步骤:

  1. 预处理最后出现位置:遍历一次字符串,用一个长度为 26 的数组或哈希表 last_pos 记录每个小写字母在字符串中最后一次出现的下标。
  2. 贪心扩展与切分:
    • 维护当前区间的起始索引 start = 0 和当前必须覆盖到的最远右边界 end = 0。
    • 再次从左到右遍历字符串:
      • 访问到字符 s[i] 时,该字符的最后出现位置为 last_pos[s[i] - 'a'],当前区间的右边界必须向右扩展:end = max(end, last_pos[s[i] - 'a'])。
      • 当遍历指针 i 刚好追上 end 时(即 i == end),说明当前区间内的所有字符在后面都不会再出现了,当前片段可以切断。
      • 记录片段长度 end - start + 1,并将 start 更新为 end + 1,开始寻找下一个片段。

2、解题代码

cpp 复制代码
class Solution {
public:
    vector<int> partitionLabels(string s) {
        // last[c - 'a'] 记录字符 c 在字符串中最后依次出现的索引位置
        int last[26] = {0};
        int n = s.size();

        // 第一次遍历:统计每个字符在字符串中最后出现的下标
        for (int i = 0; i < n; i++) {
            last[s[i] - 'a'] = i;
        }

        vector<int> result;
        int start = 0;    // 当前切分片段的起始下标
        int end = 0;      // 当前片段中所有字符所要求的最远右边界

        // 第二次遍历:贪心地扩展右边界并在满足条件时进行切分
        for (int i = 0; i < n; i++) {
            // 当前片段必须至少延伸到字符 s[i] 的最后出现位置
            end = max(end, last[s[i] - 'a']);

            // 当遍历到达当前片段所要求的最远边界时,说明该片段内的所有字符在之后的字符串中都不会再出现,此时可以完成依次切分
            if (i == end) {
                result.push_back(end - start + 1);   // 记录当前片段长度
                start = end + 1;    // 更新下一个片段的起始位置
            }
        }
        return result;
    }
};

复杂度分析

  • 时间复杂度: O ( n ) O(n) O(n),只需两次遍历字符串,其中 n n n 为字符串长度。
  • 空间复杂度: O ( 1 ) O(1) O(1)(或 O ( ∣ Σ ∣ ) O(\vert{}\Sigma\vert{}) O(∣Σ∣)),字符集仅包含 26 个小写字母,占用的辅助数组空间为常数级别。

三、知识风暴

贪心算法(Greedy Algorithm) 是本题的核心算法思想。它通过在每一步做出当前看起来最优的选择,期望最终得到全局最优解。对于「划分字母区间」这类具有最优子结构性质的问题,贪心策略往往能以 O ( n ) O(n) O(n) 的复杂度高效求解。

算法核心思想:

  • 局部最优推导全局最优 :每一步都让当前片段「尽可能短」,同时保证片段内所有字符的最后出现位置都被覆盖。本题中,维护当前片段必须覆盖到的最远右边界 end,当遍历指针追上 end 时即可切分,从而保证片段数量最多。
  • 无需回溯 :贪心算法不回溯、不枚举所有可能的切分方案,只关注当前片段内字符的最远出现位置,因此时间复杂度仅为 O ( n ) O(n) O(n)。
  • 与动态规划的区别 :动态规划需要枚举所有可能的切分点并逐一比较;而贪心只维护「当前片段的起始位置」和「必须覆盖的最远右边界」两个变量,空间复杂度降为 O ( 1 ) O(1) O(1)。

常见对比:贪心 vs 动态规划

  • 贪心算法 :时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)。适合每一步的局部最优能直接推导全局最优的场景,代码简洁高效。
  • 动态规划 :时间复杂度 O ( n 2 ) O(n^2) O(n2),空间复杂度 O ( n ) O(n) O(n)。适合需要枚举所有子问题、且局部最优不能直接决定全局最优的场景,通用性更强但开销更大。
  • 共同点:两者都依赖「最优子结构」性质。区别在于贪心只保留一个当前最优状态,而动态规划需要维护一张状态表。

贪心算法的设计思想:

  • 核心思想:在遍历过程中,始终维护「当前片段内所有字符的最后出现位置」所要求的最远右边界。当遍历指针到达该边界时,说明当前片段内的所有字符在后续字符串中都不会再出现,此时可以安全切分。
  • 与本题的联系:划分字母区间问题保证每个字母至少出现一次,因此贪心策略不会出现「无法切分」的失败情况。我们只需在遍历过程中不断扩展右边界,即可得到尽可能多的片段。
  • 注意事项:贪心算法并不总是正确,需要先证明「局部最优能推出全局最优」。本题中,让每个片段尽可能短不会比让片段更长更差,因此贪心成立。

使用要点:

  • 边界变量 :start 记录当前片段的起始下标,end 记录当前片段内所有字符所要求的最远右边界。
  • 更新时机 :访问到字符 s[i] 时,将 end 更新为 max(end, last_pos[s[i] - 'a']),表示当前片段必须至少延伸到该字符的最后出现位置。
  • 切分时机 :当遍历指针 i == end 时,说明当前片段内的所有字符在后续字符串中都不会再出现,此时记录片段长度 end - start + 1,并将 start 更新为 end + 1。
  • 结果返回 :遍历结束后,result 即为所有片段的长度列表。

算法变体与扩展:

  • 合并区间(LeetCode 56):将每个字母的首次与最后出现位置视为一个区间,再合并重叠区间,同样可以得到划分结果,是本题的区间合并视角。
  • 无重叠区间(LeetCode 435):贪心按区间右端点排序后选择不重叠区间,与本题「尽可能短」的贪心思路异曲同工。
  • 用最少数量的箭引爆气球(LeetCode 452):按右端点排序的贪心策略,与本题维护最远右边界的思想一致。
  • 跳跃游戏 II(LeetCode 45):同样是贪心维护「最远边界」的经典题目,与本题共享「边界扩展」的核心模式。

相关 LeetCode 例题:

相关推荐
拾饵9421 小时前
第六周第二节那个RAG工业项目遇到的问题
算法
强壮的CAT1 小时前
VINS-Fusion 移植 ROS2 Jazzy 踩坑(一):环境与编译
人工智能·算法·机器人·自动驾驶
Omics Pro1 小时前
计算虚拟扰动:网络毒理+虚拟敲除
数据库·人工智能·算法·机器学习·自然语言处理
yyy(十一月限定版)1 小时前
CF2137D Replace with Occurrences
算法
谢亮_vipxieliang1 小时前
GC 入门:G1 与 ZGC 怎么选
java·jvm·算法
兔小盈1 小时前
有效三角形个数与和为s的两个数字
算法·双指针
海上小飞龙10 小时前
【KMP算法-下篇】同一道题:Java 库函数 2600 微秒,手写 KMP 16 微秒
java·开发语言·算法
青少儿编程课堂11 小时前
后缀自动机解析:本质不同子串与最长重复片段统计
c++·python·算法·bfs·信息学竞赛
朝朝辞暮i11 小时前
C++ 第 12 课:局部变量、作用域、变量生命周期
开发语言·c++·算法