目录
[1. 外观数列](#1. 外观数列)
[1.1 题目解析](#1.1 题目解析)
[1.2 解法](#1.2 解法)
[1.3 代码实现](#1.3 代码实现)
[2. 数青蛙](#2. 数青蛙)
[2.1 题目解析](#2.1 题目解析)
[2.2 解法](#2.2 解法)
[2.3 代码实现](#2.3 代码实现)
1. 外观数列
「外观数列」是一个数位字符串序列,由递归公式定义:
countAndSay(1) = "1"
countAndSay(n)
是countAndSay(n-1)
的行程长度编码。
行程长度编码(RLE)是一种字符串压缩方法,其工作原理是通过将连续相同字符(重复两次或更多次)替换为字符重复次数(运行长度)和字符的串联。例如,要压缩字符串 "3322251"
,我们将 "33"
用 "23"
替换,将 "222"
用 "32"
替换,将 "5"
用 "15"
替换并将 "1"
用 "11"
替换。因此压缩后字符串变为 "23321511"
。
给定一个整数 n
,返回 外观数列 的第 n
个元素。
示例 1:
**输入:**n = 4
输出:"1211"
解释:
countAndSay(1) = "1"
countAndSay(2) = "1" 的行程长度编码 = "11"
countAndSay(3) = "11" 的行程长度编码 = "21"
countAndSay(4) = "21" 的行程长度编码 = "1211"
示例 2:
**输入:**n = 1
输出:"1"
解释:
这是基本情况。
提示:
1 <= n <= 30
1.1 题目解析
题目本质
这题是在做"从 1
开始,不断对上一次字符串做行程长度编码 (Run-Length Encoding, RLE) ",得到第 n 项。RLE 的规则就是:把连续相同字符的一段,用"这段的长度 + 该字符"来表示。
常规解法
递归:countAndSay(n) = RLE(countAndSay(n-1)),直到 n=1 返回 "1"。
问题分析
递归每次都要建立新字符串,函数栈也会递归到深度 n;虽然也能过,但实现上不如迭代直观。核心成本其实来自给字符串做 RLE ,一轮扫描就够了,所以完全可以从 1 迭代到 n,每次把当前串编码出下一串。
思路转折
要高效 ⇒ 每一轮都对"上一个字符串"线性扫描一遍 ,把连续段压缩即可。实现上用双指针/游标(或单指针+计数)最自然:
-
指定一段的起点,向右扩展到这段结束;
-
把 (长度)(字符) 追加到 StringBuilder;
-
继续处理下一段。
这样总复杂度是所有中间串长度之和,对 n ≤ 30 十分可控。
1.2 解法
算法思想
迭代从 ret = "1" 开始做 n-1 次编码,RLE 用连续段的"长度 + 字符"替换该段。每一轮只需 O(当前串长)。
**i)**初始化 ret = "1";若 n==1 直接返回。
**ii)**循环 i=2..n:ret = encode(ret)。
**iii)**encode(s):
-
用指针 i 从左到右扫描;
-
记当前字符 ch = s.charAt(i),用 j 往右走到 s[j] != ch;
-
追加 (j-i) 和 ch 到 StringBuilder;令 i = j 继续;
-
返回构建好的字符串。
iiii)最终返回 ret。
1.3 代码实现
java
class Solution {
public String countAndSay(int n) {
String ret = "1";
for (int i = 2; i <= n; i++) {
ret = encode(ret); // 对上一次结果做 RLE,得到下一次
}
return ret;
}
// 对字符串做一次行程长度编码:连续段 -> (长度)(字符)
private String encode(String s) {
StringBuilder sb = new StringBuilder();
int i = 0, n = s.length();
while (i < n) {
char ch = s.charAt(i);
int j = i;
while (j < n && s.charAt(j) == ch) j++; // 扩展到该连续段的末尾
sb.append(j - i).append(ch); // 追加"长度+字符"
i = j; // 下一段
}
return sb.toString();
}
}
复杂度分析
-
时间:每一轮 O(当前串长),总计为所有中间结果长度之和。
-
空间:每轮一个新的 StringBuilder,峰值为当前结果长度的 O(L)。
2. 数青蛙
给你一个字符串 croakOfFrogs
,它表示不同青蛙发出的蛙鸣声(字符串 "croak"
)的组合。由于同一时间可以有多只青蛙呱呱作响,所以 croakOfFrogs
中会混合多个 "croak"
。
请你返回模拟字符串中所有蛙鸣所需不同青蛙的最少数目。
要想发出蛙鸣 "croak",青蛙必须 依序 输出 'c', 'r', 'o', 'a', 'k'
这 5 个字母。如果没有输出全部五个字母,那么它就不会发出声音。如果字符串 croakOfFrogs
不是由若干有效的 "croak" 字符混合而成,请返回 -1
。
示例 1:
输入:croakOfFrogs = "croakcroak"
输出:1
解释:一只青蛙 “呱呱” 两次
示例 2:
输入:croakOfFrogs = "crcoakroak"
输出:2
解释:最少需要两只青蛙,“呱呱” 声用黑体标注
第一只青蛙 "crcoakroak"
第二只青蛙 "crcoakroak"
示例 3:
输入:croakOfFrogs = "croakcrook"
输出:-1
解释:给出的字符串不是 "croak" 的有效组合。
提示:
1 <= croakOfFrogs.length <= 105
- 字符串中的字符只有
'c'
,'r'
,'o'
,'a'
或者'k'
2.1 题目解析
题目本质
把一串混合的 "croak" 拆分为若干只按顺序发音的青蛙序列,问最少需要多少只青蛙 才能完成这些发声。等价于:在扫描过程中,最多同时处在进行中(没有到 k)的青蛙数量 是多少;若出现顺序非法或有青蛙未叫完,则不合法返回 -1
。
常规解法(直观想法)
开 5 计数器分别统计当前有多少只停在 c/r/o/a/k 阶段;字符到来就把青蛙从前一阶段"搬"到后一阶段;遇到 c 时若无空闲可复用青蛙,则新增一只;一路维护"在场(未到 k)的数量"的最大值为答案。
问题分析
只要线性扫描一遍字符串即可,每个字符只做 O(1) 的阶段迁移,时间 O(|S|),空间 O(1)。难点是如何优雅地表达"阶段迁移 + 复用已完成的青蛙" ,并在结尾校验没有半途而废。
思路转折
将 "croak" 的 5 个字母视作 5 个阶段(0..4):
-
维护一个长度为 5 的数组 hash[i],表示当前停在阶段 i 的青蛙数;
-
用 Map<Character,Integer> 把 'c','r','o','a','k' 映射到 0..4;
-
扫描:
-
若是 'c':优先复用已完成(阶段 4)的青蛙 hash[4]--,否则相当于新增一只;然后让它进入阶段 0(hash[0]++)
-
若是其它字母 x:找到其阶段 i=index.get(x),要求 hash[i-1]>0,然后从前一阶段转到当前阶段(hash[i-1]--, hash[i]++)
-
-
结束后必须保证 hash[0..3] 都为 0(没有未完成者),答案即为 hash[4](完成且可复用的总只数 = 全程最少青蛙数)。
为何返回 hash[4]?因为每只合法青蛙最终一定停在 'k' 阶段(完成一次 croak)。若字符串整体合法,所有青蛙都会"收尾"到阶段 4,数量正是最少需要的不同青蛙数。 映射的关键在 t="croak" 与 index.put(t.charAt(i), i):因此 'k' 的阶段编号是 4,也就是 n-1,所以 hash[n-1] 自然对应 'k' 阶段。
2.2 解法
算法思想
把 "croak" 看成 5 个有序阶段,数组 hash[0..4] 表示每个阶段的在途青蛙数;字符到来即做阶段迁移。
-
'c':若有 hash[4]>0 先复用(表示上一轮刚结束的一只),否则等价于新增一只;然后 hash[0]++
-
其它字母:i=index.get(ch),要求 hash[i-1]>0,再做 hash[i-1]--, hash[i]++
**i)**预处理:t="croak",建立 index: 字符 -> 阶段编号(0..4);新建 int[5] hash。
**ii)**扫描字符串:
-
若 ch=='c':if (hash[4]>0) hash[4]--; hash[0]++;
-
否则:令 i=index.get(ch),若 hash[i-1]==0 返回 -1;否则 hash[i-1]--, hash[i]++。
**iii)**扫描完毕:若 hash[0]..hash[3] 有非零,返回 -1。
**iiii)**返回 hash[4] 作为最少青蛙数
2.3 代码实现
java
import java.util.*;
class Solution {
public int minNumberOfFrogs(String croakOfFrogs) {
String t = "croak";
int n = t.length(); // 5
int[] hash = new int[n]; // hash[i]:处在第 i 阶段的青蛙数(i: c/r/o/a/k)
Map<Character, Integer> index = new HashMap<>(5);
for (int i = 0; i < n; i++) index.put(t.charAt(i), i);
for (char ch : croakOfFrogs.toCharArray()) {
if (ch == 'c') {
// 先复用一只刚完成的青蛙(阶段 4),否则相当于新增一只
if (hash[n - 1] > 0) hash[n - 1]--;
hash[0]++; // 进入 'c' 阶段
} else {
Integer i = index.get(ch);
if (i == null || i == 0) return -1; // 非法字符或顺序错误
if (hash[i - 1] == 0) return -1; // 前一阶段没人可转移
hash[i - 1]--; // 从前一阶段拿一只
hash[i]++; // 进入当前阶段
}
}
// 不能有未完成的青蛙(停在 c/r/o/a),否则不合法
for (int i = 0; i < n - 1; i++) {
if (hash[i] != 0) return -1;
}
// 合法:所有青蛙都收尾在 'k' 阶段
return hash[n - 1];
}
}
复杂度分析
-
时间:O(|S|),单次线性扫描;
-
空间:O(1),仅 5 个阶段计数 + 一个小映射。