模拟过程的技巧
算法题中有一种题型叫做模拟过程,这种算法题在面试中出现的频率非常之高,因为比较简单,代码量一般而言并不算长。因其并没有一套比较固定的框架,也能考验候选人的编程思维。当遇到一道算法题,发现其并不具备其他经典算法的特征时,可以尝试模拟过程的方向进行求解
方法
一般这种题目分为以下几个步骤进行求解:为什么要这么分步骤进行,因为这样可以使得循环的过程中某些变量的值进行处理的时候有一个明确的顺序而不会发生混淆(反正按照下面的模板进行肯定有好处,特别是处理当前层 和 转移 的顺序)
核心:找到当前层是什么以及当前层的操作和变量转移操作
模板
//初始化,一般按照实际含义就是在开始进入第一层处理之前的值。如果是递推(比如fib数列),这个初始化可能还有初始化前几项
while(){ //for() 核心循环
//注意 可能要处理一个变量重定位问题,比如加油站,还有约瑟夫环问题中的处理成环的条件,注意有少数情况重定位在这里处理更好,比如退格问题。
//处理当前层
//重定位,也可以在这里进行处理,发现下一个转移的地方出了边界,进行重定向。一般都在这里进行。比较方便
//转移变量发生转移
}
确定核心循环
一般模拟的问题都是考察使用for,while循环的熟练程度,甚至连数组都不会有所涉及,所以确定当前问题的一个核心循环十分关键,有些复杂的模拟过程问题,一旦找到了核心循环,对题目的理解就豁然开朗。有一个方向,思路会变得明朗。如果做到这个核心循环,这种没有固定框架模板可以套用的算法题将导致感觉无从下手。
- 如果循环次数确定,而且每次循环中循环变量都固定发生变化(i ++ ,j ++)>使用for循环,如果循环次数不确定,或者循环变量不是每层(一次循环我将其称为层)都固定发生转移(比如不是每层都是i ++ ,j ++ ,i -- ,i += 2)>使用while循环
- 在开始进行循环操作之前,转移变量和操作变量需要进行初始化一个起始的值。操作遍历初始化为尚未进行任何处理的值,转移变量初始化为第一层的值。
处理当前层
对操作变量进行操作处理,对当前层的逻辑进行操作,最难的地方就在于分清楚哪些变量是需要在当前层进行操作的。哪些变量是在转移层才进行操作的,各个变量处理的顺序是什么
转移变量转移至下一层
转移变量:就是主要用途是用于驱动循环,其初始化要初始化为刚进第一层的值,不知道怎么描述,只是个人的理解,大家可以直接理解为循环变量
转移变量转移到下一层其应该有的值,最简单的就是i ++ ,j ++这样的
注意转移可能有条件,不同条件下的转移方式可能不一样
例子
斐波拉契数列
菲波那契数列是指这样的数列: 数列的第一个和第二个数都为1,接下来每个数都等于前面2个数之和。给出一个正整数k,要求菲波那契数列中第k个数是多少。
【输入】
输入一行,包含一个正整数k。(1 ≤ k ≤ 46)
【输出】
输出一行,包含一个正整数,表示菲波那契数列中第k个数的大小。
【输入样例】
19
【输出样例】
4181
#include <iostream>
using namespace std;
int main(){
int k;
cin >> k;
//初始化
int a = 1,b = 1;//斐波拉契数列的前两层,这个是转移变量,所以初始化的时候,初始化为第一层的值,也就是数列的前两个项
//核心循环
for(int i = 3;i <= k;i ++){
//当前层操作
int t = a + b;
//转移变量发生转移,至下一层
a = b;
b = t;
}
cout << b;
return 0;
}
134. 加油站
在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。
你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。
如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。
说明:
如果题目有解,该答案即为唯一答案。
输入数组均为非空数组,且长度相同。
输入数组中的元素均为非负数。
示例 1:
输入:
gas = [1,2,3,4,5]
cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int n = gas.length;
for(int i = 0;i < n;i ++) {//枚举从每个加油站出发。看看能否符合条件
int f = 0,t =i,s = 0;//初始化,其中t,f是转移变量,s是操作变量,f表示当前层走过的车站数,s表示当前层的汽油量
boolean flag = false;
//核心循环是刚好走过的车站书等于车站总数,说明饶了一圈
while(f != n){
if(t == n) t = 0;//对成环的处理,如果当前层的t已经走到了数组的最右边,则重置到0
//对当前层进行逻辑操作
s += gas[t];//对操作变量进行修改
if(s - cost[t] < 0) break;
else{
s -= cost[t];
}
//转移变量发生转移,转移给下一层
f ++;
t ++;
}
//出循环判断,只有两种可能一种是f = n 出循环 一种是break出循环,前者是找到了解,后者继续循环,不进行处理
if(f == n) return i;
}
return -1;
}
}
约瑟夫环问题的模拟解法
有N个人围成一圈,从第一个人开始报数,数到M的人就被枪毙,再有下一个人开始重新报数,数到M的那个人被枪毙,求最后活着的人是几号?
#include <iostream>
#include<cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
bool a[101];
int main(){
int n,m;
cin >> n >> m;
//循环前进行初始化
memset(a,0,sizeof a);//初始化数组,一开始全都是活着的人
int f = 0,s = 0,t = 0;//f表示已经被枪毙的人的数量,s表示当前报数的是什么,t表示当前报数的人的编号。
while(f != n - 1) {//被枪毙的人的数目达到了n - 1个的时候,核心循环
//当前层的操作
if(t == n) t = 0;//因为是一个环,所以最后一个与第一个相连。
if(!a[t]) s ++;//如果当前这个编号的人活着 就会报数 就会s ++
if(s == m) {//当有人报数到M的时候,这个人被枪毙
s = 0;//又重新开始报数,计数器归0
cout << t << " ";//输出这个被枪毙的 人的编号
a[t] = true;//标记为被枪毙
//转移变量进行转移,转移给下一层
f ++;//被枪毙的人数+ 1
}
//转移变量进行转移,转移给下一层
t ++;//下一个人继续报数,
}
return 0;
}
关于约瑟夫环的递归解法可以看我的博客
金币
国王将金币作为工资,发放给忠诚的骑士。第1天,骑士收到一枚金币;之后两天(第2天和第3天)里,每天收到两枚金币;之后三天(第4、5、6天)里,每天收到三枚金币;之后四天(第7、8、9、10天)里,每天收到四枚金币……这种工资发放模式会一直这样延续下去:当连续 n 天每天收到 n 枚金币后,骑士会在之后的连续n+1天里,每天收到n+1枚金币(n为任意正整数)。
你需要编写一个程序,确定从第一天开始的给定天数内,骑士一共获得了多少金币。
【输入】
一个整数(范围1到10000),表示天数
【输出】
骑士获得的金币数。
【输入样例】
6
【输出样例】
14
核心循环很好确定,就是遍历每一天
#include <iostream>
using namespace std;
int main(){
int day;
cin >> day;
int sum = 0;//操作变量
int now_gold = 1,now_day = 0;//当前每天发的金币数,转移变量
for(int i = 1;i <= day;i ++) {
//对当前层的处理
sum += now_gold;
//转移变量发生转移,至下一层
now_day ++;//领当前金币数的天数又多了一天
if(now_gold == now_day) {//now_day now_gold的转移是有条件的,如果领当前金币数的天数已经等于金币数了,就发生如下转移。
now_day = 0;
now_gold ++;
}
}
cout << sum;
return 0;
}
进制转化问题
将一个m进制的字符串转化为10进制,1 < m < 10,模板如下:
#include <iostream>
using namespace std;
int main(){
string s;
cin >> s;
int m ;
cin >> m;
int t = 0;
for(int i = 0;i < s.size() ;i ++){
t = t * m + s[i] - '0';
}
cout << t;
return 0;
}
如果要考虑进制问题:
#include <iostream>
using namespace std;
int main(){
string a;
int m;
cin >> a>> m;
int t = 0 ;
for(int i = 0;i < a.size();i ++) {
if(a[i] >='0' && a[i] <= '9') {
t = t * m + a[i] - '0';
}else{
t = t * m + a[i] - 'A' + 10;
}
}
cout << t;
return 0;
}
将一个十进制的数X转化为m进制数(1< m < 10),而且为正整数的情况下,如果要考虑负数,在前面加个判断是否为负数,然后翻转一下符号,在最后输出符号即可,如果考虑0,那就直接返回0就完事了
#include <iostream>
using namespace std;
int main(){
int n,m;
cin >> n >> m ;
while(n) {
//处理当前层
cout << n % m;
//转移变量发生转移
n /= m;
}
return 0;
}
如果要处理m > 10的情况
#include <iostream>
using namespace std;
int main(){
int t,m;
cin >> t >> m;
string s;
while(t) {
int now = t % m;
char k;
if(now >= 10) {
k = now - 10 + 'A';
} else k = now + '0';
s = k + s;
t /= m;
}
cout << s;
return 0;
}
将一个数逆序输出
比如123 输出321
#include <iostream>
using namespace std;
int main(){
int n ;
cin >> n;
int t = 0;
while(n){
//处理当前层
t = t * 10 + n % 10;
//转移变量发生转移
n /= 10;
}
cout << t;
return 0;
}
将一个字符串解析为整数
int fun(string s){
bool neg = false;
int i = 0 ;
if(s[0] == '-') {
neg = true;
i = 1;
}
int t = 0 ;
for(;i < s.size();i ++) t = t * 10 + s[i] - '0';
if(neg) t *= -1;
return t;
}
分解质因数
int main(){
int n;
cin >> n;
for(int i = 2;i <= n / i;i ++) {//从2开始枚举,枚举到n / i就行,因为一个数最多只有一个大于根号n的质因数,最后那个数最后单独处理就行
if(n % i == 0) {//符合这个条件的约数i一定是质数,因为如果符合条件的i是一个合数,他肯定在之前他的质数被枚举到的时候就被除掉了,相当于被筛掉了
int s = 0 ;
while(n % i == 0) {
s ++;//处理当前层
n /= i;//变量发生转移
}
printf("%d %d\n",i,s);
}
}
if(n > 1) printf("%d 1\n",n);。//单独处理最后一个大于根号n的质数
return 0;
}
按照规律生成字符串或者数列问题
一般这种题目都需要自己给出字符串或者数列的前几项,然后再走框架,这个时候就要注意处理边界值,比如自己给出了前三项,那么如果要输出前三项就要直接返回。
1.神奇字符串(leetcode 481)
神奇的字符串 S 只包含 '1' 和 '2',并遵守以下规则:
字符串 S 是神奇的,因为串联字符 '1' 和 '2' 的连续出现次数会生成字符串 S 本身。
字符串 S 的前几个元素如下:S = “1221121221221121122 ......”
如果我们将 S 中连续的 1 和 2 进行分组,它将变成:
1 22 11 2 1 22 1 22 11 2 11 22 ......
并且每个组中 '1' 或 '2' 的出现次数分别是:
1 2 2 1 1 2 1 2 2 1 2 2 ......
你可以看到上面的出现次数就是 S 本身。
给定一个整数 N 作为输入,返回神奇字符串 S 中前 N 个数字中的 '1' 的数目。
注意:N 不会超过 100,000。
class Solution {
//生成字符串 生成数组的模拟问题 一般都要有一定的初始化值
public int magicalString(int n) {
String str = "122";
int i = 2;
int c = 1;
if(n == 0) return 0;
if(n == 1 || n == 2 || n == 3) return 1;//特殊处理边界值
int f = 3,cnt = 1,t = str.charAt(i) - '0';
while(f < n) {//确定核心循环
if(t == 0) {
i ++;
c = 3-c ;
t = str.charAt(i) - '0';
}
str += c;
if(c == 1) cnt ++;
//处理当前层结束 开始进行转移变量转移
f ++ ;
t --;
}
return cnt;
}
}
模拟运算与高精度运算
易错点:注意string转为vector存储的时候,要减去'0'
链表模拟两数相加:(要链表逆序存储两个数字)
给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。
如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。
您可以假设除了数字 0 之外,这两个数都不会以 0 开头。
比如:
输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(-1);
ListNode res = dummy;
int t =0;
while(l1 != null && l2 != null){
int sum = l1.val + l2.val + t;//处理当前层
if(sum >= 10) {
sum -= 10;
t = 1;
}
else t = 0;
res.next = new ListNode(sum);//变量转移
res = res.next;
l1 = l1.next;
l2 = l2.next;
}
while(l1 != null) {
int sum = l1.val + t;
if(sum >= 10) {
sum -= 10;
t = 1;
}
else t = 0;
res.next = new ListNode(sum);
res = res.next;
l1 = l1.next;
}
while(l2 != null) {
int sum = l2.val + t;
if(sum >= 10) {
sum -= 10;
t = 1;
}
else t = 0;
res.next = new ListNode(sum);
res = res.next;
l2 = l2.next;
}
if(t == 1) res.next = new ListNode (1);//千万不要忘记了如果最后还有进位则加上一个1的节点
return dummy.next;
}
}
高精度加法:
#include<iostream>
#include <vector>
using namespace std;
const int N = 100010;
//一个是忘记处理多出来的进位
//一个是忘记 - '0'
vector<int> add(vector<int> &A,vector<int> &B){
int t = 0 ;
vector<int> C;
for(int i = 0;i < A.size() || i < B.size();i ++) {
//处理当前层
int res = t;
if(i < A.size()) res += A[i];
if(i < B.size()) res += B[i];
if(res >= 10) {
res -= 10;
//处理进位
t = 1;
}else t = 0;
C.push_back(res);
}
if(t == 1) C.push_back(1);//千万不要忘记最后如果又进位就加上一个1
return C;
}
int main(){
string a,b;
cin >> a >> b;//将两个数用字符串的形式输入
vector<int> A,B,C;
for(int i = a.size() - 1;i >= 0;i --) A.push_back(a[i] -'0');//容器中要逆序存储这个数,因为模拟运算的时候要从个位开始相加运算,而且如果最后如果还有进位,还要在末尾补上1,在数组末尾补1要方便许多
for(int i = b.size() - 1;i >= 0;i --) B.push_back(b[i] - '0');
C= add(A,B);
for(int i= C.size() -1;i >= 0;i --) cout << C[i];
return 0;
}
高精度减法
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
string a, b;
int cmp(string a,string b) {
if (a.size() > b.size()) return 1;
if (a.size() < b.size()) return -1;
for (int i = 0; i < a.size() && i < b.size(); i++) {
if (a[i] > b[i]) return 1;
else if (a[i] == b[i]) continue;
else return -1;
}
}
vector<int> sub(vector<int> & A, vector<int> &B) {
int t = 0;
vector<int> C;
for (int i = 0; i < A.size(); i++) {
int res = A[i] - t;
if (i < B.size()) res -= B[i];
if (res < 0) {
res += 10;
t = 1;
}
else {
t = 0;
}
C.push_back(res);
}
while (C.size() > 1 && C.back() == 0) C.pop_back();
return C;
}
int main() {
cin >> a >> b;
vector<int> A, B;
for (int i = a.size() - 1; i >= 0; i--) A.push_back(a[i] - '0');
for (int i = b.size() - 1; i >= 0; i--) B.push_back(b[i] - '0');
vector<int> C;
bool flag = false;
if (cmp(a, b) == 1) {
C = sub(A, B);
}
else if(cmp(a,b) == 0){
cout << 0;
}
else {
C = sub(B, A);
flag= true;
}
if(flag) cout << "-";
for (int i = C.size() - 1; i >= 0; i--) cout << C[i];
return 0;
}
高精度乘法
高精乘低精度
#include <iostream>
#include <vector>
using namespace std;
string a;
vector<int> multiply(vector<int> &A,int b) {
int t = 0 ;
vector<int> C;
for(int i = 0;i < A.size();i ++) {
int res = t;
res += A[i] * b;
C.push_back(res % 10);
t = res / 10;
}
while(t) {
C.push_back(t % 10);
t /= 10;
}
return C;
}
int main(){
int b;
cin >> a >> b;
vector<int> A;
if(b == 0) {//只有b =0 会出现前导零
cout << 0;
return 0;
}
for(int i = a.size() -1;i >= 0;i --) A.push_back(a[i] - '0');
vector<int> C = multiply(A,b);
for(int i = C.size() - 1;i >= 0;i --) {
cout << C[i];
}
return 0;
}
高精度乘高精度(统一模式进行处理的思想)
#include <iostream>
using namespace std;
const int N = 210;
string s1,s2;
int a[N],b[N],c[N * N];
int main(){
cin >> s1 >> s2;
int k = 1;
for(int i = s1.size() - 1;i >= 0;i -- ) {
a[k++] = s1[i] - '0';
}
k = 1 ;
for(int i = s2.size() - 1;i >= 0;i --) {
b[k ++] = s2[i] - '0';
}
for(int i = 1;i <= s1.size();i ++ ) {
int t = 0 ;
for(int j = 1;j <= s2.size();j ++) {
int res = t + a[i] * b[j] + c[i + j - 1];//寻找下标的规律
if(res >= 10) {
t = res / 10;
c[i + j - 1] = res % 10;
}else {
c[i + j - 1] = res;
t = 0;
}
}
c[i + s2.size()] = t;//进位 不管有没有进位 ,我都设置在这里 这样可以统一处理,
}
int lenc = s1.size() + s2.size();//看手写乘法的样式,如果不管有没有进位都统一处理进位,最后结果的长度就是lena + lenb;
while(lenc > 1 && c[lenc] == 0) lenc--;
for(int i = lenc;i >= 1;i --) cout << c[i];
cout <<endl;
return 0;
}
高精度数除于低精度的数
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 110;
vector<int> div(vector<int> &A,int b,int &r){
vector<int> C;
int t = 0;
for(int i = A.size() - 1;i >= 0;i --) {
//处理当前层,找到当前层是什么十分重要
t = t * 10 + A[i];
C.push_back(t / b);
//变量转移
t %= b;
}
r = t;//处理余数
reverse(C.begin(),C.end());
while(C.size() > 1 && C.back() == 0) C.pop_back();//取出前导零
return C;
}
int main(){
vector<int> A;
string a;
int b;
cin >> a >> b;
for(int i =a.size() - 1;i >= 0;i --) A.push_back(a[i] -'0');
int r = 0;
vector<int> C;
C = div(A,b,r);
for(int i = C.size() - 1;i >= 0;i --) cout << C[i];//为了保证加减乘除的一致性,还是高位存在数组末尾
cout << endl;
cout << r;
return 0;
}
计算2的N次方
#include <iostream>
#include <vector>
using namespace std;
int n;
int main(){
vector<int> C;
vector<int> A;
A.push_back(1);
cin >> n;
for(int i = 0;i < n;i ++) {
C.clear();
int t = 0;
for(int i = 0;i < A.size() ;i ++) {
int res = A[i] * 2 + t;
if(res >= 10) {
C.push_back(res % 10);
t = res / 10;
}else {
C.push_back(res);
t = 0;
}
}
while(t) {
C.push_back(t % 10);
t /= 10;
}
A.assign(C.begin(),C.end());
}
for(int i = C.size() - 1;i >= 0;i --) cout << C[i];
return 0;
}
leetcode 38 外观数列
「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。
你可以将其视作是由递归公式定义的数字字符串序列:
countAndSay(1) = "1"
countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串。
前五项如下:
-
1
-
11
-
21
-
1211
-
111221
第一项是数字 1
描述前一项,这个数是 1 即 “ 一 个 1 ”,记作 "11"
描述前一项,这个数是 11 即 “ 二 个 1 ” ,记作 "21"
描述前一项,这个数是 21 即 “ 一 个 2 + 一 个 1 ” ,记作 "1211"
描述前一项,这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ,记作 "111221"
要 描述 一个数字字符串,首先要将字符串分割为 最小 数量的组,每个组都由连续的最多 相同字符 组成。然后对于每个组,先描述字符的数量,然后描述字符,形成一个描述组。要将描述转换为数字字符串,先将每组中的字符数量用数字替换,再将所有描述组连接起来。
class Solution {
public:
string countAndSay(int n) {
string s = "1";
n --;
while(n --) {
int i = 0;
string t = "";
while(i < s.size()) {
int j = i;
int cnt = 0 ;
while(j < s.size() && s[j] == s[i]) {
cnt ++;
j ++;
}
t = t + to_string(cnt) + s[i];
i = j;
}
s = t;
}
return s;
}
};
leetcode 160 相交链表
编写一个程序,找到两个单链表相交的起始节点。
```java
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
LisNode p = headA,q = headB;
while(p != q) {
//处理当前层
if(p == null) p = headB;
else p = p.next;//转移到下一层
if(q == null) q = headA;
else q = q.next;//转移到下一层
}
return p;
}
}
爱吃香蕉的珂珂
```c++
class Solution {
public:
int n ;
int minEatingSpeed(vector<int>& piles, int H) {
n = piles.size();
if(n == 0) return 0;
int max_k = 0;
for(int p : piles){
max_k = max(max_k,p);
}
int l = 1,r = max_k;
while(l < r) {
int mid = l + r >> 1;
cout << mid << endl;
if(check(mid,piles,H)) r = mid;
else l = mid + 1;
}
if(!check(l,piles,H)) return -1;
return l;
}
bool check(int k,vector<int> & piles,int H){
int i = 0 ,h = 0;
while(i < n) {
if(k > piles[i]) {
h ++;
i ++;
if(h > H) return false;
}
else if(k <= piles[i])
{
int t = piles[i];
int need = t / k;
h += need;
if(t % k) h ++;
if(h > H) return false;
i ++;
}
}
return true;
}
};
还有很多这种模拟过程的题目,接下来几天还会继续补充~