- 0 引言
- 1.1 设计思路
- 1.2 程序功能介绍
- 1.3 例子
- 1.4 程序原理(流程图)
- 2 结论
- 3 核心源代码
化学方程式是化学反应简明的表达形式,它从“质”和“量”两个方面表达了化学反应的意义。故化学方程式的书写是我们学习化学的过程中不可或缺的一个重要环节。当我们遇到简单的化学方程式例如:2H2 + O2 = 2H2O 时,配平则是毫无压力,但是若遇到类似Fe36Si5 + H3PO4 + K2CrO7 = FePO4 + SiO2 + K3PO4+ CrPO4 + H2O 这种元素种类繁多,化学计量数复杂的化学方程式,若仅依靠人力去配平可能是一件极为困难的事情。而计算机具有庞大的计算能力,故我们想到编写程序来解决复杂化学方程的配平。本程序将实现化学方程式和离子方程式的配平,并支持检测输入的方程式是否配平。
1.1 设计思路 对于我们来说,配平化学方程式主要利用元素守恒、化合价升降。而对于每一个化合物,其中元素的化合价都不尽相同,缺少一个标准,并且当我们自己去判断化合价时,也常会出现错误,故计算机难以利用化合价去配平方程式,所以元素守恒对于计算机是最佳的选择。而元素守恒配平方程式其本质就是解线性方程组,故我们只需将配平化学方程式转化成求n元线性方程组的解即可。
首先我们需要知道化学方程式中有哪几种元素,并按ASCII码由上到下升序 排成一列,再读取每个反应物(生成物)中拥有每个元素的个数,最终形成一个元素矩阵 。
其次,便是求解方程组。我们知道,求解n元线性方程组无非就是消元,先得到只含一个未知数的方程,再通过上述求解出的未知数逐步解出剩下的未知数。但是该方法存在一个问题,就是会有浮点数出现,而浮点数在计算机存储时会出现浮点错误 ,故我们需要避开浮点数。这里便用到解n元线性方程的另一种方法——克莱姆法则1 。但是使用克莱姆法则必须满足n个方程对应n个未知数,若未知数的个数比方程组的个数多1,我们便可以使用待定系数法,设其中一个未知数为1,达到减少未知数的效果;若是未知数的个数小于方程组的个数,我们便需要考虑这些多出来的方程组是否存在重复的,若无重复,则去掉多余的方程组仍然能求解出来;若存在重复,则需要进行去重 ,再去求解方程组;若未知数的个数比方程组的个数多2及以上,则该方程组有无穷组解,即无法配平。虽然克莱姆法则求出的解也是分数形式,但观察这些分数形式,不难得出这些分数的分母均为D,所以我们只需提前给所有解都乘上D,以防止分数的出现,再对结果进行约分即可。
接下来我们要处理的是如何将这些系数正确地输出,在化学方程式中,若某个反应物(生成物)前面的化学计量数为1的话,则可以省略不写。故我们需要判断计算出的值是否为1,若为1则不输出,反之输出。
最后,我们输出的反应物(生成物)的位置存在三种情况:1、等号左边 2、化学方程式的末尾 3、除1,2以外的情况。我们需要分别对上述三种情况进行不同的输出,若为情况1,则输出“反应物”+ “=”;若为情况2,则输出“生成物”;若为情况3,则输出“生成物(反应物)”+“+”。
该程序根据巴科斯范式2将化学方程式键入进计算机,若输入方程已配平,则程序会输出“已配平!”:
若未配平且输入正确,则会输出已配平的方程式并以空格隔开:
若方程式错误或无法求解,则会输出“ERROR: 无法求解!”:
按钮功能如下:
① :点击青蒿素的结构简式程序将自动清空输入、输出框的所有内容。
② :最小化窗口。
③ :弹出help窗口,如图
④ :关闭窗口,若
H
e
l
p
Help
Help窗口此时处于打开状态,则会一并关闭。
⑤ :输入框。
⑥ :输出框。
我们以下列方程式为例:KMnO4 + HCl = KCl + MnCl2 + Cl2 + H2O,归边写成KMnO4 + HCl - KCl - MnCl2 - Cl2 = H2O,接下来得到元素矩阵:
| KMnO4 | HCl | KCl | MnCl2 | Cl2 | H2O | |
|---|---|---|---|---|---|---|
| K | 1 | 0 | 1 | 0 | 0 | 0 |
| Mn | 1 | 0 | 0 | -1 | 0 | 0 |
| O | 4 | 0 | 0 | 0 | 0 | 1 |
| H | 0 | 1 | 0 | 0 | 0 | 2 |
| Cl | 0 | 1 | -1 | -2 | -2 | 0 |
然后通过待定系数使最后一列的系数为1,并利用高斯消元对行列式进行去重,得到矩阵如下:
再利用克莱姆法则计算出上述方程的解,再经过通分、约分得到最简整数解,最终完成配平方程式。
本程序可以支持绝大多数的化学、离子方程配平和检测是否配平,其算法效率较高,并且通过我们的不断 D e b u g Debug Debug使该程序趋于完善。但是仍存在一个问题,也是所有矩阵法配平的通病,就是对于(元素种类数-反应物数-生成物数)大于 2 2 2的方程式是无法求解的,用方程表示就是(未知数个数-方程组个数)大于 2 2 2,显然这样的方程有无穷多组解,对应化学方程式便是无法求出其具体系数。这个问题的本质是没有结合元素化合价升降去配平,导致条件不足,无法求出方程组的唯一解。
3 核心源代码完整源代码地址
#include#include #define Get GetStdHandle(STD_OUTPUT_HANDLE) #define White FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE using namespace std; const int N = 100; int d1[N], d2[N]; // d1为最终系数,d2为原方程式的系数 int ma[N][N], ma1[N][N]; //ma为增广矩阵,ma1为判重矩阵 map elem; //用来存放对应元素的数量 map ::iterator iter; //迭代器 string s; string s1[N]; //用来储存每一个反应物和生成物 int left_sum = 0, right_sum = 0, iloc; //分别记录反应物、生成物种类和等号的下标 int sum, D; //sum=left_sum+right_sum,D为行列式的值 int flag = 1; //1代表有解 inline int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } void ReadElem() { //读取所有的物质 for (register int i = 0; i < iloc; i++) { //因为原子守恒所以只读取左边的 string temp = ""; if (isupper(s[i])) { if (islower(s[i + 1])) { temp = string("") + char(s[i]) + char(s[i + 1]); } else temp = string("") + char(s[i]); elem[temp] = 0; } else if (s[i] == '<') elem["e"] = 0; } } inline int E_GetInt(string temp, int pos) { pos++; int x = 0; if (isdigit(temp[pos])) { while (isdigit(temp[pos])) { x = (x << 1) + (x << 3) + int(char(temp[pos]) - '0'); pos++; } } else x = 1; if (temp[pos] == '+') return x; else return ~(x - 1); } int check(int i) { //判断正负 return i < left_sum ? 1 : -1; } string check1(int k) { //判断系数是否为1 return k == 1 ? "" : to_string(k); } void InitElem() { for (register int i = 0; i < s.length(); i++) if (s[i] == '=') { iloc = i; break; } int head = 0, count = -1; for (register int i = 0; i < s.length(); i++) { if (s[i] == '+' && s[i + 1] != '>') { if (i < iloc)left_sum++; else right_sum++; count += 1; s1[count] = s.substr(head, i - head); head = i + 1; } if (i == iloc) { //读到等号 count += 1; s1[count] = s.substr(head, i - head); head = i + 1; } if (i == s.length() - 1) { //最后一个 count += 1; s1[count] = s.substr(head, i + 1 - head); } } left_sum++, right_sum++; sum = left_sum + right_sum; //上述将化学式分为左式和右式,并将每一个反应物和生成物切片。s1[0,left_sum-1]为左式,s1[left_sum,sum-1]为右式 } inline int GetInt(string temp, int pos) { //读数 pos++; if (islower(temp[pos]))pos++; //小写 if (!isdigit(temp[pos]))return 1; //非数字就是1 else { int x = 0; while (isdigit(temp[pos])) { //读元素后面的数字 x = (x << 1) + (x << 3) + int(char(temp[pos]) - '0'); pos++; } return x; } } void ToMatr() { for (register int i = 0; i < sum; i++) { //遍历每一个反应(生成)物 for (iter = elem.begin(); iter != elem.end(); iter++) iter->second = 0; //初始化map string temp = s1[i]; //临时存贮 int k1 = 1, k2 = 1; //分别代表()后的数字,[]后的数字。 for (register int j = 0; j < temp.length(); j++) { if (temp[j] == '(') { int m = j + 1; while (temp[m] != ')')m++; k1 = GetInt(temp, m); } if (temp[j] == ')')k1 = 1; if (temp[j] == '[') { int m = j + 1; while (temp[m] != ']')m++; k2 = GetInt(temp, m); } if (temp[j] == ']')k2 = 1; if (isupper(temp[j])) { if (islower(temp[j + 1])) { string temp1 = string("") + char(temp[j]) + char(temp[j + 1]); elem[temp1] += GetInt(temp, j) * k1 * k2 * check(i); } else { string temp1 = string("") + char(temp[j]); elem[temp1] += GetInt(temp, j) * k1 * k2 * check(i); } } else if (temp[j] == '<') elem["e"] = E_GetInt(temp, j) * check(i); } iter = elem.begin(); int x = 0; while (iter != elem.end()) { ma[x][i] = iter->second; x++; iter++; } } } int determinant(int n) { int ans = 1, v = 1; for (int i = 0; i < n; i++) //列 for (int j = i + 1; j < n; j++) { //行 while (ma1[i][i]) { //辗转相除消元 int x = ma1[j][i] / ma1[i][i]; for (int k = i; k < n; k++) //转换成下三角行列式 ma1[j][k] -= x * ma1[i][k]; swap(ma1[i], ma1[j]), v = -v; //性质 4 } swap(ma1[i], ma1[j]), v = -v; } for (int i = 0; i < n; i++) ans *= ma1[i][i]; //对角线求值 return ans * v; } void solve() { flag = 1; int ele = elem.size(); for (register int i = 0; i < ele; i++) //默认最后一个生成物系数为1 ma[i][sum - 1] *= -1; for (int i = 0; i < sum - 1; i++) //列 for (int j = i + 1; j < ele; j++) { //行 while (ma[i][i]) { int x = ma[j][i] / ma[i][i]; for (int k = i; k < sum; k++) ma[j][k] -= x * ma[i][k]; swap(ma[i], ma[j]); } swap(ma[i], ma[j]); } if (sum - ele >= 2) { flag = 0; return; } else { ele = min(ele, sum - 1); for (register int i = 0; i < ele; i++) for (register int j = 0; j < ele; j++) ma1[i][j] = ma[i][j]; D = determinant(ele); //求行列式 for (register int i = 0; i < ele; i++) { //分别对sum-1个行列式求值 for (register int j = 0; j < ele; j++) //行 for (register int k = 0; k < ele; k++) {//列 if (k == i) ma1[j][k] = ma[j][ele]; else ma1[j][k] = ma[j][k]; } d1[i] = determinant(ele); } d1[sum - 1] = D; int Gcd = d1[0]; for (register int i = 1; i < sum; i++) Gcd = gcd(Gcd, d1[i]); if (Gcd == 0) { flag = 0; return; } for (register int i = 0; i < sum; i++) { d1[i] /= Gcd; if (d1[i] <= 0) { flag = 0; return; } } } } void InitAll() { s = ""; elem.clear(); left_sum = right_sum = iloc = D = 0; for (register int i = 0; i < N; i++) { d1[i] = 0; d2[i] = 1; s1[i] = ""; for (register int j = 0; j < N; j++) ma1[i][j] = ma[i][j] = 0; } } void print(int i) { SetConsoleTextAttribute(Get, FOREGROUND_INTENSITY | FOREGROUND_RED); cout << check1(d1[i]); SetConsoleTextAttribute(Get, White); } string DelSpace(string s) { string ans = ""; for (register int i = 0; i < s.length(); i++) { if (char(s[i]) != ' ') ans.push_back(char(s[i])); } return ans; } int main() { system("mode con cols=120 lines=30"); ios::sync_with_stdio(false); cin.tie(0); system("color 0F"); cout << " ********************************************************************************************" << endl; cout << " * 简单化学(离子)方程式配平,支持检测是否配平 *" << endl; cout << " * 区分大小写,暂时不支持极少数氧化还原反应,有机方程请省略化学键,离子方程请用<> *" << endl; cout << " ********************************************************************************************" << endl; while (1) { InitAll(); cout << "请输入正确的化学方程式: " << endl; getline(cin, s); s = DelSpace(s); InitElem(); for (register int i = 0; i < sum; i++) { if (!isdigit(s1[i][0])) d2[i] = 1; else { int x = 0, pos = 0; while (isdigit(s1[i][pos])) { x = (x << 1) + (x << 3) + int(char(s1[i][pos]) - '0'); pos++; } d2[i] = x; string m = to_string(x); s1[i] = s1[i].substr(m.length(), s1[i].length() - m.length()); } } ReadElem(); ToMatr(); solve(); if (flag) { int cnt = 0; for (cnt = 0; cnt < sum; cnt++) if (d1[cnt] != d2[cnt]) break; if (cnt < sum) { cout << "配平结果: "; for (register int i = 0; i < sum; i++) { if (i == left_sum - 1) { print(i); cout << s1[i] << '='; } else if (i == sum - 1) { print(i); cout << s1[i]; } else { print(i); cout << s1[i] << '+'; } } cout << endl; } else cout << "已配平!" << endl; } else { SetConsoleTextAttribute(Get, FOREGROUND_INTENSITY | FOREGROUND_RED); cout << "ERROR: 无法求解!" << endl; SetConsoleTextAttribute(Get, White); } cout << endl; } return 0; }
n n n元线性方程式中:
系数构成的行列式称为该方程组的系数行列式 D D D,即:
若线性方程组的系数矩阵可逆(非奇异),即系数行列式 D ≠ 0 Dnot =0 D=0,则线性方程组有唯一解,其解为:
其中 D j D_j Dj ( j = 1 , 2 , 3 … n ) (j=1,2,3…n) (j=1,2,3…n)是把 D D D中第 j j j列元素对应地换成常数项而其余各列保持不变所得到的行列式。如: ↩︎巴科斯范式( B a c k u s − N a u r f o r m Backus-Naur form Backus−Naurform, B N F BNF BNF)给出的形式化定义如下:
::= "="
::= | "+"
::= | "" |
::= "<" "+>" | "<" "->" | ""
::= { }
::= "0" | "1" | ... | "9"
::= |
::= | "(" ")" | "[" "]"
::= |
::= "A" | "B" | ... | "Z"
::= "a" | "b" | ... | "z" ↩︎



