【csp202403-3】化学方程式配平【第33次CCF计算机软件能力认证】
题目背景
近日来,西西艾弗岛化学研究中心的研究员们向岛上的初中学生开展了化学科普活动。在活动中发现,初学化学的同学们十分苦恼于正确配平化学方程式。 而还有一些同学,则提出了一些稀奇古怪的方程式,让研究员们帮忙配平。在配平之前,研究员们需要先判断这个方程式是否能够配平。
一个化学方程式,也叫化学反应方程式,是用化学式表示化学反应的式子。其等号左右两侧分别列举了化学反应的全部反应物和生成物。每种物质都用其化学式表示。一个物质的化学式,列举了构成该物质的各元素的原子数目。例如,水的化学式是H2O,表示水分子中含有两个氢原子和一个氧原子。化学方程式中每种物质的化学式前面都有一个系数,表示参与反应或生成的物质的相对数目比例。例如,方程式2H2+O2=2H2O表示二分子氢气和一分子氧气反应生成二分子水。 我们称一个化学方程式是配平的,是指该方程式中的反应物和生成物中,各元素原子总数目相等。例如上述方程式中,左侧氢原子、氧原子的总数目分别为4和2,右侧氢原子、氧原子的总数目分别为4和2,因此该方程式是配平的。
问题描述
为了配平一个化学方程式,我们可以令方程式中各物质的系数为未知数,然后针对涉及的每一种元素,列出关于系数的方程,形成一个齐次线性方程组。然后求解这个方程组,得到各物质的系数。这样,我们就把化学方程式配平的问题,转化为了求解齐次线性方程组的问题。 如果方程组没有非零解,那么这个方程式是不可以配平的。反之,如果方程组有非零解,我们就可能得到一个配平的方程式。当然,最终得到的方程式仍然需要结合化学知识进行检验,对此我们不再进一步考虑,仅考虑非零解的存在。
例如要配平化学方程式:Al2(SO4)3+NH3⋅H2O→Al(OH)3+(NH4)2SO4Al2(SO4)3+NH3⋅H2O→Al(OH)3+(NH4)2SO4
首先假定所有物质在方程的同一侧,即不考虑哪个是反应物,哪个是生成物,分别设这些物质的系数为𝑥1,𝑥2,𝑥3,𝑥4,则可以针对出现的各个元素,列出如下的方程组:
用矩阵的形式表示为:
对系数矩阵实施高斯消元,得到系数矩阵的一个行阶梯形式:
由此可见,系数矩阵的秩为3。根据线性代数的知识,我们知道,齐次线性方程组𝐴𝑋=0的解空间的维数等于其未知数个数减去系数矩阵的秩rank𝐴。而要让方程式配平,即要求方程组存在非零解,那么就需要让解空间的维数大于0,即系数矩阵的秩小于未知数个数。因此,我们可以通过判断系数矩阵的秩是否小于未知数个数,来判断方程式是否可以配平。如果可以配平,则可以通过解的符号来判断反应物和生成物的位置。
本题中,我们将给出一些化学方程式,请你按照上述方法判断它们是否可以配平。为了便于程序处理,我们用到的化学式,会被化简为只包含小写字母和数字的字符串,不包含括号。其中连续的字母表示一种元素,随后的数字表示原子个数。原子个数为1时不省略数字;一个化学式中包含的元素不重复。例如,上述方程式中的化学式可以化简为al2s3o12
、n1h5o1
、al1o3h3
、n2h8s1o4
。
输入格式
从标准输入读入数据。
输入的第一行包含一个正整数𝑛,表示需要判断的化学方程式的个数。
接下来的𝑛行,每行描述了一个需要被配平的化学方程式。包含空格分隔的一个正整数和全部涉及物质的化学式。其中,正整数𝑚表示方程式中的物质;随后的𝑚个字符串,依次给出方程式中的反应物的化学式和生成物的化学式。
输出格式
输出到标准输出。
输出包含𝑛行,每行包含字母Y
或N
,表示按题设方法,所给待配平化学方程式能否配平。
样例输入
6
2 o2 o3
3 c1o1 c1o2 o2
2 n2o4 n1o2
4 cu1 h1n1o3 cu1n2o6 h2o1
4 al2s3o12 n1h5o1 al1o3h3 n2h8s1o4
4 c1o1 c1o2 o2 h2o1
样例输出
Y
Y
Y
N
Y
Y
样例解释
输入中给出了 5 个待配平的化学方程式,其中各方程式的配平情况为:
- 3O2=2O3
- 2CO+O2=2CO2
- N2O4=2NO2
- 因为缺少生成物NO或NO2,所以不可以配平
- Al2(SO4)3+6NH3⋅H2O=2Al(OH)3+3(NH4)2SO4
- 2CO+O2=2CO2,本方程式对应的线性方程组求解后,得到H2O的系数为0,说明其未参与反应,属多余的物质。在这种情况下,由于对应的线性方程组存在非零解,所以我们仍然认为这个方程式是可以配平的。
数据范围
对于20%的数据,每个方程中物质的个数不超过2,每个方程中涉及的全部元素不超过2种;
对于60%的数据,每个方程中物质的个数不超过3,每个方程中涉及的全部元素不超过3种;
对于100%的数据,每个方程中物质的个数不超过40,每个方程中涉及的全部元素不超过40种;且有1≤𝑛≤10,且化学式中各元素的原子个数不超过50。
提示
-
对矩阵进行高斯消元的一种方法是:
- 考察矩阵的第一列上的元素:对除去第一行第一列的子矩阵重复上述操作,直至不再余下子矩阵。
- 若全都为零,则对除去该列的子矩阵重复上述判断;
- 若不全为零,则:
- 考察第一行第一列的元素:
- 如果其为 0,则将该行与后面的某一个第一列非 0 的行交换,使第一行第一列的元素非 0;
- 令后续所有行减去第一行的适当倍数,使得后续所有行的第一列元素为 0;
- 考察第一行第一列的元素:
- 对除去第一行第一列的子矩阵重复上述操作,直至不再余下子矩阵。
- 考察矩阵的第一列上的元素:对除去第一行第一列的子矩阵重复上述操作,直至不再余下子矩阵。
-
对系数矩阵高斯消元后,不全为 0 的行的数目即为系数矩阵的秩。
-
评测环境仅提供各语言的标准库,特别地,不提供任何线性代数库。
题解
几句话概括题意:
提取每一种元素在各个物质中出现的次数,列成矩阵行列式,然后计算矩阵的秩,秩小于元素个数输出Y,否则输出N。
有几种物质就有几个未知数,有几种元素就有几个方程。
对每一种元素列一个方程,该元素在每一种物质中出现的个数就是相应未知数的系数,没有就是0。
把所有未知数的系数(包括0)按顺序提出来摆一起就是我们要处理的行列式。
输入给的格式是元素后面跟着个数,一种元素可能由不止一个字母组成,我们先把元素和数字分开,对于每一种物质,用map储存该物质中出现的元素及其个数
然后,遍历每种元素,一种元素对应行列式的一行,对于每种元素,遍历每个物质,取出之前存在map中的相应的个数,即为行列式上相应位置的值
接下来处理行列式,遍历每一行,对于第i行,如果全为0则忽略,如果不为0,检查第i行第i列是否为0,如果为0,则往下找一个第i列不为0的行,把两行所有元素交换。然后,我们要令从第i+1行到最后一行的第i列上的数都变成0,由于这是由计算机来计算的,不需要我们手动计算,因此我们可以不用考虑计算结果的复杂度,直接计算每一行第i列相对于第i行第i列的倍数,将每一行减去第i行的相应倍数就好了。计算过程可能存在分数,我直接用double来储存结果,实际上可能会有一定的精度问题。
由于我们只需要知道矩阵的秩,之后不需要其它计算,因此我们也不需要交换行的位置,处理完整个行列式后直接统计不全为0的行数即可
1 #include <iostream> 2 #include <cstdio> 3 #include <string> 4 #include <map> 5 using namespace std; 6 int n,m; 7 double a[50][50]; // 储存行列式 8 double tmp; 9 string str,ch; 10 map<string,int> mp[50]; // 统计物质中各种元素的个数 11 map<string,bool> cnt; // 标记方程式中出现过的元素 12 bool fg; 13 int main() 14 { 15 int i,j,k,len,s,t,z; 16 double p; 17 scanf("%d",&n); 18 while (n--) 19 { 20 scanf("%d",&m); 21 for (i=1;i<50;i++) 22 mp[i].clear(); 23 cnt.clear(); 24 for (i=1;i<=m;i++) 25 { 26 cin>>str; 27 len=str.length(); 28 for (j=0;j<len;) 29 { 30 ch=""; 31 s=0; 32 // 分离元素和数字 33 for (;j<len && str[j]>='a' && str[j]<='z';j++) 34 ch=ch+str[j]; 35 for (;j<len && str[j]>='0' && str[j]<='9';j++) 36 s=s*10+str[j]-'0'; 37 mp[i][ch]=s; // 在第i个物质中元素ch的个数为s 38 cnt[ch]=1; // 标记在方程式中出现过元素ch 39 } 40 } 41 t=0; 42 for (auto &p:cnt) // 遍历所有出现过的元素 43 { 44 t++; 45 for (i=1;i<=m;i++) // 遍历每个物质 46 { 47 a[t][i]=mp[i][p.first]; 48 } 49 } 50 for (i=1;i<=t;i++) 51 { 52 // 检验全为0则忽略该行 53 for (j=1;j<=m;j++) 54 if (a[i][j]!=0) 55 break; 56 if (j>m) continue; 57 if (a[i][i]==0) 58 { // 如果第i列为0,向下找一个不为0的整行交换 59 for (j=i+1;j<=t && a[j][i]==0;j++); 60 for (k=1;k<=m;k++) 61 tmp=a[i][k], 62 a[i][k]=a[j][k], 63 a[j][k]=tmp; 64 } 65 for (j=i+1;j<=t;j++) 66 { //将后面每一行减去第i行相应倍数,使第i列变成0 67 p=a[j][i]/a[i][i]; 68 for (k=i;k<=m;k++) 69 a[j][k]=a[j][k]-a[i][k]*p; 70 } 71 } 72 z=0; 73 for (i=1;i<=t;i++) // 统计不全为0的行数,即矩阵的秩 74 { 75 for (j=1;j<=m && a[i][j]==0;j++); 76 if (j<=m) z++; 77 } 78 if (z<m) printf("Y\n"); 79 else printf("N\n"); 80 } 81 return 0; 82 }