LeetCode算法日记 - Day 23: 外观数列、数青蛙
目录
1. 外观数列
1.1 题目解析
1.2 解法
1.3 代码实现
2. 数青蛙
2.1 题目解析
2.2 解法
2.3 代码实现
1. 外观数列
38. 外观数列 - 力扣(LeetCode)
「外观数列」是一个数位字符串序列,由递归公式定义:
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 代码实现
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. 数青蛙
1419. 数青蛙 - 力扣(LeetCode)
给你一个字符串 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 代码实现
import java.util.*;class Solution {public int minNumberOfFrogs(String croakOfFrogs) {String t = "croak";int n = t.length(); // 5int[] 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 个阶段计数 + 一个小映射。