LeetCode算法日记 - Day 23: 外观数列、数青蛙

目录

[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. 外观数列

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 代码实现

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. 数青蛙

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 代码实现

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 个阶段计数 + 一个小映射。