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

模拟算法专题总结:直接按题意实现的艺术

模拟算法专题总结:直接按题意实现的艺术

在这里插入图片描述


目录

  • 刷题过程
  • 模拟算法核心概念
  • 题目详解
    • 1. 替换所有的问号 (LeetCode 1576)
    • 2. 提莫攻击 (LeetCode 495)
    • 3. Z字形变换 (LeetCode 6) ⭐⭐⭐
    • 4. 外观数列 (LeetCode 38)
    • 5. 数青蛙 (LeetCode 1419) ⭐⭐
  • 核心收获
  • 易错点总结

刷题过程

模拟算法专题集中刷题,涵盖字符串模拟、数组模拟、状态转换三大类型:

  • 字符串模拟:替换问号、外观数列
  • 数组模拟:提莫攻击
  • 状态转换:Z字形变换、数青蛙
  • 第1题踩坑 = vs ==,第3题对比了模拟法和优化法两种实现

模拟算法核心概念

什么是模拟算法?

直接按照题目描述的逻辑实现代码,不需要复杂的算法技巧。

关键是:

  • 理解题意,抓住核心规律
  • 处理好边界条件
  • 注意细节实现

模拟算法 vs 其他算法

与双指针、滑动窗口的区别:

  • 双指针/滑动窗口:有固定的套路和模板
  • 模拟算法:每道题都是独立的逻辑,没有通用模板

模拟算法的特点:

  • 直观性:代码逻辑和题目描述一一对应
  • 细节性:重点在各种边界情况的处理
  • 实现性:考验代码实现能力而非算法设计

模拟算法的常见类型

通过这5道题,我总结出以下类型:

1. 字符串处理模拟

特点: 按规则处理字符串,逐字符遍历

典型题目:

  • 替换所有的问号(LeetCode 1576)
  • 外观数列(LeetCode 38)

关键点: 字符边界检查、字符串拼接

2. 数组/时间区间模拟

特点: 模拟时间流逝、事件发生

典型题目:

  • 提莫攻击(LeetCode 495)

关键点: 区间重叠处理

3. 状态转换模拟

特点: 模拟状态机、方向切换等复杂逻辑

典型题目:

  • Z字形变换(LeetCode 6)
  • 数青蛙(LeetCode 1419)

关键点: 状态切换时机、状态统计


题目详解

1. 替换所有的问号 (LeetCode 1576)

难度: Easy
耗时: 20分钟(包含调试)

题目描述

给你一个仅包含小写英文字母和 ‘?’ 的字符串 s,请你将所有的 ‘?’ 转换为某些小写字母,使得最终的字符串不包含任何连续重复的字符。

解题思路

遍历字符串,遇到’?'就尝试用’a’到’z’的字母替换,找到第一个不与前后字符相同的字母即可。

踩坑记录

第一次提交:Runtime Error!

// 错误代码
if((i = 0 || ch != s[i-1]) && ...)  // ❌ 赋值运算符!

错误原因:

  • i = 0 是赋值操作,把i改成了0
  • 然后访问 s[i-1] 就是 s[-1],数组越界!

正确写法:

if((i == 0 || ch != s[i-1]) && ...)  // ✅ 比较运算符
AC代码
class Solution {
public:string modifyString(string s) {int n = s.size();for(int i = 0; i < n; i++) {if(s[i] == '?') {for(int ch = 'a'; ch <= 'z'; ch++) {if((i == 0 || ch != s[i-1]) && ((i == n-1) || ch != s[i+1])) {s[i] = ch;break;}}}}return s;}
};
关键理解
  1. 贪心策略:每次选择第一个可用字母即可,不需要考虑全局最优
  2. 边界检查:访问 s[i-1] 前确保 i > 0,访问 s[i+1] 前确保 i < n-1
  3. 经典错误= vs == 的区别,赋值会改变变量值并可能导致越界

2. 提莫攻击 (LeetCode 495)

难度: Easy
耗时: 17分钟

题目描述

提莫的攻击可以让艾希进入中毒状态,每次攻击持续 duration 秒。给你一个攻击时间数组 timeSeries 和持续时间 duration,返回艾希处于中毒状态的总秒数。

解题思路

关键在于处理时间区间的重叠:

  • 如果两次攻击间隔 ≥ duration,累加完整的 duration
  • 如果两次攻击间隔 < duration,累加实际间隔(有重叠)
AC代码
class Solution {
public:int findPoisonedDuration(vector<int>& timeSeries, int duration) {int ret = 0;for(int i = 1; i < timeSeries.size(); i++) {if((timeSeries[i] - timeSeries[i-1]) > duration) ret += duration;else ret += (timeSeries[i] - timeSeries[i-1]);}return ret + duration;  // 最后一次攻击的完整duration}
};
关键理解
  1. 时间区间重叠处理:比较相邻两次攻击的时间间隔与duration的大小
  2. 两种遍历思路
    • 向后看:for(i=1; i<n; i++) 计算 timeSeries[i] - timeSeries[i-1]
    • 向前看:for(i=0; i<n-1; i++) 计算 timeSeries[i+1] - timeSeries[i]
    • 两种方法等价,都需要最后单独处理最后一次攻击
  3. 边界处理:无论哪种写法,最后一次攻击都要加上完整的duration时间

3. Z字形变换 (LeetCode 6) ⭐⭐⭐

难度: Medium
耗时: 35分钟

题目描述

将字符串 s 根据给定的行数 numRows,以从上往下、从左到右进行 Z 字形排列,然后按行读取。

例如 “PAYPALISHIRING”,numRows = 3:

P   A   H   R
A P L S I I G
Y   I   N

输出:“PAHNAPLSIIGYIR”

解题思路

解法1:二维数组模拟
直接模拟Z字形的填充过程:

  1. 创建二维数组存储字符
  2. 用方向标记控制向下/斜向上的移动
  3. 按行遍历输出结果

解法2:找规律优化
观察每行字符在原串中的下标规律,直接按行输出。

AC代码(解法1:模拟)
class Solution {
public:string convert(string s, int numRows) {if(numRows == 1) return s;  // 特殊情况vector<vector<char>> ret(numRows, vector<char>(s.size(), ' '));int row = 0, col = 0;bool goingDown = true;for(char c : s) {ret[row][col] = c;// 移动前判断方向切换(避免越界后回退)if(row == 0) {goingDown = true;} else if(row == numRows - 1) {goingDown = false;}// 根据方向移动if(goingDown) {row++;  // 向下:行+1,列不变} else {row--;  // 斜向上:行-1,列+1col++;}}// 按行遍历拼接结果string result = "";for(int i = 0; i < numRows; i++) {for(int j = 0; j < s.size(); j++) {if(ret[i][j] != ' ') {result += ret[i][j];}}}return result;}
};
优化解法(找规律)
class Solution {
public:string convert(string s, int numRows) {if(numRows == 1) return s;string ret;int d = 2 * numRows - 2;  // 周期:每个完整Z字的字符数int n = s.size();// 1. 处理第一行for(int i = 0; i < n; i += d)ret += s[i];// 2. 处理中间行for(int k = 1; k < numRows - 1; k++) {for(int i = k, j = d - k; i < n || j < n; i += d, j += d) {if(i < n) ret += s[i];if(j < n) ret += s[j];}}// 3. 处理最后一行for(int i = numRows - 1; i < n; i += d)ret += s[i];return ret;}
};
关键理解
  1. 模拟解法

    • 空间复杂度 O(n×numRows),逻辑直观
    • 方向切换时机:移动前判断避免越界
    • 边界情况:numRows == 1 时需要特殊处理
  2. 优化解法

    • 空间复杂度 O(1),直接按行输出
    • 核心:周期 d = 2×numRows - 2
    • 第一行和最后一行:等差数列
    • 中间第k行:两组下标交替(竖线 + 斜线)
  3. 两种解法对比

    • 模拟法:易于理解和实现,适合初学
    • 规律法:需要观察和推导,但效率更高

4. 外观数列 (LeetCode 38)

难度: Medium
耗时: 20分钟

题目描述

外观数列是一个整数序列,每一项都是对前一项的描述:

  1. 1
  2. 11(一个1)
  3. 21(两个1)
  4. 1211(一个2,一个1)
  5. 111221(一个1,一个2,两个1)
解题思路

从"1"开始,每次对当前字符串进行"读数"描述,生成下一项。用双指针统计连续相同字符的个数。

AC代码
class Solution {
public:string countAndSay(int n) {string ret = "1";for(int i = 1; i < n; i++) {string tmp;  // 每次循环重新创建,保证清空int len = ret.size();for(int left = 0, right = 0; right < len;) {// right向右扫描,找到第一个不同字符while(right < len && ret[left] == ret[right]) right++;// 统计:right-left个ret[left]tmp += to_string(right - left) + ret[left];left = right;}ret = tmp;}return ret;}
};
关键理解
  1. 双指针统计left 指向连续段起点,right 扫描到第一个不同字符

    • 连续段长度:right - left
    • 连续段字符:ret[left]
  2. tmp的作用:每次循环都要对新的 ret 进行描述,所以 tmp 必须在循环内定义(或手动清空)

  3. 时间复杂度:O(2^n),因为字符串长度随 n 指数增长

执行过程示例(n=4)
初始: ret = "1"
第1次: "1" → 1个'1' → "11"
第2次: "11" → 2个'1' → "21"
第3次: "21" → 1个'2' + 1个'1' → "1211"

5. 数青蛙 (LeetCode 1419) ⭐⭐

难度: Medium
耗时: 30分钟

题目描述

给你一个字符串 croakOfFrogs,表示不同青蛙发出的蛙鸣声(“croak”)的组合。返回模拟字符串所需不同青蛙的最少数目。

青蛙必须依序输出 ‘c’, ‘r’, ‘o’, ‘a’, ‘k’。

解题思路

用状态机思想,记录处于每个状态的青蛙数量。每只青蛙按顺序经历:

空闲 → c → r → o → a → k → 空闲(可复用)
踩坑记录

第一次尝试:编译错误

unordered_map hash{'c','r','o','a','k'};  // ❌ 语法错误

错误原因:

  1. unordered_map 需要指定键值对类型 <char, int>
  2. 变量名冲突:如果用 c, r, o, a, k 作为变量名,就不能用字符 'c' 索引
AC代码
class Solution {
public:int minNumberOfFrogs(string croakOfFrogs) {int cnt_c = 0, cnt_r = 0, cnt_o = 0, cnt_a = 0, cnt_k = 0;if (croakOfFrogs[0] != 'c') return -1;cnt_c++;int maxFrogs = 1;for(int i = 1; i < croakOfFrogs.size(); i++) {char cur = croakOfFrogs[i];if(cur != 'c') {if((cur == 'r') && cnt_c != 0) {cnt_c--; cnt_r++;}else if((cur == 'o') && cnt_r != 0) {cnt_r--; cnt_o++;} else if((cur == 'a') && cnt_o != 0) {cnt_o--; cnt_a++;} else if((cur == 'k') && cnt_a != 0) {cnt_a--; cnt_k++;}else return -1; } else {if(cnt_k != 0) {cnt_k--;  // 复用空闲青蛙cnt_c++;} else {cnt_c++;  // 新青蛙}}// 统计当前正在叫的青蛙数int current = cnt_c + cnt_r + cnt_o + cnt_a;maxFrogs = max(maxFrogs, current);}return (cnt_c == 0 && cnt_r == 0 && cnt_o == 0 && cnt_a == 0) ? maxFrogs : -1;}
};
关键理解
  1. 状态机思想:每只青蛙按顺序经历 5 个状态

  2. 青蛙复用:叫完’k’的青蛙变为空闲,遇到新的’c’时优先复用

  3. 统计技巧

    • cnt_c + cnt_r + cnt_o + cnt_a:当前正在叫的青蛙数
    • cnt_k:已完成、空闲的青蛙(可复用,不计入当前占用)
    • maxFrogs:同一时刻最多有几只青蛙在叫(即答案)
  4. 为什么不直接返回cnt_k?

    • 例如 “croakcroak”:1只青蛙叫两次,maxFrogs=1 ✅
    • 例如 “crcoakroak”:2只青蛙交错叫,maxFrogs=2
    • 题目求的是"同时最多需要几只",而非"一共完成了几次"
数据结构选择:哈希表 vs 简单变量

三种实现方式:

方案时间复杂度空间复杂度代码可读性效率
简单变量O(n)O(1)⭐⭐⭐⭐⭐最高
数组模拟O(n)O(128)⭐⭐⭐⭐
unordered_mapO(n)O(1)⭐⭐⭐中等

选择建议:

  • 状态≤10个:简单变量(最优)
  • 状态10-100个:数组模拟(平衡)
  • 状态不固定/稀疏:unordered_map(灵活)

核心收获

1. 模拟算法的本质

模拟算法不是"套模板",而是:

  • 理解题意:抓住题目描述的核心规律
  • 直接实现:按照逻辑一步步翻译成代码
  • 处理细节:注意各种边界情况

2. 常见模拟类型总结

类型特点代表题目关键点
字符串处理逐字符遍历替换问号、外观数列边界检查、字符串拼接
区间模拟时间/事件模拟提莫攻击重叠处理
状态转换方向/状态切换Z字形、数青蛙切换时机、状态统计

3. 解题思路对比

模拟法 vs 优化法(以Z字形为例):

  • 模拟法:易于理解,适合初学,空间换时间
  • 优化法:需要观察规律,空间优,但不易想到

选择策略

  • 面试时先用模拟法快速AC
  • 有时间再考虑优化

4. 数据结构的选择智慧

不同场景选择不同数据结构:

  • 状态少且固定(≤10个)→ 简单变量
  • 状态多但连续(字母等)→ 数组模拟哈希
  • 状态不连续或动态 → unordered_map

易错点总结

1. 经典低级错误

= vs ==

if(i = 0 || ...)  // ❌ 赋值,改变了i的值
if(i == 0 || ...)  // ✅ 比较

启示:这种错误很隐蔽,编译器不报错,但会导致逻辑错误甚至数组越界

2. 边界条件处理

访问数组前的检查

if(i > 0 && s[i-1] == ...)     // ✅ 先判断i>0
if(i < n-1 && s[i+1] == ...)   // ✅ 先判断i<n-1

特殊情况的处理

if(numRows == 1) return s;  // Z字形变换的边界

3. 状态切换的时机

移动前判断 vs 移动后判断

  • 移动前判断:逻辑清晰,不会越界
  • 移动后判断:需要回退,容易出错

推荐:移动前判断方向切换

4. 临时变量的作用域

tmp必须在循环内定义

for(int i = 1; i < n; i++) {string tmp;  // ✅ 每次循环都是空的// 对ret进行描述,存到tmpret = tmp;
}

如果定义在外面,需要手动清空:

string tmp;
for(int i = 1; i < n; i++) {tmp.clear();  // 或 tmp = "";// ...
}

5. 理解题目的真正含义

数青蛙问题

  • 问的是"最少需要几只青蛙"
  • 而不是"一共完成了几次叫声"
  • 要统计的是同时存在的青蛙数的最大值

总结

模拟算法看似简单,但细节很重要。这5道题让我学会了:

  1. 仔细审题:理解题意是第一步
  2. 注重细节:边界条件、特殊情况都要考虑
  3. 先实现再优化:模拟法保证AC,有余力再优化
  4. 数据结构选择:根据实际情况选择最合适的
  5. 避免低级错误= vs ==、数组越界等

模拟算法是算法学习的基础,虽然没有固定套路,但通过大量练习可以培养出良好的代码实现能力和细节处理能力。

http://www.dtcms.com/a/488020.html

相关文章:

  • 昭阳区住房和城乡建设管理局网站网站建设最低价
  • 济南市住房和城乡建设局网站wordpress多图主题
  • TOP TOY闯关港股上市:三大关键挑战亟待破局,品牌如何独立增长?
  • TDengine 数学函数 FLOOR 用户手册
  • 第三方课题验收测试机构:【API测试工具Apifox使用指南】
  • 前端-APIs-day2
  • 织梦个人网站模板西安旅游
  • 个人网站设计与实现源码在线做网站黄
  • Highcharts 绘制之道(1):用数据构建基础图形
  • 【机器学习02】梯度下降、多维特征线性回归、特征缩放
  • 一个网站每年维护费用品牌营销网站
  • 有哪些做的很漂亮的网站商城小程序介绍
  • Vue3+Three.js:第05期 时间控制,requestAnimationFrame vs Clock
  • 松江做微网站电子商务网站的优点有那些
  • 个体营业执照网站备案做网站都需要用到什么
  • Python CGI 编程
  • 网页传奇平台优化关键词的作用
  • 定制网站建设和运营网站开发合同履约
  • java枚举能继承接口吗
  • 三分钟学懂3D建模中的UV Position Map
  • 广州网站推广公司wordpress 教学
  • 做外贸的网站哪个好cent7.4安装wordpress
  • 网站建设电话销售网站app生成器下载
  • 网站seo关键词排名优化wordpress自动发文章工具
  • /etc/login.defs vs chage:什么时候用什么?
  • 10.15 作业
  • seo短视频网页入口引流在线观看网站网站友情链接美化代码
  • 机器视觉旋转标定算法+补偿角度计算讲解(现场应用版)
  • 湖北网站推广公司技巧网站微信支付申请流程
  • 上海定制建站网站建设网站开发教育