当前位置: 首页 > web >正文

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

http://www.xdnf.cn/news/19042.html

相关文章:

  • LeetCode - 155. 最小栈
  • 8.28 模拟
  • rust语言(1.88.0)sqlite数据库rusqlite库(0.37.0)学习笔记
  • 蘑兔音乐:帮你把灵感落地
  • 【新版发布】Apache DolphinScheduler 3.3.1 正式上线:更稳、更快、更安全!
  • 【Django + Pure Admin】基于Django+Vue3的前后端分离管理系统框架设计
  • 预处理详解
  • 【Spring Cloud 微服务】5.架构的智慧枢纽:深度剖析 Nacos 注册中心
  • 《Vuejs设计与实现》第 17 章(编译优化)
  • JMeter 5.3 性能测试:文件下载脚本编写与导出文件接收完整指南
  • 数据结构:堆排序 (Heap Sort)
  • spire.doc在word中生成公式
  • 设计模式理解
  • Shader开发(十七)着色器中的纹理采样与渲染
  • 农业物联网:科技赋能现代农业新篇章
  • 数模笔记day01(数据预处理、K-means聚类、遗传算法、概率密度分布)
  • UE5蓝图接口的创建和使用方法
  • 有鹿机器人如何用科技与创新模式破解行业难题
  • linux下的网络编程(2)
  • 智能体协作体系核心逻辑:Prompt、Agent、Function Calling 与 MCP 解析
  • AV1到达开始和约束时间
  • 分治法——二分答案
  • XFile v2 系统架构文档
  • Ansible 核心模块与实操练习
  • 第十三章项目资源管理--13.3 规划资源管理
  • Apifox 8 月更新|新增测试用例、支持自定义请求示例代码、提升导入/导出 OpenAPI/Swagger 数据的兼容性
  • 手写MyBatis第37弹: 深入MyBatis MapperProxy:揭秘SQL命令类型与动态方法调用的完美适配
  • AI赋能前端性能优化:核心技术与实战策略
  • Swift 解法详解 LeetCode 364:嵌套列表加权和 II
  • 713 乘积小于k的子数组