[小专题]栈与表达式求值
划水&摸鱼了好多天…解决了高中一直没搞清楚的表达式求值的内容…
写篇博客记录一下
1.栈(Stack)
直观理解 如图所示,类似于厨房用来放调羹的那个槽…先放进去的会被压在底下,一般叫做栈底(bottom)元素,最后进去的则作为栈顶(top)元素,我们把一个调羹放到槽里面就叫做把这个元素压入(push)栈里,把顶部的调羹拿出来就叫做把一个元素从栈中弹出(pop),栈相关的操作基本就是这些,C++的STL也自带了stack类型,声明形式为 stack<type>s; ,type是要插入栈的元素类型。使用时只要include头文件stack就可以调用啦
当然我们自己也容易用数组实现一个确定空间上界的栈:
struct Stack { ll s[N],cnt; Stack() { cnt=0; memset(s,0,sizeof(s)); } inline ll top(){return s[cnt];} inline ll pop(){return s[cnt--];} inline void push(ll x){s[++cnt]=x;} inline bool empty(){return (cnt==0);} };
C++自带的栈当然方便啦,不过在算法竞赛中可能为了常数小一些经常会选择手写栈
2.概念补充:表达式、运算符、表达式树
不出意外的话屏幕前的你是一个人类(?),人类们似乎能比较容易地判断类似于$2+3*5^2-3*(5+2)$这样的表达式(notation)要先计算谁后计算谁,但是现在我们想让计算机帮我们做运算,自然要设计一套算法让计算机能够知道先计算谁后计算谁。也就是我们希望能建立一套成系统的方法,把一个记录表达式的字符串拆成若干个元素,运算对象(operand)归操作对象,运算符(operator)归运算符,最好再把运算符的优先级处理好。
首先明确一下优先级:遇到括号应该先算括号内的,括号内的优先级最高,然后是幂运算>乘除>加减。
需要注意的是通常情况下优先级相同的从左到右左边的先算(比如6*3/2一般会认为先算乘法后算除法)
但是常用的运算符有个比较特殊的:幂运算,比如2^3^4我们通常会理解为是2^(3^4)而不是(2^3)^4。
于是对于+-*/这类运算符我们就说他是左结合的(Left-Associativity),而像幂运算^这类运算符我们就说它是右结合的(Right-Associativity),这些概念会在下面用到。
表达式树同样不陌生,对于每个表达式我们选择一个优先级最低的运算符作为树根,运算符的左右两边作为左右子树,左右子树再进行同样的操作,例如上面的表达式可以构建如下的表达式树:
对这颗二叉树进行前种后序遍历可以分别得到原表达式的前缀(prefix)、中缀(infix)、后缀(postfix)表达式
而后缀表达式则又被成为逆波兰表达式(reverse Polish notation),前缀表达式也被成为波兰表达式。
3.调度场算法(shunting-yard algorithm)
例子中的2+3*5^2-这部分,先读到运算对象2和运算符+,接着是3,这里我们不能急着做加法,原因是不知道后面有什么,所以这里正确的操作应该是先把“这里有个加法运算”这件事存起来,同时最好运算对象也被按顺序存好。接着往下我们发现一个乘法,同样不知道后面会不会有优先级更高的运算,同样存起来。好的我们读到了“^”,它比乘法优先级更高,继续往下读读到一个减法,优先级比当前的幂运算低,这意味着前面的部分可以开始算了。
现在开始倒着进行运算:因为我们都是保留优先级低的操作,所以到此为止的运算符优先级都是单调递增的,也就是要对靠后的运算符先进行操作,同样对于运算对象也是,基于这点我们联想到用栈来做这件事。
栈S1储存数字,栈S2储存运算符,到这里S1的元素分别是:2,3,5,2,而S2的元素分别是:+,*,^,取出S2的一个幂运算和S1的2和5,注意顺序是倒着来的,所以应该是以5为底,求得5^2,
再把计算结果25压入S1,接下来的操作类似,S1有2,3,25,S2有+,*,取出乘法和25和3,计算结果3*25=75压入S1,继续取出S2的+和S1的75和2,计算结果2+75压入S1,直到S2变空,这时候S1剩下的一个元素就是运算结果
得到一个简单的伪代码:
while there are tokens to be read: read a token. if(the token is an operator) while((S2 not empty)and(precedence of the top of S2>=precedence of this operator)) calculate push the operator onto S2 else //the token is an operand push it onto S1
while(S2 not empty)calculate
print the top of S1
括号怎么处理?
括号优先级最高,遇到左括号先存起来,进行calculate则需要额外加一个S2的top不是左括号
如果读到左括号直接压到运算符栈S2,如果读到右括号则一直进行计算直到读到左括号,最后再把左括号扔掉。
(因为括号内能先算的其实都已经算了,所以只会剩下优先级单调不减的运算符,直接算就行)
左结合和右结合?
如果S2栈顶元素和当前运算符优先级相同且都是右结合的(比如^),那也不能进行运算,也就是把
precedence of the top of S2>=precedence of this operator
稍微修改一下变成
(precedence of the top of S2>precedence of this operator)or((precedence of the top of S2==precedence of this operator)and(this operator is not Right associativity))
到此为止代码就差不多成形了:
//n is the length of string str //pre[x] records the precedence of operator x //calc() is the function calculating for(register int i=1;i<=n;i++) { if(oper(str[i])) { while(!S2.empty()&&oper(S.top())&&((pre[S2.top()]>pre[str[i]])||(pre[S2.top()]==pre[str[i]]&&str[i]!='^'))&&S.top()!='(') calc(); S2.push(str[i]); }else if(str[i]=='(') { S2.push(str[i]); }else if(str[i]==')') { while(S2.top()!='(')calc(); if(S2.top()=='(')S2.pop(); }else if(num(str[i])) { ll sum=0,j=0; for(;num(str[i+j]);j++)sum=sum*10+str[i+j]-'0'; i+=j-1; S1.push(sum); } } while(!S.empty())calc();
上面这个算法就是标题的调度场算法shunting-yard algorithm啦,是Dijkstra大佬提出的~算法的起名大概是因为这个原理很像下图的结构:
↑图转自Wikipedia
4.和逆波兰表达式的关系
不难发现如果把计算改成输出运算符,把压入数字改成输出数字,就得到了原来表达式的逆波兰表达式啦。
而逆波兰表达式的计算也很简单,省去了对优先级的考虑——读到运算符就对前两个数字进行运算,就能够给出答案
5.来道题!
Luogu2379:给若干个单项式或者多项式,包含+、-、*、和^,字母只包括小写字母,输出运算结果
话说这题我没A,因为我写了个用set维护多项式字典序的东西…后面发现只要保证每个项按字典序排序,多项式直接按照输入顺序输出就行了…然后我懒得改(
表达式计算的框架和上面一样,对于每个项我选择用一个27个元素的数组s[]记录,s[0]记录前面的系数,s[1]到s[26]分别表示a到z的次数,然后再用一个set储存每个项,手写一个比较函数来维护多项式字典序。
加减乘法再手写一下~就得到一个挺长的代码啦…
1 #include<cstdio> 2 #include<set> 3 #include<stack> 4 #include<cstring> 5 using namespace std; 6 #define rep(i,a,b) for(register int i=(a);i<=(b);i++) 7 8 typedef long long ll; 9 10 const int N=300; 11 12 //term 13 struct term 14 { 15 ll s[27]; 16 term(){memset(s,0,sizeof(s));} 17 inline void print()const 18 { 19 if(s[0]!=1)printf("%d",s[0]); 20 rep(i,1,26)if(s[i]) 21 { 22 printf("%c",'a'+i-1); 23 if(s[i]>1)printf("^%d",s[i]); 24 } 25 } 26 }; 27 28 struct cmp 29 { 30 inline bool operator () (const term &A,const term &B) 31 { 32 rep(i,1,26)if(A.s[i]!=B.s[i]){return A.s[i]>B.s[i];} 33 return 0; 34 } 35 }; 36 37 38 inline term operator * (const term &A,const term &B) 39 { 40 term C; 41 C.s[0]=A.s[0]*B.s[0]; 42 rep(i,1,26)C.s[i]=A.s[i]+B.s[i]; 43 return C; 44 } 45 46 inline bool check(term A,term B) 47 { 48 rep(i,1,26)if(A.s[i]!=B.s[i])return 0; 49 return 1; 50 } 51 52 inline term operator +(term A,term B) 53 { 54 term C; 55 C.s[0]=A.s[0]+B.s[0]; 56 if(C.s[0]==0)return C; 57 rep(i,1,26)C.s[i]=A.s[i]; 58 return C; 59 } 60 61 inline term operator -(term A,term B) 62 { 63 term C; 64 C.s[0]=A.s[0]+B.s[0]; 65 if(C.s[0]==0)return C; 66 rep(i,1,26)C.s[i]=A.s[i]; 67 return C; 68 } 69 70 //polynomial 71 72 struct P 73 { 74 set<term,cmp>p; 75 inline void insert(const term &A) 76 { 77 set<term,cmp>::iterator itr=p.begin(); 78 bool b=1; 79 for(;itr!=p.end();itr++) 80 { 81 if(check(*itr,A)) 82 { 83 term temp=*itr; 84 temp=temp+A; 85 p.erase(itr); 86 p.insert(temp); 87 b=0; 88 break; 89 } 90 } 91 if(b)p.insert(A); 92 } 93 }; 94 95 inline P operator +(const P &A,const P &B) 96 { 97 P res=A; 98 99 for(set<term,cmp>::iterator itr=B.p.begin();itr!=B.p.end();itr++)res.insert(*itr); 100 101 return res; 102 } 103 104 inline P operator -(const P &A,const P &B) 105 { 106 P res=A; 107 P t; 108 for(set<term,cmp>::iterator itr=B.p.begin();itr!=B.p.end();itr++) 109 { 110 term temp=*itr; 111 temp.s[0]*=-1; 112 t.insert(temp); 113 } 114 for(set<term,cmp>::iterator itr=t.p.begin();itr!=t.p.end();itr++)res.insert(*itr); 115 116 return res; 117 } 118 119 inline P operator *(const P &A,const P &B) 120 { 121 P res; 122 set<term,cmp>::iterator ita,itb; 123 ita=A.p.begin();itb=B.p.begin(); 124 125 for(;ita!=A.p.end();ita++)for(itb=B.p.begin();itb!=B.p.end();itb++)res.insert((*ita)*(*itb)); 126 return res; 127 } 128 129 struct deal 130 { 131 stack<char>S1;//oper 132 stack<P>S2;//poly 133 char str[N]; 134 int pre[300]; 135 int n; 136 137 inline bool op(char c) 138 { 139 return (c=='+'||c=='-'||c=='*'); 140 } 141 142 inline void calc() 143 { 144 char t=S1.top();S1.pop(); 145 P p1,p2,res; 146 p2=S2.top();S2.pop(); 147 p1=S2.top();S2.pop(); 148 if(t=='+')res=p1+p2; 149 else if(t=='-')res=p1-p2; 150 else res=p1*p2; 151 S2.push(res); 152 } 153 154 inline void work() 155 { 156 scanf("%s",str+1);n=strlen(str+1); 157 pre['+']=pre['-']=1;pre['*']=2; 158 rep(i,1,n) 159 { 160 if(str[i]=='[')str[i]='('; 161 if(str[i]==']')str[i]=')'; 162 } 163 rep(i,1,n) 164 { 165 if(op(str[i])) 166 { 167 while(!S1.empty()&&pre[S1.top()]>=pre[str[i]]&&S1.top()!='(') 168 calc(); 169 S1.push(str[i]); 170 }else if(str[i]=='(') 171 { 172 S1.push(str[i]); 173 }else if(str[i]==')') 174 { 175 while(!S1.empty()&&S1.top()!='(')calc(); 176 if(S1.top()=='(')S1.pop(); 177 }else 178 { 179 int j=0,sum=0; 180 P poly; 181 term res=term(); 182 for(;str[i+j]>='0'&&str[i+j]<='9';j++)sum=sum*10+str[i+j]-'0'; 183 for(;str[i+j]>='a'&&str[i+j]<='z';j++) 184 { 185 if(str[i+j+1]=='^') 186 { 187 res.s[str[i+j]-'a'+1]=str[i+j+2]-'0'; 188 j+=2; 189 } 190 else res.s[str[i+j]-'a'+1]=1; 191 192 } 193 194 if(sum==0)sum=1; 195 res.s[0]=sum; 196 197 poly.insert(res); 198 S2.push(poly); 199 i+=j-1; 200 } 201 } 202 while(!S1.empty())calc(); 203 204 set<term,cmp> res=S2.top().p; 205 set<term,cmp>::iterator itr=res.begin();//*itr->term 206 207 for(int i=0;itr!=res.end();itr++,i++) 208 { 209 if((*itr).s[0]==0)continue; 210 if((*itr).s[0]>0&&i!=0)printf("+"); 211 212 if((*itr).s[0]!=1) 213 { 214 if((*itr).s[0]==-1)printf("-"); 215 else printf("%d",(*itr).s[0]); 216 } 217 218 rep(j,1,26)if((*itr).s[j]) 219 { 220 printf("%c",'a'+j-1); 221 if((*itr).s[j]>1)printf("^%d",(*itr).s[j]); 222 } 223 } 224 } 225 226 }; 227 int main() 228 { 229 deal D; 230 D.work(); 231 return 0; 232 }
以上~