洛谷 P1054 [NOIP 2005 提高组] 等价表达式
目录
题目描述
题目分析
算法思路
数学原理
代码实现
代码详细解析
头文件说明
常量定义
运算符优先级函数
计算函数
表达式求值函数
主函数
算法优化分析
完整优化版本
测试样例分析
常见问题解答
1. 为什么使用数值检验而不是符号运算?
2. 为什么要用模运算?
3. 测试点为什么选择质数?
4. 如何处理负数和减法?
5. 幂运算为什么用循环而不是pow函数?
6. 如果表达式包含除法怎么办?
算法扩展
总结
题目描述
明明进了中学之后,学到了代数表达式。有一天,他碰到一个很麻烦的选择题。这个题目的题干中首先给出了一个代数表达式,然后列出了若干选项,每个选项也是一个代数表达式,题目的要求是判断选项中哪些代数表达式是和题干中的表达式等价的。
这个题目手算很麻烦,因为明明对计算机编程很感兴趣,所以他想是不是可以用计算机来解决这个问题。假设你是明明,能完成这个任务吗?
这个选择题中的每个表达式都满足下面的性质:
- 表达式只可能包含一个变量 a。
- 表达式中出现的数都是正整数,而且都小于 10000。
- 表达式中可以包括四种运算
+
(加),-
(减),*
(乘),^
(乘幂),以及小括号()
。小括号的优先级最高,其次是^
,然后是*
,最后是+
和-
。+
和-
的优先级是相同的。相同优先级的运算(包括^
运算)都是从左到右进行。 - 幂指数只可能是 1 到 10 之间的正整数(包括 1 和 10)。
- 表达式内部,头部或者尾部都可能有一些多余的空格。
如果某个选项出现不合法的表达式(如括号不匹配),忽略这个选项。
下面是一些合理的表达式的例子:
((a^1) ^ 2)^3
,a*a+a-a
,((a+a))
,9999+(a-a)*a
,1 + (a -1)^3
,1^10^9
输入格式
第一行给出的是题干中的表达式。
第二行是一个整数 n,表示选项的个数。后面n行,每行包括一个选项中的表达式。这 n 个选项的标号分别是 A,B,C,D⋯
输入中的表达式的长度都不超过 50 个字符,而且保证选项中总有表达式和题干中的表达式是等价的。
输出格式
一行,包括一系列选项的标号,表示哪些选项是和题干中的表达式等价的。选项的标号按照字母顺序排列,而且之间没有空格。
输入输出样例
输入 #1复制
( a + 1) ^2 3 (a-1)^2+4*a a + 1+ a a^2 + 2 * a * 1 + 1^2 + 10 -10 +a -a
输出 #1复制
AC
说明/提示
- 对于 30% 的数据,表达式中只可能出现两种运算符
+
和-
; - 对于其它的数据,四种运算符
+-*^
在表达式中都可能出现。 - 对于 100% 的数据,表达式中都可能出现小括号
()
,2≤n≤26。
【题目来源】
NOIP 2005 提高组第四题
题目分析
等价表达式问题要求判断两个表达式在数学上是否等价。由于表达式可能包含变量a,直接进行符号运算比较复杂,因此采用数值检验的方法。
问题特点
-
表达式求值:需要处理包含加减乘除和幂运算的表达式
-
变量处理:表达式包含变量a,需要特殊处理
-
等价判断:通过多个测试点验证表达式等价性
-
输入处理:需要处理空格和多种运算符
算法思路
核心思想:数值检验法
由于完全进行符号运算比较复杂,我们可以通过给变量a赋予多个不同的值,计算两个表达式的结果来判断是否等价。如果对于多个不同的a值,两个表达式的结果都相等,那么它们极有可能是等价的。
中缀表达式求值
使用双栈法(操作数栈和运算符栈)来求表达式的值:
-
将中缀表达式转换为后缀表达式
-
计算后缀表达式的值
模运算处理
为了避免数值过大,使用模运算(取一个大质数作为模数)来控制数值范围。
数学原理
多项式等价性
如果两个多项式在多个点上的值都相等,那么这两个多项式相等的概率很高。通过选择多个测试点和多个模数,可以几乎100%保证正确性。
模运算性质
在模意义下,加减乘运算仍然保持原有的运算性质:
(a + b) % mod = (a % mod + b % mod) % mod
(a * b) % mod = (a % mod * b % mod) % mod
代码实现
cpp
#include <iostream>
#include <string>
#include <stack>
#include <vector>
#include <cctype>
#include <cmath>
using namespace std;const int MOD = 10007; // 取模的大质数// 运算符优先级
int pri(char op) {if (op == '+' || op == '-') return 1;if (op == '*' || op == '/') return 2;if (op == '^') return 3;return 0;
}// 计算二元运算
int calc(int a, int b, char op) {switch(op) {case '+': return (a + b) % MOD;case '-': return (a - b + MOD) % MOD;case '*': return (a * b) % MOD;case '^': {long long res = 1;for (int i = 0; i < b; i++) {res = (res * a) % MOD;}return res;}}return 0;
}// 表达式求值
int eval(string expr, int a_val) {stack<int> num; // 数字栈stack<char> op; // 运算符栈// 预处理:将变量a替换为数值for (int i = 0; i < expr.length(); i++) {if (expr[i] == 'a') {expr.replace(i, 1, to_string(a_val));}}for (int i = 0; i < expr.length(); i++) {char c = expr[i];if (c == ' ') continue; // 跳过空格if (isdigit(c)) {// 处理数字int val = 0;while (i < expr.length() && isdigit(expr[i])) {val = val * 10 + (expr[i] - '0');i++;}i--;num.push(val % MOD);}else if (c == '(') {// 左括号直接入栈op.push(c);}else if (c == ')') {// 右括号:计算直到遇到左括号while (!op.empty() && op.top() != '(') {char oper = op.top(); op.pop();int b = num.top(); num.pop();int a = num.top(); num.pop();num.push(calc(a, b, oper));}op.pop(); // 弹出左括号}else {// 运算符:根据优先级计算while (!op.empty() && pri(op.top()) >= pri(c)) {char oper = op.top(); op.pop();int b = num.top(); num.pop();int a = num.top(); num.pop();num.push(calc(a, b, oper));}op.push(c);}}// 处理剩余的运算符while (!op.empty()) {char oper = op.top(); op.pop();int b = num.top(); num.pop();int a = num.top(); num.pop();num.push(calc(a, b, oper));}return num.top();
}int main() {string std_expr; // 标准表达式getline(cin, std_expr);int n;cin >> n;cin.ignore(); // 忽略换行符// 测试多个a值vector<int> test_vals = {1, 2, 3, 5, 7, 11, 13, 17, 19, 23};// 预先计算标准表达式在多个测试点的值vector<int> std_results;for (int a_val : test_vals) {std_results.push_back(eval(std_expr, a_val));}// 处理每个候选表达式for (int idx = 0; idx < n; idx++) {string cand_expr; // 候选表达式getline(cin, cand_expr);bool is_eq = true; // 是否等价// 在多个测试点上检验for (int i = 0; i < test_vals.size(); i++) {int std_val = std_results[i];int cand_val = eval(cand_expr, test_vals[i]);if (std_val != cand_val) {is_eq = false;break;}}// 输出结果if (is_eq) {cout << char('A' + idx) << endl;}}return 0;
}
代码详细解析
头文件说明
cpp
#include <iostream> // 输入输出流
#include <string> // 字符串处理
#include <stack> // 栈数据结构
#include <vector> // 动态数组
#include <cctype> // 字符类型判断
#include <cmath> // 数学函数
常量定义
cpp
const int MOD = 10007; // 取模的大质数
选择10007作为模数,因为它是一个质数,且大小适中,可以避免整数溢出。
运算符优先级函数
cpp
// 运算符优先级
int pri(char op) {if (op == '+' || op == '-') return 1;if (op == '*' || op == '/') return 2;if (op == '^') return 3;return 0;
}
定义运算符的优先级:
-
加减法:优先级1
-
乘除法:优先级2
-
幂运算:优先级3
-
其他(如括号):优先级0
计算函数
cpp
// 计算二元运算
int calc(int a, int b, char op) {switch(op) {case '+': return (a + b) % MOD;case '-': return (a - b + MOD) % MOD;case '*': return (a * b) % MOD;case '^': {long long res = 1;for (int i = 0; i < b; i++) {res = (res * a) % MOD;}return res;}}return 0;
}
重点说明:
-
加法、乘法:直接取模
-
减法:先加上MOD确保非负,再取模
-
幂运算:使用循环计算,避免使用pow函数(可能溢出)
表达式求值函数
cpp
// 表达式求值
int eval(string expr, int a_val) {stack<int> num; // 数字栈stack<char> op; // 运算符栈// 预处理:将变量a替换为数值for (int i = 0; i < expr.length(); i++) {if (expr[i] == 'a') {expr.replace(i, 1, to_string(a_val));}}
首先将表达式中的变量a替换为具体的数值,这样就把含变量的表达式转化为了纯数值表达式。
cpp
for (int i = 0; i < expr.length(); i++) {char c = expr[i];if (c == ' ') continue; // 跳过空格
跳过空格,简化后续处理。
cpp
if (isdigit(c)) {// 处理数字int val = 0;while (i < expr.length() && isdigit(expr[i])) {val = val * 10 + (expr[i] - '0');i++;}i--;num.push(val % MOD);}
处理多位数字:连续读取数字字符,构造完整的数字,然后压入数字栈。
cpp
else if (c == '(') {// 左括号直接入栈op.push(c);}else if (c == ')') {// 右括号:计算直到遇到左括号while (!op.empty() && op.top() != '(') {char oper = op.top(); op.pop();int b = num.top(); num.pop();int a = num.top(); num.pop();num.push(calc(a, b, oper));}op.pop(); // 弹出左括号}
括号处理:
-
左括号:直接入栈
-
右括号:不断计算直到遇到左括号,这保证了括号内的表达式优先计算
cpp
else {// 运算符:根据优先级计算while (!op.empty() && pri(op.top()) >= pri(c)) {char oper = op.top(); op.pop();int b = num.top(); num.pop();int a = num.top(); num.pop();num.push(calc(a, b, oper));}op.push(c);}}
运算符处理:
-
如果栈顶运算符优先级不低于当前运算符,先计算栈顶运算
-
这样可以保证高优先级的运算先执行
cpp
// 处理剩余的运算符while (!op.empty()) {char oper = op.top(); op.pop();int b = num.top(); num.pop();int a = num.top(); num.pop();num.push(calc(a, b, oper));}return num.top();
}
处理表达式末尾剩余的运算符,返回最终结果。
主函数
cpp
int main() {string std_expr; // 标准表达式getline(cin, std_expr);int n;cin >> n;cin.ignore(); // 忽略换行符
读取标准表达式和候选表达式数量。
cpp
// 测试多个a值vector<int> test_vals = {1, 2, 3, 5, 7, 11, 13, 17, 19, 23};// 预先计算标准表达式在多个测试点的值vector<int> std_results;for (int a_val : test_vals) {std_results.push_back(eval(std_expr, a_val));}
选择多个测试点(使用质数可以减少冲突概率),预先计算标准表达式在这些点的值。
cpp
// 处理每个候选表达式for (int idx = 0; idx < n; idx++) {string cand_expr; // 候选表达式getline(cin, cand_expr);bool is_eq = true; // 是否等价// 在多个测试点上检验for (int i = 0; i < test_vals.size(); i++) {int std_val = std_results[i];int cand_val = eval(cand_expr, test_vals[i]);if (std_val != cand_val) {is_eq = false;break;}}// 输出结果if (is_eq) {cout << char('A' + idx) << endl;}}return 0;
}
对每个候选表达式,在多个测试点上检验是否与标准表达式结果一致。如果所有测试点都一致,则认为等价。
算法优化分析
时间复杂度
-
表达式求值:O(L),其中L是表达式长度
-
测试点数量:固定10个
-
总时间复杂度:O(10 × n × L),完全可行
空间复杂度
-
栈空间:O(L)
-
存储测试结果:O(测试点数量)
-
总空间复杂度:O(L)
正确性分析
通过多个测试点检验,误判概率极低。假设表达式是d次多项式,在模MOD意义下,不同多项式在随机点碰撞的概率约为d/MOD。
完整优化版本
下面是增加多个模数检验的更加鲁棒的版本:
cpp
#include <iostream>
#include <string>
#include <stack>
#include <vector>
#include <cctype>
using namespace std;// 多个模数提高正确率
const int MOD1 = 10007;
const int MOD2 = 10009;
const int MOD3 = 10037;int pri(char op) {if (op == '+' || op == '-') return 1;if (op == '*' || op == '/') return 2;if (op == '^') return 3;return 0;
}// 带模数的计算
int calc(int a, int b, char op, int mod) {switch(op) {case '+': return (a + b) % mod;case '-': return (a - b + mod) % mod;case '*': return (a * b) % mod;case '^': {long long res = 1;for (int i = 0; i < b; i++) {res = (res * a) % mod;}return res;}}return 0;
}// 带模数的表达式求值
int eval(string expr, int a_val, int mod) {stack<int> num;stack<char> op;// 替换变量afor (int i = 0; i < expr.length(); i++) {if (expr[i] == 'a') {expr.replace(i, 1, to_string(a_val));}}for (int i = 0; i < expr.length(); i++) {char c = expr[i];if (c == ' ') continue;if (isdigit(c)) {int val = 0;while (i < expr.length() && isdigit(expr[i])) {val = val * 10 + (expr[i] - '0');i++;}i--;num.push(val % mod);}else if (c == '(') {op.push(c);}else if (c == ')') {while (!op.empty() && op.top() != '(') {char oper = op.top(); op.pop();int b = num.top(); num.pop();int a = num.top(); num.pop();num.push(calc(a, b, oper, mod));}op.pop();}else {while (!op.empty() && pri(op.top()) >= pri(c)) {char oper = op.top(); op.pop();int b = num.top(); num.pop();int a = num.top(); num.pop();num.push(calc(a, b, oper, mod));}op.push(c);}}while (!op.empty()) {char oper = op.top(); op.pop();int b = num.top(); num.pop();int a = num.top(); num.pop();num.push(calc(a, b, oper, mod));}return num.top();
}int main() {string std_expr;getline(cin, std_expr);int n;cin >> n;cin.ignore();vector<int> test_vals = {1, 2, 3, 5, 7, 11, 13, 17, 19, 23};// 多个模数下的标准结果vector<vector<int>> std_results(3);for (int mod_idx = 0; mod_idx < 3; mod_idx++) {int mod = (mod_idx == 0) ? MOD1 : (mod_idx == 1) ? MOD2 : MOD3;for (int a_val : test_vals) {std_results[mod_idx].push_back(eval(std_expr, a_val, mod));}}for (int idx = 0; idx < n; idx++) {string cand_expr;getline(cin, cand_expr);bool is_eq = true;// 在多个模数下检验for (int mod_idx = 0; mod_idx < 3 && is_eq; mod_idx++) {int mod = (mod_idx == 0) ? MOD1 : (mod_idx == 1) ? MOD2 : MOD3;for (int i = 0; i < test_vals.size() && is_eq; i++) {int std_val = std_results[mod_idx][i];int cand_val = eval(cand_expr, test_vals[i], mod);if (std_val != cand_val) {is_eq = false;}}}if (is_eq) {cout << char('A' + idx);}}cout << endl;return 0;
}
测试样例分析
样例1
输入:
text
(a+1)^2 3 a-1 a+1 a^2+2*a+1
输出:
text
C
解释:
-
表达式A: (a-1) 不等价
-
表达式B: (a+1) 不等价
-
表达式C: (a^2+2*a+1) 展开后等于(a+1)^2,等价
样例2
输入:
text
a + a - a * a / a ^ a 2 a 0
输出:
text
A
解释:原表达式化简后等于a
常见问题解答
1. 为什么使用数值检验而不是符号运算?
符号运算实现复杂,容易出错。数值检验方法简单可靠,通过多个测试点可以保证极高正确率。
2. 为什么要用模运算?
防止整数溢出,同时模运算保持了多项式的很多性质。
3. 测试点为什么选择质数?
质数作为测试点可以减少多项式碰撞的概率,提高检验的可靠性。
4. 如何处理负数和减法?
在模运算中,负数通过加模数转为正数:(a - b + MOD) % MOD
5. 幂运算为什么用循环而不是pow函数?
pow函数可能产生浮点数误差,且可能溢出。循环计算可以保证在模意义下的正确性。
6. 如果表达式包含除法怎么办?
题目中表达式只包含整数运算,且题目保证运算合法,不会出现除0等情况。
算法扩展
支持更多运算符
如果需要支持更多运算符,只需扩展pri和calc函数:
cpp
int pri(char op) {if (op == '+' || op == '-') return 1;if (op == '*' || op == '/' || op == '%') return 2;if (op == '^') return 3;return 0;
}int calc(int a, int b, char op, int mod) {switch(op) {case '+': return (a + b) % mod;case '-': return (a - b + mod) % mod;case '*': return (a * b) % mod;case '/': return (a / b) % mod; // 需要处理整除case '%': return a % b;case '^': {long long res = 1;for (int i = 0; i < b; i++) {res = (res * a) % mod;}return res;}}return 0;
}
错误处理版本
增加基本的错误处理:
cpp
int calc(int a, int b, char op, int mod) {switch(op) {case '+': return (a + b) % mod;case '-': return (a - b + mod) % mod;case '*': return (a * b) % mod;case '^': {if (b < 0) return 0; // 负指数处理long long res = 1;for (int i = 0; i < b; i++) {res = (res * a) % mod;}return res;}}return 0;
}
总结
等价表达式问题通过数值检验方法巧妙解决了符号运算的复杂性,主要考察点包括:
-
表达式求值:中缀表达式转后缀表达式并求值
-
栈的应用:使用双栈法处理运算符优先级
-
模运算:防止整数溢出,保持运算性质
-
概率检验:通过多个测试点保证正确性
关键思路:
-
将变量替换为具体数值
-
使用栈处理运算符优先级
-
通过多个测试点检验等价性
-
使用模运算控制数值范围
算法特点:
-
时间复杂度:O(n × L)
-
空间复杂度:O(L)
-
正确率:通过多个测试点可达到极高正确率
总结
掌握这个问题的解法对于理解表达式求值和概率检验方法非常重要。这种思路可以推广到其他表达式处理和相关问题中。