本来打算写一个普普通通的DAY5学习报告,DAY5的总体知识量小但难度不算小,而这个解算式(我称之为计算器问题)则是其中最难的。因此,我就通过简单地解析这一类题,来研究一下队列。
一.解前缀表达式(难度1)
例题1(openjudge 2.2)(难度1):
(此处提示非常有用)
对于计算器问题,一般有两种解法:递归解法和队列解法。我们要先分析一下,这题用递归还是用队列。
首先观察一下这个问题,对于每一个运算符,都要对它下面的两个数进行操作,如果不够两个数,就进行下一组运算凑够两个数。例如这里的样例,就是(11+12)*(24+35)=1357。因此这道题我们是先明确运算规则再明确具体数值,没有见到数值就已经知道要怎么操作了,故使用递归方案比较简洁。具体的方案大致如下:
if(s[i] == 某运算规则) 结果 = dfs()+dfs().
这样一来,问题就迎刃而解了。
(要注意提示中告诉我们的方法,这种方法对于用空格作分隔符的问题很便利)
核心代码如下:
double ret;
char s[1001];
scanf("%s",s);
if(s[0] == '+') ret = P() + P();
if(s[0] == '-') ret = P() - P();
if(s[0] == '*') ret = P() * P();
if(s[0] == '/') ret = P() / P();
if(s[0] >= '0' && s[0] <= '9') ret = atof(s);
return ret;
}
这道题简单的地方有二:一是先有规则后有值;二是空格分隔,用scanf逐个读入就可以,十分方便。
通过这个题,我们可以得出核心结论1:对于先有运算符后有数的问题,采取递归求解。
二.解后缀表达式(难度2)
例题2(P1449)(难度2+):
我们首先不考虑其他的,由易入难,只考虑后缀表达式的核心解法:
(1)规定:每一个输入的数都是一位的自然数(难度2)。
这道题是后缀表达式,对于每一个运算符,都对他前面最近的两个数进行运算,运算符从左到右依次进行,如果不足两个数,就等着前面的运算结束。因此我们是先有值后有运算规则,如果我们不存储值,等拿到运算规则的时候黄花菜都凉了。所以,需要用一个栈来存一下数,遇到运算符的时候就取出最近存的两个数(也就是栈顶)运算,把结果再扔回去,之后的操作同理。这样一来,思路就得到了,其核心代码如下:
int main(){
int i,rear = 0,n;
long long a[1001];
char s[1001];
scanf("%s",s);
n = strlen(s);
for(i = 0;i < n;i++){//注意字符串从0开始
if(s[i] >= '0' && s[i] <= '9');
a[++rear] = s[i] - '0';//字符串转整型
if(s[i] == '+'){
--rear;
a[rear] += a[rear + 1];
}
if(s[i] == '-'){
--rear;
a[rear] -= a[rear + 1];
}
if(s[i] == '*'){
--rear;
a[rear] *= a[rear + 1];
}
if(s[i] == '/'){
--rear;
a[rear] /= a[rear + 1];
}
if(s[i] == '@') break;
}
printf("%lld",a[rear]);//最后rear一定等于1,a[rear]即为结果
return 0;
}
(由于这道题有@做边界,所以范围是否带n不影响结果)
有了思路,我们再来考虑一下这题可以AC的完整情况:
(2)规定:每一个数和结果都是long long范围内的自然数(难度3)。
由于这道题没有空格分隔,只有数字,而且又不要求浮点型,所以我们不再使用atof。这里的方法也很简单:我们把所有读进来的数存进一个临时的数组,如果遇到分隔符".",就把这个数组转化成一个整数存到新的数组里。计算的时候,就把这个数组代替原来的字符串用来参加计算。
这部分代码如下:
if(s[i] >= '0' && s[i] <= '9') temp[++top] = s[i] - '0';
if(s[i] == '.'){
++rear;
a[rear] = 0;
for(j = 1;j <= top;j++){
a[rear] += kuaimi(10,top - j) * temp[j];
}
memset(temp,0,sizeof(temp));
top = 0;
}
(这里的其实并不是整型计算的完整情况,其他的我们在下一题进行讨论)
从此题,我们得到核心结论2:对于先有数后有运算符的问题,用栈来求解。
三.中缀表达式(难度3)
我们还是由易入难来解决此题。
(1)只把中缀表达式转化为后缀表达式,每个数都是0-9的自然数(难度3)。
首先,这题希望我们转移成一个后缀表达式,所以最终得到的应该是一个字符串,故我们基本上也要采取栈和队列。
之后,我们明确运算优先级:乘方>乘除>加减>括号。
由于后缀表达式当中,运算符是自左向右进行,所以在运算中,我们首先考虑一下怎么把中缀的运算符顺序转化为后缀的运算符顺序。
首先,前面几项都是数,而根据上一题的经验,已经经过的一定要存起来,所以说数和运算符应该是分开存的。但是由于这两者在结果上是混合的,故我们应该在过程中把运算符存到存数的那个字符串里面。
之后,我们再研究什么时候应该把运算符放到数字字符串里面。
考虑一下第一组运算符乘和加,他们都是括号内的,所以我们可以得出结论1:遇到右括号的时候就开始往数字字符串转移,并且是从右至左拿出放入,直到遇到左括号为止。
假如我们把这个括号内的算式改为3*2+6,那么输出的情况应该是8 3 2 * 6 +,这时出现了分组,也就是这里的转移与括号无关了。结合顺序可知,导致乘号存入的是加号,而且我们可以想象出,假设这个乘号变成加号,这个前面的加号还是要先转移,而前乘后加就不会导致乘号转移。所以我们又得出结论2:遇到优先级小于自己的运算符,就入栈;否则,把栈顶的运算符转移,直到遇到优先级小于自己的运算符或者是栈被搬空。 根据最开始我们知道的优先级可以发现,其实结论1是结论2的一个特殊情况。为了便于判断,我们可以用数代表优先级。
但是尽管我们认为括号运算优先级最低,但是我们却没有立刻把减号转移,这样我们补充一个结论3:左括号可以无视优先级存入运算符栈。
我们继续向下看,会发现后面的情况其实与我们总结出的结论是吻合的,这就说明我们得到的结论是可靠的。这样,我们就得出了转移的方法。
(为了输出方便,我们需要从左侧取出,因此要开一个deque)
int kuaimi(int p,int b){//普通的快速幂
int ret = 1;
while(b != 0){
if(b % 2 == 1) ret = ret * p;
p *= p;
b /= 2;
}
return ret;
}
int value(char c){//优先级操作
if(c == '+' || c == '-') return 1;
if(c == '*' || c == '/') return 2;
if(c == '^') return 3;
return 0;
}
int main(){
int i,n,top = 0,rear = 0;
char s[101];
scanf("%s",s);
n = strlen(s);
for(i = 0;i < n;i++){
if(s[i] >= '0' && s[i] <= '9'){
Q1.push_back(s[i]);
}
else{
if(s[i] == ')'){
while(Q2.back() != '('){
Q1.push_back(Q2.back());
Q2.pop_back();
}
Q2.pop_back();//把左括号扔掉
}
else{
if(Q2.empty() || value(s[i]) > value(Q2.back()) || s[i] == '('){
Q2.push_back(s[i]);
}
else{
while(!Q2.empty() && value(s[i]) <= value(Q2.back())){
Q1.push_back(Q2.back());
Q2.pop_back();
}
Q2.push_back(s[i]);
}
}
}
}
while(!Q2.empty()){//把未转移的都依次转移过去
Q1.push_back(Q2.back());
Q2.pop_back();
}
while(!Q1.empty()){
printf("%c ",Q1.front());//由于只是一个过程,就不在乎多的空格了
Q1.pop_front();
}
}
(2)原题:把中缀表达式转化为后缀表达式然后求解,并输出整个过程。过程中的数都是0-9的自然数(难度3.5)。
这里我们唯一需要另外考虑的只有输出方式了。我们可以知道,在一次操作后,会把两个数变成一个数存入,减少一个运算符,前面、后面的都不变。前面的一定都是数,这些数都已经存在结果的数组里了;后面的还是原来的字符串。这样的话我们就得到了操作方法:先从头至尾输出一遍结果数组,然后从进行运算的那个运算符下一位输出一遍字符串。注意,由于我们在转后缀表达式的过程中去掉了括号,后缀表达式的字符串一定不长于中缀表达式的长度,因此要重置一下长度。
另外,不要忘记,我们在计算前要先输出一遍,还要输出结果。
以上的这些题中,我们总结出核心结论3:计算器(栈间转移)问题的每一位数都必须进行(预)操作,判断可以结合后面的几位判断,但不能在之后回头判断。
输出部分核心代码如下:
int a[101],top;
char res[102];
void print(int p,int b,int c){
int i;
for(i = 1;i <= p;i++) printf("%d ",a[i]);
for(i = b + 1;i <= c;i++){
if(i > b + 1) printf(" ");
printf("%c",res[i]);
}
printf("\n");
}
int main(){
//转移部分省略
while(!Q1.empty()){
res[++top] = Q1.front();
Q1.pop_front();
}
int i,n,rear = 0;
n = top;
print(0,0,n);//先输出一遍后缀表达式
for(i = 1;i <= n;i++){
if(res[i] >= '0' && res[i] <= '9') a[++rear] = res[i] - '0';
else{
if(res[i] == '+'){
--rear;
a[rear] += a[rear + 1];
}
if(res[i] == '-'){
--rear;
a[rear] -= a[rear + 1];
}
if(res[i] == '*'){
--rear;
a[rear] *= a[rear + 1];
}
if(res[i] == '/'){
--rear;
a[rear] /= a[rear + 1];
}
if(res[i] == '^'){
--rear;
a[rear] = kuaimi(a[rear],a[rear + 1]);
}
print(rear,i,n);
}
}
return 0;
}
原题完整AC代码如下:
#include<cstdio>
#include<deque>
#include<cstring>
#include<algorithm>
using namespace std;
deque<char> Q1,Q2;
int a[101];
char res[102];
void print(int p,int b,int c){
int i;
for(i = 1;i <= p;i++) printf("%d ",a[i]);
for(i = b + 1;i <= c;i++){
if(i > b + 1) printf(" ");
printf("%c",res[i]);
}
printf("\n");
}
int kuaimi(int p,int b){
int ret = 1;
while(b != 0){
if(b % 2 == 1) ret = ret * p;
p *= p;
b /= 2;
}
return ret;
}
int value(char c){
if(c == '+' || c == '-') return 1;
if(c == '*' || c == '/') return 2;
if(c == '^') return 3;
return 0;
}
int main(){
int i,n,top = 0,rear = 0;
char s[101];
scanf("%s",s);
n = strlen(s);
for(i = 0;i < n;i++){
if(s[i] >= '0' && s[i] <= '9'){
Q1.push_back(s[i]);
}
else{
if(s[i] == ')'){
while(Q2.back() != '('){
Q1.push_back(Q2.back());
Q2.pop_back();
}
Q2.pop_back();
}
else{
if(Q2.empty() || value(s[i]) > value(Q2.back()) || s[i] == '('){
Q2.push_back(s[i]);
}
else{
while(!Q2.empty() && value(s[i]) <= value(Q2.back())){
Q1.push_back(Q2.back());
Q2.pop_back();
}
Q2.push_back(s[i]);
}
}
}
}
while(!Q2.empty()){
Q1.push_back(Q2.back());
Q2.pop_back();
}
while(!Q1.empty()){
res[++top] = Q1.front();
Q1.pop_front();
}
n = top;
print(0,0,n);
printf("\n");
for(i = 1;i <= n;i++){
if(res[i] >= '0' && res[i] <= '9') a[++rear] = res[i] - '0';
else{
if(res[i] == '+'){
--rear;
a[rear] += a[rear + 1];
}
if(res[i] == '-'){
--rear;
a[rear] -= a[rear + 1];
}
if(res[i] == '*'){
--rear;
a[rear] *= a[rear + 1];
}
if(res[i] == '/'){
--rear;
a[rear] /= a[rear + 1];
}
if(res[i] == '^'){
--rear;
a[rear] = kuaimi(a[rear],a[rear + 1]);
}
print(rear,i,n);
}
}
return 0;
}
这两道原题都已经解决了,但是如果我们想自己设计一款计算器供自己使用,仅仅能算个0-9肯定是不够的,这样的式子我们算的也不比计算机快多少,算上开机关机的时间我们很可能比计算机都快。因此我们再继续向下深挖,得到以下两个问题:
(3)把中缀表达式转化为后缀表达式然后求解,并输出整个过程。过程中的数都是int范围内的正数(难度4)。
这道题和前面后缀表达式完整版的最大区别在于,由于我们有一个转移的过程,如果用自带的deque很难操作数的合成,因为没等我们合成各位的数都已经存进去了,而整个队列全都是数,我们又不知道取到何时为止,所以我们不得不自己手写栈(这里就不用费神考虑怎么手写deque了,因为我们对队首的唯一操作是输出,这个手写栈可以做得到)。这样一来,两个栈就不够了,因为存数的栈是数组,存运算符的是字符串,没办法转移,因此就开三个栈,一个存数,一个是用来研究转移的临时栈,一个是存运算符的栈,第一个和第三个要能恰好拼成一个后缀表达式,即对于每一位,第一个栈和第三个栈有且只有一个存了值,另一个为0/为空。在这个基础上,我们再结合之前后缀表达式存多位数的方式就可以解决了。
除此之外,由于我们的数组操作需要保证后面有一个字符才能进行,因此会导致最后一个数无法读入,我们把s[n]设一个右括号就可以了(这时候临时栈肯定已经空了,不会有影响)
代码如下:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int Q1[101];
char Q2[101],Q3[101];
int a[101],end,rear1,front1,rear2,front2,k,temp[10];
char res[102];
void print(int p,int b,int c){
int i;
for(i = 1;i <= p;i++) printf("%d ",a[i]);
for(i = b + 1;i <= c;i++){
if(i > b + 1) printf(" ");
if(Q1[i]) printf("%d",Q1[i]);
else printf("%c",Q3[i]);
}
printf("\n");
}
int kuaimi(int p,int b){
int ret = 1;
while(b != 0){
if(b % 2 == 1) ret = ret * p;
p *= p;
b /= 2;
}
return ret;
}
void trans(){
int i;
++rear1;
for(i = 1;i <= k;i++){
Q1[rear1] += kuaimi(10,k - i) * temp[i];
}
Q3[rear1] = ' ';//字符串对应位置留空
k = 0;
}
int value(char c){
if(c == '+' || c == '-') return 1;
if(c == '*' || c == '/') return 2;
if(c == '^') return 3;
return 0;
}
int main(){
int i,n;
char s[102];
scanf("%s",s);
n = strlen(s);
s[n] = ')';
for(i = 0;i <= n;i++){
if(s[i] >= '0' && s[i] <= '9') temp[++k] = s[i] - '0';
else{
if(s[i - 1] >= '0' && s[i - 1] <= '9') trans();//注1
if(s[i] == ')' && i < n){
while(Q2[rear2] != '('){
Q3[++rear1] = Q2[rear2];
Q1[rear1] = 0;//数组对应位为空(以0代替)
--rear2;
}
--rear2;
}
else{
if(!rear2 || value(s[i]) > value(Q2[rear2]) || s[i] == '(') Q2[++rear2] = s[i];
else{
while(rear2 && value(s[i]) <= value(Q2[rear2])){
Q3[++rear1] = Q2[rear2];
Q1[rear1] = 0;
--rear2;
}
Q2[++rear2] = s[i];
}
}
}
}
while(rear2 > 1){
Q3[++rear1] = Q2[rear2];
Q1[rear1] = 0;
--rear2;
}
n = rear1;
print(0,0,rear1);
for(i = 1;i <= rear1;i++){
if(Q1[i]) a[++end] = Q1[i];
else{
if(Q3[i] == '+'){
--end;
a[end] += a[end + 1];
}
if(Q3[i] == '-'){
--end;
a[end] -= a[end + 1];
}
if(Q3[i] == '*'){
--end;
a[end] *= a[end + 1];
}
if(Q3[i] == '/'){
--end;
a[end] /= a[end + 1];
}
if(Q3[i] == '^'){
--end;
a[end] = kuaimi(a[end],a[end + 1]);
}
print(end,i,n);
}
}
return 0;
}
(注1:只有上一位是数字且这一位不是才能判断一个数字已经输出完了,因为存在括号的话可能好几位都是运算符)
(4)把中缀表达式转化为后缀表达式然后求解,并输出整个过程。过程中的数都是int范围内的非零整数(难度5)。
这道题我们终于开始考虑负数了。
首先我们明确,我们只考虑在一个式子首位的负号,因为别的我们都能用减号代替。对于负号,我们要像处理多位数那样把这个负号存给数组,然后跳过这个负号继续研究。
但是我们要考虑另一个问题,就是负数有时候还要自带一组括号,例如(-3)* (-4),那么这个括号是否要忽略呢?答案是否定的,因为我们要考虑到还有形如(-3-1)*(-4)这样括号不能忽略的。对于第一种,等到处理到右括号的时候,我们在临时栈内什么也找不到,所以就直接跳过了右括号和左括号。因此,只需要忽略负号而不需要忽略括号。
根据核心结论3,我们必须超前判断。设judge用于判断是否要跳过操作、把这个数变为负数(因为一个负号只能把一个数变为负数,变化之后judge自然归0),如果judge为2,说明需要跳过这次操作;judge为1,说明需要在下一次操作数的时候变为负数。
同时要考虑到,在judge赋值时,如果这一位是第一位,立刻就要跳过,故赋值为1并continue;如果我们是在左括号就判断出下一位是负号,这一位不必跳过,下一位再跳过,故赋值为2。
这样一来,我们就得出了一套完整的负数判断和赋值方法:
#include<cstdio>
#include<cstring>
using namespace std;
int Q1[101];
char Q2[101],Q3[101];
int end,rear1,front1,rear2,front2,k,temp[10],judge,judge1;
long long a[101];
char res[102];
void trans(){
int i;
++rear1;
for(i = 1;i <= k;i++) Q1[rear1] += kuaimi(10,k - i) * temp[i];
Q3[rear1] = ' ';
k = 0;
if(judge) Q1[rear1] = -Q1[rear1];
judge = 0;
}
//value、print、kuaimi函数省略
int main(){
int i,n;
char s[202];
scanf("%s",s);
n = strlen(s);
s[n] = ')';
for(i = 0;i <= n;i++){
if(judge > 1){
judge--;
continue;
}
if(s[i] >= '0' && s[i] <= '9') temp[++k] = s[i] - '0';
else{
if(s[i] == '(' && s[i + 1] == '-') judge = 2;
if(s[i] == '-' && i == 0){//如果直接写s[0],会导致一直跳过
judge = 1;
continue;
}
//以下求解后缀表达式过程省略
}
当然,对于此题,我们也可以尝试把结果改为小数,只要把最后的数组设为浮点型,每一次操作都*1.0即可,这样保证了除法运算有更高的精度。对于一开始的式子中就含有浮点型变量的问题,由于太过于复杂,故不在此进行研究。
最后附上(5)的完整代码(有兴趣的读者可以拿下来自己当计算器用,看看是否有问题,如有问题,请及时指正):
#include<cstdio>
#include<cstring>
using namespace std;
int Q1[101];
char Q2[101],Q3[101];
int end,rear1,front1,rear2,front2,k,temp[10],judge;
long long a[101];
char res[102];
void print(int p,int b,int c){
int i;
for(i = 1;i <= p;i++) printf("%lld ",a[i]);
for(i = b + 1;i <= c;i++){
if(i > b + 1) printf(" ");
if(Q1[i]) printf("%d",Q1[i]);
else printf("%c",Q3[i]);
}
printf("\n");
}
int kuaimi(int p,int b){
int ret = 1;
while(b != 0){
if(b % 2 == 1) ret *= p;
p *= p;
b /= 2;
}
return ret;
}
void trans(){
int i;
++rear1;
for(i = 1;i <= k;i++) Q1[rear1] += kuaimi(10,k - i) * temp[i];
Q3[rear1] = ' ';
k = 0;
if(judge) Q1[rear1] = -Q1[rear1];
judge = 0;
}
int value(char c){
if(c == '+' || c == '-') return 1;
if(c == '*' || c == '/') return 2;
if(c == '^') return 3;
return 0;
}
int main(){
int i,n;
char s[202];
scanf("%s",s);
n = strlen(s);
s[n] = ')';
for(i = 0;i <= n;i++){
if(judge > 1){
judge--;
continue;
}
if(s[i] >= '0' && s[i] <= '9') temp[++k] = s[i] - '0';
else{
if(s[i] == '(' && s[i + 1] == '-') judge = 2;
if(s[i] == '-' && i == 0){
judge = 1;
continue;
}
if(s[i - 1] >= '0' && s[i - 1] <= '9') trans();
if(s[i] == ')' && i < n){
while(Q2[rear2] != '('){
Q3[++rear1] = Q2[rear2];
Q1[rear1] = 0;
--rear2;
}
--rear2;
}
else{
if(!rear2 || value(s[i]) > value(Q2[rear2]) || s[i] == '(') Q2[++rear2] = s[i];
else{
while(rear2 && value(s[i]) <= value(Q2[rear2])){
Q3[++rear1] = Q2[rear2];
Q1[rear1] = 0;
--rear2;
}
Q2[++rear2] = s[i];
}
}
}
}
while(rear2 > 1){
Q3[++rear1] = Q2[rear2];
Q1[rear1] = 0;
--rear2;
}
n = rear1;
print(0,0,rear1);
for(i = 1;i <= rear1;i++){
if(Q1[i]) a[++end] = Q1[i];
else{
if(Q3[i] == '+'){
--end;
a[end] += a[end + 1];
}
if(Q3[i] == '-'){
--end;
a[end] -= a[end + 1];
}
if(Q3[i] == '*'){
--end;
a[end] *= a[end + 1];
}
if(Q3[i] == '/'){
--end;
a[end] /= a[end + 1];
}
if(Q3[i] == '^'){
--end;
a[end] = kuaimi(a[end],a[end + 1]);
}
print(end,i,n);
}
}
return 0;
}
(注:对于我自己改编的(4)(5)两问,已经交到原题上检查,保证仍然可以AC)
总而言之,计算器问题是一个混合的栈问题,既要考虑整型和字符的区分,又要考虑如何转换、如何合并求值,还有很缜密的分析、分类讨论思维,非常有思考价值。
Thank you for reading!