Hot 100 --- 划分字母区间

本文概览:本文讲解划分字母区间的核心思路:先记录每个字符最后出现位置,再用"最远边界"贪心切割,i == curMaxEnd 时切一刀,O(n) 完成


一、题目


二、题目分析

1. 题目要求

给定一个字符串 s,将其划分为尽可能多 的片段,使得每个字母最多出现在一个片段中。返回每个片段的长度列表。

例子s = "ababcbacadefegdehijhklij"

复制代码
a b a b c b a c a | d e f e g d e | h i j h k l i j
0 1 2 3 4 5 6 7 8   9 ...     15   16 ...         23
      第1段(9)          第2段(7)       第3段(8)

输出:[9, 7, 8]

切割规则

  • 每个字母只能出现在一个片段里(不能跨片段)
  • 片段数要尽可能多(每段尽可能短)

比如 "ababcbacadefegde", "hijhklij" 这样切成 2 段是错的------第 1 段太长,还能再切。

2. 怎么切才能切得尽可能多?

这是一个切割问题,目标是切出尽可能多的片段。

先想切割规则:每个字母只能出现在一个片段里。这意味着什么?

  • 如果字母 a 出现在位置 0 和位置 8,那么位置 0 到位置 8 必须属于同一个片段(不能把 a 拆开)
  • 如果字母 b 出现在位置 1 和位置 5,那么位置 1 到位置 5 必须属于同一个片段
  • 以此类推...

关键观察:每个字母的"首次出现"到"最后一次出现"之间,必须是一个完整的片段。

怎么切才能切得尽可能多?

从前往后遍历,如果已经遍历过的所有字符后面都不会再出现,就可以在这里切一刀。这样切出来的段最短,片段数最多。

但这里有两个难点

难点一:怎么知道某个字符后面还有没有?

难点二 :怎么同时判断多个字符后面都不再出现?(遍历到位置 i 时,前面可能已经出现过 a、b、c 等多个字符,怎么知道它们后面都没了?)


三、思路概览

java 复制代码
public List<Integer> partitionLabels(String s) {
    // 1. 先统计每个字符最后一次出现的位置
    int[] last = new int[26];
    for (int i = 0; i < s.length(); i++) {
        last[s.charAt(i) - 'a'] = i;
    }
    // 2. 遍历字符串,curMaxEnd 为当前范围内最后一个字符的最后一次出现位置
    int curMaxEnd = last[s.charAt(0) - 'a'];
    int start = 0;
    List<Integer> res = new ArrayList<>();
    for (int i = 0; i < s.length(); i++) {
        // 不断扩展当前片段的右边界
        curMaxEnd = Math.max(curMaxEnd, last[s.charAt(i) - 'a']);

        // 当遍历到的位置等于当前片段的最远边界时,说明找到了一个完整片段
        if (i == curMaxEnd) {
            res.add(i - start + 1); 
            start = i + 1;
        }
    }
    return res;
}

思路简要说明:

  1. 预处理 last 数组last[c] 记录字符 c 的最后一次出现位置
  2. curMaxEnd :当前段的最远边界,初始为 last[s.charAt(0)]
  3. 遍历字符串 :每到一个位置 i,用 last[s.charAt(i)] 刷新 curMaxEnd
  4. i == curMaxEnd 时 :当前段内所有字符的最后出现都 ≤ i,可以切一刀,记录长度 i - start + 1start 更新为 i + 1
  5. 时间复杂度 O(n):两遍遍历,第一遍统计 last,第二遍切割

四、思路详解

第一步:解决难点一------怎么知道字符后面还有没有?

预处理 :先遍历一遍字符串,记录每个字符最后一次出现的位置

复制代码
s = "ababcbacadefegdehijhklij"

last['a'] = 8    ← a 最后出现在索引 8
last['b'] = 5    ← b 最后出现在索引 5
last['c'] = 7    ← c 最后出现在索引 7
last['d'] = 14   ← d 最后出现在索引 14
...

这样遍历到位置 i 时,last[s.charAt(i)] 就是字符 si 的最后一次出现。如果 i == last[s.charAt(i)],说明这个字符后面不会再出现了。

last 数组把"后面还有没有"这个判断,从 O(n) 扫一遍降到了 O(1) 查表

第二步:解决难点二------怎么同时判断多个字符?

遍历到位置 i 时,前面可能已经出现过 a、b、c 等多个字符。即使 a 的最后一次出现是 8,b 的最后一次出现是 5,c 的最后一次出现是 7,我们怎么知道这三个字符在位置 8 之后都不会再出现?

贪心思路 :维护一个当前段的最远边界 curMaxEnd ------它是当前段内所有字符的最后一次出现位置的最大值

复制代码
遍历到 i=0, 字符 a: curMaxEnd = last['a'] = 8
遍历到 i=1, 字符 b: curMaxEnd = max(8, last['b']) = max(8, 5) = 8
遍历到 i=4, 字符 c: curMaxEnd = max(8, last['c']) = max(8, 7) = 8

curMaxEnd = max(8, 5, 7) = 8 。只要 i < 8,说明当前段内至少有一个字符 (这里是 a)在位置 8 还会出现,这段还没结束。当 i == 8 时,说明当前段内所有字符的最后出现位置都 ≤ 8,后面不会再出现了------可以切一刀。

curMaxEnd 的本质 :它是当前段内所有字符的最后出现位置的最大值 。只要 i < curMaxEnd,段内至少有一个字符在 curMaxEnd 还会出现;当 i == curMaxEnd,段内所有字符的最后出现都 ≤ i,后面没了。

这和跳跃游戏的思路一模一样:跳跃游戏维护"最远可达距离",这题维护"最远边界",都是用一个变量代表多个状态的聚合信息。

第三步:为什么贪心是对的?

贪心策略 :每次 i == curMaxEnd 就切一刀,切出来的段尽可能短。

为什么这样能保证片段数最多?

因为切割条件是"当前段内所有字符后面都不再出现"------这是最短的合法段。如果推迟切割(比如等到 i > curMaxEnd 再切),段会变长,片段数会变少。

为什么这样能保证合法?

因为 curMaxEnd 是当前段内所有字符的最后出现位置的最大值。当 i == curMaxEnd 时,段内所有字符的最后出现都 ≤ i,后面不会再出现了------满足"每个字母最多出现在一个片段中"。

完整执行过程

s = "ababcbacadefegdehijhklij" 为例(只展示关键节点):

复制代码
预处理 last 数组:
last['a'] = 8, last['b'] = 5, last['c'] = 7
last['d'] = 14, last['e'] = 15, last['f'] = 11
last['g'] = 13, last['h'] = 19, last['i'] = 22
last['j'] = 23, last['k'] = 20, last['l'] = 21

初始:curMaxEnd = last['a'] = 8, start = 0

i=0, a: curMaxEnd = max(8, 8) = 8
i=1, b: curMaxEnd = max(8, 5) = 8
i=2, a: curMaxEnd = max(8, 8) = 8
i=3, b: curMaxEnd = max(8, 5) = 8
i=4, c: curMaxEnd = max(8, 7) = 8
i=5, b: curMaxEnd = max(8, 5) = 8
i=6, a: curMaxEnd = max(8, 8) = 8
i=7, c: curMaxEnd = max(8, 7) = 8
i=8, a: curMaxEnd = max(8, 8) = 8, i==8 → 切割,长度 9,start=9

i=9, d: curMaxEnd = max(8, 14) = 14
i=10, e: curMaxEnd = max(14, 15) = 15
i=11, f: curMaxEnd = max(15, 11) = 15
i=12, e: curMaxEnd = max(15, 15) = 15
i=13, g: curMaxEnd = max(15, 13) = 15
i=14, d: curMaxEnd = max(15, 14) = 15
i=15, e: curMaxEnd = max(15, 15) = 15, i==15 → 切割,长度 7,start=16

i=16, h: curMaxEnd = max(15, 19) = 19
i=17, i: curMaxEnd = max(19, 22) = 22
i=18, j: curMaxEnd = max(22, 23) = 23
i=19, h: curMaxEnd = max(23, 19) = 23
i=20, k: curMaxEnd = max(23, 20) = 23
i=21, l: curMaxEnd = max(23, 21) = 23
i=22, i: curMaxEnd = max(23, 22) = 23
i=23, j: curMaxEnd = max(23, 23) = 23, i==23 → 切割,长度 8,start=24

返回 [9, 7, 8] ✓

关键观察

  • 第 1 段:i 从 0 到 8,curMaxEnd 一直是 8(a 的最后出现),i==8 时切割
  • 第 2 段:i 从 9 到 15,curMaxEnd 从 14(d)刷新到 15(e),i==15 时切割
  • 第 3 段:i 从 16 到 23,curMaxEnd 从 19(h)刷新到 22(i)再到 23(j),i==23 时切割
复杂度分析

时间复杂度 O(n)

  • 第一遍遍历:统计 last 数组,O(n)
  • 第二遍遍历:切割字符串,O(n)
  • 总共 O(n)

空间复杂度 O(1):last 数组固定 26 个位置,常数空间。


五、总结

划分字母区间的核心思路:

  1. 题目要求:切割字符串,每个字母最多出现在一个片段,片段数尽可能多
  2. 切割条件:当前段内所有字符在后面都不再出现
  3. 预处理 last 数组:记录每个字符的最后一次出现位置,O(1) 判断"后面还有没有"
  4. curMaxEnd 贪心 :维护当前段的最远边界(段内所有字符最后出现位置的最大值),i == curMaxEnd 时切一刀
  5. 和跳跃游戏的类比:都是用一个变量(最远可达 / 最远边界)代表多个状态的聚合信息

贪心思想在本题的体现 :每次 i == curMaxEnd 就切,切出来的段是最短的合法段,片段数自然最多。

套路总结:遇到"切割 / 分段"类问题,先想想能不能用"最远边界"贪心------维护一个变量代表当前段的右边界,遍历到边界就切一刀。跳跃游戏、划分字母区间、合并区间都是这个套路。

相关推荐
Zzzzmo_1 小时前
SpringBoot 配置文件
java·spring boot
君顾11 小时前
外卖CPS小程序开发实战指南:从零到上线的完整流程
java·开发语言·外卖
無a伟1 小时前
RabbitMq高级特性:消息确认,持久化,重试机制
java·分布式·rabbitmq
Sunsets_Red1 小时前
浅谈扫描线
c++·算法·编程·题解·洛谷·扫描线·信息学竞赛
流烟默1 小时前
xxl-job 接入 Spring 的两种方式:手动装配与自动装配(含 initMethod 双启动踩坑实录)
java·spring·xxl-job
a187927218311 小时前
【算法】双指针与滑动窗口(一):框架总纲——三类问题、一个原理与判决书
算法·leetcode·区间·双指针·滑动窗口·原理·算法讲解
鹅城剑仙1 小时前
Spring Boot 3 全局异常处理与统一响应封装进阶实战
java·spring boot·后端
shehuiyuelaiyuehao1 小时前
算法42,模拟算法,模拟z字形变换
算法
朝朝辞暮i1 小时前
C++第一课
开发语言·c++·算法