基础算法——表达式求值 学习笔记
基础算法——表达式求值 学习笔记
优先级和结合性
优先级:
-
优先级高的优先计算,优先级低的不优先计算,括号优先级最高;
-
本文只考虑【乘方,乘除,加减】三级二元运算,这三级优先级递减。
结合性:
-
左结合性:例如 \(a+b+c:=(a+b)+c\).
-
右结合性:例如 \(\mathrm{a^\wedge b^\wedge c}:=a^{\left(b^c\right)}\neq \left(a^b\right)^c\).
注意乘方是最常见的右结合性的运算符,处理这个运算特判即可。
基本表达式类型
-
前缀表达式(波兰表达式):简记为,数字在前,符号在后。
-
后缀表达式(逆波兰表达式):简记为,数字在后,符号在前。
-
中缀表达式:人类的表达式,符号在数字中间。
下文将直接通过表达式树讲解。
表达式树
我们对于一个表达式 \(S\),
-
在这个表达式中,最后运算的符号作为根,
-
运算符两边的各自为一棵子树,表达式树的子树也是一棵表达式树。
形如,
我们可以观察到一些性质,
-
非叶子节点一定是符号,叶子节点一定是数字;
-
每次合并两个数字,替换其父亲,最终根处就是表达式的值。
我们有(前中后指的是父亲的位置),
-
表达式树的前序遍历(父亲,左儿子,右儿子),就是前缀表达式。
-
表达式树的中序遍历(左儿子,父亲,右儿子),就是中缀表达式。
-
表达式树的后序遍历(左儿子,右儿子,父亲),就是后缀表达式。
-
同时,因为只有叶子节点是数值,因此前中后缀表达式不会改变数字的相对位置。
我们现在假设我们已经有表达式树了,考虑计算,
但是我们通常得到的是一个中缀表达式,
如果按照表达式树的标准形式直接暴力运算,
我们一般需要 \(\mathcal O(n)\) 的找到最后运算的运算符的,
因此,这样暴力做,时间复杂度是可以被卡到 \(\mathcal O(n^2)\) 的。
例如:\(1+1+1+1+1+1+1+1+1+1+1\)。
我们先不考虑优化,先来几个简单的。
后缀表达式直接求值
例如,表达式,
我们记一个栈,数字栈 \(S\),
-
从前往后考虑每一个字符,
-
如果是数字:直接加入数字栈 \(S\)。
-
如果是符号:取出栈顶两元素,进行运算后压入栈。
注意符号的操作顺序,
-
根据表达式树,我们的运算 \(a\otimes b\) 的 \(a\) 是先入栈、后出栈的,
-
因此顺序是先弹出的做 \(b\),后弹出的做 \(a\)。
-
一般 \(+,\times\) 没有顺序要求,但是 \(-,\div\) 是有的。
代码(P1449 后缀表达式):
inline int awa(int a, int b, char c) {
if (c == '+') return a + b;
if (c == '-') return a - b;
if (c == '*') return a * b;
if (c == '/') return a / b;
__builtin_unreachable();
}
signed main() {
stack<int> stk;
char str[60] = {0};
while (1) {
int status = scanf("%[1234567890].", str);
if (status == 0) {
char c = getchar();
if (c == '@') break;
int a = stk.top(); stk.pop();
int b = stk.top(); stk.pop();
stk.push(awa(b, a, c));
}
else stk.push(stod(str));
}
cout << stk.top() << endl;
return 0;
}
前缀表达式直接求值
和后缀表达式类似,
-
从后往前考虑每一个字符,
-
如果是数字:直接加入数字栈 \(S\)。
-
如果是符号:取出栈顶两元素,进行运算后压入栈。
注意符号的操作顺序,
-
根据表达式树,我们的运算 \(a\otimes b\) 的 \(a\) 是后入栈、先出栈的,
-
因此顺序是先弹出的做 \(a\),后弹出的做 \(b\)。
-
一般 \(+,\times\) 没有顺序要求,但是 \(-,\div\) 是有的。
中缀表达式的后缀求值
我们不考虑仔细的证明,考虑观察结论,
-
对于
1 * 2 + 3
后缀表达式为1 2 * 3 +
; -
对于
1 + 2 * 3
后缀表达式为1 2 3 * +
。
考虑从左到右遍历原表达式,
-
在第一个例子中,
+
使得*
运算弹出; -
在第二个例子中,
*
并没有使+
弹出。
我们大胆猜测结论,
-
加入一个运算符时,
-
其前面比他优先级高的运算符弹出,并同步计算;
-
将这个运算符压入栈。
于是,我们要维护一个优先级非严格单调递增的操作符序列。
每次加入时,按照上面的操作弹栈,使得序列依旧有序,那么这就是个单调栈。
注意到乘方的右结合性,同时优先级最高,
我们只需要特判,没有任何运算符(括号除外)可以弹出乘方即可。
代码(AcWing 3302 表达式求值):
int pri[256];
stack<int> num;
stack<char> op;
void init() {
pri['+'] = pri['-'] = 1;
pri['*'] = pri['/'] = 2;
pri['^'] = 3;
}
int powi(int a, int b) {
int r = 1;
for (; b; b >>= 1) {
if (b & 1) r = r * a;
a = a * a;
}
return r;
}
inline int awa(int a, int b, char c) {
if (c == '+') return a + b;
if (c == '-') return a - b;
if (c == '*') return a * b;
if (c == '/') return a / b;
if (c == '^') return powi(a, b);
__builtin_unreachable();
}
void eval() {
int b = num.top(); num.pop();
int a = num.top(); num.pop();
char c = op.top(); op.pop();
num.push(awa(a, b, c));
}
int calc(string str) {
int n = str.size();
for (int i = 0; i < n; ++i) {
if (isdigit(str[i])) {
int r = 0, j = i;
while (j < n && isdigit(str[j])) {
r = r * 10 + str[j] - '0';
++j;
}
i = j - 1, num.push(r);
}
else if (str[i] == '(') op.push('(');
else if (str[i] == ')') {
while (op.top() != '(') eval();
op.pop();
}
else if (str[i] == '^') op.push('^');
else {
while (!op.empty() && pri[op.top()] >= pri[str[i]])
eval();
op.push(str[i]);
}
}
while (!op.empty()) eval();
return num.top();
}
中缀表达式转后缀表达式
首先我们注意到上面的代码本质就是后缀表达式的形式。
我们把前面 eval
函数魔改一下就可以了(改成加入到后缀表达式中)。
另外前面的结论,后缀表达式数字顺序不变,我们在弹出的时候加入符号即可。
代码:
int pri[256];
struct node {
int type;
union {
int num;
char op;
} val;
node() = default;
node(int num): type(0) { val.num = num; }
node(char op): type(1) { val.op = op; }
friend ostream& operator <<(ostream &out, const node a) {
if (a.type) out << a.val.op;
else out << a.val.num;
return out;
}
};
stack<char> op;
vector<node> res;
void init() {
pri['+'] = pri['-'] = 1;
pri['*'] = pri['/'] = 2;
pri['^'] = 3;
}
void eval() {
res.emplace_back((char)op.top());
op.pop();
}
void calc(string str) {
int n = str.size();
for (int i = 0; i < n; ++i) {
if (isdigit(str[i])) {
int r = 0, j = i;
while (j < n && isdigit(str[j])) {
r = r * 10 + str[j] - '0';
++j;
}
i = j - 1, res.emplace_back(r);
}
else if (str[i] == '(') op.emplace('(');
else if (str[i] == ')') {
while (op.top() != '(') eval();
op.pop();
}
else if (str[i] == '^') op.emplace('^');
else {
while (!op.empty() && pri[op.top()] >= pri[str[i]])
eval();
op.emplace(str[i]);
}
}
while (!op.empty()) eval();
}
附:我在 P1175 表达式的转换 的抽象输出代码。
int pre[10100], del[10100];
void print() {
for (int i = 0; i < (int)res.size(); ++i) {
if (del[i]) continue;
cout << res[i] << " ";
}
cout << endl;
}
void welcome() {
for (int i = 0; i < (int)res.size(); ++i) {
pre[i + 1] = i;
if (!res[i].type) continue;
print();
res[pre[pre[i]]].val.num = awa(
res[pre[pre[i]]].val.num,
res[pre[i]].val.num,
res[i].val.op
);
del[pre[i]] = del[i] = 1;
pre[i + 1] = pre[pre[i]];
}
print();
}
前缀表达式与运算函数
首先还是前面的结论:前缀表达式不改变数字相对顺序。
我们可以发现一个有意思的结论,表达式转为前缀后,例如,
我们就可以将一个只由函数和数字组合的表达式,
-
通过拆括号和去逗号的方式转为前缀表达式,
-
在循环运算的时候甚至可以直接跳过这些省略的字符。
因此,我们就找到了一种全新的表达式计算方法,
- 递归处理前缀表达式,是数字的时候直接跳过。
全文完。
参考 @tiger2005 的,
本文来自博客园,作者:RainPPR,转载请注明原文链接:https://www.cnblogs.com/RainPPR/p/18327181
如有侵权请联系我(或 2125773894@qq.com)删除。