一、什么是模拟
模拟是对真实事物或者过程的虚拟。在编程时为了实现某个功能,可以用语言来模拟那个功能,模拟成功也就相应地表示编程成功。
二、模拟算法的思路
模拟算法是一种基本的算法思想,可用于考查程序员的基本编程能力,其解决方法就是根据题目给出的规则对题目要求的相关过程进行编程模拟,说白了,就是题目说什么就做什么。在解决模拟类问题时,需要注意字符串处理、特殊情况处理和对题目意思的理解。
在C语言中,通常使用函数 srand() 和 rand() 来生成随机数。其中,函数 srand() 用于初始化随机数发生器,然后使用函数 rand() 来生成随机数。如果要使用上述两个函数,则需要在源程序头部包含 time.h 文件。在程序设计过程中,可使用随机函数来模拟自然界中发生的不可预测情况。在解题时,需要仔细分析题目给出的规则,要尽可能地做到全面考虑所有可能出现的情况,这是解模拟类问题的关键点之一。
三、模拟算法的特点和技巧
特点:
代码量大
操作多
思路复杂
较为复杂的模拟题出错后难定位错误
技巧:
1、在动手写代码之前,在草纸上尽可能地写好要实现的流程.
2、在代码中,尽量把每个部分模块化,写成函数、结构体或类
3、对于一些可能重复用到的概念,可以统一转化,方便处理:如,某题给你"YY-MM-DD 时:分" 把它抽取到一个函数,处理成秒,会减少概念混淆
4、调试时分块调试。模块化的好处就是可以方便的单独调某一部分
5、写代码的时候一定要思路清晰,不要想到什么写什么,要按照落在纸上的步骤写
三、模拟OJ题
3.1、替换所有的问号
1576. 替换所有的问号 - 力扣(LeetCode)
题目描述
算法思路
class Solution {
public:
string modifyString(string s)
{
for(int i=0;i<s.size();i++)
{
if(s[i]=='?')
{
for(char j='a';j<='z';j++)
{
if((i==0||j!=s[i-1])&&(i==s.size()-1||j!=s[i+1]))
{
s[i]=j;
}
}
}
}
return s;
}
};
3.2、提莫攻击
495. 提莫攻击 - 力扣(LeetCode)
题目描述
算法思路
模拟 + 分情况讨论。
计算相邻两个时间点的差值:
i. 如果差值⼤于等于中毒时间,说明上次中毒可以持续 duration 秒;
class Solution {
public:
int findPoisonedDuration(vector<int>& timeSeries, int duration)
{
int n=timeSeries.size();
int sum=0;
for(int i=1;i<n;i++)
{
if(timeSeries[i]-timeSeries[i-1]>=duration)
{
sum+=duration;
}
else
{
sum+=timeSeries[i]-timeSeries[i-1];
}
}
return sum+duration;
}
};
3.3、N字形变换
6. Z 字形变换 - 力扣(LeetCode)
题目描述
算法思路
这个 Z
字型其实是这样的:
3
行的
示例1
, 它的字符数分布是这样的

对于前面的 4
行的 示例2
, 它的字符数分布是这样的:
那么对于 n
行的字符数分布是这样的:
如上图所示,我们可以发现:
1.当前行 curRow 为 0 或 n-1 时,箭头发生反向转折。
方法一: 从左到右按箭头方向迭代 s ,将每个字符添加到合适的行。之后从上到下遍历行即可。
我们假定 n=numRows :
class Solution {
public:
string convert(string s, int numRows) {
if (numRows == 1) return s;
vector<string> rows(min(numRows, int(s.size()))); // 防止s的长度小于行数
int curRow = 0;
bool goingDown = false;
for (char c : s) {
rows[curRow] += c;
if (curRow == 0 || curRow == numRows - 1) {// 当前行curRow为0或numRows -1时,箭头发生反向转折
goingDown = !goingDown;
}
curRow += goingDown ? 1 : -1;
}
string ret;
for (string row : rows) {// 从上到下遍历行
ret += row;
}
return ret;
}
};
3.4、外观数列
38. 外观数列 - 力扣(LeetCode)
题目描述
算法思路
代码实现
class Solution {
public:
string countAndSay(int n)
{
string ret="1";
for(int i=1;i<n;i++) //翻译n-1次
{
string temp;
int len =ret.size();
for(int left=0,right=0;right<len;)
{
while(right<len&&ret[right]==ret[left]) right++;
temp+=(right-left)+'0';
temp+=ret[left];
left=right;
}
ret=temp;
}
return ret;
}
};
3.5、数青蛙
题目描述
算法思路
(快排思想 - 三指针法使数组分三块)
◦ [cur, right - 1] 内的元素是待定元素
◦ [right, n] 内的元素都是 2 。
a. 初始化 cur = 0 , left = -1 , right = numsSize ;b. 当 cur < right 的时候(因为 right 表⽰的是 2 序列的左边界,因此当 cur 碰到right 的时候,说明已经将所有数据扫描完毕了),⼀直进⾏下⾯循环:根据 nums[cur] 的值,可以分为下⾯三种情况:i. nums[cur] == 0 ;说明此时这个位置的元素需要在 left + 1 的位置上,因此交换 left + 1 与 cur 位置的元素,并且让 left++ (指向 0 序列的右边界),cur++ (为什么可以 ++ 呢,是因为 left + 1 位置要么是 0 ,要么是 cur ,交换完毕之后,这个位置的值已经符合我们的要求,因此 cur++ii. nums[cur] == 1 ;说明这个位置应该在 left 和 cur 之间,此时⽆需交换,直接让 cur++ ,判断下⼀个元素即可;iii. nums[cur] == 2 ;说明这个位置的元素应该在 right - 1 的位置,因此交换right - 1 与 cur 位置的元素,并且让 right-- (指向 2 序列的左边界),cur 不变(因为交换过来的数是没有被判断过的,因此需要在下轮循环中判断)c. 当循环结束之后:[0, left] 表⽰ 0 序列;[left + 1, right - 1] 表⽰ 1 序列;[right, numsSize - 1] 表⽰ 2 序列。
代码实现
class Solution {
public:
int minNumberOfFrogs(string croakOfFrogs)
{
string t="croak";
int n=t.size();
vector<int> hash(n);
unordered_map<char ,int > index;
for(int i=0;i<n;i++)
{
index[t[i]]=i;
}
for(auto ch:croakOfFrogs)
{
if(ch=='c')
{
if(hash[n-1]!=0) hash[n-1]--;
hash[0]++;
}
else
{
int i=index[ch];
if(hash[i-1]==0) return -1;
hash[i-1]--; hash[i]++;
}
}
for(int i=0;i<n-1;i++)
{
if(hash[i]!=0)
{
return -1;
}
}
return hash[n-1];
}
};