日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

【洛谷日?qǐng)?bào)#176】淺談表達(dá)式的求值(后綴表達(dá)式)

 長(zhǎng)沙7喜 2019-06-15

0:序言

我們?cè)谧鲆恍╊}目的時(shí)候,需要求一些惡心的表達(dá)式的值。那么,我們需要用一些快一些的方法求值。

我們能最先想到的就是暴力求值,也就是:

一步步將可運(yùn)算的地方運(yùn)算好,最后剩下的就是表達(dá)式的值了。

舉個(gè)栗子:

 (6+2*3)/4-5
=(6+6)/4-5
=(12)/4-5
=3-5
=-2

但是,這種方法很容易被卡掉。例如,1+(2+(3+(4+(5+6))))這個(gè)式子中,每一次可以執(zhí)行的符號(hào)只有最里面括號(hào)的值(因?yàn)槠渌\(yùn)算符都因?yàn)橛疫叺倪\(yùn)算沒有結(jié)果而不能被運(yùn)算)

這個(gè)時(shí)候時(shí)間復(fù)雜度降到了,非常慢。

這個(gè)時(shí)候,我們就要想一些更快的方法。

1:表達(dá)式的樹

實(shí)際上,我們可以將整個(gè)表達(dá)式看成一個(gè)二叉樹,每個(gè)非葉子節(jié)點(diǎn)上表示的是一個(gè)運(yùn)算符,左右為這個(gè)運(yùn)算符在原來的表達(dá)式中左右的值。葉子節(jié)點(diǎn)表示的是一個(gè)值。

在計(jì)算時(shí),我們可以用DFS的方法,在一個(gè)節(jié)點(diǎn)處先搜索左右兒子代表的值,之后計(jì)算。

偽代碼如下:

f() 參數(shù):一個(gè)整數(shù)。返回值:一個(gè)整數(shù)。
f(now) 
    if(now是葉子節(jié)點(diǎn))    return 這個(gè)葉子節(jié)點(diǎn)代表的值
    return f(左兒子)[now所代表的運(yùn)算符]f(右兒子)

我們還可以這么看:

很多個(gè)數(shù)排在一起。每一次,兩個(gè)相鄰的數(shù)通過某種方式(就是根代表的運(yùn)算符)合并成一個(gè)數(shù),最后只剩下一個(gè)數(shù),這就是表達(dá)式的值。

舉個(gè)例子:

(6+2*3)/4-5

合并過程長(zhǎng)這樣:

6 2 3 4 5
6 6 4 5
12 4 5
3 5
-2

過程如下:


我們通過以下方式處理字符串(又是偽代碼):

tr() 參數(shù):字符串S 返回:一棵樹
tr(S)
    if(S只包含一個(gè)數(shù)字)
        return 以這個(gè)數(shù)字為根的樹(只有一個(gè)節(jié)點(diǎn))
    找到最后運(yùn)行的運(yùn)算符X
    將X設(shè)為這個(gè)樹的根
    將左兒子設(shè)為tr(S以X為分界線分開的左邊部分)
    右兒子設(shè)為tr(S以X為分界線分開的右邊部分)
    return 這個(gè)樹

最后運(yùn)行的運(yùn)算符很好找,只要找這個(gè)表達(dá)式最外層的運(yùn)算符中優(yōu)先級(jí)最小的就好(不會(huì)優(yōu)先級(jí)的出門左轉(zhuǎn))

有多個(gè)只用取其中一個(gè),這只會(huì)影響計(jì)算的先后,不影響結(jié)果。

很棒。所以我們找到了另一個(gè)求表達(dá)式值的方法——

轉(zhuǎn)換為樹的時(shí)候,通過回溯計(jì)算值。

但是,很可惜。這個(gè)方法中,我們每一次構(gòu)造的時(shí)候,要掃一次字符串并取出一個(gè)計(jì)算符。還是能用1+(2+(3+(4+(5+6))))這個(gè)例子卡成

那怎么辦?

2:表達(dá)式的變形

我們想到,一個(gè)數(shù)有它的三種遍歷方式:[前|中|后]序遍歷

我們把剛才這個(gè)樹遍歷:

前:- / + 6 * 2 3 4 5
中:6 + 2 * 3 / 4 - 5
后:6 2 3 * + 4 / 5 -

中序遍歷就是原式, 但是 我們通過運(yùn)算優(yōu)先級(jí)建樹,這時(shí)候受到括號(hào)的影響,計(jì)算的優(yōu)先級(jí)會(huì)改變(括號(hào)里面的優(yōu)先)。

判斷的方式很簡(jiǎn)單。

就比如除號(hào),它在樹中左邊是加號(hào),運(yùn)算符優(yōu)先級(jí)比它小,但是竟然先被計(jì)算,所以,加號(hào)所在子樹左右應(yīng)該加上括號(hào)。

我們盯著[前|后]序遍歷看。

前序的時(shí)候,假設(shè)有一個(gè)排列如下:

計(jì)算符 數(shù)字1 數(shù)字2

那么這三個(gè)數(shù)可以被數(shù)字1[計(jì)算符]數(shù)字2代替(就是一次計(jì)算)

后序的時(shí)候,假設(shè)有一個(gè)排列如下:

數(shù)字1 數(shù)字2 計(jì)算符

那么這三個(gè)數(shù)可以被數(shù)字1[計(jì)算符]數(shù)字2代替(就是一次計(jì)算)

這個(gè)性質(zhì)由前后序遍歷中根不在左右子樹中間而來。

由于后序遍歷的結(jié)果可以用forin range計(jì)算(利用棧即可),我們用后序遍歷的結(jié)果計(jì)算。

P.S. :表達(dá)式的[前|中|后]序遍歷有對(duì)應(yīng)的名字:前綴表達(dá)式(波蘭表達(dá)式),中綴表達(dá)式,后綴表達(dá)式(逆波蘭表達(dá)式)

3:求后綴表達(dá)式的簡(jiǎn)便方法

我們旨在用的時(shí)間求出表達(dá)式的值,所以我們只能遍歷表達(dá)式常數(shù)次。

我們抓住1*2+3這個(gè)栗子看,后綴表達(dá)式為1 2 * 3 +

我們?cè)僮?code>1+2*3這個(gè)栗子看,后綴表達(dá)式為1 2 3 * +

我們從左往右遍歷這個(gè)式子,我們發(fā)現(xiàn),這兩個(gè)式子中,

在遍歷到第二個(gè)運(yùn)算符的時(shí)候,兩者的操作不一樣——一個(gè)將*加入后綴表達(dá)式,一個(gè)不是。

這僅僅是*+的優(yōu)先級(jí)有差異。

所以,我們實(shí)際上就是要維護(hù)一個(gè)運(yùn)算優(yōu)先級(jí)非降的運(yùn)算符序列,在添加運(yùn)算符的時(shí)候,我們僅僅需要在這個(gè)序列中去掉后面的元素,讓這個(gè)序列添加這個(gè)運(yùn)算符的時(shí)候依然有序。

當(dāng)你維護(hù)一個(gè)單調(diào)的序列的時(shí)候,你能想到什么?

單調(diào)隊(duì)列!

我們可以想到,當(dāng)掃到一個(gè)數(shù)字的時(shí)候,直接加到后綴表達(dá)式里面,掃到一個(gè)運(yùn)算符的時(shí)候,就把它丟到一個(gè)單調(diào)隊(duì)列里面,并且這個(gè)單調(diào)隊(duì)列維護(hù)的是運(yùn)算優(yōu)先級(jí)非降的一個(gè)字符列表。

也就是說:

* s[N],ret[N];
stack<char> pri;
for i from 1 to N
    if(s[i]是一個(gè)數(shù))    直接加到ret中
    else
        while(pri頂部字符的優(yōu)先級(jí)大于s[i]的優(yōu)先級(jí))
            把pri頂端的字符加到ret里面,之后從pri里面彈出
        把s[i]加到pri里面
while(pri里面還有字符)
    把pri頂端的字符加到ret里面,之后從pri里面彈出
ret -> 后綴表達(dá)式

好了,我們已經(jīng)處理完了不含括號(hào)的時(shí)候后綴表達(dá)式的計(jì)算。

那么,當(dāng)表達(dá)式有了括號(hào)的時(shí)候,怎么辦呢?

我們想到,括號(hào)里面的計(jì)算符的計(jì)算優(yōu)先級(jí)比外面的高,所以我們可以這樣處理:

碰到(時(shí),直接加入到隊(duì)列(不進(jìn)行任何彈出操作),并設(shè)置(的優(yōu)先級(jí)為負(fù)無窮(這樣能保證(不被彈出)
碰到)時(shí),從pri瘋狂彈出字符,直到碰到(,把(彈出

為什么要瘋狂彈出呢?

很簡(jiǎn)單,我們要計(jì)算完括號(hào)里面的計(jì)算才能往下走,所以我們需要把括號(hào)里面的計(jì)算符先彈出,在后綴表達(dá)式的計(jì)算中相當(dāng)于計(jì)算完括號(hào)里面的值。

所以,真正的后綴表達(dá)式的尋找方法應(yīng)該是這樣

* s[N],ret[N];
stack<char> pri;
for i from 1 to N
    if(s[i]是一個(gè)數(shù))    直接加到ret中
    else if(s[i]是'(')    直接加到pri中
    else if(s[i]是')')
        while(pri頂部字符不是'(')
            把pri頂端的字符加到ret里面,之后從pri里面彈出
        從pri里面彈出'('
    else
        while(pri頂部字符的優(yōu)先級(jí)大于s[i]的優(yōu)先級(jí))
            把pri頂端的字符加到ret里面,之后從pri里面彈出
        把s[i]加到pri里面
while(pri里面還有字符)
    把pri頂端的字符加到ret里面,之后從pri里面彈出
ret -> 后綴表達(dá)式

模擬(6+2*3)/4-5的計(jì)算

掃到(:直接彈入pri。
---
ret : 
pri : (
---
掃到6:直接加入ret。
---
ret : 6
pri : (
---
掃到+:加入到pri,因?yàn)?的優(yōu)先級(jí)更小,所以沒有彈出。
---
ret : 6
pri : ( +
---
掃到2:直接加入ret。
---
ret : 6 2
pri : ( +
---
掃到*:加入到pri,因?yàn)?的優(yōu)先級(jí)更小,所以沒有彈出。
---
ret : 6 2
pri : ( + *
---
掃到3:直接加入到ret。
---
ret : 6 2 3
pri : ( + *
---
掃到):將pri中的字符瘋狂彈出,直到碰到(,將(彈出。
---
ret : 6 2 3 * +
pri : 
---
掃到/:直接加入到pri(pri是空的)。
---
ret : 6 2 3 * +
pri : /
---
掃到4:直接加到ret。
---
ret : 6 2 3 * + 4
pri : /
---
掃到-:加入到pri,因?yàn)?的優(yōu)先級(jí)更大,將/彈出并加入到ret。
---
ret : 6 2 3 * + 4 /
pri : -
---
掃到5:直接加入到ret。
---
ret : 6 2 3 * + 4 / 5
pri : -
---
清空pri
ret : 6 2 3 * + 4 / 5 -

因?yàn)橛?jì)算的過程比較簡(jiǎn)單,所以我相信模擬可以讓你們明白。

模擬計(jì)算過程:

掃到6,加入棧
+------------
| 6|  |  |  |
+------------
掃到2,加入棧
+------------
|
 6| 2|  |  |
+------------
掃到3,加入棧
+------------
| 6| 2| 3|  |
+------------
掃到*,計(jì)算2*3,返回6,把6加入棧中
+------------
|
 6| 6|  |  |
+------------
掃到+,計(jì)算6+6,返回12,把12加入棧中
+------------
|12|  |  |  |
+------------
掃到4,加入棧
+------------
|
12| 4|  |  |
+------------
掃到/,計(jì)算12/4,返回3,把3加入棧中
+------------
| 3|  |  |  |
+------------
掃到5,加入棧
+------------
|
 3| 5|  |  |
+------------
掃到-,計(jì)算3-5,返回-2,把-2加入棧中
+------------
|-2|  |  |  |
+------------
結(jié)束,返回-2

所以,表達(dá)式的計(jì)算成功降到了

4:例題

P1175 表達(dá)式的轉(zhuǎn)換

注意 : 這道題中,pri維護(hù)的是升序(不能等于),每次運(yùn)算需要找到第一個(gè)字符并計(jì)算。

#include<cstdio>
#include<cstring>
#include<stack>
#include<algorithm>
#include<cmath>
using namespace std;
//8 - 18行均為運(yùn)算符的優(yōu)先級(jí)比較 
int ope(char q){
    if(q=='(')  return -1;
    if(q=='+')  return 0;
    if(q=='-')  return 0;
    if(q=='*')  return 1;
    if(q=='/')  return 1;
    if(q=='^')    return 2;
    return -2/*default*/;
}
bool cmp(char a,char b){
    return ope(a)>=ope(b);
}
struct Node{
    bool is_num;  //是否為運(yùn)算符 
    int nm;       //數(shù)字 
    char op;      //運(yùn)算符 
    Node(bool is_num=false,int nm=0,char op='\0'):is_num(is_num),nm(nm),op(op){}
}ret[105];        //后綴表達(dá)式 
stack<char> pri;
int N;            //后綴表達(dá)式長(zhǎng)度 
char A[105];
void print(){
    for(int i=0;i<N;i++){
        if(ret[i].is_num)    printf('%d ',ret[i].nm);
        else    printf('%c ',ret[i].op);
    }
    printf('\n');
}
void solve(){
    for(int i=0;A[i];i++){
        if(A[i]>='0' && A[i]<='9')    ret[N++]=Node(true,A[i]-'0','\0');
        else if(A[i]=='(')    pri.push(A[i]);
        else if(A[i]==')'){
            while(pri.top()!='('){
                //如果保證表達(dá)式?jīng)]有毛病,那么一個(gè))一定對(duì)應(yīng)一個(gè)( ,此時(shí)不用加!pri.empty() 
                ret[N++]=Node(false,0,pri.top());
                pri.pop();
            }
            pri.pop();
        }
        else{
            while(!pri.empty() && cmp(pri.top(),A[i])){
                //這里要加!pri.empty(),因?yàn)橛袝r(shí)候在瘋狂彈出的時(shí)候到頭了(栗子中的/和-) 
                ret[N++]=Node(false,0,pri.top());
                pri.pop();
            }
            pri.push(A[i]);
        }
    }
    while(!pri.empty()){
        ret[N++]=Node(false,0,pri.top());
        pri.pop();
    }
    print();
    while(N!=1){
        //找到第一個(gè)計(jì)算符 
        int l=0;
        while(ret[l].is_num)    ++l;
        //暴力計(jì)算 
        switch(ret[l].op){
            case '+':
                ret[l-2]=Node(true,ret[l-2].nm+ret[l-1].nm,'\0');
                break;
            case '-':
                ret[l-2]=Node(true,ret[l-2].nm-ret[l-1].nm,'\0');
                break;
            case '*':
                ret[l-2]=Node(true,ret[l-2].nm*ret[l-1].nm,'\0');
                break;
            case '/':
                ret[l-2]=Node(true,ret[l-2].nm/ret[l-1].nm,'\0');
                break;
            case '^':
                ret[l-2]=Node(true,pow(ret[l-2].nm,ret[l-1].nm),'\0');
                break;
            default:
                break;
        }
        //往左挪兩格 
        for(int i=l-1;i<N;i++)    ret[i]=ret[i+2];
        print();
        N-=2;
    } 
}
int main(){
    scanf('%s',A);
    solve();
}

提交記錄

5:In the end

表達(dá)式的求值在一些大模擬題目中很常見(比如說未來程序·改中的語句)。當(dāng)然,在平常編寫科學(xué)計(jì)算器的時(shí)候也是一個(gè)重要的知識(shí)點(diǎn)。

所以,后綴表達(dá)式在表達(dá)式求值的題中節(jié)省了時(shí)間()。

完結(jié)撒花!★,°:.☆( ̄▽ ̄)/$:.°★ 。

放心吧,我不會(huì)推薦未來程序·改的


洛谷日?qǐng)?bào)接受投稿,采用后有薄禮奉送,請(qǐng)戳 https://www./discuss/show/47327 .


    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多